diff --git a/.agent/skills/README.md b/.agent/skills/README.md index 4b10d4f540df..9b2533fe0695 100644 --- a/.agent/skills/README.md +++ b/.agent/skills/README.md @@ -25,15 +25,19 @@ 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 | | [io-connectors](io-connectors/SKILL.md) | 51+ I/O connectors, testing patterns, usage examples | +| [developing-new-io-connectors](developing-new-io-connectors/SKILL.md) | A detailed guide on developing new I/O connectors | | [java-development](java-development/SKILL.md) | Java SDK development, building, testing, project structure | | [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 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..14176e75ce40 --- /dev/null +++ b/.agent/skills/developing-new-io-connectors/SKILL.md @@ -0,0 +1,347 @@ +--- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT 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. + +For more details see the [Developing I/O connectors](https://beam.apache.org/documentation/io/developing-io-overview) guide. diff --git a/.agent/skills/io-connectors/SKILL.md b/.agent/skills/io-connectors/SKILL.md index 596b602add6c..e0adae841feb 100644 --- a/.agent/skills/io-connectors/SKILL.md +++ b/.agent/skills/io-connectors/SKILL.md @@ -189,9 +189,10 @@ Beam supports using I/O connectors from one SDK in another via the expansion ser ``` ## Creating New Connectors -See [Developing I/O connectors](https://beam.apache.org/documentation/io/developing-io-overview) Key components: 1. **Source** - Reads data (bounded or unbounded) 2. **Sink** - Writes data 3. **Read/Write transforms** - User-facing API + +For more detailed information on developing new I/O connectors see the [Developing new I/O connectors SKILL](../developing-new-io-connectors/SKILL.md). \ No newline at end of file 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. diff --git a/.asf.yaml b/.asf.yaml index e69dcbc6ff96..b2af7e083959 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -51,6 +51,10 @@ github: protected_branches: master: {} + release-2.76: {} + release-2.75.0-postrelease: {} + release-2.75: {} + release-2.74.0-postrelease: {} release-2.74: {} release-2.73.0-postrelease: {} release-2.73: {} diff --git a/.github/actions/setup-default-test-properties/test-properties.json b/.github/actions/setup-default-test-properties/test-properties.json index e3f1d7a3abcb..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.17", "1.18", "1.19", "1.20"], + "FLINK_VERSIONS": ["1.19", "1.20", "2.0", "2.1", "2.2"], "SPARK_VERSIONS": ["3"] }, "GoTestProperties": { diff --git a/.github/actions/setup-environment-action/action.yml b/.github/actions/setup-environment-action/action.yml index aa10257e3f93..b37d30953ed9 100644 --- a/.github/actions/setup-environment-action/action.yml +++ b/.github/actions/setup-environment-action/action.yml @@ -56,27 +56,45 @@ runs: sdks/python/setup.py sdks/python/tox.ini + # Multiline python-version (e.g. 3.10\n3.11\n...) must not be embedded raw + # in cache keys: actions/cache treats each newline-separated line as a + # separate key and fails when there are more than 10 ("Keys are limited to + # a maximum of 10"). Collapse whitespace into a single key fragment. + - name: Sanitize Python version for tox cache key + if: ${{ inputs.python-version != '' && inputs.tox-cache == 'true' }} + id: py-cache-key + shell: bash + env: + PYTHON_VERSION_INPUT: ${{ inputs.python-version }} + run: | + if [[ "$PYTHON_VERSION_INPUT" == "default" ]]; then + sanitized="310" + else + sanitized=$(printf '%s' "$PYTHON_VERSION_INPUT" | tr -s '[:space:]' '-' | sed 's/^-//;s/-$//') + fi + echo "sanitized=$sanitized" >> "$GITHUB_OUTPUT" + - name: Cache tox environments if: ${{ inputs.python-version != '' && inputs.tox-cache == 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v6 with: path: | sdks/python/target/.tox !sdks/python/target/.tox/**/log !sdks/python/target/.tox/.package_cache - key: tox-${{ runner.os }}-py${{ inputs.python-version == 'default' && '310' || inputs.python-version }}-${{ hashFiles('sdks/python/tox.ini') }}-${{ hashFiles('sdks/python/setup.py') }} + key: tox-${{ runner.os }}-py${{ steps.py-cache-key.outputs.sanitized }}-${{ hashFiles('sdks/python/tox.ini') }}-${{ hashFiles('sdks/python/setup.py') }} restore-keys: | - tox-${{ runner.os }}-py${{ inputs.python-version == 'default' && '310' || inputs.python-version }}-${{ hashFiles('sdks/python/tox.ini') }}- - tox-${{ runner.os }}-py${{ inputs.python-version == 'default' && '310' || inputs.python-version }}- + tox-${{ runner.os }}-py${{ steps.py-cache-key.outputs.sanitized }}-${{ hashFiles('sdks/python/tox.ini') }}- + tox-${{ runner.os }}-py${{ steps.py-cache-key.outputs.sanitized }}- - name: Install Java if: ${{ inputs.java-version != '' }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: ${{ inputs.java-version == 'default' && '11' || inputs.java-version }} + java-version: ${{ inputs.java-version == 'default' && '21' || inputs.java-version }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-disabled: ${{ inputs.disable-cache }} validate-wrappers: false diff --git a/.github/actions/setup-k8s-access/action.yml b/.github/actions/setup-k8s-access/action.yml index cb00c853738b..264aa77cfc88 100644 --- a/.github/actions/setup-k8s-access/action.yml +++ b/.github/actions/setup-k8s-access/action.yml @@ -68,7 +68,7 @@ runs: run: | kubectl config set-context --current --namespace=${{ steps.replace_namespace.outputs.TEST_NAMESPACE }} - name: Post cleanup - uses: pyTooling/Actions/with-post-step@v4.2.2 + uses: pytooling/actions/with-post-step@42e17fae05f224e5ac3d79d021a4e3577878efe5 # v4.2.2 with: main: echo "Post Cleanup" post: | 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/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/.github/gh-actions-self-hosted-runners/arc/images/Dockerfile b/.github/gh-actions-self-hosted-runners/arc/images/Dockerfile index bc9494124f11..a2bae4dac74b 100644 --- a/.github/gh-actions-self-hosted-runners/arc/images/Dockerfile +++ b/.github/gh-actions-self-hosted-runners/arc/images/Dockerfile @@ -26,7 +26,7 @@ RUN docker buildx install && docker buildx version USER root # Native build toolchain for Python C extensions. RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential time rsync openssh-client && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential time rsync openssh-client wget && \ rm -rf /var/lib/apt/lists/* #Install Node RUN curl -OL https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-x64.tar.xz && \ @@ -41,16 +41,16 @@ RUN curl -OL https://go.dev/dl/go${go_version}.linux-amd64.tar.gz && \ rm go${go_version}.linux-amd64.tar.gz ENV PATH="${PATH}:/usr/local/go/bin" #Install Java -RUN curl -OL https://cdn.azul.com/zulu/bin/zulu11.74.15-ca-jdk11.0.24-linux_x64.tar.gz && \ - tar -C /usr/local -xzf zulu11.74.15-ca-jdk11.0.24-linux_x64.tar.gz && \ - rm zulu11.74.15-ca-jdk11.0.24-linux_x64.tar.gz && \ - mv /usr/local/zulu11.74.15-ca-jdk11.0.24-linux_x64 /usr/local/java +RUN curl -OL https://cdn.azul.com/zulu/bin/zulu21.50.19-ca-jdk21.0.11-linux_x64.tar.gz && \ + tar -C /usr/local -xzf zulu21.50.19-ca-jdk21.0.11-linux_x64.tar.gz && \ + rm zulu21.50.19-ca-jdk21.0.11-linux_x64.tar.gz && \ + mv /usr/local/zulu21.50.19-ca-jdk21.0.11-linux_x64 /usr/local/java ENV PATH="${PATH}:/usr/local/java/bin" #Install Gradle -RUN curl -OL https://services.gradle.org/distributions/gradle-8.4-bin.zip && \ - unzip gradle-8.4-bin.zip && \ - rm gradle-8.4-bin.zip && \ - mv gradle-8.4 /usr/local/gradle +RUN curl -OL https://services.gradle.org/distributions/gradle-8.14.3-bin.zip && \ + unzip gradle-8.14.3-bin.zip && \ + rm gradle-8.14.3-bin.zip && \ + mv gradle-8.14.3 /usr/local/gradle ENV PATH="${PATH}:/usr/local/gradle/bin" #Install GitHub CLI RUN curl -OL https://github.com/cli/cli/releases/download/v2.31.0/gh_2.31.0_linux_amd64.tar.gz && \ diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json b/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json index 3a009261f4f9..5abe02fc09c7 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": 2 + "modification": 1 } 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/.github/trigger_files/beam_PostCommit_Go.json b/.github/trigger_files/beam_PostCommit_Go.json index b73af5e61a43..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": 1 + "modification": 3 } 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/.github/trigger_files/beam_PostCommit_Go_VR_Spark.json b/.github/trigger_files/beam_PostCommit_Go_VR_Spark.json index 72b690e649d3..0dcf05b4215b 100644 --- a/.github/trigger_files/beam_PostCommit_Go_VR_Spark.json +++ b/.github/trigger_files/beam_PostCommit_Go_VR_Spark.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1, - "https://github.com/apache/beam/pull/36527": "skip a processing time timer test in spark", + "modification": 2, + "https://github.com/apache/beam/pull/36527": "skip a processing time timer test in spark" } diff --git a/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json b/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json index ae6cb268ff68..aff502e19620 100644 --- a/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json +++ b/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json @@ -1,5 +1,5 @@ { - "https://github.com/apache/beam/pull/36138": "Cleanly separating v1 worker and v2 sdk harness container image handling", + "https://github.com/apache/beam/pull/39330": "Fix DataflowV1 test failure by fixing getSimpleName access", "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", diff --git a/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json b/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json index 73012c45df18..c4a6954cecb7 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": 9, "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface" } diff --git a/.github/trigger_files/beam_PostCommit_Java_Delta_IO_Dataflow.json b/.github/trigger_files/beam_PostCommit_Java_Delta_IO_Dataflow.json new file mode 100644 index 000000000000..12481ae0dbc8 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Java_Delta_IO_Dataflow.json @@ -0,0 +1,4 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run.", + "modification": 4 +} diff --git a/.github/trigger_files/beam_PostCommit_Java_Hadoop_Versions.json b/.github/trigger_files/beam_PostCommit_Java_Hadoop_Versions.json index 1bd74515152c..f1ba03a243ee 100644 --- a/.github/trigger_files/beam_PostCommit_Java_Hadoop_Versions.json +++ b/.github/trigger_files/beam_PostCommit_Java_Hadoop_Versions.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 4 -} \ No newline at end of file + "modification": 5 +} 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_Nexmark_Spark.json b/.github/trigger_files/beam_PostCommit_Java_Nexmark_Spark.json new file mode 100644 index 000000000000..e3d6056a5de9 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Java_Nexmark_Spark.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/trigger_files/beam_PostCommit_Java_PVR_Flink_Streaming.json b/.github/trigger_files/beam_PostCommit_Java_PVR_Flink_Streaming.json index 2dd3a2471d89..d7f4c03aaf5b 100644 --- a/.github/trigger_files/beam_PostCommit_Java_PVR_Flink_Streaming.json +++ b/.github/trigger_files/beam_PostCommit_Java_PVR_Flink_Streaming.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1, + "modification": 2, "https://github.com/apache/beam/pull/32440": "test new datastream runner for batch" } diff --git a/.github/trigger_files/beam_PostCommit_Java_PVR_Spark3_Streaming.json b/.github/trigger_files/beam_PostCommit_Java_PVR_Spark3_Streaming.json index f1ba03a243ee..d6a91b7e2e86 100644 --- a/.github/trigger_files/beam_PostCommit_Java_PVR_Spark3_Streaming.json +++ b/.github/trigger_files/beam_PostCommit_Java_PVR_Spark3_Streaming.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 5 + "modification": 7 } diff --git a/.github/trigger_files/beam_PostCommit_Java_PVR_Spark_Batch.json b/.github/trigger_files/beam_PostCommit_Java_PVR_Spark_Batch.json index 455144f02a35..d6a91b7e2e86 100644 --- a/.github/trigger_files/beam_PostCommit_Java_PVR_Spark_Batch.json +++ b/.github/trigger_files/beam_PostCommit_Java_PVR_Spark_Batch.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 6 + "modification": 7 } 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/trigger_files/beam_PostCommit_Java_Tpcds_Spark.json b/.github/trigger_files/beam_PostCommit_Java_Tpcds_Spark.json new file mode 100644 index 000000000000..e3d6056a5de9 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Java_Tpcds_Spark.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/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_Streaming.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json index e623d3373a93..50d17c108f2e 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": 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..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": 3 + "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..3f63c0c9975f 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": 2 } diff --git a/.github/trigger_files/beam_PostCommit_PortableJar_Spark.json b/.github/trigger_files/beam_PostCommit_PortableJar_Spark.json new file mode 100644 index 000000000000..e3d6056a5de9 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_PortableJar_Spark.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/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 91226bd08ee3..89cec619b020 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 + "pr": "38701", + "modification": 56 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Arm.json b/.github/trigger_files/beam_PostCommit_Python_Arm.json new file mode 100644 index 000000000000..1efc8e9e4405 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Python_Arm.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/trigger_files/beam_PostCommit_Python_Dependency.json b/.github/trigger_files/beam_PostCommit_Python_Dependency.json index 96e4dc0aa998..16209484727a 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Dependency.json +++ b/.github/trigger_files/beam_PostCommit_Python_Dependency.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 3 - } \ No newline at end of file + "modification": 4 + } 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 +} diff --git a/.github/trigger_files/beam_PostCommit_Python_Examples_Spark.json b/.github/trigger_files/beam_PostCommit_Python_Examples_Spark.json new file mode 100644 index 000000000000..e3d6056a5de9 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Python_Examples_Spark.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/trigger_files/beam_PostCommit_Python_ValidatesContainer_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_ValidatesContainer_Dataflow.json index 60eb7272a936..327fe1987c50 100644 --- a/.github/trigger_files/beam_PostCommit_Python_ValidatesContainer_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesContainer_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 } \ No newline at end of file diff --git a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json index b26833333238..462412f7bd34 100644 --- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json @@ -1,4 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 2 + "modification": 5, + "https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR test" } diff --git a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Flink.json b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Flink.json index b08e2bb81150..962795fcb0ae 100644 --- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Flink.json +++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Flink.json @@ -1,5 +1,6 @@ { "https://github.com/apache/beam/pull/32648": "testing addition of Flink 1.19 support", "https://github.com/apache/beam/pull/34830": "testing", - "trigger-2026-04-04": "portable_runner expand_sdf opt-in" + "trigger-2026-04-04": "portable_runner expand_sdf opt-in", + "https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR test" } diff --git a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json index bd7b284625bb..6384446f50e4 100644 --- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json +++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json @@ -1,5 +1,8 @@ { "https://github.com/apache/beam/pull/34830": "testing", "https://github.com/apache/beam/issues/35429": "testing", - "trigger-2026-04-04": "portable_runner expand_sdf opt-in" + "trigger-2026-04-04": "portable_runner expand_sdf opt-in", + "https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR test", + "modification": 1, + "https://github.com/apache/beam/issues/19468": "SDF self-checkpointing and bundle finalization" } diff --git a/.github/trigger_files/beam_PostCommit_Python_Versions.json b/.github/trigger_files/beam_PostCommit_Python_Versions.json index a975cd1cd104..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": 1 + "revision": 5 } 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 c5309eebb070..e3d6056a5de9 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": 17 + "modification": 1 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Dataflow.json index e3d6056a5de9..c537844dc84a 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_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/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json index c537844dc84a..b26833333238 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": 2 } 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..d6a91b7e2e86 --- /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": 7 +} diff --git a/.github/trigger_files/beam_PostCommit_SQL.json b/.github/trigger_files/beam_PostCommit_SQL.json index 5df3841d2363..1b6aa099172e 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": 3 + "modification": 6 } 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/.github/trigger_files/beam_PostCommit_XVR_Spark3.json b/.github/trigger_files/beam_PostCommit_XVR_Spark3.json index 03bb52a7ef46..1be4456862dc 100644 --- a/.github/trigger_files/beam_PostCommit_XVR_Spark3.json +++ b/.github/trigger_files/beam_PostCommit_XVR_Spark3.json @@ -1,3 +1,3 @@ { - "trigger-2026-04-04": "portable_runner expand_sdf opt-in" + "trigger-2026-07-08": "portable_runner expand_sdf opt-in 2" } diff --git a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json index 541dc4ea8e87..58746dc93010 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": 9 } diff --git a/.github/trigger_files/beam_PreCommit_Flink_Container.json b/.github/trigger_files/beam_PreCommit_Flink_Container.json index bbdc3a3910ef..62ae7886c573 100644 --- a/.github/trigger_files/beam_PreCommit_Flink_Container.json +++ b/.github/trigger_files/beam_PreCommit_Flink_Container.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/.github/trigger_files/beam_PreCommit_Java.json b/.github/trigger_files/beam_PreCommit_Java.json index 5abe02fc09c7..0e9f1cacf9bc 100644 --- a/.github/trigger_files/beam_PreCommit_Java.json +++ b/.github/trigger_files/beam_PreCommit_Java.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 1 + "modification": 9 } 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/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/.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/.github/trigger_files/beam_PreCommit_SQL.json b/.github/trigger_files/beam_PreCommit_SQL.json index 07d1fb889961..ab4daeae2349 100644 --- a/.github/trigger_files/beam_PreCommit_SQL.json +++ b/.github/trigger_files/beam_PreCommit_SQL.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 0 + "modification": 3 } 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/IO_Iceberg_Integration_Tests.yml b/.github/workflows/IO_Iceberg_Integration_Tests.yml index 6341c51124c5..8780bfee5b3a 100644 --- a/.github/workflows/IO_Iceberg_Integration_Tests.yml +++ b/.github/workflows/IO_Iceberg_Integration_Tests.yml @@ -63,7 +63,9 @@ jobs: job_name: ["IO_Iceberg_Integration_Tests"] job_phrase: ["Run IcebergIO Integration Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -75,4 +77,4 @@ jobs: - name: Run IcebergIO Integration Test uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:java:io:iceberg:integrationTest --info \ No newline at end of file + gradle-command: :sdks:java:io:iceberg:integrationTest --info diff --git a/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml b/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml index 38a3a2514855..3ec2845d7704 100644 --- a/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml +++ b/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -75,4 +77,4 @@ jobs: - name: Run IcebergIO Integration Tests on Dataflow uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:java:io:iceberg:dataflowIntegrationTest --info \ No newline at end of file + gradle-command: :sdks:java:io:iceberg:dataflowIntegrationTest --info diff --git a/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml b/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml index ba72a85204a1..4cf38c3ff3fd 100644 --- a/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml +++ b/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -78,4 +80,4 @@ jobs: gradle-command: :sdks:java:io:iceberg:dataflowIntegrationTest arguments: | --info \ - -PenableManagedTransforms \ No newline at end of file + -PenableManagedTransforms diff --git a/.github/workflows/IO_Iceberg_Performance_Tests.yml b/.github/workflows/IO_Iceberg_Performance_Tests.yml index e3ec8b569012..3cb9c24c26b6 100644 --- a/.github/workflows/IO_Iceberg_Performance_Tests.yml +++ b/.github/workflows/IO_Iceberg_Performance_Tests.yml @@ -63,7 +63,9 @@ jobs: job_name: ["IO_Iceberg_Performance_Tests"] job_phrase: ["Run IcebergIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -75,4 +77,4 @@ jobs: - name: Run IcebergIO Performance Test uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:java:io:iceberg:loadTest \ No newline at end of file + gradle-command: :sdks:java:io:iceberg:loadTest diff --git a/.github/workflows/IO_Iceberg_Unit_Tests.yml b/.github/workflows/IO_Iceberg_Unit_Tests.yml index dd8069c74af0..f3fd63a9d479 100644 --- a/.github/workflows/IO_Iceberg_Unit_Tests.yml +++ b/.github/workflows/IO_Iceberg_Unit_Tests.yml @@ -82,7 +82,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/README.md b/.github/workflows/README.md index e70d5e17d7a7..4fd8c8d36a61 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -58,6 +58,7 @@ On top of normal Actions workflow steps, all new CI workflows (excluding release 4) A set of GitHub token permissions 5) Concurrency Groups 6) Comment Triggering Support +7) Allowed Actions (See below) Each of these is described in more detail below. ## Name and Phrase @@ -165,6 +166,14 @@ In order to make it easier for non-committers to interact with workflows, workfl **Note:** Comment triggering is found not scalable ([#28909](https://github.com/apache/beam/issues/28909)) and is currently limited to a subset of suites. For more information see the [Running Workflows Manually](#running-workflows-manually) section. +## Allowed Actions + +GitHub Actions under the trusted namespaces `actions/*`, `github/*`, and `apache/*` are automatically approved. Any third-party actions outside of these namespaces must be explicitly approved and added to the ASF-wide allowlist before use. + +Additionally, third-party actions must be pinned to a specific Git commit SHA (e.g., `zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02`) instead of a version tag or branch name to comply with ASF security policy. + +For details on the allowlist and how to request new actions, see the [ASF Infrastructure Actions Allowlist Check Documentation](https://github.com/apache/infrastructure-actions/blob/main/allowlist-check/README.md). + # Testing new workflows or workflow updates ## Testing New Workflows @@ -237,10 +246,11 @@ 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) | -| [ PreCommit Java Examples Dataflow Java21 ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml) | N/A | `Run Java_Examples_Dataflow_Java21 PreCommit` | [![.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml?query=event%3Aschedule) | +| [ PreCommit Java Examples Dataflow Java11 ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow_Java11.yml) | N/A | `Run Java_Examples_Dataflow_Java11 PreCommit` | [![.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java11.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow_Java11.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow_Java11.yml?query=event%3Aschedule) | | [ PreCommit Java File-schema-transform IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml) | N/A |`Run Java_File-schema-transform_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Flink Versions ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Flink_Versions.yml) | N/A |`Run Java_Flink_Versions PreCommit`| [![.github/workflows/beam_PreCommit_Java_Flink_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Flink_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Flink_Versions.yml?query=event%3Aschedule) | | [ PreCommit Java GCP IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml) | N/A |`Run Java_GCP_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml?query=event%3Aschedule) | @@ -330,6 +340,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ 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) | | [ PostCommit Java Dataflow V1 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_DataflowV1.yml) | N/A |`beam_PostCommit_Java_DataflowV1.json`| [![.github/workflows/beam_PostCommit_Java_DataflowV1.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_DataflowV1.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_DataflowV1.yml?query=event%3Aschedule) | | [ PostCommit Java Dataflow V2 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_DataflowV2.yml) | N/A |`beam_PostCommit_Java_DataflowV2.json`| [![.github/workflows/beam_PostCommit_Java_DataflowV2.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_DataflowV2.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_DataflowV2.yml?query=event%3Aschedule) | +| [ PostCommit Java Delta IO Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Delta_IO_Dataflow.yml) | N/A |`beam_PostCommit_Java_Delta_IO_Dataflow.json`| [![.github/workflows/beam_PostCommit_Java_Delta_IO_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Delta_IO_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Delta_IO_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Java Examples Dataflow ARM ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml) | ['8','11','17','21','25'] |`beam_PostCommit_Java_Examples_Dataflow_ARM.json`| [![.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml?query=event%3Aschedule) | | [ PostCommit Java Examples Dataflow](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow.yml) | N/A |`beam_PostCommit_Java_Examples_Dataflow.json`| [![.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Java Examples Dataflow Java ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml) | ['8','17','21','25'] |`beam_PostCommit_Java_Examples_Dataflow_Java.json`| [![.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml?query=event%3Aschedule) | @@ -414,6 +425,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 @@ -533,6 +545,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) | @@ -543,3 +556,5 @@ 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) | +| [ Upgrade GCP Libraries BOM ](https://github.com/apache/beam/actions/workflows/beam_Upgrade_GCP_BOM.yml) | N/A | [![.github/workflows/beam_Upgrade_GCP_BOM.yml](https://github.com/apache/beam/actions/workflows/beam_Upgrade_GCP_BOM.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Upgrade_GCP_BOM.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/assign_milestone.yml b/.github/workflows/assign_milestone.yml index 542d2a551f64..9cb72d4c13dd 100644 --- a/.github/workflows/assign_milestone.yml +++ b/.github/workflows/assign_milestone.yml @@ -31,9 +31,10 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 2 + persist-credentials: false - uses: actions/github-script@v9 with: diff --git a/.github/workflows/beam_CancelStaleDataflowJobs.yml b/.github/workflows/beam_CancelStaleDataflowJobs.yml index 1328cf080c8c..24a8219b1d5b 100644 --- a/.github/workflows/beam_CancelStaleDataflowJobs.yml +++ b/.github/workflows/beam_CancelStaleDataflowJobs.yml @@ -62,7 +62,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Cancel Stale Dataflow Jobs' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..c7004b1251cf 100644 --- a/.github/workflows/beam_CleanUpDataprocResources.yml +++ b/.github/workflows/beam_CleanUpDataprocResources.yml @@ -55,7 +55,9 @@ jobs: timeout-minutes: 100 name: "beam_CleanUpDataprocResources" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 + cd ${{ github.workspace }}/.test-infra/dataproc; ./cleanup.sh -xe diff --git a/.github/workflows/beam_CleanUpGCPResources.yml b/.github/workflows/beam_CleanUpGCPResources.yml index d148b1e00b8f..fccdc666a390 100644 --- a/.github/workflows/beam_CleanUpGCPResources.yml +++ b/.github/workflows/beam_CleanUpGCPResources.yml @@ -62,7 +62,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Clean GCP Resources' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..09f1941de838 100644 --- a/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml +++ b/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml @@ -62,7 +62,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Clean Prebuilt Images' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,4 +82,4 @@ jobs: - name: run remove stale sdk container images uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :beam-test-tools:removeStaleSDKContainerImages \ No newline at end of file + gradle-command: :beam-test-tools:removeStaleSDKContainerImages diff --git a/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml b/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml index 69ec18d225fc..1a7747cfc2f3 100644 --- a/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_CloudML_Benchmarks_Dataflow"] job_phrase: ["Run TFT Criteo Benchmarks"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..68bf18fd371c 100644 --- a/.github/workflows/beam_IODatastoresCredentialsRotation.yml +++ b/.github/workflows/beam_IODatastoresCredentialsRotation.yml @@ -59,7 +59,9 @@ jobs: job_name: ["beam_IODatastoresCredentialsRotation"] job_phrase: ["N/A"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 61de2b54e730..fb7fecd2c01f 100644 --- a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml @@ -55,14 +55,16 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Inference Benchmarks' runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 1000 + timeout-minutes: 3000 name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) strategy: matrix: job_name: ["beam_Inference_Python_Benchmarks_Dataflow"] job_phrase: ["Run Inference Benchmarks"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,6 +96,14 @@ 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_Pytorch_Image_Classification_Rightfit.txt + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Object_Detection.txt + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Captioning.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 + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_GPU_Batch.txt + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Text_Embedding_Batch.txt + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_CPU_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 @@ -191,7 +201,7 @@ jobs: -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ -PloadTest.requirementsTxtFile=apache_beam/ml/inference/torch_tests_requirements.txt \ - '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_5 }} --job_name=benchmark-tests-pytorch-imagenet-python-gpu-${{env.NOW_UTC}} --output=gs://temp-storage-for-end-to-end-tests/torch/result_resnet152_gpu-${{env.NOW_UTC}}.txt' + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_5 }} --job_name=benchmark-tests-pytorch-imagenet-python-gpu-${{env.NOW_UTC}} --output=gs://temp-storage-for-end-to-end-tests/torch/result_resnet152_gpu-${{env.NOW_UTC}}.txt' \ - name: run Table Row Inference Sklearn Batch uses: ./.github/actions/gradle-command-self-hosted-action timeout-minutes: 180 @@ -202,7 +212,7 @@ jobs: -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ -PloadTest.requirementsTxtFile=apache_beam/ml/inference/table_row_inference_requirements.txt \ - '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_9 }} --autoscaling_algorithm=NONE --metrics_table=result_table_row_inference_batch --influx_measurement=result_table_row_inference_batch --mode=batch --input_file=gs://apache-beam-ml/testing/inputs/table_rows_100k_benchmark.jsonl --input_expand_factor=100 --output_table=apache-beam-testing:beam_run_inference.result_table_row_inference_batch_outputs --job_name=benchmark-tests-table-row-inference-batch-${{env.NOW_UTC}}' + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_9 }} --autoscaling_algorithm=NONE --metrics_table=result_table_row_inference_batch --influx_measurement=result_table_row_inference_batch --mode=batch --input_file=gs://apache-beam-ml/testing/inputs/table_rows_100k_benchmark.jsonl --input_expand_factor=100 --output_table=apache-beam-testing:beam_run_inference.result_table_row_inference_batch_outputs --job_name=benchmark-tests-table-row-inference-batch-${{env.NOW_UTC}}' \ - name: run Table Row Inference Sklearn Stream uses: ./.github/actions/gradle-command-self-hosted-action timeout-minutes: 180 @@ -213,4 +223,191 @@ jobs: -Prunner=DataflowRunner \ -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}}' + '-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 PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch CPU + 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.pytorch_image_object_detection_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --device=CPU --mode=batch --job_name=benchmark-tests-pytorch-image-object-detection-batch-${{env.NOW_UTC}} --metrics_table=torch_inference_image_object_detection_batch_cpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_object_detection_batch_cpu' \ + - name: run PyTorch Image Captioning BLIP + CLIP Batch CPU + 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.pytorch_image_captioning_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }} --device=CPU --mode=batch --job_name=benchmark-tests-pytorch-image-captioning-batch-${{env.NOW_UTC}} --metrics_table=torch_inference_image_captioning_batch_cpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_captioning_batch_cpu' + - name: run PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch GPU + 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.pytorch_image_object_detection_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --device=GPU --worker_accelerator=type=nvidia-tesla-t4,count=1,install-nvidia-driver=true --mode=batch --job_name=benchmark-tests-pytorch-image-object-detection-batch-${{env.NOW_UTC}} --metrics_table=torch_inference_image_object_detection_batch_gpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_object_detection_batch_gpu' \ + - name: run PyTorch Image Captioning BLIP + CLIP Batch GPU + 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.pytorch_image_captioning_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }} --device=GPU --worker_accelerator=type=nvidia-tesla-t4,count=1,install-nvidia-driver=true --mode=batch --job_name=benchmark-tests-pytorch-image-captioning-batch-${{env.NOW_UTC}} --metrics_table=torch_inference_image_captioning_batch_gpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_captioning_batch_gpu' \ + - name: run PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming CPU + 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.pytorch_image_object_detection_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --device=CPU --mode=streaming --job_name=benchmark-tests-pytorch-image-object-detection-streaming-${{env.NOW_UTC}} --metrics_table=torch_inference_image_object_detection_streaming_cpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_object_detection_streaming_cpu' \ + - name: run PyTorch Image Captioning BLIP + CLIP Streaming CPU + 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.pytorch_image_captioning_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }} --device=CPU --mode=streaming --job_name=benchmark-tests-pytorch-image-captioning-streaming-${{env.NOW_UTC}} --metrics_table=torch_inference_image_captioning_streaming_cpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_captioning_streaming_cpu' + - name: run PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming GPU + 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.pytorch_image_object_detection_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --device=GPU --worker_accelerator=type=nvidia-tesla-t4,count=1,install-nvidia-driver=true --mode=streaming --job_name=benchmark-tests-pytorch-image-object-detection-streaming-${{env.NOW_UTC}} --metrics_table=torch_inference_image_object_detection_streaming_gpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_object_detection_streaming_gpu' \ + - name: run PyTorch Image Captioning BLIP + CLIP Streaming GPU + 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.pytorch_image_captioning_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }} --device=GPU --worker_accelerator=type=nvidia-tesla-t4,count=1,install-nvidia-driver=true --mode=streaming --job_name=benchmark-tests-pytorch-image-captioning-streaming-${{env.NOW_UTC}} --metrics_table=torch_inference_image_captioning_streaming_gpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_captioning_streaming_gpu' \ + - name: run PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) CPU + 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.pytorch_imagenet_rightfit_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_rightfit_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --device=CPU --experiments=enable_streaming_rightfitting --mode=streaming --job_name=benchmark-tests-pytorch-imagenet-rightfit-streaming-${{env.NOW_UTC}} --metrics_table=torch_inference_imagenet_stream_rightfit_cpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_imagenet_stream_rightfit_cpu' \ + - name: run PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting Exactly-once) CPU + 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.pytorch_imagenet_rightfit_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_rightfit_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --device=CPU --experiments=enable_streaming_rightfitting --mode=streaming --job_name=benchmark-tests-pytorch-imagenet-rightfit-streaming-${{env.NOW_UTC}} --metrics_table=torch_inference_imagenet_stream_rightfit_once_cpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_imagenet_stream_rightfit_once_cpu' \ + - name: run PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) GPU + 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.pytorch_imagenet_rightfit_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_rightfit_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --device=GPU --experiments=enable_streaming_rightfitting,worker_accelerator=type:nvidia-tesla-t4;count:1;install-nvidia-driver:5xx --mode=streaming --job_name=benchmark-tests-pytorch-imagenet-rightfit-streaming-${{env.NOW_UTC}} --metrics_table=torch_inference_imagenet_stream_rightfit_gpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_imagenet_stream_rightfit_gpu' \ + - name: run PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting Exactly-once) GPU + 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.pytorch_imagenet_rightfit_benchmarks \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_rightfit_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --device=GPU --experiments=enable_streaming_rightfitting,worker_accelerator=type:nvidia-tesla-t4;count:1;install-nvidia-driver:5xx --mode=streaming --job_name=benchmark-tests-pytorch-imagenet-rightfit-streaming-${{env.NOW_UTC}} --metrics_table=torch_inference_imagenet_stream_rightfit_once_gpu --output_table=apache-beam-testing.beam_run_inference.result_torch_inference_imagenet_stream_rightfit_once_gpu' \ + - 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_14 }} --job_name=benchmark-tests-mltransform-generate-vocab-batch-${{env.NOW_UTC}} --artifact_location=gs://temp-storage-for-perf-tests/mltransform/vocab_artifacts_${{env.NOW_UTC}} --output_vocab=gs://temp-storage-for-perf-tests/mltransform/vocab_outputs/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 \ + -PloadTest.requirementsTxtFile=apache_beam/examples/ml_transform/mltransform_one_hot_encoding_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_15 }} --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}}' + - name: run MLTransform Image Embedding GPU 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_image_embedding_benchmark \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_16 }} --artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_image_embedding_gpu/artifacts-${{env.NOW_UTC}} --job_name=benchmark-tests-mltransform-image-embedding-gpu-batch-${{env.NOW_UTC}}' + - name: run MLTransform Text Embedding 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_text_embedding_benchmark \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_17 }} --output=gs://temp-storage-for-perf-tests/loadtests/mltransform_text_embedding/results-${{env.NOW_UTC}} --artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_text_embedding/artifacts-${{env.NOW_UTC}} --job_name=benchmark-tests-mltransform-text-embedding-batch-${{env.NOW_UTC}}' + - name: run MLTransform Image Embedding CPU 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_image_embedding_benchmark \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_18 }} --artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_image_embedding_cpu/artifacts-${{env.NOW_UTC}} --job_name=benchmark-tests-mltransform-image-embedding-cpu-batch-${{env.NOW_UTC}}' diff --git a/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml b/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml new file mode 100644 index 000000000000..37d6ecbd362c --- /dev/null +++ b/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml @@ -0,0 +1,76 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT 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@v7 + with: + persist-credentials: true + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db + + - name: Setup Python + uses: actions/setup-python@v7 + 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 }} + SMTP_SERVER: smtp.gmail.com + SMTP_PORT: 465 + EMAIL_ADDRESS: ${{ secrets.ISSUE_REPORT_SENDER_EMAIL_ADDRESS }} + EMAIL_PASSWORD: ${{ secrets.ISSUE_REPORT_SENDER_EMAIL_PASSWORD }} + EMAIL_RECIPIENT: "dev@beam.apache.org" + run: python account_keys.py --action announce + + + + diff --git a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml index 75e5c00cb767..52f851d2cdaa 100644 --- a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml +++ b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml @@ -42,10 +42,12 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: '3.13' diff --git a/.github/workflows/beam_Infrastructure_SecurityLogging.yml b/.github/workflows/beam_Infrastructure_SecurityLogging.yml index ea0a33a6d108..fa5bcb347663 100644 --- a/.github/workflows/beam_Infrastructure_SecurityLogging.yml +++ b/.github/workflows/beam_Infrastructure_SecurityLogging.yml @@ -44,10 +44,12 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: '3.13' diff --git a/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml b/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml index b133a3543dfb..2520e6bf77b2 100644 --- a/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml +++ b/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml @@ -48,12 +48,14 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup gcloud uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: '3.13' diff --git a/.github/workflows/beam_Infrastructure_UsersPermissions.yml b/.github/workflows/beam_Infrastructure_UsersPermissions.yml index b084eec2a705..b188e5ff875b 100644 --- a/.github/workflows/beam_Infrastructure_UsersPermissions.yml +++ b/.github/workflows/beam_Infrastructure_UsersPermissions.yml @@ -45,13 +45,14 @@ 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 }} + persist-credentials: false - name: Setup gcloud uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Install Terraform - uses: hashicorp/setup-terraform@v4 + uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 with: terraform_version: 1.12.2 - name: Initialize Terraform diff --git a/.github/workflows/beam_Java_JMH.yml b/.github/workflows/beam_Java_JMH.yml index 68fcf0e91789..cd20a3c9e1ed 100644 --- a/.github/workflows/beam_Java_JMH.yml +++ b/.github/workflows/beam_Java_JMH.yml @@ -62,7 +62,9 @@ jobs: timeout-minutes: 900 name: "beam_Java_JMH" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup environment uses: ./.github/actions/setup-environment-action - name: run the Java JMH micro-benchmark harness suite @@ -72,4 +74,4 @@ jobs: - name: run the Java JMH micro-benchmark core suite uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:java:core:jmh:jmh \ No newline at end of file + gradle-command: :sdks:java:core:jmh:jmh diff --git a/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml b/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml index 56b36dfdcb64..c62264966832 100644 --- a/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml +++ b/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,4 +106,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CombineLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_Java_LoadTests_Combine_Smoke_test_arguments_3 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_Java_LoadTests_Combine_Smoke_test_arguments_3 }}' diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml index c682a0d359b9..c55cee5267c7 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -114,4 +116,4 @@ jobs: arguments: | -PloadTest.mainClass=cogbk \ -Prunner=DataflowRunner \ - '-PloadTest.args=${{ env.beam_LoadTests_Go_CoGBK_Dataflow_Batch_test_arguments_4 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Go_CoGBK_Dataflow_Batch_test_arguments_4 }}' diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml index eb5d44521229..f59ad9900e55 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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 + FLINK_TASKMANAGER_SLOTS: 2 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-cogbk-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - 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..ff215c901d61 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -106,4 +108,4 @@ jobs: arguments: | -PloadTest.mainClass=combine \ -Prunner=DataflowRunner \ - '-PloadTest.args=${{ env.beam_LoadTests_Go_Combine_Dataflow_Batch_test_arguments_3 }} --job_name=load-tests-go-dataflow-batch-combine-3-${{env.NOW_UTC}}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Go_Combine_Dataflow_Batch_test_arguments_3 }} --job_name=load-tests-go-dataflow-batch-combine-3-${{env.NOW_UTC}}' diff --git a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml index 4978cd778275..03699c96ef55 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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 + FLINK_TASKMANAGER_SLOTS: 2 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-combine-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -109,11 +109,11 @@ jobs: -PloadTest.mainClass=combine \ -Prunner=FlinkRunner \ '-PloadTest.args=${{ env.beam_LoadTests_Go_Combine_Flink_Batch_test_arguments_1 }}' \ - - name: Restart Flink with parallelism 16 + - name: Resize Flink to 10 workers env: - FLINK_NUM_WORKERS: 16 + FLINK_NUM_WORKERS: 10 run: | - cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh restart + cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh resize - name: run Combine Flink Batch Go Load Test 2 (fanout 4) timeout-minutes: 240 uses: ./.github/actions/gradle-command-self-hosted-action diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml index aaddaaee7e0c..c52389e84fd2 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -142,4 +144,4 @@ jobs: arguments: | -PloadTest.mainClass=group_by_key \ -Prunner=DataflowRunner \ - '-PloadTest.args=${{ env.beam_LoadTests_Go_GBK_Dataflow_Batch_test_arguments_7 }} --job_name=load-tests-go-dataflow-batch-gbk-7-${{env.NOW_UTC}}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Go_GBK_Dataflow_Batch_test_arguments_7 }} --job_name=load-tests-go-dataflow-batch-gbk-7-${{env.NOW_UTC}}' diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml index 74181f1708da..731c5b21274a 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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 + FLINK_TASKMANAGER_SLOTS: 2 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-gbk-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -139,11 +139,11 @@ jobs: -PloadTest.mainClass=group_by_key \ -Prunner=FlinkRunner \ '-PloadTest.args=${{ env.beam_LoadTests_Go_GBK_Flink_Batch_test_arguments_6 }}' \ - - name: Restart Flink with parallelism 16 + - name: Resize Flink to 10 workers env: - FLINK_NUM_WORKERS: 16 + FLINK_NUM_WORKERS: 10 run: | - cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh restart + cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh resize - name: run GBK Flink Batch Go Load Test 4 (fanout 4) timeout-minutes: 120 uses: ./.github/actions/gradle-command-self-hosted-action diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml index 96b2af740c38..f8643238e4e9 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -114,4 +116,4 @@ jobs: arguments: | -PloadTest.mainClass=pardo \ -Prunner=DataflowRunner \ - '-PloadTest.args=${{ env.beam_LoadTests_Go_ParDo_Dataflow_Batch_test_arguments_4 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Go_ParDo_Dataflow_Batch_test_arguments_4 }}' diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml index 1055b18b119a..fe112061c648 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-pardo-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - 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..59bf9bd952b8 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -96,4 +98,4 @@ jobs: arguments: | -PloadTest.mainClass=sideinput \ -Prunner=DataflowRunner \ - '-PloadTest.args=${{ env.beam_LoadTests_Go_SideInput_Dataflow_Batch_test_arguments_2 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Go_SideInput_Dataflow_Batch_test_arguments_2 }}' diff --git a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml index 1c63c632ee19..f18604b590b1 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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 + FLINK_TASKMANAGER_SLOTS: 2 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-sideinput-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - 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..2be67a5098a6 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -113,4 +115,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CoGroupByKeyLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_Dataflow_Batch_test_arguments_4 }} --appName=load_tests_Java_Dataflow_batch_CoGBK_4' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_Dataflow_Batch_test_arguments_4 }} --appName=load_tests_Java_Dataflow_batch_CoGBK_4' diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml index 76cb31ae5ada..cd4e3c440242 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -125,4 +127,4 @@ jobs: if: always() with: files: '**/build/test-results/**/*.xml' - large_files: true \ No newline at end of file + large_files: true 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..5766d13483f4 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,9 @@ jobs: job_phrase_2: ["CoGBK Dataflow V2 Batch"] java_version: ['11','17'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -129,4 +131,4 @@ jobs: -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CoGroupByKeyLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions_test_arguments_4 }} --appName=load_tests_Java${{ matrix.java_version }}_Dataflow_V2_batch_CoGBK_4' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions_test_arguments_4 }} --appName=load_tests_Java${{ matrix.java_version }}_Dataflow_V2_batch_CoGBK_4' 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..597d3c8c9bd6 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,9 @@ jobs: job_phrase_2: ["CoGBK Dataflow V2 Streaming"] java_version: ['11','17'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -129,4 +131,4 @@ jobs: -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CoGroupByKeyLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions_test_arguments_4 }} --appName=load_tests_Java${{ matrix.java_version }}_Dataflow_V2_streaming_CoGBK_4' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions_test_arguments_4 }} --appName=load_tests_Java${{ matrix.java_version }}_Dataflow_V2_streaming_CoGBK_4' diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml index 7501c70b7ab6..026bb08d098c 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -113,4 +115,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CoGroupByKeyLoadTest \ -Prunner=:runners:spark:3 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch_test_arguments_4 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch_test_arguments_4 }}' diff --git a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml index 0de3f585e703..0267d77c8afa 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,4 +105,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CombineLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_Combine_Dataflow_Batch_test_arguments_3 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_Combine_Dataflow_Batch_test_arguments_3 }}' diff --git a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml index 3651895a76d6..5107db698609 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,4 +106,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CombineLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_Combine_Dataflow_Streaming_test_arguments_3 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_Combine_Dataflow_Streaming_test_arguments_3 }}' diff --git a/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml index 4840de2bb84c..0e422d9b5f90 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,4 +106,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.CombineLoadTest \ -Prunner=:runners:spark:3 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch_test_arguments_3 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch_test_arguments_3 }}' diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml index bd662ac7f42e..df6b4c1c8dc2 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -140,4 +142,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.GroupByKeyLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_Batch_test_arguments_7 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_Batch_test_arguments_7 }}' diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml index 433d12a7a35a..c4c99566aefb 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -140,4 +142,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.GroupByKeyLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_Streaming_test_arguments_7 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_Streaming_test_arguments_7 }}' 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..ecfd3f5752d2 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -149,4 +151,4 @@ jobs: -PloadTest.mainClass=org.apache.beam.sdk.loadtests.GroupByKeyLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ -Prunner.version=V2 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_V2_Batch_test_arguments_7 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_V2_Batch_test_arguments_7 }}' 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..3057bfbd6695 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -165,4 +167,4 @@ jobs: -Prunner.version=V2 \ -PtestJavaVersion=17 \ -Pjava17Home=$JAVA_HOME_17_X64 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17_test_arguments_7 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17_test_arguments_7 }}' 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..090d500ee5d6 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -149,4 +151,4 @@ jobs: -PloadTest.mainClass=org.apache.beam.sdk.loadtests.GroupByKeyLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ -Prunner.version=V2 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_test_arguments_7 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_test_arguments_7 }}' 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..61fe5eb6d6e6 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -165,4 +167,4 @@ jobs: -Prunner.version=V2 \ -PtestJavaVersion=17 \ -Pjava17Home=$JAVA_HOME_17_X64 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17_test_arguments_7 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17_test_arguments_7 }}' diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml b/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml index 9f3ab971c88e..6a26957ef933 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml @@ -59,7 +59,9 @@ 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 + with: + persist-credentials: false - 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..4f6eb9d66db7 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -140,4 +142,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.GroupByKeyLoadTest \ -Prunner=:runners:spark:3 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch_test_arguments_7 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch_test_arguments_7 }}' diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml index 47046f000a0f..6ba3e4b884a1 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -113,4 +115,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.ParDoLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_Dataflow_Batch_test_arguments_4 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_Dataflow_Batch_test_arguments_4 }}' diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml index 37bcb095c126..2ef48d7aaf67 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -113,4 +115,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.ParDoLoadTest \ -Prunner=:runners:google-cloud-dataflow-java \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_Dataflow_Streaming_test_arguments_4 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_Dataflow_Streaming_test_arguments_4 }}' 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..bcc43739d938 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,9 @@ jobs: job_phrase_2: ["ParDo Dataflow V2 Batch"] java_version: ['11','17'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -133,4 +135,4 @@ jobs: -Prunner.version=V2 \ -PtestJavaVersion=${{ matrix.java_version }} \ -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions_test_arguments_4 }} --appName=load_tests_Java${{ matrix.java_version }}_Dataflow_V2_batch_ParDo_4' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions_test_arguments_4 }} --appName=load_tests_Java${{ matrix.java_version }}_Dataflow_V2_batch_ParDo_4' 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..15ee079dcad7 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,9 @@ jobs: job_phrase_2: ["ParDo Dataflow V2 Streaming"] java_version: ['11','17'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -133,4 +135,4 @@ jobs: -Prunner.version=V2 \ -PtestJavaVersion=${{ matrix.java_version }} \ -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions_test_arguments_4 }} --appName=load_tests_Java${{ matrix.java_version }}_Dataflow_V2_streaming_ParDo_4' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions_test_arguments_4 }} --appName=load_tests_Java${{ matrix.java_version }}_Dataflow_V2_streaming_ParDo_4' diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml index d2a5a0748a00..95d7bbf46c9f 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -113,4 +115,4 @@ jobs: arguments: | -PloadTest.mainClass=org.apache.beam.sdk.loadtests.ParDoLoadTest \ -Prunner=:runners:spark:3 \ - '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch_test_arguments_4 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch_test_arguments_4 }}' diff --git a/.github/workflows/beam_LoadTests_Java_PubsubIO.yml b/.github/workflows/beam_LoadTests_Java_PubsubIO.yml index 6267bf959fde..6aa9b0b4982d 100644 --- a/.github/workflows/beam_LoadTests_Java_PubsubIO.yml +++ b/.github/workflows/beam_LoadTests_Java_PubsubIO.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_LoadTests_Java_PubsubIO"] job_phrase: ["Run Load Tests Java PubsubIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,4 +76,4 @@ jobs: - name: run PubSub Performance test uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :it:google-cloud-platform:PubsubLoadTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_load_test_pubsub" \ No newline at end of file + gradle-command: :it:google-cloud-platform:PubsubLoadTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_load_test_pubsub" diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml index 16b53af2f698..d72781e0b88c 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -124,4 +126,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.co_group_by_key_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_CoGBK_Dataflow_Batch_test_arguments_4 }} --job_name=load-tests-python-dataflow-batch-cogbk-4-${{ steps.datetime.outputs.datetime }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_CoGBK_Dataflow_Batch_test_arguments_4 }} --job_name=load-tests-python-dataflow-batch-cogbk-4-${{ steps.datetime.outputs.datetime }}' diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml index 85364a2964da..45d75c85a834 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -123,4 +125,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.co_group_by_key_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_CoGBK_Dataflow_Streaming_test_arguments_4 }} --job_name=load-tests-python-dataflow-streaming-cogbk-4-${{ steps.datetime.outputs.datetime }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_CoGBK_Dataflow_Streaming_test_arguments_4 }} --job_name=load-tests-python-dataflow-streaming-cogbk-4-${{ steps.datetime.outputs.datetime }}' diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml index 429eed69f182..6967500335eb 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-cogbk-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - 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..4a2fdfcadc51 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,4 +113,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.combine_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_Combine_Dataflow_Batch_test_arguments_3 }} --job_name=load-tests-python-dataflow-batch-combine-3-${{env.NOW_UTC}}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_Combine_Dataflow_Batch_test_arguments_3 }} --job_name=load-tests-python-dataflow-batch-combine-3-${{env.NOW_UTC}}' diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml index e275b14b9911..f41d58bf2175 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,4 +113,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.combine_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_Combine_Dataflow_Streaming_test_arguments_3 }} --job_name=load-tests-python-dataflow-streaming-combine-5-${{env.NOW_UTC}}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_Combine_Dataflow_Streaming_test_arguments_3 }} --job_name=load-tests-python-dataflow-streaming-combine-5-${{env.NOW_UTC}}' diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml index 17ac98ffd804..0fd5a819b19c 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-cmb-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,7 +99,7 @@ jobs: cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh create - name: get current time run: echo "NOW_UTC=$(date '+%m%d%H%M%S' --utc)" >> $GITHUB_ENV - # The env variables are created and populated in the test-arguments-action as "_test_arguments_" + # The env variables are created and populated in the test-arguments-action as "_test_arguments_" - name: run Load test 2GB 10 byte records env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} @@ -111,11 +111,11 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.combine_test \ -Prunner=PortableRunner \ '-PloadTest.args=${{ env.beam_LoadTests_Python_Combine_Flink_Batch_test_arguments_1 }} --job_name=load-tests-python-flink-batch-combine-1-${{env.NOW_UTC}}' \ - - name: Restart Flink with parallelism 16 + - name: Resize Flink to 10 workers env: - FLINK_NUM_WORKERS: 16 + FLINK_NUM_WORKERS: 10 run: | - cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh restart + cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh resize - name: run Load test 2GB Fanout 4 uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -137,4 +137,4 @@ jobs: - name: Teardown Flink if: always() run: | - ${{ github.workspace }}/.test-infra/dataproc/flink_cluster.sh delete \ No newline at end of file + ${{ github.workspace }}/.test-infra/dataproc/flink_cluster.sh delete diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml index 0ea3876371b2..1b6d691e4a69 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-cmb-flink-streaming-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - 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..60f8422aed0b 100644 --- a/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml +++ b/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -88,4 +90,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.microbenchmarks_test \ -Prunner=DirectRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_FnApiRunner_Microbenchmark_test_arguments_1 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_FnApiRunner_Microbenchmark_test_arguments_1 }}' diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml index 6597e32ff808..ed63e9650e78 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -131,4 +133,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.group_by_key_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_GBK_Dataflow_Batch_test_arguments_5 }} --job_name=load-tests-python-dataflow-batch-gbk-5-${{env.NOW_UTC}}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_GBK_Dataflow_Batch_test_arguments_5 }} --job_name=load-tests-python-dataflow-batch-gbk-5-${{env.NOW_UTC}}' diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml index d71c96c8b8a8..c553372c9c37 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -97,4 +99,4 @@ jobs: # load-tests-python-dataflow-streaming-gbk-1 # load-tests-python-dataflow-streaming-gbk-2 # load-tests-python-dataflow-streaming-gbk-4 - # load-tests-python-dataflow-streaming-gbk-5 \ No newline at end of file + # load-tests-python-dataflow-streaming-gbk-5 diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml index 7dfc9c2a5403..0ce42ed31ea1 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-gbk-flk-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -126,11 +126,11 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.group_by_key_test \ -Prunner=PortableRunner \ '-PloadTest.args=${{ env.beam_LoadTests_Python_GBK_Flink_Batch_test_arguments_5 }} --job_name=load-tests-python-flink-batch-gbk-6-${{env.NOW_UTC}}' \ - - name: Restart Flink with parallelism 16 + - name: Resize Flink to 10 workers env: - FLINK_NUM_WORKERS: 16 + FLINK_NUM_WORKERS: 10 run: | - cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh restart + cd ${{ github.workspace }}/.test-infra/dataproc; ./flink_cluster.sh resize - name: run fanout 4 times with 2GB 10-byte records total test uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -152,4 +152,4 @@ jobs: run: | ${{ github.workspace }}/.test-infra/dataproc/flink_cluster.sh delete - # TODO(https://github.com/apache/beam/issues/20146) Re-enable auto builds after these tests pass. \ No newline at end of file + # TODO(https://github.com/apache/beam/issues/20146) Re-enable auto builds after these tests pass. 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..ce3aeb724281 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -101,4 +103,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.group_by_key_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch_test_arguments_2 }} --job_name=load-tests-python-dataflow-batch-gbk-7-${{env.NOW_UTC}}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch_test_arguments_2 }} --job_name=load-tests-python-dataflow-batch-gbk-7-${{env.NOW_UTC}}' 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..18476e5172fe 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,9 @@ 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 + with: + persist-credentials: false - 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..e0b5a0a3075b 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -123,4 +125,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.pardo_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_ParDo_Dataflow_Batch_test_arguments_4 }} --job_name=load-tests-python-dataflow-batch-pardo-4-${{ steps.datetime.outputs.datetime }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_ParDo_Dataflow_Batch_test_arguments_4 }} --job_name=load-tests-python-dataflow-batch-pardo-4-${{ steps.datetime.outputs.datetime }}' diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml index 8efde4fa36d6..be19a46d381c 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -123,4 +125,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.pardo_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_ParDo_Dataflow_Streaming_test_arguments_4 }} --job_name=load-tests-python-dataflow-streaming-pardo-4-${{ steps.datetime.outputs.datetime }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_ParDo_Dataflow_Streaming_test_arguments_4 }} --job_name=load-tests-python-dataflow-streaming-pardo-4-${{ steps.datetime.outputs.datetime }}' diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml index 06c3f8479f60..28834c4ac3a5 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-pardo-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -128,4 +128,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.pardo_test \ -Prunner=PortableRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_ParDo_Flink_Batch_test_arguments_3 }} --job_name=load-tests-python-flink-batch-pardo-4-${{ steps.datetime.outputs.datetime }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_ParDo_Flink_Batch_test_arguments_3 }} --job_name=load-tests-python-flink-batch-pardo-4-${{ steps.datetime.outputs.datetime }}' diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml index b63dc9596e70..4437eb8e159f 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml @@ -47,15 +47,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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.2 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-pardo-flink-stream-${{ github.run_id }} jobs: @@ -72,7 +70,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -152,4 +152,4 @@ jobs: - name: Teardown Flink if: always() run: | - ${{ github.workspace }}/.test-infra/dataproc/flink_cluster.sh delete \ No newline at end of file + ${{ github.workspace }}/.test-infra/dataproc/flink_cluster.sh delete diff --git a/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml index 2dc81f470e67..143ded560613 100644 --- a/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -183,4 +185,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.sideinput_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_SideInput_Dataflow_Batch_test_arguments_10 }} --job_name=load-tests-python-dataflow-batch-sideinput-10-${{ steps.datetime.outputs.datetime }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_SideInput_Dataflow_Batch_test_arguments_10 }} --job_name=load-tests-python-dataflow-batch-sideinput-10-${{ steps.datetime.outputs.datetime }}' diff --git a/.github/workflows/beam_LoadTests_Python_Smoke.yml b/.github/workflows/beam_LoadTests_Python_Smoke.yml index d8f22e4b7b5d..86638cfd6a85 100644 --- a/.github/workflows/beam_LoadTests_Python_Smoke.yml +++ b/.github/workflows/beam_LoadTests_Python_Smoke.yml @@ -59,7 +59,9 @@ jobs: job_name: ["beam_LoadTests_Python_Smoke"] job_phrase: ["Run Python Load Tests Smoke"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,4 +102,4 @@ jobs: -PloadTest.mainClass=apache_beam.testing.load_tests.group_by_key_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_LoadTests_Python_Smoke_test_arguments_2 }} --job_name=load-tests-python-dataflow-batch-gbk-smoke-${{ steps.datetime.outputs.datetime }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_LoadTests_Python_Smoke_test_arguments_2 }} --job_name=load-tests-python-dataflow-batch-gbk-smoke-${{ steps.datetime.outputs.datetime }}' diff --git a/.github/workflows/beam_MetricsCredentialsRotation.yml b/.github/workflows/beam_MetricsCredentialsRotation.yml index 3a8c8d424709..74da6fcc2964 100644 --- a/.github/workflows/beam_MetricsCredentialsRotation.yml +++ b/.github/workflows/beam_MetricsCredentialsRotation.yml @@ -59,7 +59,9 @@ jobs: job_name: ["beam_MetricsCredentialsRotation"] job_phrase: ["N/A"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..6c1ed828ff46 100644 --- a/.github/workflows/beam_Metrics_Report.yml +++ b/.github/workflows/beam_Metrics_Report.yml @@ -61,7 +61,9 @@ jobs: ) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..3d5816f432db 100644 --- a/.github/workflows/beam_PerformanceTests_AvroIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_AvroIOIT.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_AvroIOIT"] job_phrase: ["Run Java AvroIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..f7e326d744bf 100644 --- a/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_AvroIOIT_HDFS"] job_phrase: ["Run Java AvroIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..71b9405c233f 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,4 +105,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml index b7b933e5bb6f..53e0d26c2e43 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,4 +105,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml index 5c414c4a94fd..690a126635a3 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,4 +105,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml index 58de7f6dfeac..14ad834b970f 100644 --- a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml +++ b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: -PloadTest.mainClass=apache_beam.io.gcp.bigquery_read_perf_test \ -PpythonVersion=3.10 \ -Prunner=DataflowRunner \ - '-PloadTest.args=${{env.beam_PerformanceTests_BiqQueryIO_Read_Python_test_arguments_1}}' \ No newline at end of file + '-PloadTest.args=${{env.beam_PerformanceTests_BiqQueryIO_Read_Python_test_arguments_1}}' diff --git a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml index d7cb776ae265..b157b566784f 100644 --- a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml +++ b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: -PloadTest.mainClass=apache_beam.io.gcp.bigquery_write_perf_test \ -PpythonVersion=3.10 \ -Prunner=DataflowRunner \ - '-PloadTest.args=${{env.beam_PerformanceTests_BiqQueryIO_Write_Python_Batch_test_arguments_1}}' \ No newline at end of file + '-PloadTest.args=${{env.beam_PerformanceTests_BiqQueryIO_Write_Python_Batch_test_arguments_1}}' diff --git a/.github/workflows/beam_PerformanceTests_Cdap.yml b/.github/workflows/beam_PerformanceTests_Cdap.yml index 94a43679470c..36da99d9f42b 100644 --- a/.github/workflows/beam_PerformanceTests_Cdap.yml +++ b/.github/workflows/beam_PerformanceTests_Cdap.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_Cdap"] job_phrase: ["Run Java CdapIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..9dcd9868fbff 100644 --- a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_Compressed_TextIOIT"] job_phrase: ["Run Java CompressedTextIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: --info \ -Dfilesystem=gcs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_Compressed_TextIOIT_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_Compressed_TextIOIT_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml index 3803d275b8a3..9343cbc6b478 100644 --- a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: --info \ -Dfilesystem=hdfs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_Compressed_TextIOIT_HDFS_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_Compressed_TextIOIT_HDFS_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_HadoopFormat.yml b/.github/workflows/beam_PerformanceTests_HadoopFormat.yml index dc6d5b6914dc..7e4ba8d23d36 100644 --- a/.github/workflows/beam_PerformanceTests_HadoopFormat.yml +++ b/.github/workflows/beam_PerformanceTests_HadoopFormat.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_HadoopFormat"] job_phrase: ["Run Java HadoopFormatIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..5c5e8d2400e2 100644 --- a/.github/workflows/beam_PerformanceTests_JDBC.yml +++ b/.github/workflows/beam_PerformanceTests_JDBC.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_JDBC"] job_phrase: ["Run Java JdbcIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: --info \ -Dfilesystem=hdfs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{env.beam_PerformanceTests_JDBC_test_arguments_1}}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{env.beam_PerformanceTests_JDBC_test_arguments_1}}]' diff --git a/.github/workflows/beam_PerformanceTests_Kafka_IO.yml b/.github/workflows/beam_PerformanceTests_Kafka_IO.yml index fa13bceb6d76..5434c383fddb 100644 --- a/.github/workflows/beam_PerformanceTests_Kafka_IO.yml +++ b/.github/workflows/beam_PerformanceTests_Kafka_IO.yml @@ -64,7 +64,9 @@ jobs: env: KAFKA_SERVICE_PORT: 32400 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -120,4 +122,4 @@ jobs: --tests org.apache.beam.sdk.io.kafka.KafkaIOIT.testKafkaIOReadsAndWritesCorrectlyInBatch \ --info \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{env.beam_PerformanceTests_Kafka_IO_test_arguments_2}}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{env.beam_PerformanceTests_Kafka_IO_test_arguments_2}}]' diff --git a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml index feb909c1d338..a41ba911410a 100644 --- a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_ManyFiles_TextIOIT"] job_phrase: ["Run Java ManyFilesTextIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: --info \ -Dfilesystem=gcs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_ManyFiles_TextIOIT_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_ManyFiles_TextIOIT_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml index 3699d1cbace4..6f7f4b2c340e 100644 --- a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: --info \ -Dfilesystem=hdfs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_ManyFiles_TextIOIT_HDFS_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_ManyFiles_TextIOIT_HDFS_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml b/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml index db2d786d09f5..296058d9fc9f 100644 --- a/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml +++ b/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_MongoDBIO_IT"] job_phrase: ["Run Java MongoDBIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..aa5833f94d3b 100644 --- a/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_ParquetIOIT"] job_phrase: ["Run Java ParquetIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: --info \ -Dfilesystem=gcs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_ParquetIOIT_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_ParquetIOIT_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml index af5b6680d6ea..ab2777129875 100644 --- a/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_ParquetIOIT_HDFS"] job_phrase: ["Run Java ParquetIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: --info \ -Dfilesystem=hdfs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_ParquetIOIT_HDFS_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_ParquetIOIT_HDFS_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml b/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml index 8ff05fb05f5d..72c9783790fe 100644 --- a/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml +++ b/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_PubsubIOIT_Python_Streaming"] job_phrase: ["Run PubsubIO Performance Test Python"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: -PloadTest.mainClass=apache_beam.io.gcp.pubsub_io_perf_test \ -Prunner=TestDataflowRunner \ -PpythonVersion=3.10 \ - '-PloadTest.args=${{ env.beam_PerformanceTests_PubsubIOIT_Python_Streaming_test_arguments_1 }}' \ No newline at end of file + '-PloadTest.args=${{ env.beam_PerformanceTests_PubsubIOIT_Python_Streaming_test_arguments_1 }}' diff --git a/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml b/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml index 8fb2c8710e09..bf573fa5f9a9 100644 --- a/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml +++ b/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml index 8c12533e9cc5..a444a88a1ea8 100644 --- a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml +++ b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml @@ -65,7 +65,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java SingleStoreIO Performance Test' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +85,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: | @@ -111,4 +113,4 @@ jobs: --tests org.apache.beam.sdk.io.singlestore.SingleStoreIOPerformanceIT \ --info \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_SingleStoreIO_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_SingleStoreIO_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml b/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml index 52c296652df3..e27ccd91c637 100644 --- a/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml +++ b/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml @@ -62,7 +62,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: -PloadTest.mainClass=apache_beam.io.gcp.experimental.spannerio_read_perf_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - -PloadTest.args='${{env.beam_PerformanceTests_SpannerIO_Read_2GB_Python_test_arguments_1}}' \ No newline at end of file + -PloadTest.args='${{env.beam_PerformanceTests_SpannerIO_Read_2GB_Python_test_arguments_1}}' 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..68ae38d8de49 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: -PloadTest.mainClass=apache_beam.io.gcp.experimental.spannerio_write_perf_test \ -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ - -PloadTest.args='${{env.beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch_test_arguments_1}}' \ No newline at end of file + -PloadTest.args='${{env.beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch_test_arguments_1}}' diff --git a/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml b/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml index f170f3ca4a3a..ade25aacde3b 100644 --- a/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml +++ b/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_SparkReceiver_IO"] job_phrase: ["Run Java SparkReceiverIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..b7db2577e9b9 100644 --- a/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_TFRecordIOIT"] job_phrase: ["Run Java TFRecordIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: --info \ -Dfilesystem=gcs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_TFRecordIOIT_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_TFRecordIOIT_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml index a2da2ca848e3..9808f89b4d44 100644 --- a/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml @@ -64,7 +64,9 @@ jobs: job_name: ["beam_PerformanceTests_TFRecordIOIT_HDFS"] job_phrase: ["Run Java TFRecordIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -105,4 +107,4 @@ jobs: --tests org.apache.beam.sdk.io.tfrecord.TFRecordIOIT \ -Dfilesystem=hdfs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_TFRecordIOIT_HDFS_test_arguments_1 }}]' \ \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_TFRecordIOIT_HDFS_test_arguments_1 }}]' \ diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_TextIOIT.yml index 538866170fa6..ebf57d7ba7dd 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT"] job_phrase: ["Run Java TextIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: --info \ -Dfilesystem=gcs \ -DintegrationTestRunner=dataflow \ - '-DintegrationTestPipelineOptions=[${{env.beam_PerformanceTests_TextIOIT_test_arguments_1}}]' \ No newline at end of file + '-DintegrationTestPipelineOptions=[${{env.beam_PerformanceTests_TextIOIT_test_arguments_1}}]' diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml index e4e4a607580a..a29d3ecb69a8 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT_HDFS"] job_phrase: ["Run Java TextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,4 +105,4 @@ jobs: --info \ -Dfilesystem=hdfs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{env.beam_PerformanceTests_TextIOIT_HDFS_test_arguments_1}}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{env.beam_PerformanceTests_TextIOIT_HDFS_test_arguments_1}}]' diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml b/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml index e396812b187c..8ab9e611b89b 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT_Python"] job_phrase: ["Run Python TextIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: -PpythonVersion=3.10 \ -PloadTest.mainClass=apache_beam.io.filebasedio_perf_test \ -Prunner=DataflowRunner \ - '-PloadTest.args=${{env.beam_PerformanceTests_TextIOIT_Python_test_arguments_1}}' \ No newline at end of file + '-PloadTest.args=${{env.beam_PerformanceTests_TextIOIT_Python_test_arguments_1}}' diff --git a/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml b/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml index ccbe286fc2f1..95339bfe2c1f 100644 --- a/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml +++ b/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml @@ -66,7 +66,9 @@ jobs: job_phrase_2: [WordCountIT Performance Test] python_version: ['3.10'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -116,4 +118,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PerformanceTests_XmlIOIT.yml b/.github/workflows/beam_PerformanceTests_XmlIOIT.yml index 6c3e3cdbdc99..472c832b94ea 100644 --- a/.github/workflows/beam_PerformanceTests_XmlIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_XmlIOIT.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_XmlIOIT"] job_phrase: ["Run Java XmlIO Performance Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: --info \ -Dfilesystem=gcs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_XmlIOIT_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_XmlIOIT_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml index 7a1f86f687fd..a685796c4f44 100644 --- a/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PerformanceTests_XmlIOIT_HDFS"] job_phrase: ["Run Java XmlIO Performance Test HDFS"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: --info \ -Dfilesystem=hdfs \ -DintegrationTestRunner=dataflow \ - -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_XmlIOIT_HDFS_test_arguments_1 }}]' \ No newline at end of file + -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_XmlIOIT_HDFS_test_arguments_1 }}]' diff --git a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml index 7ed3fce19a0e..5adb09f24624 100644 --- a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml +++ b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,6 +75,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: '17' python-version: default - name: Set k8s access uses: ./.github/actions/setup-k8s-access diff --git a/.github/workflows/beam_Playground_CI_Nightly.yml b/.github/workflows/beam_Playground_CI_Nightly.yml index fc79c6fda9bc..690e8c00b76b 100644 --- a/.github/workflows/beam_Playground_CI_Nightly.yml +++ b/.github/workflows/beam_Playground_CI_Nightly.yml @@ -61,7 +61,9 @@ jobs: sdk: ["python", "java", "go"] fail-fast: false steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -90,7 +92,7 @@ jobs: echo "container_id=$CONTAINER_ID" >> $GITHUB_ENV - name: Get Container IP run: | - CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${{ env.container_id }}) + CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${container_id}) echo "container_ip=$CONTAINER_IP" >> $GITHUB_ENV - name: Run CI env: diff --git a/.github/workflows/beam_Playground_Precommit.yml b/.github/workflows/beam_Playground_Precommit.yml index 379184fbd8a8..589cba367873 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] @@ -45,9 +46,11 @@ jobs: env: DATASTORE_EMULATOR_VERSION: '423.0.0' PYTHON_VERSION: '3.10' - JAVA_VERSION: '11' + JAVA_VERSION: '17' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 5b42821f3cd5..305a3995feec 100644 --- a/.github/workflows/beam_PostCommit_Go.yml +++ b/.github/workflows/beam_PostCommit_Go.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_Go"] job_phrase: ["Run Go PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,7 +75,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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io @@ -85,4 +87,4 @@ jobs: gradle-command: :goPostCommit arguments: | --no-parallel \ - -Pdocker-repository-root=us.gcr.io/apache-beam-testing/github-actions \ No newline at end of file + -Pdocker-repository-root=us.gcr.io/apache-beam-testing/github-actions diff --git a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml index d71ca934cdff..58c4775e192d 100644 --- a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml @@ -65,7 +65,9 @@ jobs: job_name: ["beam_PostCommit_Go_Dataflow_ARM"] job_phrase: ["Run Go PostCommit Dataflow ARM"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -78,7 +80,7 @@ jobs: java-version: default go-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_PostCommit_Go_VR_Flink.yml b/.github/workflows/beam_PostCommit_Go_VR_Flink.yml index 6ea7ee837716..5f9173fa275c 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Flink.yml +++ b/.github/workflows/beam_PostCommit_Go_VR_Flink.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_Go_VR_Flink"] job_phrase: ["Run Go Flink ValidatesRunner"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -77,4 +79,4 @@ jobs: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:go:test:flinkValidatesRunner \ No newline at end of file + gradle-command: :sdks:go:test:flinkValidatesRunner diff --git a/.github/workflows/beam_PostCommit_Go_VR_Spark.yml b/.github/workflows/beam_PostCommit_Go_VR_Spark.yml index abbded6c3bcf..63905b9483f6 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Spark.yml +++ b/.github/workflows/beam_PostCommit_Go_VR_Spark.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_Go_VR_Spark"] job_phrase: ["Run Go Spark ValidatesRunner"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -78,4 +80,4 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:go:test:sparkValidatesRunner - \ No newline at end of file + diff --git a/.github/workflows/beam_PostCommit_Java.yml b/.github/workflows/beam_PostCommit_Java.yml index 98c7a6bfae06..1afd320b84dd 100644 --- a/.github/workflows/beam_PostCommit_Java.yml +++ b/.github/workflows/beam_PostCommit_Java.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml b/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml index 4ca15d53aa20..f6362b48b41f 100644 --- a/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml b/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml index 61481b1cd8ea..1b604b2d3986 100644 --- a/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml +++ b/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - 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..05d4132b2535 100644 --- a/.github/workflows/beam_PostCommit_Java_DataflowV1.yml +++ b/.github/workflows/beam_PostCommit_Java_DataflowV1.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,4 +95,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_DataflowV2.yml b/.github/workflows/beam_PostCommit_Java_DataflowV2.yml index d87685a63c25..7e38e8ce1c55 100644 --- a/.github/workflows/beam_PostCommit_Java_DataflowV2.yml +++ b/.github/workflows/beam_PostCommit_Java_DataflowV2.yml @@ -54,7 +54,7 @@ jobs: beam_PostCommit_Java_DataflowV2: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 360 + timeout-minutes: 480 strategy: matrix: job_name: [beam_PostCommit_Java_DataflowV2] @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Delta_IO_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Delta_IO_Dataflow.yml new file mode 100644 index 000000000000..d2444795d3bf --- /dev/null +++ b/.github/workflows/beam_PostCommit_Java_Delta_IO_Dataflow.yml @@ -0,0 +1,89 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 Delta IO Dataflow + +on: + schedule: + - cron: '30 4/6 * * *' + pull_request_target: + paths: [ 'release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_Delta_IO_Dataflow.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_Delta_IO_Dataflow: + if: | + github.event_name == 'pull_request_target' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || + github.event.comment.body == 'Run PostCommit Java Delta IO Dataflow' + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 120 + name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) + strategy: + matrix: + job_name: ["beam_PostCommit_Java_Delta_IO_Dataflow"] + job_phrase: ["Run PostCommit Java Delta IO Dataflow"] + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - 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 + - name: Run PostCommit Java Delta IO Dataflow + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: :sdks:java:io:delta:dataflowIntegrationTest --info + - name: Run Delta IO S3 Integration Test Locally + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: :sdks:java:io:delta:integrationTest + arguments: | + --tests "org.apache.beam.sdk.io.delta.DeltaIOS3IT" \ + -PdisableSpotlessCheck=true \ + -PdisableCheckStyle=true \ + --info diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml index 5e6e090a9794..696d9496268a 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: if: always() with: files: '**/build/test-results/**/*.xml' - large_files: true \ No newline at end of file + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml index ea1302c825d8..928df75b7d5e 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml @@ -70,7 +70,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -84,7 +86,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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml index c45725652dc6..51b2593e1c7d 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml @@ -67,7 +67,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,4 +100,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml index 274e579c19ed..c36c9db7d8c0 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - 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..762ab81e75a6 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -105,4 +107,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml b/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml index ef6f7f3b77a0..469706c6f6c5 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,4 +95,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml index 9f60e1529d15..c58721208eaa 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +82,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_Examples_Spark.yml b/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml index 380167cc99a2..f3332996ed8d 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,4 +95,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml index 4ae2b3c6dd74..4b0ad8033944 100644 --- a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -89,4 +91,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml b/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml index 8820e32812ea..383db7e137fd 100644 --- a/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml +++ b/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml @@ -66,7 +66,9 @@ jobs: job_phrase: ["Run Java PostCommit IO Performance Tests"] test_case: ["GCSPerformanceTest", "BigTablePerformanceTest", "BigQueryStorageApiStreamingPerformanceTest"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,10 +82,11 @@ 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 + persist-credentials: false - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml b/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml index 2a12e9652c28..32a42305b760 100644 --- a/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml @@ -65,7 +65,9 @@ jobs: github.event_name == 'pull_request_target' || github.event.comment.body == 'Run Java InfluxDbIO_IT' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..928ced196782 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Dataflow"] job_phrase: ["Run Jpms Dataflow PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -72,13 +74,10 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - with: - java-version: 11 - name: run PostCommit Java Jpms Dataflow script uses: ./.github/actions/gradle-command-self-hosted-action with: 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@v7 if: ${{ !success() }} diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml index b5ac893f60b8..07eff3d9cb8b 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml @@ -64,7 +64,9 @@ jobs: job_phrase: ["Run Jpms Dataflow Versions PostCommit"] java_version: ["17", "21", "25"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..1d12e2c21169 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Direct"] job_phrase: ["Run Jpms Direct PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -72,8 +74,6 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - with: - java-version: 11 - name: run PostCommit Java Jpms Direct script uses: ./.github/actions/gradle-command-self-hosted-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..a3d6a54b5375 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml @@ -64,7 +64,9 @@ jobs: job_phrase: ["Run Jpms Direct Versions PostCommit"] java_version: ["17", "21", "25"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..cfbff90970c0 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -92,4 +94,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml index 307d55c16d96..1077065388fe 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -92,4 +94,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml index d648a8c4ba08..a88281151e5a 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml @@ -93,7 +93,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: gradle-command: :sdks:java:testing:nexmark:run arguments: | -Pnexmark.runner=:runners:google-cloud-dataflow-java \ - "${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}" \ No newline at end of file + "${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}" diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml index 4c91daf38bec..ba00e5cf8cbc 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml @@ -93,7 +93,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -115,4 +117,4 @@ jobs: -Pjava8Home=$JAVA_HOME_8_X64 \ -Pnexmark.runner.version=V2 \ -Pnexmark.runner=:runners:google-cloud-dataflow-java \ - '${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}' \ No newline at end of file + '${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}' 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..a3410e4acc00 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -115,4 +117,4 @@ jobs: -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ -Pnexmark.runner.version=V2 \ -Pnexmark.runner=:runners:google-cloud-dataflow-java \ - '${{ env.GRADLE_COMMAND_ARGUMENTS }}--influxTags={"runnerVersion":"V2","javaVersion":"${{ matrix.java_version }}"}--streaming=${{ matrix.streaming }}' \ No newline at end of file + '${{ env.GRADLE_COMMAND_ARGUMENTS }}--influxTags={"runnerVersion":"V2","javaVersion":"${{ matrix.java_version }}"}--streaming=${{ matrix.streaming }}' diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml index db8eff3e748c..aa87c809038b 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml @@ -88,7 +88,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -112,4 +114,4 @@ jobs: gradle-command: :sdks:java:testing:nexmark:run arguments: | -Pnexmark.runner=:runners:direct-java \ - "${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}" \ No newline at end of file + "${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}" diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml index 9d7ca2043d74..11d015633db7 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml @@ -87,7 +87,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,13 +104,14 @@ 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' 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:1.19 \ + -Pnexmark.runner=:runners:flink:1.20 \ "${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{ matrix.streaming }}" diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml index f397c7ae18a5..ea18aad79c39 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml @@ -87,7 +87,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,4 +113,4 @@ jobs: gradle-command: :sdks:java:testing:nexmark:run arguments: | -Pnexmark.runner=:runners:spark:3 \ - "${{ env.GRADLE_COMMAND_ARGUMENTS }}--runner=${{ matrix.runner }}" \ No newline at end of file + "${{ env.GRADLE_COMMAND_ARGUMENTS }}--runner=${{ matrix.runner }}" diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml index 476793bfa8b6..b900ccace0cf 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: | @@ -79,7 +79,9 @@ 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 + with: + persist-credentials: false - 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 d5f7edeb11d0..3961475de33f 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml @@ -56,18 +56,20 @@ 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' || (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 + with: + persist-credentials: false - 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..aef3f86655ac 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml index 9a34099cebb9..331b0f5f49cc 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml @@ -64,7 +64,9 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..0ef53787a8ff 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,4 +95,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml index e2ba7ec824ee..2c0a09430416 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml @@ -64,7 +64,9 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 d16069b9e915..938149972df5 100644 --- a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml @@ -67,7 +67,9 @@ jobs: github.event_name == 'pull_request_target' || github.event.comment.body == 'Run Java SingleStoreIO_IT' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,7 +89,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_Tpcds_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml index 25890c3545e3..fb57bebdc510 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml @@ -90,7 +90,9 @@ jobs: job_name: ["beam_PostCommit_Java_Tpcds_Dataflow"] job_phrase: ["Run Dataflow Runner Tpcds Tests"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 ae3dcb7d6ad2..07d275553447 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml @@ -87,7 +87,9 @@ jobs: job_name: ["beam_PostCommit_Java_Tpcds_Flink"] job_phrase: ["Run Flink Runner Tpcds Tests"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -101,5 +103,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_Tpcds_Spark.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml index 557db1c89794..6b03a2890158 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml @@ -86,7 +86,9 @@ jobs: job_phrase: ["Run Spark Runner Tpcds Tests"] runner: [SparkRunner, SparkStructuredStreamingRunner] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..74adf33c4ba1 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,4 +96,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml index e5d4a3d26f83..99128e5eb599 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml @@ -60,14 +60,16 @@ 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' || (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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -101,4 +103,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml index f70cbf0db32f..bb38a8c1b542 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,4 +96,4 @@ jobs: 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 + large_files: true 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..ede9147ca119 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,9 @@ 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 + with: + persist-credentials: false - 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..93efd86931b8 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,9 @@ 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 + with: + persist-credentials: false - 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..60a88cfad3c8 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,4 +96,4 @@ jobs: 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 + large_files: true 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..08fec63a0adf 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -101,4 +103,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml index 29d42d8e6edc..92d44315dc91 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml index 48fd46342d22..5d9ec380019e 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml @@ -67,7 +67,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,4 +102,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml index febec1afa592..679c22b627d3 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,14 +58,16 @@ 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' || (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Flink ValidatesRunner' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..0accb7809946 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml index c6e742204894..6b7b66d64e43 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - 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..80173f2ec212 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml index 9a10cf8c2f9c..0579fa2c89a1 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,4 +93,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml index 74400d0934ad..512b94cae0aa 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Javadoc.yml b/.github/workflows/beam_PostCommit_Javadoc.yml index 538648c46e7d..2ed3777d1514 100644 --- a/.github/workflows/beam_PostCommit_Javadoc.yml +++ b/.github/workflows/beam_PostCommit_Javadoc.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,4 +92,4 @@ jobs: uses: actions/upload-artifact@v7 with: name: Javadoc Results - path: '**/sdks/java/javadoc/build/docs/javadoc/**' \ No newline at end of file + path: '**/sdks/java/javadoc/build/docs/javadoc/**' diff --git a/.github/workflows/beam_PostCommit_PortableJar_Flink.yml b/.github/workflows/beam_PostCommit_PortableJar_Flink.yml index 9dd6efb968d7..4d3571eecff1 100644 --- a/.github/workflows/beam_PostCommit_PortableJar_Flink.yml +++ b/.github/workflows/beam_PostCommit_PortableJar_Flink.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_PortableJar_Flink"] job_phrase: ["Run PortableJar_Flink PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..2a7ef43d6a9d 100644 --- a/.github/workflows/beam_PostCommit_PortableJar_Spark.yml +++ b/.github/workflows/beam_PostCommit_PortableJar_Spark.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_PortableJar_Spark"] job_phrase: ["Run PortableJar_Spark PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -95,4 +97,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Python.yml b/.github/workflows/beam_PostCommit_Python.yml index 41f72fa50e92..f9df7e2583ea 100644 --- a/.github/workflows/beam_PostCommit_Python.yml +++ b/.github/workflows/beam_PostCommit_Python.yml @@ -69,7 +69,9 @@ 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 + with: + persist-credentials: false - 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 32dd751d79e7..acefdcc44a94 100644 --- a/.github/workflows/beam_PostCommit_Python_Arm.yml +++ b/.github/workflows/beam_PostCommit_Python_Arm.yml @@ -54,7 +54,9 @@ jobs: beam_PostCommit_Python_Arm: name: ${{ matrix.job_name }} ${{ matrix.python_version }} runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 480 + # Python ARM PostCommit uses prebuilt Snapshots SDK images, so it no longer + # spends hours on multiarch container builds. Keep headroom for the IT suite. + timeout-minutes: 180 strategy: fail-fast: false matrix: @@ -66,7 +68,9 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository @@ -90,20 +94,12 @@ jobs: 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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - - name: GCloud Docker credential helper - run: | - gcloud auth configure-docker us.gcr.io - 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: Generate TAG unique variable based on timestamp - id: set_tag - run: echo "TAG=$(date +'%Y%m%d-%H%M%S%N')" >> $GITHUB_OUTPUT - name: run PostCommit Python ${{ matrix.python_version }} script uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -111,12 +107,10 @@ jobs: arguments: | -PuseWheelDistribution \ -PpythonVersion=${{ matrix.python_version }} \ - -Pcontainer-architecture-list=arm64,amd64 \ - -Pdocker-repository-root=us.gcr.io/apache-beam-testing/github-actions \ - -Pdocker-tag=${{ steps.set_tag.outputs.TAG }} \ - -Ppush-containers \ + -PusePrebuiltSdkContainer \ env: - MULTIARCH_TAG: ${{ steps.set_tag.outputs.TAG }} + # Use multiarch images published by Publish Beam SDK Snapshots. + SDK_CONTAINER_IMAGE: gcr.io/apache-beam-testing/beam-sdk/beam_python${{ matrix.python_version }}_sdk:latest USER: github-actions - name: Archive Python Test Results uses: actions/upload-artifact@v7 diff --git a/.github/workflows/beam_PostCommit_Python_Dependency.yml b/.github/workflows/beam_PostCommit_Python_Dependency.yml index 18349ab173ec..ed10007829f3 100644 --- a/.github/workflows/beam_PostCommit_Python_Dependency.yml +++ b/.github/workflows/beam_PostCommit_Python_Dependency.yml @@ -67,7 +67,9 @@ 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 + with: + persist-credentials: false - 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..1eb7c7c6d07e 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_Python_Examples_Dataflow"] job_phrase: ["Run Python Examples_Dataflow"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..98c8f7284655 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml index 037dbca6a0a8..b7f987a73dd9 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml @@ -65,7 +65,9 @@ jobs: job_phrase: ["Run Python Examples_Flink"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..b9f5f6a705ad 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml @@ -65,7 +65,9 @@ jobs: job_phrase: ["Run Python Examples_Spark"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml b/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml index 305960e130e2..9c8084df040e 100644 --- a/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_Python_MongoDBIO_IT"] job_phrase: ["Run Python MongoDBIO_IT"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..69166589b4c1 100644 --- a/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml @@ -108,7 +108,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -136,4 +138,4 @@ jobs: -PpythonVersion=3.10 \ "-Pnexmark.args=${{ env.GRADLE_PYTHON_COMMAND_ARGUMENTS }} \ --query=${{ matrix.query }} \ - --input=gs://temp-storage-for-perf-tests/nexmark/eventFiles/beam_PostCommit_Python_Nexmark_Direct/query${{ matrix.query }}-\*" \ No newline at end of file + --input=gs://temp-storage-for-perf-tests/nexmark/eventFiles/beam_PostCommit_Python_Nexmark_Direct/query${{ matrix.query }}-\*" diff --git a/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml b/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml index 78af1ff48e9a..9b05df10d94f 100644 --- a/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml @@ -67,7 +67,9 @@ 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 + with: + persist-credentials: false - 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..f7e72d7506ed 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml @@ -67,7 +67,9 @@ 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 + with: + persist-credentials: false - 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..030250cb06be 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -125,4 +127,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml index 1b3aa2890783..cfce84b677f9 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml @@ -65,7 +65,9 @@ jobs: job_phrase: ["Run Python Dataflow ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -110,4 +112,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml index 1b769788fd51..b18a4a6502b5 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml @@ -65,7 +65,9 @@ jobs: job_phrase: ["Run Python Flink ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,4 +106,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml index fc68dc021db5..0e7ef761eebc 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml @@ -65,7 +65,9 @@ jobs: job_phrase: ["Run Python Spark ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Python_Versions.yml b/.github/workflows/beam_PostCommit_Python_Versions.yml index c0f79faefdda..ef485994fe78 100644 --- a/.github/workflows/beam_PostCommit_Python_Versions.yml +++ b/.github/workflows/beam_PostCommit_Python_Versions.yml @@ -88,7 +88,9 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event_name == 'workflow_dispatch' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 01997b22515d..48e7f953f21b 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml @@ -64,7 +64,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +76,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default python-version: | 3.10 3.14 @@ -83,7 +86,7 @@ jobs: gradle-command: :sdks:python:test-suites:dataflow:gcpCrossLanguagePostCommit arguments: | -PpythonVersion=3.14 \ - -PuseWheelDistribution \ + -PuseWheelDistribution - 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..d1f004276e7e 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml @@ -64,7 +64,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +76,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default python-version: | 3.10 3.14 diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml index 832db23a73fc..20d93902328f 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,6 +75,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: '17' python-version: | 3.10 3.14 @@ -83,7 +86,7 @@ jobs: arguments: | -PpythonVersion=3.14 \ -PuseWheelDistribution \ - -PkafkaBootstrapServer=10.128.0.40:9094,10.128.0.28:9094,10.128.0.165:9094 \ + -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@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..0767796b1ee0 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml @@ -63,7 +63,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,6 +75,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: '17' python-version: | 3.10 3.14 @@ -80,7 +83,8 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:test-suites:direct:ioCrossLanguagePostCommit - arguments: -PuseWheelDistribution + arguments: | + -PuseWheelDistribution - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() @@ -94,4 +98,4 @@ jobs: 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 + large_files: true 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..88ac36e8d5d9 --- /dev/null +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml @@ -0,0 +1,99 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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@v7 + with: + persist-credentials: false + - 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/.github/workflows/beam_PostCommit_SQL.yml b/.github/workflows/beam_PostCommit_SQL.yml index 54fde69e28a0..22bbe051933d 100644 --- a/.github/workflows/beam_PostCommit_SQL.yml +++ b/.github/workflows/beam_PostCommit_SQL.yml @@ -65,7 +65,9 @@ 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 + with: + persist-credentials: false - 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 a127cb5b6046..ee942bbcab6d 100644 --- a/.github/workflows/beam_PostCommit_TransformService_Direct.yml +++ b/.github/workflows/beam_PostCommit_TransformService_Direct.yml @@ -64,7 +64,9 @@ jobs: job_phrase: ["Run TransformService_Direct PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,7 +76,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: 11 + java-version: default python-version: | 3.10 ${{ matrix.python_version }} @@ -83,9 +85,7 @@ jobs: with: gradle-command: :sdks:python:test-suites:direct:xlang:transformServicePythonUsingJava arguments: | - -PtestJavaVersion=11 \ - -Pjava11Home=$JAVA_HOME_11_X64 \ - -PpythonVersion=${{ matrix.python_version }} \ + -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PostCommit_Website_Test.yml b/.github/workflows/beam_PostCommit_Website_Test.yml index c58a2727fbe8..332551e23e30 100644 --- a/.github/workflows/beam_PostCommit_Website_Test.yml +++ b/.github/workflows/beam_PostCommit_Website_Test.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_Website_Test"] job_phrase: ["Run Full Website Test"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -76,4 +78,4 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :website:testWebsite - arguments: -PdisableExternal=false \ No newline at end of file + arguments: -PdisableExternal=false diff --git a/.github/workflows/beam_PostCommit_XVR_Direct.yml b/.github/workflows/beam_PostCommit_XVR_Direct.yml index f2174aa3ec98..62cdcaa67a53 100644 --- a/.github/workflows/beam_PostCommit_XVR_Direct.yml +++ b/.github/workflows/beam_PostCommit_XVR_Direct.yml @@ -64,7 +64,9 @@ jobs: job_phrase: ["Run XVR_Direct PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +76,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Direct script env: @@ -97,4 +100,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_XVR_Flink.yml b/.github/workflows/beam_PostCommit_XVR_Flink.yml index 44cb264ad9dd..cabfba27148f 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: @@ -65,7 +65,9 @@ jobs: job_phrase: ["Run XVR_Flink PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -75,6 +77,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Flink script env: diff --git a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml index 3961517c2707..1b79efedbe43 100644 --- a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml @@ -66,7 +66,9 @@ jobs: job_name: ["beam_PostCommit_XVR_GoUsingJava_Dataflow"] job_phrase: ["Run XVR_GoUsingJava_Dataflow PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -76,9 +78,10 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default python-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - name: run XVR GoUsingJava Dataflow script env: USER: github-actions diff --git a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml index c282f6c47069..286303ff6e0a 100644 --- a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml @@ -64,7 +64,9 @@ jobs: job_phrase: ["Run XVR_JavaUsingPython_Dataflow PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +76,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default python-version: | 3.10 ${{ matrix.python_version }} @@ -82,7 +85,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerJavaUsingPython arguments: | - -PpythonVersion=${{ matrix.python_version }} \ + -PpythonVersion=${{ matrix.python_version }} - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 if: ${{ !success() }} @@ -96,4 +99,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml index 95496621f9ce..14b17a97636b 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml @@ -63,7 +63,9 @@ jobs: job_name: ["beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow"] job_phrase: ["Run XVR_PythonUsingJavaSQL_Dataflow PostCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,13 +75,14 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default 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: | - -PpythonVersion=3.14 \ + -PpythonVersion=3.14 - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml index 0cd9790e6793..fafda7506bf4 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml @@ -64,7 +64,9 @@ jobs: job_phrase: ["Run XVR_PythonUsingJava_Dataflow PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +76,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default python-version: | 3.10 ${{ matrix.python_version }} @@ -82,7 +85,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerPythonUsingJava arguments: | - -PpythonVersion=${{ matrix.python_version }} \ + -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results uses: actions/upload-artifact@v7 if: failure() diff --git a/.github/workflows/beam_PostCommit_XVR_Spark3.yml b/.github/workflows/beam_PostCommit_XVR_Spark3.yml index 2c7104df6115..4cde1ac2cbfa 100644 --- a/.github/workflows/beam_PostCommit_XVR_Spark3.yml +++ b/.github/workflows/beam_PostCommit_XVR_Spark3.yml @@ -64,7 +64,9 @@ jobs: job_phrase: ["Run XVR_Spark3 PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +76,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: default python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Spark3 script env: @@ -97,4 +100,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index ea1c255f7cc9..8324c192e948 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: @@ -63,7 +63,9 @@ jobs: job_phrase: ["Run Yaml_Xlang_Direct PostCommit"] test_set: ["data", "databases", "messaging"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,13 +76,13 @@ jobs: uses: ./.github/actions/setup-environment-action with: python-version: default - java-version: '11' + java-version: '17' - 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=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() diff --git a/.github/workflows/beam_PostRelease_NightlySnapshot.yml b/.github/workflows/beam_PostRelease_NightlySnapshot.yml index 5e811fa47581..d119dfb7754a 100644 --- a/.github/workflows/beam_PostRelease_NightlySnapshot.yml +++ b/.github/workflows/beam_PostRelease_NightlySnapshot.yml @@ -19,12 +19,13 @@ on: workflow_dispatch: inputs: RELEASE: - description: Beam version of current release (e.g. 2.XX.0) - required: true + description: Beam version of current release (pass in empty string for nightly SNAPSHOT) + required: false default: '2.XX.0' SNAPSHOT_URL: - description: Location of the staged artifacts in Maven central (https://repository.apache.org/content/repositories/orgapachebeam-NNNN/). - required: true + description: Location of the staged artifacts in Maven central (https://repository.apache.org/content/repositories/orgapachebeam-NNNN/ or leave empty for snapshots). + required: false + default: '' schedule: - cron: '15 16 * * *' @@ -54,7 +55,9 @@ jobs: name: beam_PostRelease_NightlySnapshot runs-on: [self-hosted, ubuntu-24.04, highmem] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -72,4 +75,6 @@ jobs: -PmavenLocalPath='${{ steps.setup_local_maven.outputs.NEW_TEMP_DIR }}' - name: Clean up local maven if: steps.setup_local_maven.outcome == 'success' - run: rm -rf '${{ steps.setup_local_maven.outputs.NEW_TEMP_DIR }}' + run: rm -rf '${STEPS_SETUP_LOCAL_MAVEN_OUTPUTS_NEW_TEMP_DIR}' + env: + STEPS_SETUP_LOCAL_MAVEN_OUTPUTS_NEW_TEMP_DIR: ${{ steps.setup_local_maven.outputs.NEW_TEMP_DIR }} diff --git a/.github/workflows/beam_PreCommit_CommunityMetrics.yml b/.github/workflows/beam_PreCommit_CommunityMetrics.yml index 6ea7a5019711..c674ed3ee625 100644 --- a/.github/workflows/beam_PreCommit_CommunityMetrics.yml +++ b/.github/workflows/beam_PreCommit_CommunityMetrics.yml @@ -71,7 +71,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run CommunityMetrics PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +85,7 @@ jobs: with: java-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - name: Remove default github maven configuration run: rm ~/.m2/settings.xml - name: Install docker compose @@ -95,4 +97,4 @@ jobs: with: gradle-command: :communityMetricsPreCommit arguments: | - -PKUBE_CONFIG_PATH='$HOME/.kube/config' \ No newline at end of file + -PKUBE_CONFIG_PATH='$HOME/.kube/config' diff --git a/.github/workflows/beam_PreCommit_Flink_Container.yml b/.github/workflows/beam_PreCommit_Flink_Container.yml index 0febb8981176..4ac77c212020 100644 --- a/.github/workflows/beam_PreCommit_Flink_Container.yml +++ b/.github/workflows/beam_PreCommit_Flink_Container.yml @@ -67,15 +67,13 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - GCLOUD_ZONE: us-central1-a + GCLOUD_REGION: us-central1 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 - 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.2 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' }} @@ -98,7 +96,9 @@ jobs: job_name: ["beam_PreCommit_Flink_Container"] job_phrase: ["Run Flink Container PreCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -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 @@ -184,4 +184,4 @@ jobs: - name: Cleanup Python SDK Container if: ${{ always() && github.event_name == 'pull_request_target' }} run: | - gcloud container images delete ${{ env.DOCKER_REPOSITORY_ROOT }}/beam_python${{ env.PYTHON_VERSION }}_sdk:${{ env.PYTHON_SDK_IMAGE_TAG }} --force-delete-tags --quiet + gcloud container images delete ${DOCKER_REPOSITORY_ROOT}/beam_python${{ env.PYTHON_VERSION }}_sdk:${{ env.PYTHON_SDK_IMAGE_TAG }} --force-delete-tags --quiet diff --git a/.github/workflows/beam_PreCommit_GHA.yml b/.github/workflows/beam_PreCommit_GHA.yml index 76c5b1cba0fd..253442b67dfe 100644 --- a/.github/workflows/beam_PreCommit_GHA.yml +++ b/.github/workflows/beam_PreCommit_GHA.yml @@ -42,7 +42,7 @@ permissions: packages: read pages: read repository-projects: read - security-events: read + security-events: write statuses: read # This allows a subsequently queued workflow run to interrupt previous runs @@ -71,19 +71,45 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GHA PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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: Check for gsutil references + run: | + echo "Checking codebase for gsutil..." + # Search for 'gsutil', excluding this workflow file itself to avoid false positives. + if git grep -n "gsutil" -- ':!.github/workflows/beam_PreCommit_GHA.yml'; then + echo "ERROR: Found references to gsutil in the codebase. Please use 'gcloud storage' instead." + exit 1 + elif [ "$(date +%Y%m)" -ge 202704 ]; then + echo "ERROR: Current date is April 2027 or later." + echo "Please verify gsutil deprecation date is still March 2027 (Reference: https://docs.cloud.google.com/storage/docs/gsutil)." + echo "If so, then delete this workflow step." + exit 1 + else + echo "SUCCESS: No references to gsutil found." + fi + shell: bash - name: Setup environment uses: ./.github/actions/setup-environment-action with: java-version: default go-version: default + python-version: default + # For details on the ASF GHA allowlist, see: https://github.com/apache/infrastructure-actions/blob/main/allowlist-check/README.md + - name: Validate GHA Allowlist + uses: apache/infrastructure-actions/allowlist-check@main # zizmor: ignore[unpinned-uses] + - name: Run zizmor + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + with: + advanced-security: true - name: run GHA PreCommit script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :beam-test-gha:preCommit \ No newline at end of file + gradle-command: :beam-test-gha:preCommit diff --git a/.github/workflows/beam_PreCommit_Go.yml b/.github/workflows/beam_PreCommit_Go.yml index c382815bdbd1..335d531f7435 100644 --- a/.github/workflows/beam_PreCommit_Go.yml +++ b/.github/workflows/beam_PreCommit_Go.yml @@ -71,7 +71,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Go PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,4 +88,4 @@ jobs: - name: run goPreCommit script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :goPreCommit \ No newline at end of file + gradle-command: :goPreCommit diff --git a/.github/workflows/beam_PreCommit_GoPortable.yml b/.github/workflows/beam_PreCommit_GoPortable.yml index dad631236112..4239555c038b 100644 --- a/.github/workflows/beam_PreCommit_GoPortable.yml +++ b/.github/workflows/beam_PreCommit_GoPortable.yml @@ -73,7 +73,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GoPortable PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..cac49331592f 100644 --- a/.github/workflows/beam_PreCommit_GoPrism.yml +++ b/.github/workflows/beam_PreCommit_GoPrism.yml @@ -71,7 +71,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GoPrism PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,4 +88,4 @@ jobs: - name: Run goPrismPreCommit script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :goPrismPreCommit \ No newline at end of file + gradle-command: :goPrismPreCommit diff --git a/.github/workflows/beam_PreCommit_ItFramework.yml b/.github/workflows/beam_PreCommit_ItFramework.yml index 5f30ac844703..c0b666a61231 100644 --- a/.github/workflows/beam_PreCommit_ItFramework.yml +++ b/.github/workflows/beam_PreCommit_ItFramework.yml @@ -76,7 +76,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run It_Framework PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PreCommit_Java.yml b/.github/workflows/beam_PreCommit_Java.yml index 1d8d5fe53fd1..0d4e876b350a 100644 --- a/.github/workflows/beam_PreCommit_Java.yml +++ b/.github/workflows/beam_PreCommit_Java.yml @@ -170,7 +170,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..9e9e33ef2888 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -142,4 +144,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml index 0a477eaec9b7..aed74b85e7be 100644 --- a/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -116,4 +118,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml index 384a8fe5f83a..5f7d89c5ea33 100644 --- a/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml @@ -94,7 +94,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -135,4 +137,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml index 5aeddb5107c6..8e2f4041c153 100644 --- a/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -85,6 +87,8 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action + with: + java-version: 11 - name: run Cassandra IO build script uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -117,4 +121,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml index 2617601216ea..8118273ed62f 100644 --- a/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml @@ -80,7 +80,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -121,4 +123,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml index 50368fdbd7e9..35c1ec4aeeb5 100644 --- a/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -124,4 +126,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml index 1dcaeb168e45..4d4a72317607 100644 --- a/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml index 7501b342959f..57233c8a0d69 100644 --- a/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Dataflow.yml b/.github/workflows/beam_PreCommit_Java_Dataflow.yml index 53044970d117..febcf9ab3ffd 100644 --- a/.github/workflows/beam_PreCommit_Java_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Dataflow.yml @@ -77,7 +77,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -132,4 +134,4 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 with: - flags: java \ No newline at end of file + flags: java diff --git a/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml index dbd3dfef3e42..8b22c8405f3e 100644 --- a/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,18 +88,14 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: | - 17 - 11 + java-version: default - name: run Debezium IO build task uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:java:io:debezium:build arguments: | -PdisableSpotlessCheck=true \ - -PdisableCheckStyle=true \ - -PtestJavaVersion=17 \ - -Pjava17Home=$JAVA_HOME_17_X64 \ + -PdisableCheckStyle=true - name: run Debezium IO additional tasks uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -106,9 +104,7 @@ jobs: :sdks:java:io:debezium:integrationTest \ arguments: | -PdisableSpotlessCheck=true \ - -PdisableCheckStyle=true \ - -PtestJavaVersion=17 \ - -Pjava17Home=$JAVA_HOME_17_X64 \ + -PdisableCheckStyle=true - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 if: ${{ !success() }} 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..d7624d5ccd36 --- /dev/null +++ b/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml @@ -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. + +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@v7 + with: + persist-credentials: false + - 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' diff --git a/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml index 5bc2d491de40..bec4fe27e8e9 100644 --- a/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml @@ -78,7 +78,9 @@ 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 + with: + persist-credentials: false - 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..7c1286ce418a 100644 --- a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml @@ -87,7 +87,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_Examples_Dataflow PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -119,4 +121,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java11.yml similarity index 91% rename from .github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml rename to .github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java11.yml index 455b12b73b39..3504d06bbba4 100644 --- a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml +++ b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java11.yml @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: PreCommit Java Examples Dataflow Java21 +name: PreCommit Java Examples Dataflow Java11 on: push: @@ -26,7 +26,7 @@ on: - 'examples/java/**' - 'examples/kotlin/**' - 'release/**' - - '.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml' + - '.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java11.yml' pull_request_target: branches: ['master', 'release-*'] paths: @@ -37,7 +37,7 @@ on: - 'examples/kotlin/**' - 'release/**' - 'release/trigger_all_tests.json' - - '.github/trigger_files/beam_PreCommit_Java_Examples_Dataflow_Java21.json' + - '.github/trigger_files/beam_PreCommit_Java_Examples_Dataflow_Java11.json' issue_comment: types: [created] schedule: @@ -71,22 +71,24 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PreCommit_Java_Examples_Dataflow_Java21: + beam_PreCommit_Java_Examples_Dataflow_Java11: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) strategy: matrix: - job_name: ["beam_PreCommit_Java_Examples_Dataflow_Java21"] - job_phrase: ["Run Java_Examples_Dataflow_Java21 PreCommit"] + job_name: ["beam_PreCommit_Java_Examples_Dataflow_Java11"] + job_phrase: ["Run Java_Examples_Dataflow_Java11 PreCommit"] timeout-minutes: 75 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_Examples_Dataflow_Java21 PreCommit' + github.event.comment.body == 'Run Java_Examples_Dataflow_Java11 PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -116,9 +118,9 @@ jobs: arguments: | -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - -PtestJavaVersion=21 \ + -PtestJavaVersion=11 \ -PskipCheckerFramework \ - -Pjava21Home=$JAVA_HOME_21_X64 \ + -Pjava11Home=$JAVA_HOME_11_X64 \ -Pdocker-pull-licenses \ max-workers: 12 - name: Archive JUnit Test Results @@ -139,4 +141,4 @@ jobs: uses: actions/upload-artifact@v7 with: name: SpotBugs Results - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' 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..3f9223eaba63 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -118,4 +120,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml b/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml index 541dc08146c9..f7832008b696 100644 --- a/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml @@ -75,7 +75,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -84,7 +86,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: default + java-version: 11 python-version: default - name: run Java Flink Versions PreCommit script uses: ./.github/actions/gradle-command-self-hosted-action @@ -105,4 +107,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml index 2cf942f5d488..9ebb56b76d5e 100644 --- a/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml @@ -94,7 +94,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -153,4 +155,4 @@ jobs: uses: codecov/codecov-action@v3 with: file: ${{ steps.jacoco_report_path.outputs.path }} - flags: java \ No newline at end of file + flags: java 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..6d6ae12d1d15 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,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -115,4 +117,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml index 4a7553ddcd2c..e5fdaf5ebf60 100644 --- a/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml @@ -78,7 +78,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,6 +89,8 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action + with: + java-version: 11 - name: run HBase IO build script uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -119,4 +123,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml index 0a9de29e968b..d8c15259c308 100644 --- a/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml @@ -78,7 +78,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,6 +89,8 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action + with: + java-version: 11 - name: run HCatalog IO build script uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -119,4 +123,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml index b8a085f8447f..9b53c6875252 100644 --- a/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml @@ -102,7 +102,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -157,4 +159,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml b/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml index 349d6d1dd06c..a0c7197d947b 100644 --- a/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml @@ -79,7 +79,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -88,6 +90,10 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action + with: + java-version: | + 11 + 21 - name: run Java IOs PreCommit script uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -96,6 +102,15 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ -Dfile.encoding=UTF-8 \ + - name: Run IOs need Java11 + uses: ./.github/actions/gradle-command-self-hosted-action + with: + gradle-command: :sdks:java:io:cassandra:build :sdks:java:io:hbase:build :sdks:java:io:hcatalog:build + arguments: | + -PdisableSpotlessCheck=true \ + -PdisableCheckStyle=true \ + -PtestJavaVersion=11 \ + -Pjava11Home=$JAVA_HOME_11_X64 \ - name: Archive JUnit Test Results uses: actions/upload-artifact@v7 if: ${{ !success() }} @@ -121,4 +136,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml index dabac47afa4f..3ff0d49bd644 100644 --- a/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml index fd595d867a39..96621b494353 100644 --- a/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -124,4 +126,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml index d032b22d3527..3336c214414d 100644 --- a/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -124,4 +126,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml index 94ac5a6fec8e..c9f484b075f8 100644 --- a/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml @@ -84,7 +84,9 @@ 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 + with: + persist-credentials: false - 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..70617ceffbc8 100644 --- a/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml index 91bf02f0c0f3..afaec9df36f7 100644 --- a/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml index a202a12d57a6..661cb7b67afc 100644 --- a/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml index c17c69c9a5a6..97b52e1a52da 100644 --- a/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml @@ -78,7 +78,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -126,4 +128,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml index 9e04f103a779..4bac198b2da0 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml @@ -82,7 +82,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,7 +96,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..5dcddb1beff8 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml @@ -87,7 +87,9 @@ jobs: github.event.comment.body == 'Run Java_PVR_Flink_Docker PreCommit' timeout-minutes: 240 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,7 +101,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_PreCommit_Java_PVR_Prism_Loopback.yml b/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml index c66b4a133bcf..56fcabddc9d7 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml @@ -88,7 +88,9 @@ 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 + with: + persist-credentials: false - 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..6e95f71673d1 100644 --- a/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml index 59c5aeb249f8..ec825890bdc2 100644 --- a/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - 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..5770b9f3e552 100644 --- a/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml index de5d80c9cb32..9441278f83b1 100644 --- a/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml index 78b290fb45e4..b4e55572ab22 100644 --- a/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -115,4 +117,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml index b5816df45c2b..f7d4347f3ee0 100644 --- a/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml @@ -78,7 +78,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -119,4 +121,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml index ce2d64387944..678a0dbff191 100644 --- a/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml @@ -80,7 +80,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -128,4 +130,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml index 42d8009c6b79..5d387b38b568 100644 --- a/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml @@ -76,7 +76,9 @@ jobs: github.event.comment.body == 'Run Java_Solace_IO_Direct PreCommit' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -124,4 +126,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml index bcd6ea621282..6857e142cfba 100644 --- a/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml b/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml index 6f8689d00caa..71f1ccb21a4a 100644 --- a/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml @@ -78,7 +78,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_Spark_Versions PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -88,9 +90,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: | - 17 - 11 + java-version: 11 - name: run sparkVersionsTest script uses: ./.github/actions/gradle-command-self-hosted-action with: @@ -111,4 +111,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml index 797390c81683..a5140f391d6e 100644 --- a/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml index a85e8cdaf60b..515184ba5c76 100644 --- a/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml index c4cb837fb3a9..fd4214ad4203 100644 --- a/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml @@ -76,7 +76,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Kotlin_Examples.yml b/.github/workflows/beam_PreCommit_Kotlin_Examples.yml index b05fff74e51c..18ed78e29f0c 100644 --- a/.github/workflows/beam_PreCommit_Kotlin_Examples.yml +++ b/.github/workflows/beam_PreCommit_Kotlin_Examples.yml @@ -88,7 +88,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Kotlin_Examples PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,4 +104,10 @@ jobs: - name: run Kotlin Examples script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :examples:kotlin:preCommit \ No newline at end of file + gradle-command: :examples:kotlin:preCommit + - name: Archive JUnit Test Results + uses: actions/upload-artifact@v7 + if: ${{ !success() }} + with: + name: JUnit Test Results + path: "**/build/reports/tests/" diff --git a/.github/workflows/beam_PreCommit_Portable_Python.yml b/.github/workflows/beam_PreCommit_Portable_Python.yml index fedb485bac8c..bfc6c2fcb1c1 100644 --- a/.github/workflows/beam_PreCommit_Portable_Python.yml +++ b/.github/workflows/beam_PreCommit_Portable_Python.yml @@ -90,7 +90,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -113,4 +115,4 @@ jobs: - name: run Portable Python 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}}:preCommitPy${{steps.set_py_ver_clean.outputs.py_ver_clean}} \ No newline at end of file + gradle-command: :sdks:python:test-suites:portable:py${{steps.set_py_ver_clean.outputs.py_ver_clean}}:preCommitPy${{steps.set_py_ver_clean.outputs.py_ver_clean}} diff --git a/.github/workflows/beam_PreCommit_Prism_Python.yml b/.github/workflows/beam_PreCommit_Prism_Python.yml index ca2e6a08a23c..8a5e2b6433b1 100644 --- a/.github/workflows/beam_PreCommit_Prism_Python.yml +++ b/.github/workflows/beam_PreCommit_Prism_Python.yml @@ -84,7 +84,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -107,4 +109,4 @@ jobs: - name: run Prism Python Validates Runner 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}}:prismValidatesRunner \ No newline at end of file + gradle-command: :sdks:python:test-suites:portable:py${{steps.set_py_ver_clean.outputs.py_ver_clean}}:prismValidatesRunner diff --git a/.github/workflows/beam_PreCommit_Python.yml b/.github/workflows/beam_PreCommit_Python.yml index 4ba7a4758592..e897b371b94d 100644 --- a/.github/workflows/beam_PreCommit_Python.yml +++ b/.github/workflows/beam_PreCommit_Python.yml @@ -91,7 +91,9 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 9499499affe3..a1f1afed008c 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: @@ -69,7 +69,9 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +85,7 @@ jobs: python-version: ${{ matrix.python_version }} go-version: default - name: Setup Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c with: install: true driver: 'docker' diff --git a/.github/workflows/beam_PreCommit_PythonDocs.yml b/.github/workflows/beam_PreCommit_PythonDocs.yml index b4fca4070277..1a32212705d6 100644 --- a/.github/workflows/beam_PreCommit_PythonDocs.yml +++ b/.github/workflows/beam_PreCommit_PythonDocs.yml @@ -71,7 +71,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonDocs PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..5b052d9376a8 100644 --- a/.github/workflows/beam_PreCommit_PythonFormatter.yml +++ b/.github/workflows/beam_PreCommit_PythonFormatter.yml @@ -70,7 +70,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonFormatter PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..637470db711f 100644 --- a/.github/workflows/beam_PreCommit_PythonLint.yml +++ b/.github/workflows/beam_PreCommit_PythonLint.yml @@ -70,7 +70,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonLint PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..17bc17e33c15 100644 --- a/.github/workflows/beam_PreCommit_Python_Coverage.yml +++ b/.github/workflows/beam_PreCommit_Python_Coverage.yml @@ -74,7 +74,9 @@ 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 + with: + persist-credentials: false - 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..95515c729644 100644 --- a/.github/workflows/beam_PreCommit_Python_Dataframes.yml +++ b/.github/workflows/beam_PreCommit_Python_Dataframes.yml @@ -72,7 +72,9 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Dataframes PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -110,4 +112,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PreCommit_Python_Dill.yml b/.github/workflows/beam_PreCommit_Python_Dill.yml index 3ee44408d1a0..f5f7c937beda 100644 --- a/.github/workflows/beam_PreCommit_Python_Dill.yml +++ b/.github/workflows/beam_PreCommit_Python_Dill.yml @@ -81,7 +81,9 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Dill PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..d4ddb18cbe30 100644 --- a/.github/workflows/beam_PreCommit_Python_Examples.yml +++ b/.github/workflows/beam_PreCommit_Python_Examples.yml @@ -73,7 +73,9 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Examples PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..0aa813a673db 100644 --- a/.github/workflows/beam_PreCommit_Python_Integration.yml +++ b/.github/workflows/beam_PreCommit_Python_Integration.yml @@ -72,7 +72,9 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Integration PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,4 +119,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PreCommit_Python_ML.yml b/.github/workflows/beam_PreCommit_Python_ML.yml index 94a953f82d40..b9b6a9df687a 100644 --- a/.github/workflows/beam_PreCommit_Python_ML.yml +++ b/.github/workflows/beam_PreCommit_Python_ML.yml @@ -87,7 +87,9 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_ML PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..3b4b6acf7334 100644 --- a/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml +++ b/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml @@ -90,7 +90,9 @@ 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 + with: + persist-credentials: false - 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..c04c7736c5fd 100644 --- a/.github/workflows/beam_PreCommit_Python_Runners.yml +++ b/.github/workflows/beam_PreCommit_Python_Runners.yml @@ -72,7 +72,9 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Runners PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -110,4 +112,4 @@ jobs: 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 + large_files: true diff --git a/.github/workflows/beam_PreCommit_Python_Transforms.yml b/.github/workflows/beam_PreCommit_Python_Transforms.yml index df13c2e41ee1..22ad88300085 100644 --- a/.github/workflows/beam_PreCommit_Python_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Python_Transforms.yml @@ -73,7 +73,9 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Transforms PreCommit') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..66c17ce082a3 100644 --- a/.github/workflows/beam_PreCommit_RAT.yml +++ b/.github/workflows/beam_PreCommit_RAT.yml @@ -69,7 +69,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run RAT PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..339cc016c636 100644 --- a/.github/workflows/beam_PreCommit_SQL.yml +++ b/.github/workflows/beam_PreCommit_SQL.yml @@ -71,7 +71,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run SQL PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -129,4 +131,4 @@ jobs: uses: codecov/codecov-action@v3 with: file: ${{ steps.jacoco_report_path.outputs.path }} - flags: java \ No newline at end of file + flags: java diff --git a/.github/workflows/beam_PreCommit_SQL_Java17.yml b/.github/workflows/beam_PreCommit_SQL_Java17.yml index 4bb5a6a5d69f..48822a1404f2 100644 --- a/.github/workflows/beam_PreCommit_SQL_Java17.yml +++ b/.github/workflows/beam_PreCommit_SQL_Java17.yml @@ -71,7 +71,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -122,4 +124,4 @@ jobs: if: always() with: name: Publish SpotBugs - path: '**/build/reports/spotbugs/*.html' \ No newline at end of file + path: '**/build/reports/spotbugs/*.html' diff --git a/.github/workflows/beam_PreCommit_Spotless.yml b/.github/workflows/beam_PreCommit_Spotless.yml index 145e31c4448a..003d5754a7da 100644 --- a/.github/workflows/beam_PreCommit_Spotless.yml +++ b/.github/workflows/beam_PreCommit_Spotless.yml @@ -76,7 +76,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Spotless PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,4 +101,4 @@ jobs: uses: jwgmeligmeyling/checkstyle-github-action@v1 if: always() with: - path: '**/build/reports/checkstyle/*.xml' \ No newline at end of file + path: '**/build/reports/checkstyle/*.xml' diff --git a/.github/workflows/beam_PreCommit_Typescript.yml b/.github/workflows/beam_PreCommit_Typescript.yml index 1fe8898d6d8e..4fef4386e816 100644 --- a/.github/workflows/beam_PreCommit_Typescript.yml +++ b/.github/workflows/beam_PreCommit_Typescript.yml @@ -72,7 +72,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Typescript PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,4 +89,4 @@ jobs: - name: run typescriptPreCommit script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :typescriptPreCommit \ No newline at end of file + gradle-command: :typescriptPreCommit diff --git a/.github/workflows/beam_PreCommit_Website.yml b/.github/workflows/beam_PreCommit_Website.yml index 103735e0b151..c8fb87130743 100644 --- a/.github/workflows/beam_PreCommit_Website.yml +++ b/.github/workflows/beam_PreCommit_Website.yml @@ -71,7 +71,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Website PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -85,4 +87,4 @@ jobs: - name: run websitePreCommit script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :websitePreCommit \ No newline at end of file + gradle-command: :websitePreCommit diff --git a/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml b/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml index 150cfe06bad3..8d90f76c6a14 100644 --- a/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml +++ b/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml @@ -73,7 +73,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Website_Stage_GCS PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..6b95073dc3a0 100644 --- a/.github/workflows/beam_PreCommit_Whitespace.yml +++ b/.github/workflows/beam_PreCommit_Whitespace.yml @@ -70,7 +70,9 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Whitespace PreCommit' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 959f36234d70..fa94a2b13204 100644 --- a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml @@ -92,7 +92,9 @@ 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 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,7 +104,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/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 7d17fd2140c9..cf36408537bf 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -65,19 +65,22 @@ 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') || 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: job_name: ["beam_PreCommit_Yaml_Xlang_Direct"] job_phrase: ["Run Yaml_Xlang_Direct PreCommit"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,11 +90,12 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: '17' 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=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/.github/workflows/beam_Prober_CommunityMetrics.yml b/.github/workflows/beam_Prober_CommunityMetrics.yml index 7a37a93fdfc2..d4f055eb13ae 100644 --- a/.github/workflows/beam_Prober_CommunityMetrics.yml +++ b/.github/workflows/beam_Prober_CommunityMetrics.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_Prober_CommunityMetrics"] job_phrase: ["Run Community Metrics Prober"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -76,4 +78,4 @@ jobs: with: gradle-command: :communityMetricsProber arguments: | - --rerun-tasks \ No newline at end of file + --rerun-tasks diff --git a/.github/workflows/beam_Publish_BeamMetrics.yml b/.github/workflows/beam_Publish_BeamMetrics.yml index db2e791c7dbd..99c47429892a 100644 --- a/.github/workflows/beam_Publish_BeamMetrics.yml +++ b/.github/workflows/beam_Publish_BeamMetrics.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_PostCommit_BeamMetrics_Publish"] job_phrase: ["Run Beam Metrics Deployment"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 96648c6b79bd..e0d3f662bf6d 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 }} @@ -79,7 +74,9 @@ jobs: - "python:container:ml:py313:docker" - "java:expansion-service:container:docker" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository @@ -93,13 +90,26 @@ jobs: # This is needed to run pipelines that use the default environment at HEAD, for example, when a # pipeline uses an expansion service built from HEAD. run: | - BEAM_VERSION_LINE=$(cat gradle.properties | grep "sdk_version") - echo "BEAM_VERSION=${BEAM_VERSION_LINE#*sdk_version=}" >> $GITHUB_ENV + BEAM_VERSION_LINE=$(grep -m1 '^sdk_version=' gradle.properties || true) + if [ -z "$BEAM_VERSION_LINE" ]; then + echo "Could not find sdk_version in gradle.properties" + exit 1 + fi + + BEAM_VERSION="${BEAM_VERSION_LINE#sdk_version=}" + + # Prevent environment file injection via CR/LF. + if [[ "$BEAM_VERSION" =~ [$'\r\n'] ]]; then + echo "Invalid sdk_version: contains newline characters" + exit 1 + fi + + printf 'BEAM_VERSION=%s\n' "$BEAM_VERSION" >> "$GITHUB_ENV" - name: Set latest tag only on master branch 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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - name: Authenticate on GCP uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 with: @@ -114,7 +124,7 @@ jobs: if: ${{ startsWith(matrix.container_task, 'java') }} uses: ./.github/actions/setup-environment-action with: - java-version: 11 + java-version: default - name: Setup Python environment if: ${{ startsWith(matrix.container_task, 'python') }} uses: ./.github/actions/setup-environment-action diff --git a/.github/workflows/beam_Publish_Docker_Snapshots.yml b/.github/workflows/beam_Publish_Docker_Snapshots.yml index d4aa75286116..414901df944a 100644 --- a/.github/workflows/beam_Publish_Docker_Snapshots.yml +++ b/.github/workflows/beam_Publish_Docker_Snapshots.yml @@ -61,7 +61,9 @@ jobs: job_name: ["beam_Publish_Docker_Snapshots"] job_phrase: ["Publish Docker Snapshots"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -84,10 +86,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.2 uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:1.17: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/beam_Publish_Python_VLLM_Image.yml b/.github/workflows/beam_Publish_Python_VLLM_Image.yml new file mode 100644 index 000000000000..17d681d955cd --- /dev/null +++ b/.github/workflows/beam_Publish_Python_VLLM_Image.yml @@ -0,0 +1,77 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT 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 + with: + persist-credentials: false + - 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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c + - 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 \ + . diff --git a/.github/workflows/beam_Publish_Website.yml b/.github/workflows/beam_Publish_Website.yml index 51bed6a3eff3..b16b19bf5891 100644 --- a/.github/workflows/beam_Publish_Website.yml +++ b/.github/workflows/beam_Publish_Website.yml @@ -53,7 +53,9 @@ jobs: timeout-minutes: 30 name: beam_Publish_Website steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: true - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -69,4 +71,6 @@ 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. + with: + persist-credentials: false diff --git a/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml b/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml index 93f147626b7a..2752d879cbd3 100644 --- a/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml +++ b/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml @@ -61,7 +61,9 @@ jobs: job_name: ["beam_Python_CostBenchmark_Dataflow"] job_phrase: ["Run Python Dataflow Cost Benchmarks"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,4 +106,4 @@ jobs: -Prunner=DataflowRunner \ -PpythonVersion=3.10 \ -PloadTest.requirementsTxtFile=apache_beam/ml/inference/tensorflow_tests_requirements.txt \ - '-PloadTest.args=${{ env.beam_Python_Cost_Benchmarks_Dataflow_test_arguments_2 }} --job_name=benchmark-tests-tf-mnist-classification-python-${{env.NOW_UTC}} --input_file=gs://apache-beam-ml/testing/inputs/it_mnist_data.csv --output_file=gs://temp-storage-for-end-to-end-tests/inference/result_tf_mnist-${{env.NOW_UTC}}.txt --model=gs://apache-beam-ml/models/tensorflow/mnist/' \ \ No newline at end of file + '-PloadTest.args=${{ env.beam_Python_Cost_Benchmarks_Dataflow_test_arguments_2 }} --job_name=benchmark-tests-tf-mnist-classification-python-${{env.NOW_UTC}} --input_file=gs://apache-beam-ml/testing/inputs/it_mnist_data.csv --output_file=gs://temp-storage-for-end-to-end-tests/inference/result_tf_mnist-${{env.NOW_UTC}}.txt --model=gs://apache-beam-ml/models/tensorflow/mnist/' \ diff --git a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml index 9ce1dec8ef7b..6636f70197e6 100644 --- a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml +++ b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml @@ -62,8 +62,12 @@ jobs: startsWith(github.event.comment.body, 'Run Python ValidatesContainer Dataflow ARM') runs-on: ubuntu-22.04 + # Python 3.14 multiarch container builds compile many deps from source on arm64. + timeout-minutes: ${{ matrix.python_version == '3.14' && 300 || 120 }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository @@ -84,7 +88,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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_Release_NightlySnapshot.yml b/.github/workflows/beam_Release_NightlySnapshot.yml index 38c2f635d2c5..c9c41fd9ebc1 100644 --- a/.github/workflows/beam_Release_NightlySnapshot.yml +++ b/.github/workflows/beam_Release_NightlySnapshot.yml @@ -51,7 +51,9 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..51537049f7a5 100644 --- a/.github/workflows/beam_Release_Python_NightlySnapshot.yml +++ b/.github/workflows/beam_Release_Python_NightlySnapshot.yml @@ -53,7 +53,9 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -77,4 +79,4 @@ jobs: run: | bash sdks/python/scripts/run_snapshot_publish.sh env: - WORKSPACE: ${{ github.workspace }} \ No newline at end of file + WORKSPACE: ${{ github.workspace }} diff --git a/.github/workflows/beam_StressTests_Java_BigQueryIO.yml b/.github/workflows/beam_StressTests_Java_BigQueryIO.yml index 690c4bdc8900..8f1ef8f034ac 100644 --- a/.github/workflows/beam_StressTests_Java_BigQueryIO.yml +++ b/.github/workflows/beam_StressTests_Java_BigQueryIO.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_StressTests_Java_BigQueryIO"] job_phrase: ["Run Stress Tests Java BigQueryIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,4 +76,4 @@ jobs: - name: run BigQuery StressTest Large uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :it:google-cloud-platform:BigQueryStressTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_stress_test_bigquery" \ No newline at end of file + gradle-command: :it:google-cloud-platform:BigQueryStressTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_stress_test_bigquery" diff --git a/.github/workflows/beam_StressTests_Java_BigTableIO.yml b/.github/workflows/beam_StressTests_Java_BigTableIO.yml index f50f4499e3e4..1ddb10e84439 100644 --- a/.github/workflows/beam_StressTests_Java_BigTableIO.yml +++ b/.github/workflows/beam_StressTests_Java_BigTableIO.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_StressTests_Java_BigTableIO"] job_phrase: ["Run Stress Tests Java BigTableIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,4 +76,4 @@ jobs: - name: run BigTable StressTest Large uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :it:google-cloud-platform:BigTableStressTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_stress_test_bigtable" \ No newline at end of file + gradle-command: :it:google-cloud-platform:BigTableStressTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_stress_test_bigtable" diff --git a/.github/workflows/beam_StressTests_Java_KafkaIO.yml b/.github/workflows/beam_StressTests_Java_KafkaIO.yml index d4dc1a20e918..9710f36daca2 100644 --- a/.github/workflows/beam_StressTests_Java_KafkaIO.yml +++ b/.github/workflows/beam_StressTests_Java_KafkaIO.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_StressTests_Java_KafkaIO"] job_phrase: ["Run Stress Tests Java KafkaIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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..c599058f28cd 100644 --- a/.github/workflows/beam_StressTests_Java_PubSubIO.yml +++ b/.github/workflows/beam_StressTests_Java_PubSubIO.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_StressTests_Java_PubSubIO"] job_phrase: ["Run Stress Tests Java PubSubIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,4 +76,4 @@ jobs: - name: run PubSub StressTest Large uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :it:google-cloud-platform:PubSubStressTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_stress_test_pubsub" \ No newline at end of file + gradle-command: :it:google-cloud-platform:PubSubStressTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_stress_test_pubsub" diff --git a/.github/workflows/beam_StressTests_Java_SpannerIO.yml b/.github/workflows/beam_StressTests_Java_SpannerIO.yml index cb343cb70877..3f53c335799c 100644 --- a/.github/workflows/beam_StressTests_Java_SpannerIO.yml +++ b/.github/workflows/beam_StressTests_Java_SpannerIO.yml @@ -62,7 +62,9 @@ jobs: job_name: ["beam_StressTests_Java_SpannerIO"] job_phrase: ["Run Stress Tests Java SpannerIO"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,4 +76,4 @@ jobs: - name: run Spanner StressTest Large uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :it:google-cloud-platform:SpannerStressTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_stress_test_spanner" \ No newline at end of file + gradle-command: :it:google-cloud-platform:SpannerStressTestLarge --info -DinfluxHost="http://10.128.0.96:8086" -DinfluxDatabase="beam_test_metrics" -DinfluxMeasurement="java_stress_test_spanner" diff --git a/.github/workflows/beam_Upgrade_GCP_BOM.yml b/.github/workflows/beam_Upgrade_GCP_BOM.yml new file mode 100644 index 000000000000..480fb5230956 --- /dev/null +++ b/.github/workflows/beam_Upgrade_GCP_BOM.yml @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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: Upgrade GCP Libraries BOM + +on: + schedule: + - cron: "0 0 * * 0" # Weekly on Sundays at 00:00 UTC + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + checks: read + issues: read + statuses: read + +concurrency: + group: '${{ github.workflow }} @ ${{ github.ref }}' + cancel-in-progress: true + +jobs: + upgrade_gcp_bom: + runs-on: [self-hosted, ubuntu-24.04, main] + name: Upgrade GCP BOM + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Setup environment + uses: ./.github/actions/setup-environment-action + with: + python-version: 3.11 + java-version: default + go-version: default + - name: Check if new BOM is available + id: check_bom + run: python3 scripts/tools/bomupgrader.py --check + - name: Run bomupgrader + if: steps.check_bom.outputs.should_upgrade == 'true' + run: python3 scripts/tools/bomupgrader.py ${STEPS_CHECK_BOM_OUTPUTS_LATEST_VERSION} + env: + STEPS_CHECK_BOM_OUTPUTS_LATEST_VERSION: ${{ steps.check_bom.outputs.latest_version }} + - name: Install gh cli + if: steps.check_bom.outputs.should_upgrade == 'true' + uses: ./.github/actions/setup-gh-cli-linux + - name: Set git config + if: steps.check_bom.outputs.should_upgrade == 'true' + run: | + git config user.name $GITHUB_ACTOR + git config user.email actions@"$RUNNER_NAME".local + - name: Commit Changes and create PR + if: steps.check_bom.outputs.should_upgrade == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_EVENT: ${{ github.event_name }} + LATEST_VER: ${{ steps.check_bom.outputs.latest_version }} + CURRENT_VER: ${{ steps.check_bom.outputs.current_version }} + run: | + # Take the current date, subtract from a release cut date in the past (June 24, 2026), + # then get the num days % 42 (our release cadence is 42 days). + # This will ensure it only runs the week after a release branch has been cut. + days_diff=$(( ($(date +%s) - $(date --date="260624" +%s) )/(60*60*24)%42 )) + if [[ $GH_EVENT != 'workflow_dispatch' && $days_diff -gt 6 ]]; then + echo "Exiting early. We only update dependencies the week after we cut the release" + exit 0 + fi + branchName=upgrade_gcp_bom_${LATEST_VER//./_} + git checkout -b $branchName + git add -A + git diff-index --quiet HEAD || gitdiff=$? || echo $? + if [[ $gitDiff != 0 ]]; then + echo "Changes are ready to commit" + git commit -m "Upgrade GCP Libraries BOM to ${LATEST_VER}" --quiet + git push origin $branchName --quiet + + PR_BODY="This PR was created by automation. It upgrades the Google Cloud Platform Libraries BOM from **${CURRENT_VER}** to **${LATEST_VER}** and updates Netty, gRPC, Arrow, Gax, Protobuf, and OpenTelemetry versions to match. + + Please review the changes and merge if all tests pass." + + GITHUB_PR_URL=$(gh pr create --title "Upgrade GCP Libraries BOM to ${LATEST_VER}" --body "$PR_BODY" --label "dependencies" --base master) + echo "Link of the new PR: $GITHUB_PR_URL" + else + echo "No changes on the files" + fi diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index 3304c189361d..978b546b8068 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -61,12 +61,13 @@ 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 + persist-credentials: true - name: Install Java 11 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | @@ -97,7 +98,9 @@ jobs: - name: Configure git run: git config credential.helper store - name: Stage Java Artifacts into Maven - run: ./gradlew publish -Psigning.gnupg.keyName=${{steps.import_gpg.outputs.fingerprint}} -PisRelease -Pjava21Home=$JAVA_HOME_21_X64 --no-daemon --no-parallel + run: ./gradlew publish -Psigning.gnupg.keyName=${STEPS_IMPORT_GPG_OUTPUTS_FINGERPRINT} -PisRelease -Pjava21Home=$JAVA_HOME_21_X64 --no-daemon --no-parallel + env: + STEPS_IMPORT_GPG_OUTPUTS_FINGERPRINT: ${{steps.import_gpg.outputs.fingerprint}} stage_java_source: @@ -111,16 +114,19 @@ jobs: echo "::add-mask::$APACHE_PASS" - name: Validate apache id/password run: | - if [ "${{ github.event.inputs.APACHE_ID }}" == "" ] + if [ "${GITHUB_EVENT_INPUTS_APACHE_ID}" == "" ] then echo "Must provide an apache id to stage artifacts to https://dist.apache.org/repos/dist/dev/beam/" fi - if [ "${{ github.event.inputs.APACHE_PASSWORD }}" == "" ] + if [ "${GITHUB_EVENT_INPUTS_APACHE_PASSWORD}" == "" ] then echo "Must provide an apache password to stage artifacts to https://dist.apache.org/repos/dist/dev/beam/" fi + env: + GITHUB_EVENT_INPUTS_APACHE_ID: ${{ github.event.inputs.APACHE_ID }} + GITHUB_EVENT_INPUTS_APACHE_PASSWORD: ${{ github.event.inputs.APACHE_PASSWORD }} - name: Install Java 11 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '11' @@ -132,13 +138,13 @@ jobs: - name: stage source run: | svn co https://dist.apache.org/repos/dist/dev/beam - mkdir -p beam/${{ github.event.inputs.RELEASE }} - cd beam/${{ github.event.inputs.RELEASE }} - RC_DIR="beam-${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" + mkdir -p beam/${GITHUB_EVENT_INPUTS_RELEASE} + cd beam/${GITHUB_EVENT_INPUTS_RELEASE} + RC_DIR="beam-${GITHUB_EVENT_INPUTS_RELEASE}-RC${GITHUB_EVENT_INPUTS_RC}" RC_ZIP="${RC_DIR}.zip" - RELEASE_DIR="beam-${{ github.event.inputs.RELEASE }}" - RC_TAG="v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" - SOURCE_RELEASE_ZIP="apache-beam-${{ github.event.inputs.RELEASE }}-source-release.zip" + RELEASE_DIR="beam-${GITHUB_EVENT_INPUTS_RELEASE}" + RC_TAG="v${GITHUB_EVENT_INPUTS_RELEASE}-RC${GITHUB_EVENT_INPUTS_RC}" + SOURCE_RELEASE_ZIP="apache-beam-${GITHUB_EVENT_INPUTS_RELEASE}-source-release.zip" # Check whether there is an existing dist dir if (svn ls "${SOURCE_RELEASE_ZIP}"); then echo "Removing existing ${SOURCE_RELEASE_ZIP}." @@ -155,14 +161,20 @@ jobs: rm -r "$RELEASE_DIR" echo "----Signing Source Release ${SOURCE_RELEASE_ZIP}-----" - gpg --local-user "${{steps.import_gpg.outputs.name}}" --armor --batch --yes --detach-sig "${SOURCE_RELEASE_ZIP}" + gpg --local-user "${STEPS_IMPORT_GPG_OUTPUTS_NAME}" --armor --batch --yes --detach-sig "${SOURCE_RELEASE_ZIP}" echo "----Creating Hash Value for ${SOURCE_RELEASE_ZIP}----" sha512sum ${SOURCE_RELEASE_ZIP} > ${SOURCE_RELEASE_ZIP}.sha512 svn add --force . svn status - svn commit -m "Staging Java artifacts for Apache Beam ${{ github.event.inputs.RELEASE }} RC${{ github.event.inputs.RC }}" --non-interactive --username "${{ github.event.inputs.APACHE_ID }}" --password "${{ github.event.inputs.APACHE_PASSWORD }}" + svn commit -m "Staging Java artifacts for Apache Beam ${GITHUB_EVENT_INPUTS_RELEASE} RC${GITHUB_EVENT_INPUTS_RC}" --non-interactive --username "${GITHUB_EVENT_INPUTS_APACHE_ID}" --password "${GITHUB_EVENT_INPUTS_APACHE_PASSWORD}" + env: + GITHUB_EVENT_INPUTS_RELEASE: ${{ github.event.inputs.RELEASE }} + GITHUB_EVENT_INPUTS_RC: ${{ github.event.inputs.RC }} + STEPS_IMPORT_GPG_OUTPUTS_NAME: ${{steps.import_gpg.outputs.name}} + GITHUB_EVENT_INPUTS_APACHE_ID: ${{ github.event.inputs.APACHE_ID }} + GITHUB_EVENT_INPUTS_APACHE_PASSWORD: ${{ github.event.inputs.APACHE_PASSWORD }} stage_python_artifacts: if: ${{ fromJson(github.event.inputs.STAGE).python_artifacts == 'yes'}} @@ -170,7 +182,9 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: true - name: Mask Apache Password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -178,14 +192,17 @@ jobs: echo "::add-mask::$APACHE_PASS" - name: Mask apache id/password run: | - if [ "${{ github.event.inputs.APACHE_ID }}" == "" ] + if [ "${GITHUB_EVENT_INPUTS_APACHE_ID}" == "" ] then echo "Must provide an apache id to stage artifacts to https://dist.apache.org/repos/dist/dev/beam/" fi - if [ "${{ github.event.inputs.APACHE_PASSWORD }}" == "" ] + if [ "${GITHUB_EVENT_INPUTS_APACHE_PASSWORD}" == "" ] then echo "Must provide an apache password to stage artifacts to https://dist.apache.org/repos/dist/dev/beam/" fi + env: + GITHUB_EVENT_INPUTS_APACHE_ID: ${{ github.event.inputs.APACHE_ID }} + GITHUB_EVENT_INPUTS_APACHE_PASSWORD: ${{ github.event.inputs.APACHE_PASSWORD }} - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -209,6 +226,9 @@ jobs: SCRIPT_DIR: release/src/main/scripts GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SVN_ARTIFACTS_DIR: "beam/${{ github.event.inputs.RELEASE }}/python" + STEPS_IMPORT_GPG_OUTPUTS_NAME: ${{steps.import_gpg.outputs.name}} + GITHUB_EVENT_INPUTS_APACHE_ID: ${{ github.event.inputs.APACHE_ID }} + GITHUB_EVENT_INPUTS_APACHE_PASSWORD: ${{ github.event.inputs.APACHE_PASSWORD }} run: | svn co https://dist.apache.org/repos/dist/dev/beam mkdir -p "${SVN_ARTIFACTS_DIR}" @@ -231,7 +251,7 @@ jobs: sha512sum -c "apache_beam-${RELEASE}.tar.gz.sha512" echo "------Signing Source Release apache_beam-${RELEASE}.tar.gz------" - gpg --local-user "${{steps.import_gpg.outputs.name}}" --armor --detach-sig "apache_beam-${RELEASE}.tar.gz" + gpg --local-user "${STEPS_IMPORT_GPG_OUTPUTS_NAME}" --armor --detach-sig "apache_beam-${RELEASE}.tar.gz" for artifact in *.whl; do echo "----------Checking Hash Value for ${artifact} wheel-----------" @@ -240,13 +260,13 @@ jobs: for artifact in *.whl; do echo "------------------Signing ${artifact} wheel-------------------" - gpg --local-user "${{steps.import_gpg.outputs.name}}" --armor --batch --yes --detach-sig "${artifact}" + gpg --local-user "${STEPS_IMPORT_GPG_OUTPUTS_NAME}" --armor --batch --yes --detach-sig "${artifact}" done cd .. svn add --force python svn status - svn commit -m "Staging Python artifacts for Apache Beam ${RELEASE} RC${RC_NUM}" --non-interactive --username "${{ github.event.inputs.APACHE_ID }}" --password "${{ github.event.inputs.APACHE_PASSWORD }}" + svn commit -m "Staging Python artifacts for Apache Beam ${RELEASE} RC${RC_NUM}" --non-interactive --username "${GITHUB_EVENT_INPUTS_APACHE_ID}" --password "${GITHUB_EVENT_INPUTS_APACHE_PASSWORD}" stage_docker: @@ -270,35 +290,41 @@ 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 + persist-credentials: true - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - - name: Install Java 11 - uses: actions/setup-java@v4 + - name: Install Java + uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '11' + java-version: | + 21 + 11 - name: Install Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: '3.10' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - 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 # settings.xml file run: rm ~/.m2/settings.xml || true - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Push docker images - run: ./gradlew ${{ matrix.images_to_publish.gradle_task }} -PisRelease -Pdocker-pull-licenses -Pprune-images ${{ matrix.images_to_publish.include_skip_flags }} -Pdocker-tag=${{ github.event.inputs.RELEASE }}rc${{ github.event.inputs.RC }} --no-daemon --no-parallel + run: ./gradlew ${{ matrix.images_to_publish.gradle_task }} -PisRelease -Pdocker-pull-licenses -Pprune-images ${{ matrix.images_to_publish.include_skip_flags }} -Pdocker-tag=${GITHUB_EVENT_INPUTS_RELEASE}rc${GITHUB_EVENT_INPUTS_RC} -Pjava21Home=$JAVA_HOME_21_X64 --no-daemon --no-parallel + env: + GITHUB_EVENT_INPUTS_RELEASE: ${{ github.event.inputs.RELEASE }} + GITHUB_EVENT_INPUTS_RC: ${{ github.event.inputs.RC }} beam_site_pr: if: ${{ fromJson(github.event.inputs.STAGE).beam_site_pr == 'yes'}} @@ -315,28 +341,30 @@ 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 + persist-credentials: true - name: Checkout Beam Site Repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: apache/beam-site path: beam-site token: ${{ github.event.inputs.REPO_TOKEN }} ref: release-docs + persist-credentials: true - name: Install Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: '3.10' - name: Install node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: '16' - name: Install Java 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' @@ -380,31 +408,33 @@ jobs: git checkout -b $BRANCH_NAME release-docs echo "..........Copying generated javadoc into beam-site.........." - cp -r ${BEAM_ROOT_DIR}/sdks/java/javadoc/build/docs/javadoc/ javadoc/${{ github.event.inputs.RELEASE }} + cp -r ${BEAM_ROOT_DIR}/sdks/java/javadoc/build/docs/javadoc/ javadoc/${GITHUB_EVENT_INPUTS_RELEASE} # Update current symlink to point to the latest release unlink javadoc/current - ln -s ${{ github.event.inputs.RELEASE }} javadoc/current + ln -s ${GITHUB_EVENT_INPUTS_RELEASE} javadoc/current echo "............Copying generated pydoc into beam-site.........." - cp -r ${BEAM_ROOT_DIR}/sdks/python/target/docs/_build pydoc/${{ github.event.inputs.RELEASE }} + cp -r ${BEAM_ROOT_DIR}/sdks/python/target/docs/_build pydoc/${GITHUB_EVENT_INPUTS_RELEASE} # Update current symlink to point to the latest release unlink pydoc/current - ln -s ${{ github.event.inputs.RELEASE }} pydoc/current + ln -s ${GITHUB_EVENT_INPUTS_RELEASE} pydoc/current echo "............Copying generated typedoc into beam-site.........." mkdir -p typedoc - cp -r ${BEAM_ROOT_DIR}/sdks/typescript/docs typedoc/${{ github.event.inputs.RELEASE }} + cp -r ${BEAM_ROOT_DIR}/sdks/typescript/docs typedoc/${GITHUB_EVENT_INPUTS_RELEASE} # Update current symlink to point to the latest release unlink typedoc/current | true - ln -s ${{ github.event.inputs.RELEASE }} typedoc/current + ln -s ${GITHUB_EVENT_INPUTS_RELEASE} typedoc/current echo "............Copying generated yamldoc into beam-site.........." - mkdir -p yamldoc/${{ github.event.inputs.RELEASE }} - cp -r ${BEAM_ROOT_DIR}/sdks/python/build/yaml-ref.html yamldoc/${{ github.event.inputs.RELEASE }}/index.html - cp -r ${BEAM_ROOT_DIR}/sdks/python/build/yaml-examples.html yamldoc/${{ github.event.inputs.RELEASE }}/examples.html + mkdir -p yamldoc/${GITHUB_EVENT_INPUTS_RELEASE} + cp -r ${BEAM_ROOT_DIR}/sdks/python/build/yaml-ref.html yamldoc/${GITHUB_EVENT_INPUTS_RELEASE}/index.html + cp -r ${BEAM_ROOT_DIR}/sdks/python/build/yaml-examples.html yamldoc/${GITHUB_EVENT_INPUTS_RELEASE}/examples.html # Update current symlink to point to the latest release unlink yamldoc/current | true - ln -s ${{ github.event.inputs.RELEASE }} yamldoc/current + ln -s ${GITHUB_EVENT_INPUTS_RELEASE} yamldoc/current + env: + GITHUB_EVENT_INPUTS_RELEASE: ${{ github.event.inputs.RELEASE }} - name: Create commit on beam-site branch working-directory: beam run: | @@ -414,8 +444,10 @@ jobs: git config user.name $GITHUB_ACTOR git config user.email actions@"$RUNNER_NAME".local git add -A - git commit -m "Update beam-site for release ${{ github.event.inputs.RELEASE }}." -m "Content generated from commit ${RELEASE_COMMIT}." + git commit -m "Update beam-site for release ${GITHUB_EVENT_INPUTS_RELEASE}." -m "Content generated from commit ${RELEASE_COMMIT}." git push -f --set-upstream origin $BRANCH_NAME + env: + GITHUB_EVENT_INPUTS_RELEASE: ${{ github.event.inputs.RELEASE }} - name: Create beam-site PR working-directory: beam-site env: @@ -431,10 +463,11 @@ 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 + persist-credentials: true - name: Mask Apache Password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -442,15 +475,15 @@ jobs: echo "::add-mask::$APACHE_PASS" - name: Mask apache id/password run: | - if [ "${{ github.event.inputs.APACHE_ID }}" == "" ] + if [ "${GITHUB_EVENT_INPUTS_APACHE_ID}" == "" ] then echo "Must provide an apache id to stage artifacts to https://dist.apache.org/repos/dist/dev/beam/" fi - if [ "${{ github.event.inputs.APACHE_PASSWORD }}" == "" ] + if [ "${GITHUB_EVENT_INPUTS_APACHE_PASSWORD}" == "" ] then echo "Must provide an apache password to stage artifacts to https://dist.apache.org/repos/dist/dev/beam/" fi - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: '1.26' - name: Import GPG key @@ -466,6 +499,9 @@ jobs: RELEASE: "${{ github.event.inputs.RELEASE }}" GH_TOKEN: ${{ github.event.inputs.REPO_TOKEN }} SVN_ARTIFACTS_DIR: "beam/${{ github.event.inputs.RELEASE }}/prism" + STEPS_IMPORT_GPG_OUTPUTS_NAME: ${{steps.import_gpg.outputs.name}} + GITHUB_EVENT_INPUTS_APACHE_ID: ${{ github.event.inputs.APACHE_ID }} + GITHUB_EVENT_INPUTS_APACHE_PASSWORD: ${{ github.event.inputs.APACHE_PASSWORD }} run: | # TODO stop the action if a non-prerelease release exists # TODO create the draft release automatically if none exist. @@ -522,7 +558,7 @@ jobs: zip -r $ZIP_NAME $ARTIFACT # Sign and hash - gpg --local-user "${{steps.import_gpg.outputs.name}}" --armor --batch --yes --detach-sig $ZIP_NAME + gpg --local-user "${STEPS_IMPORT_GPG_OUTPUTS_NAME}" --armor --batch --yes --detach-sig $ZIP_NAME sha512sum $ZIP_NAME > "${ZIP_NAME}.sha512" # Upload to the release. @@ -548,7 +584,7 @@ jobs: svn add --force --parents prism svn status - svn commit -m "Staging Prism artifacts for Apache Beam ${RELEASE} RC${RC_NUM}" --non-interactive --username "${{ github.event.inputs.APACHE_ID }}" --password "${{ github.event.inputs.APACHE_PASSWORD }}" + svn commit -m "Staging Prism artifacts for Apache Beam ${RELEASE} RC${RC_NUM}" --non-interactive --username "${GITHUB_EVENT_INPUTS_APACHE_ID}" --password "${GITHUB_EVENT_INPUTS_APACHE_PASSWORD}" managed_io_docs_pr: if: ${{ fromJson(github.event.inputs.STAGE).managed_io_docs_pr == 'yes'}} @@ -559,7 +595,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 @@ -567,14 +603,14 @@ jobs: token: ${{ github.event.inputs.REPO_TOKEN }} persist-credentials: true - name: Install Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: '3.10' - - name: Install Java 11 - uses: actions/setup-java@v4 + - name: Install Java 17 + uses: actions/setup-java@v5 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 @@ -596,8 +632,10 @@ jobs: working-directory: beam run: | mkdir -p ~/.apache_beam/cache/jars/ - cp sdks/java/io/expansion-service/build/libs/beam-sdks-java-io-expansion-service-${{ github.event.inputs.RELEASE }}.jar ~/.apache_beam/cache/jars/ - cp sdks/java/io/google-cloud-platform/expansion-service/build/libs/beam-sdks-java-io-google-cloud-platform-expansion-service-${{ github.event.inputs.RELEASE }}.jar ~/.apache_beam/cache/jars/ + cp sdks/java/io/expansion-service/build/libs/beam-sdks-java-io-expansion-service-${GITHUB_EVENT_INPUTS_RELEASE}.jar ~/.apache_beam/cache/jars/ + cp sdks/java/io/google-cloud-platform/expansion-service/build/libs/beam-sdks-java-io-google-cloud-platform-expansion-service-${GITHUB_EVENT_INPUTS_RELEASE}.jar ~/.apache_beam/cache/jars/ + env: + GITHUB_EVENT_INPUTS_RELEASE: ${{ github.event.inputs.RELEASE }} - name: Generate Managed IO Docs working-directory: beam/sdks/python run: | @@ -611,8 +649,11 @@ jobs: git config user.name $GITHUB_ACTOR git config user.email actions@"$RUNNER_NAME".local git add ${{ env.MANAGED_IO_DOCS_PATH }} - git commit --allow-empty -m "Update managed-io.md for release ${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}." + git commit --allow-empty -m "Update managed-io.md for release ${GITHUB_EVENT_INPUTS_RELEASE}-RC${GITHUB_EVENT_INPUTS_RC}." git push -f --set-upstream origin $BRANCH_NAME + env: + GITHUB_EVENT_INPUTS_RELEASE: ${{ github.event.inputs.RELEASE }} + GITHUB_EVENT_INPUTS_RC: ${{ github.event.inputs.RC }} - name: Create beam PR working-directory: beam env: diff --git a/.github/workflows/build_runner_image.yml b/.github/workflows/build_runner_image.yml index 7990ce0a4859..48f40e2a5c48 100644 --- a/.github/workflows/build_runner_image.yml +++ b/.github/workflows/build_runner_image.yml @@ -38,16 +38,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: GCloud Docker credential helper run: | gcloud auth configure-docker ${{env.docker_registry}} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c - name: Build and Load to docker - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ${{ env.working-directory }} load: true @@ -57,7 +58,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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: context: ${{ env.working-directory }} push: true diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 4db227385c77..2d0232d9f517 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -37,7 +37,8 @@ concurrency: cancel-in-progress: true env: - GCP_PATH: "gs://${{ secrets.GCP_PYTHON_WHEELS_BUCKET }}/${GITHUB_REF##*/}/${GITHUB_SHA}-${GITHUB_RUN_ID}/" + # Use github.* context; workflow env is not bash-evaluated. + GCP_PATH: "gs://${{ secrets.GCP_PYTHON_WHEELS_BUCKET }}/${{ github.ref_name }}/${{ github.sha }}-${{ github.run_id }}/" jobs: @@ -55,7 +56,9 @@ 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 + with: + persist-credentials: true - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -88,9 +91,11 @@ jobs: rc_num: ${{ steps.get_rc_version.outputs.RC_NUM }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: true - name: Install python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: '3.11' - name: Get tag @@ -100,9 +105,11 @@ jobs: - name: Check whether an -RC tag was applied to the commit. id: is_rc run: | - echo ${{ steps.get_tag.outputs.TAG }} > temp + echo ${STEPS_GET_TAG_OUTPUTS_TAG} > temp OUTPUT=$( if grep -e '-RC.' -q temp; then echo 1; else echo 0; fi) echo "is_rc=$OUTPUT" >> $GITHUB_OUTPUT + env: + STEPS_GET_TAG_OUTPUTS_TAG: ${{ steps.get_tag.outputs.TAG }} - name: Get RELEASE_VERSION and RC_NUM if: steps.is_rc.outputs.is_rc == 1 id: get_rc_version @@ -146,9 +153,12 @@ jobs: if: steps.is_rc.outputs.is_rc == 1 working-directory: ./sdks/python run: | - RELEASE_VERSION=${{ steps.get_rc_version.outputs.RELEASE_VERSION }} - RC_NUM=${{ steps.get_rc_version.outputs.RC_NUM }} + RELEASE_VERSION=${STEPS_GET_RC_VERSION_OUTPUTS_RELEASE_VERSION} + RC_NUM=${STEPS_GET_RC_VERSION_OUTPUTS_RC_NUM} sed -i -e "s/${RELEASE_VERSION}/${RELEASE_VERSION}rc${RC_NUM}/g" apache_beam/version.py + env: + STEPS_GET_RC_VERSION_OUTPUTS_RELEASE_VERSION: ${{ steps.get_rc_version.outputs.RELEASE_VERSION }} + STEPS_GET_RC_VERSION_OUTPUTS_RC_NUM: ${{ steps.get_rc_version.outputs.RC_NUM }} - name: Build RC source if: steps.is_rc.outputs.is_rc == 1 working-directory: ./sdks/python @@ -191,7 +201,7 @@ jobs: if: needs.check_env_variables.outputs.gcp-variables-set == 'true' && github.event_name != 'pull_request' steps: - name: Remove existing files on GCS bucket - run: gsutil rm -r ${{ env.GCP_PATH }} || true + run: gcloud storage rm -r ${GCP_PATH} || true upload_source_to_gcs: name: Upload python source distribution to GCS bucket @@ -207,7 +217,7 @@ jobs: name: source_zip path: source/ - name: Copy sources to GCS bucket - run: gsutil cp -r -a public-read source/* ${{ env.GCP_PATH }} + run: gcloud storage cp -r --predefined-acl=publicRead source/* ${GCP_PATH} build_wheels: name: Build python ${{matrix.py_version}} wheels on ${{matrix.os_python.arch}} for ${{ matrix.os_python.os }} @@ -242,10 +252,10 @@ jobs: name: source_rc${{ needs.build_source.outputs.rc_num }} path: apache-beam-source-rc - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: '3.11' - - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a + - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 if: ${{matrix.os_python.arch == 'aarch64'}} name: Set up QEMU - name: Install cibuildwheel @@ -320,7 +330,7 @@ jobs: merge-multiple: true path: wheelhouse/ - name: Copy wheels to GCS bucket - run: gsutil cp -r -a public-read wheelhouse/* ${{ env.GCP_PATH }} + run: gcloud storage cp -r --predefined-acl=publicRead wheelhouse/* ${GCP_PATH} - name: Create github action information file on GCS bucket run: | cat > github_action_info <> $GITHUB_OUTPUT + else + # Filter ALL_MATRIX entries keeping only entries for languages with path changes. + FILTERED=$(echo "$ALL_MATRIX" | jq -c '[.[] | select( + (.language == "actions" and "${{ steps.filter.outputs.actions }}" == "true") or + (.language == "go" and "${{ steps.filter.outputs.go }}" == "true") or + (.language == "java-kotlin" and "${{ steps.filter.outputs['java-kotlin'] }}" == "true") or + (.language == "javascript-typescript" and "${{ steps.filter.outputs['javascript-typescript'] }}" == "true") or + (.language == "python" and "${{ steps.filter.outputs.python }}" == "true") or + (.language == "rust" and "${{ steps.filter.outputs.rust }}" == "true") + )]') + echo "matrix=$FILTERED" >> $GITHUB_OUTPUT + fi + + analyze: + name: Analyze (${{ matrix.language }}) + needs: detect-changes + if: needs.detect-changes.outputs.matrix != '[]' + # 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: ${{ fromJSON(needs.detect-changes.outputs.matrix) }} + # 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@v7 + with: + persist-credentials: false + + - name: Set up Go + if: matrix.language == 'go' + uses: actions/setup-go@v7 + 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. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4.37.7 + 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: | + 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.37.7 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/cut_release_branch.yml b/.github/workflows/cut_release_branch.yml index c5da893ee5fd..08ab1b23d35a 100644 --- a/.github/workflows/cut_release_branch.yml +++ b/.github/workflows/cut_release_branch.yml @@ -61,7 +61,9 @@ jobs: exit 1 fi - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: true - name: Set git config run: | git config user.name $GITHUB_ACTOR @@ -110,13 +112,18 @@ jobs: exit 1 fi - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: true - name: Set git config run: | 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 diff --git a/.github/workflows/dask_runner_tests.yml b/.github/workflows/dask_runner_tests.yml index 674b1ca866fb..890293e85d25 100644 --- a/.github/workflows/dask_runner_tests.yml +++ b/.github/workflows/dask_runner_tests.yml @@ -40,9 +40,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Install python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: '3.10' - name: Build source @@ -69,9 +71,11 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Install python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.params.py_ver }} - name: Install tox diff --git a/.github/workflows/deploy_release_candidate_pypi.yaml b/.github/workflows/deploy_release_candidate_pypi.yaml index 9fed87d42304..7cd98ee6d61e 100644 --- a/.github/workflows/deploy_release_candidate_pypi.yaml +++ b/.github/workflows/deploy_release_candidate_pypi.yaml @@ -30,7 +30,9 @@ 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 + with: + persist-credentials: false - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -50,6 +52,8 @@ jobs: SCRIPT_DIR: release/src/main/scripts GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SVN_ARTIFACTS_DIR: "beam/${{ github.event.inputs.RELEASE }}/python" + GITHUB_EVENT_INPUTS_RC: ${{ github.event.inputs.RC }} + GITHUB_EVENT_INPUTS_PYPI_API_TOKEN: ${{ github.event.inputs.PYPI_API_TOKEN }} run: | git fetch --all --tags --prune RELEASE_COMMIT=$(git rev-list -n 1 $RC_TAG) @@ -61,14 +65,14 @@ jobs: --rc-tag "${RC_TAG}" \ --release-commit "${RELEASE_COMMIT}" \ --artifacts_dir "${PYTHON_ARTIFACTS_DIR}" \ - --rc_number "${{ github.event.inputs.RC }}" \ + --rc_number "${GITHUB_EVENT_INPUTS_RC}" \ --yes True cd "${PYTHON_ARTIFACTS_DIR}" ls - echo "------Checking Hash Value for apache_beam-${RELEASE}rc${{ github.event.inputs.RC }}.tar.gz-----" - sha512sum -c "apache_beam-${RELEASE}rc${{ github.event.inputs.RC }}.tar.gz.sha512" + echo "------Checking Hash Value for apache_beam-${RELEASE}rc${GITHUB_EVENT_INPUTS_RC}.tar.gz-----" + sha512sum -c "apache_beam-${RELEASE}rc${GITHUB_EVENT_INPUTS_RC}.tar.gz.sha512" for artifact in *.whl; do echo "----------Checking Hash Value for ${artifact} wheel-----------" @@ -83,4 +87,4 @@ jobs: echo "Will upload the following files to PyPI:" ls - twine upload --verbose * -u __token__ -p "${{ github.event.inputs.PYPI_API_TOKEN }}" + twine upload --verbose * -u __token__ -p "${GITHUB_EVENT_INPUTS_PYPI_API_TOKEN}" diff --git a/.github/workflows/finalize_release.yml b/.github/workflows/finalize_release.yml index 5979d91517f0..14275926a896 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 @@ -41,10 +44,14 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f 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 }}" @@ -91,7 +98,9 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Mask PyPi password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -114,25 +123,32 @@ 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 - name: Deploy to Pypi env: RELEASE: "${{ github.event.inputs.RELEASE }}" + GITHUB_EVENT_INPUTS_PYPI_API_TOKEN: ${{ github.event.inputs.PYPI_API_TOKEN }} run: | wget -e robots=off -r --no-parent -A tar.gz,whl "https://dist.apache.org/repos/dist/dev/beam/${RELEASE}/python" cd "dist.apache.org/repos/dist/dev/beam/${RELEASE}/python/" echo "Will upload the following files to PyPI:" ls - twine upload * -u __token__ -p "${{ github.event.inputs.PYPI_API_TOKEN }}" + twine upload * -u __token__ -p "${GITHUB_EVENT_INPUTS_PYPI_API_TOKEN}" push_git_tags: if: ${{github.event.inputs.TAG_RELEASE == 'yes'}} 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 + persist-credentials: true - name: Set git config run: | git config user.name $GITHUB_ACTOR @@ -147,6 +163,7 @@ jobs: VERSION_TAG: "v${{ github.event.inputs.RELEASE }}" RC_TAG: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" POST_RELEASE_BRANCH: "release-${{ github.event.inputs.RELEASE }}-postrelease" + STEPS_IMPORT_GPG_OUTPUTS_NAME: ${{steps.import_gpg.outputs.name}} run: | # Ensure local tags are in sync. If there's a mismatch, it will tell you. git fetch --all --tags --prune @@ -155,15 +172,15 @@ jobs: git rev-list $RC_TAG -n 1 # 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 tag "sdks/$VERSION_TAG" "$RC_TAG"^{} -m "Tagging release" --local-user="${STEPS_IMPORT_GPG_OUTPUTS_NAME}" + 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 tag "$VERSION_TAG" "$RC_TAG"^{} -m "Tagging release" --local-user="${STEPS_IMPORT_GPG_OUTPUTS_NAME}" + 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 @@ -172,7 +189,11 @@ 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 + persist-credentials: false - 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 733a15b2037a..754e0b2c8755 100644 --- a/.github/workflows/flaky_test_detection.yml +++ b/.github/workflows/flaky_test_detection.yml @@ -38,8 +38,10 @@ jobs: flaky-test-detection: runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-python@v7 with: python-version: 3.11 - run: pip install PyGithub diff --git a/.github/workflows/git_tag_released_version.yml b/.github/workflows/git_tag_released_version.yml index 3ca113de90de..3b6567c7e4b2 100644 --- a/.github/workflows/git_tag_released_version.yml +++ b/.github/workflows/git_tag_released_version.yml @@ -37,18 +37,26 @@ jobs: VERSION_PATH: ${{ github.event.inputs.VERSION_TAG }} steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Set git config run: | git config user.name $GITHUB_ACTOR git config user.email actions@"$RUNNER_NAME".local - name: Tag for Go SDK run: | - git tag -a sdks/${{ github.event.inputs.VERSION_TAG }} -m ${{ github.event.inputs.RC_TAG }} - git push https://github.com/apache/beam sdks/${{ github.event.inputs.VERSION_TAG }} + git tag -a sdks/${GITHUB_EVENT_INPUTS_VERSION_TAG} -m ${GITHUB_EVENT_INPUTS_RC_TAG} + git push https://github.com/apache/beam sdks/${GITHUB_EVENT_INPUTS_VERSION_TAG} + env: + GITHUB_EVENT_INPUTS_VERSION_TAG: ${{ github.event.inputs.VERSION_TAG }} + GITHUB_EVENT_INPUTS_RC_TAG: ${{ github.event.inputs.RC_TAG }} - name: Tag for repo root run: | - git tag -a ${{ github.event.inputs.VERSION_TAG }} -m ${{ github.event.inputs.RC_TAG }} - git push https://github.com/apache/beam ${{ github.event.inputs.VERSION_TAG }} + git tag -a ${GITHUB_EVENT_INPUTS_VERSION_TAG} -m ${GITHUB_EVENT_INPUTS_RC_TAG} + git push https://github.com/apache/beam ${GITHUB_EVENT_INPUTS_VERSION_TAG} + env: + GITHUB_EVENT_INPUTS_VERSION_TAG: ${{ github.event.inputs.VERSION_TAG }} + GITHUB_EVENT_INPUTS_RC_TAG: ${{ github.event.inputs.RC_TAG }} diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index 61c26be9cee3..5355f80810fe 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -40,9 +40,10 @@ jobs: name: Go Build steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 2 + persist-credentials: false - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/issue-tagger.yml b/.github/workflows/issue-tagger.yml index 831bae7325cd..a494c3c9b814 100644 --- a/.github/workflows/issue-tagger.yml +++ b/.github/workflows/issue-tagger.yml @@ -24,7 +24,9 @@ jobs: permissions: issues: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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/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/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_CPU_Batch.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_CPU_Batch.txt new file mode 100644 index 000000000000..37c47ca25542 --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_CPU_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. + +--runner=DataflowRunner +--project=apache-beam-testing +--region=us-central1 +--num_workers=5 +--max_num_workers=20 +--disk_size_gb=100 +--autoscaling_algorithm=THROUGHPUT_BASED +--staging_location=gs://temp-storage-for-perf-tests/loadtests +--temp_location=gs://temp-storage-for-perf-tests/loadtests +--requirements_file=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--metrics_table=mltransform_image_embedding_cpu_batch +--input_options={} +--influx_measurement=mltransform_image_embedding_cpu_batch +--mode=batch +--input=gs://apache-beam-ml/testing/inputs/openimage_50k_benchmark.txt +--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_image_embedding_cpu/artifacts +--output_table=apache-beam-testing:beam_run_inference.result_mltransform_image_embedding_cpu_batch +--pretrained_model_name=clip-ViT-B-32 +--device=CPU +--min_batch_size=8 +--max_batch_size=64 +--embedding_min_ram=16GB +--dataflow_service_options=enable_prime +--experiments=use_runner_v2 +--timeout_ms=3600000 diff --git a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_GPU_Batch.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_GPU_Batch.txt new file mode 100644 index 000000000000..274dc43f9416 --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_GPU_Batch.txt @@ -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. + +--runner=DataflowRunner +--project=apache-beam-testing +--region=us-central1 +--num_workers=5 +--max_num_workers=20 +--disk_size_gb=100 +--autoscaling_algorithm=THROUGHPUT_BASED +--staging_location=gs://temp-storage-for-perf-tests/loadtests +--temp_location=gs://temp-storage-for-perf-tests/loadtests +--requirements_file=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--metrics_table=mltransform_image_embedding_gpu_batch +--input_options={} +--influx_measurement=mltransform_image_embedding_gpu_batch +--mode=batch +--input=gs://apache-beam-ml/testing/inputs/openimage_50k_benchmark.txt +--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_image_embedding_gpu/artifacts +--output_table=apache-beam-testing:beam_run_inference.result_mltransform_image_embedding_gpu_batch +--pretrained_model_name=clip-ViT-B-32 +--device=GPU +--min_batch_size=8 +--max_batch_size=64 +--embedding_accelerator=type:nvidia-tesla-t4;count:1;install-nvidia-driver +--sdk_location=container +--sdk_container_image=us.gcr.io/apache-beam-testing/python-postcommit-it/tensor_rt:latest +--dataflow_service_options=enable_prime +--experiments=use_runner_v2 +--timeout_ms=3600000 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..993dd0820822 --- /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/examples/ml_transform/mltransform_one_hot_encoding_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/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Text_Embedding_Batch.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Text_Embedding_Batch.txt new file mode 100644 index 000000000000..1835dc260025 --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Text_Embedding_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. + +--runner=DataflowRunner +--project=apache-beam-testing +--region=us-central1 +--machine_type=n1-standard-4 +--num_workers=10 +--max_num_workers=50 +--disk_size_gb=100 +--autoscaling_algorithm=THROUGHPUT_BASED +--staging_location=gs://temp-storage-for-perf-tests/loadtests +--temp_location=gs://temp-storage-for-perf-tests/loadtests +--requirements_file=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--metrics_table=mltransform_text_embedding_batch +--input_options={} +--influx_measurement=mltransform_text_embedding_batch +--input_file=gs://apache-beam-ml/testing/inputs/sentences_50k.txt +--output=gs://temp-storage-for-perf-tests/loadtests/mltransform_text_embedding/results +--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_text_embedding/artifacts +--model_name=sentence-transformers/all-MiniLM-L6-v2 +--min_batch_size=16 +--max_batch_size=128 +--model_batch_size=32 +--device=CPU +--sdk_location=container +--sdk_container_image=us.gcr.io/apache-beam-testing/python-postcommit-it/tensor_rt:latest +--experiments=use_runner_v2 diff --git a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Captioning.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Captioning.txt new file mode 100644 index 000000000000..c58707383ef9 --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Captioning.txt @@ -0,0 +1,37 @@ +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 +--worker_machine_type=n1-standard-4 +--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 +--requirements_file=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--input_options={} +--influx_measurement=result_torch_inference_image_captioning +--input=gs://apache-beam-ml/testing/inputs/openimage_50k_benchmark.txt +--blip_model_name=Salesforce/blip-image-captioning-base +--blip_batch_size=4 +--num_captions=5 +--max_new_tokens=30 +--num_beams=5 +--clip_model_name=openai/clip-vit-base-patch32 +--clip_batch_size=8 +--clip_score_normalize=false +--runner=DataflowRunner +--experiments=use_runner_v2 diff --git a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Classification_Rightfit.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Classification_Rightfit.txt new file mode 100644 index 000000000000..1140ee5735e0 --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Classification_Rightfit.txt @@ -0,0 +1,41 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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-4 +--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 +--requirements_file=apache_beam/ml/inference/pytorch_rightfit_requirements.txt +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--influx_measurement=torch_inference_imagenet_stream_rightfit +--pretrained_model_name=efficientnet_b0 +--input_file=gs://apache-beam-ml/testing/inputs/openimage_50k_benchmark.txt +--runner=DataflowRunner +--mode=streaming +--input_mode=gcs_uris +--input_options={} +--pubsub_topic=projects/apache-beam-testing/topics/images_topic +--pubsub_subscription=projects/apache-beam-testing/subscriptions/images_subscription +--model_state_dict_path=gs://apache-beam-ml/models/efficientnet_b0_state_dict.pth +--image_size=224 +--top_k=5 +--inference_batch_size=16 +--window_sec=60 +--trigger_proc_time_sec=30 diff --git a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Object_Detection.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Object_Detection.txt new file mode 100644 index 000000000000..18f017ad0109 --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Object_Detection.txt @@ -0,0 +1,37 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 +--worker_machine_type=n1-standard-4 +--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 +--requirements_file=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--input_options={} +--influx_measurement=result_torch_inference_image_object_detection_batch +--pretrained_model_name=fasterrcnn_resnet50_fpn +--inference_batch_size=8 +--resize_shorter_side=800 +--score_threshold=0.5 +--max_detections=50 +--input=gs://apache-beam-ml/testing/inputs/openimage_50k_benchmark.txt +--model_state_dict_path=gs://apache-beam-ml/models/torchvision.detection.fasterrcnn_resnet50_fpn.pth +--runner=DataflowRunner +--experiments=use_runner_v2 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/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_MultipleKey.txt b/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_MultipleKey.txt index 504b464eb1f5..ec2c5c3e0403 100644 --- a/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_MultipleKey.txt +++ b/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_MultipleKey.txt @@ -16,8 +16,8 @@ --influx_measurement=go_batch_cogbk_1 --influx_namespace=flink ---input_options=''{\"num_records\":10000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":5,\"hot_key_fraction\":1}'' ---co_input_options=''{\"num_records\":1000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":1000,\"hot_key_fraction\":1}'' +--input_options=''{\"num_records\":1000000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":5,\"hot_key_fraction\":1}'' +--co_input_options=''{\"num_records\":100000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":1000,\"hot_key_fraction\":1}'' --iterations=1 --parallelism=5 --endpoint=localhost:8099 diff --git a/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_Reiteration_10KB.txt b/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_Reiteration_10KB.txt index 7eb9a3c80534..7698c7aa7c75 100644 --- a/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_Reiteration_10KB.txt +++ b/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_Reiteration_10KB.txt @@ -16,8 +16,8 @@ --influx_measurement=go_batch_cogbk_2 --influx_namespace=flink ---input_options=''{\"num_records\":10000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":1000,\"hot_key_fraction\":1}'' ---co_input_options=''{\"num_records\":1000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":10,\"hot_key_fraction\":1}'' +--input_options=''{\"num_records\":1000000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":100000,\"hot_key_fraction\":1}'' +--co_input_options=''{\"num_records\":100000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":1000,\"hot_key_fraction\":1}'' --iterations=4 --parallelism=5 --endpoint=localhost:8099 diff --git a/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_Reiteration_2MB.txt b/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_Reiteration_2MB.txt index ccf5ae7cbf28..c6b1f5fcc331 100644 --- a/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_Reiteration_2MB.txt +++ b/.github/workflows/load-tests-pipeline-options/go_CoGBK_Flink_Batch_Reiteration_2MB.txt @@ -16,8 +16,8 @@ --influx_measurement=go_batch_cogbk_3 --influx_namespace=flink ---input_options=''{\"num_records\":100,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":100,\"hot_key_fraction\":1}'' ---co_input_options=''{\"num_records\":10,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":10,\"hot_key_fraction\":1}'' +--input_options=''{\"num_records\":1000000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":1000,\"hot_key_fraction\":1}'' +--co_input_options=''{\"num_records\":100000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":1000,\"hot_key_fraction\":1}'' --iterations=4 --parallelism=5 --endpoint=localhost:8099 diff --git a/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_100kb.txt b/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_100kb.txt index f02e6984c81f..a188f8c09787 100644 --- a/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_100kb.txt +++ b/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_100kb.txt @@ -19,7 +19,7 @@ --iterations=1 --fanout=1 --parallelism=5 ---input_options=''{\"num_records\":50,\"key_size\":10000,\"value_size\":90000}'' +--input_options=''{\"num_records\":1000,\"key_size\":10000,\"value_size\":90000}'' --endpoint=localhost:8099 --environment_type=DOCKER --environment_config=gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest diff --git a/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Fanout_4.txt b/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Fanout_4.txt index 0042a9b80f38..4378d56a8f8c 100644 --- a/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Fanout_4.txt +++ b/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Fanout_4.txt @@ -19,7 +19,7 @@ --iterations=1 --fanout=4 --parallelism=16 ---input_options=''{\"num_records\":10000,\"key_size\":10,\"value_size\":90}'' +--input_options=''{\"num_records\":100000,\"key_size\":10,\"value_size\":90}'' --endpoint=localhost:8099 --environment_type=DOCKER --environment_config=gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest diff --git a/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Fanout_8.txt b/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Fanout_8.txt index fb14c2da58de..43292d577170 100644 --- a/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Fanout_8.txt +++ b/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Fanout_8.txt @@ -19,7 +19,7 @@ --iterations=1 --fanout=8 --parallelism=16 ---input_options=''{\"num_records\":10000,\"key_size\":10,\"value_size\":90}'' +--input_options=''{\"num_records\":100000,\"key_size\":10,\"value_size\":90}'' --endpoint=localhost:8099 --environment_type=DOCKER --environment_config=gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest diff --git a/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Reiteration_10KB.txt b/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Reiteration_10KB.txt index ee220853c60c..c4d33c21482a 100644 --- a/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Reiteration_10KB.txt +++ b/.github/workflows/load-tests-pipeline-options/go_GBK_Flink_Batch_Reiteration_10KB.txt @@ -19,7 +19,7 @@ --iterations=4 --fanout=1 --parallelism=5 ---input_options=''{\"num_records\":10000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":100,\"hot_key_fraction\":1}'' +--input_options=''{\"num_records\":1000000,\"key_size\":10,\"value_size\":90,\"num_hot_keys\":200,\"hot_key_fraction\":1}'' --endpoint=localhost:8099 --environment_type=DOCKER --environment_config=gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest diff --git a/.github/workflows/local_env_tests.yml b/.github/workflows/local_env_tests.yml index fdee2f3492ea..1ab4f8e1c77e 100644 --- a/.github/workflows/local_env_tests.yml +++ b/.github/workflows/local_env_tests.yml @@ -45,7 +45,9 @@ jobs: name: "Ubuntu run local environment shell script" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -62,7 +64,9 @@ jobs: name: "Mac run local environment shell script" runs-on: macos-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 3c2fa18e18d3..f204a84da260 100644 --- a/.github/workflows/playground_frontend_test.yml +++ b/.github/workflows/playground_frontend_test.yml @@ -45,10 +45,12 @@ jobs: FLUTTER_VERSION: '3.10.4' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: false - 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 }} @@ -89,7 +91,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/pr-bot-new-prs.yml b/.github/workflows/pr-bot-new-prs.yml index 14254c364746..9c924c423652 100644 --- a/.github/workflows/pr-bot-new-prs.yml +++ b/.github/workflows/pr-bot-new-prs.yml @@ -33,9 +33,11 @@ jobs: if: github.repository == 'apache/beam' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: true - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 16 - name: Install pr-bot npm dependencies diff --git a/.github/workflows/pr-bot-pr-updates.yml b/.github/workflows/pr-bot-pr-updates.yml index 2038823b2f4d..e3afd5b461b6 100644 --- a/.github/workflows/pr-bot-pr-updates.yml +++ b/.github/workflows/pr-bot-pr-updates.yml @@ -36,11 +36,12 @@ 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' + persist-credentials: true - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 16 - name: Install pr-bot npm dependencies diff --git a/.github/workflows/pr-bot-prs-needing-attention.yml b/.github/workflows/pr-bot-prs-needing-attention.yml index d8a2e56d2d3d..82346ea142f9 100644 --- a/.github/workflows/pr-bot-prs-needing-attention.yml +++ b/.github/workflows/pr-bot-prs-needing-attention.yml @@ -33,9 +33,11 @@ jobs: if: github.repository == 'apache/beam' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 + with: + persist-credentials: true - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 16 - name: Install pr-bot npm dependencies @@ -45,4 +47,4 @@ jobs: - run: npm run findPrsNeedingAttention env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - working-directory: 'scripts/ci/pr-bot' \ No newline at end of file + working-directory: 'scripts/ci/pr-bot' diff --git a/.github/workflows/publish_github_release_notes.yml b/.github/workflows/publish_github_release_notes.yml index f1a1183789e6..a40b4432443e 100644 --- a/.github/workflows/publish_github_release_notes.yml +++ b/.github/workflows/publish_github_release_notes.yml @@ -36,7 +36,9 @@ jobs: properties: ${{ steps.test-properties.outputs.properties }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: false - id: test-properties uses: ./.github/actions/setup-default-test-properties @@ -49,10 +51,12 @@ jobs: name: Publish Github Release Notes steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Publish github release notes run: | - POST_PATH="website/www/site/content/en/blog/beam-${{env.RELEASE_VERSION}}.md" + POST_PATH="website/www/site/content/en/blog/beam-${RELEASE_VERSION}.md" RELEASE_NOTES=$( cat ${POST_PATH} | # Read post's content sed -n '/ -# [2.74.0] - Unreleased +# [2.77.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 + +* (Python) Fixed incorrect profiler options handling on portable runners ([#39613](https://github.com/apache/beam/issues/39613)). +* (Java) KafkaIO dynamic reads no longer require the obsolete `beam_fn_api` experiment ([#29998](https://github.com/apache/beam/issues/29998)). + +## 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.76.0] - 2026-08-?? + +## I/Os + +* Upgraded Iceberg dependency to 1.11.0 (Java) ([#38925](https://github.com/apache/beam/issues/38925)). +* Add ArrowFlight IO (Java) ([#20116](https://github.com/apache/beam/issues/20116)). +* (Python) JmsIO (IBM MQ, ActiveMQ, and other providers) is now supported in Python via cross-language ([#30716](https://github.com/apache/beam/issues/30716)). +* Added a full Iceberg batch and streaming changelog source (CDC) ([#38831](https://github.com/apache/beam/issues/38831)) +* Added a Delta Lake batch changelog source (CDC) ([#39492](https://github.com/apache/beam/issues/39492)) + +## New Features / Improvements + +* Added `GroupIntoBatches` transform and the standard + `beam:coder:sharded_key:v1` coder to the Go SDK, along with + `beam.Coder.IsDeterministic`, `beam.PCollection.WindowingStrategy`, + and `coder.RegisterDeterministicCoder` for opt-in deterministic + custom coders (Go) ([#19868](https://github.com/apache/beam/issues/19868)). +* 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) Removed the `envoy-data-plane` (and transitive `betterproto`) dependency; `EnvoyRateLimiter` now uses a small vendored protobuf definition instead, resolving dependency conflicts for downstream projects ([#37854](https://github.com/apache/beam/issues/37854)). +* (Java) Supported acknowledge mode for JmsIO ([#39253](https://github.com/apache/beam/issues/39253)). +* (Python) Staged files directory is now automatically added to `sys.path` on the Python SDK worker at startup. This makes Python files provided via the '--files_to_stage' pipeline option importable in the pipeline code and makes it easier to initialize Python SDK harness at startup via the `--beam_plugins` pipeline option. For more information, see the [Staging Individual Files](https://beam.apache.org/documentation/sdks/python-pipeline-dependencies/#staging-files) section of the dependency management docs. This behavior can be disabled by passing the '--experiments=no_staged_dir_in_sys_path' pipeline option ([#39431](https://github.com/apache/beam/issues/39431)). +* (Python) Added `equal_to_approx`, an `assert_that` matcher that compares numeric pipeline outputs with a configurable tolerance ([#18028](https://github.com/apache/beam/issues/18028)). +* (Python) `Timestamp` now supports variable subsecond precision, up to nanoseconds. The portable + `beam:logical_type:timestamp:v1` logical type now maps to Python's `Timestamp` ([#39344](https://github.com/apache/beam/issues/39344)). +* (Python) Added `UnboundedSource`, an interface for reading an infinite stream of records with checkpointing, watermark reporting, and bundle finalization. Read one with `beam.io.Read` + ([#19137](https://github.com/apache/beam/issues/19137)). +* (Python) Added `Watch`, a transform that polls a growing set of outputs for each input element, deduplicates outputs across poll rounds, and stops per a user-supplied termination condition + ([#21521](https://github.com/apache/beam/issues/21521)). +* (Python) Added support to analyze core dumps created after python worker segmentation faults with `pystack` (or `gdb` if installed) using the `--profiler_agent=coredump` pipeline option. ([#39484](https://github.com/apache/beam/issues/39484)). +* (Python) Added `Sample.Any`, the Python equivalent of Java's `Sample.any`, which returns up to n arbitrary elements from a PCollection ([#18552](https://github.com/apache/beam/issues/18552)). +* (Java) Added per-element OpenTelemetry trace propagation across stages in the Dataflow Streaming Runner. Enable it with `--experiments=enable_otel_defaults,element_metadata_supported,disable_portable_worker`. Cloud Trace incurs additional cost. ([#33176](https://github.com/apache/beam/issues/33176)) +* (Java) Added OpenTelemetry header propagation support for both reads and writes in KafkaIO and PubSubIO. ([#33176](https://github.com/apache/beam/issues/33176)) +* (Java) Added OpenTelemetry tracing support for SpannerIO change streams ([#33176](https://github.com/apache/beam/issues/33176)) + +## Breaking Changes + +* (Python) Removed `google-perftools` from the SDK container images. Users who wish to use `--profiler_agent=tcmalloc` should install google-perftools APT package in their custom container images separately ([#39323](https://github.com/apache/beam/issues/39323)). +* [IcebergIO] Reading a `timestamptz` column will now return a `Timestamp.MICROS` Beam logical type to preserve + microseconds (the old Beam `Schema.FieldType#DATETIME` primitive type truncates past milliseconds). This may break + the following use cases when a `timestamptz` column is present: + * Existing streaming read pipelines. + * Managed Iceberg batch reads when upgraded from an older SDK. + * Python reads. + + Use pipeline option `--updateCompatibilityVersion=2.75.0` (or any older version) to keep the old behavior ([#39344](https://github.com/apache/beam/issues/39344)). +* `DoFn.process` returning a `str`, `bytes`, or `dict` (instead of an iterable wrapping one) now raises a `TypeError` rather than silently iterating per-character/byte/key (Python) ([#18712](https://github.com/apache/beam/issues/18712)). +* (Java) Added `DRAINING` and `DRAINED` states to `PipelineResult`, including runner state mappings and Dataflow update handling ([#39020](https://github.com/apache/beam/issues/39020)). +* (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. +* (Java) IcebergIO and projects that use it must now be built with Java 17 or later as a result of Iceberg 1.11.0 upgrade ([#38925](https://github.com/apache/beam/issues/38925)). + +## Bugfixes + +* Fixed unresolved runtime `ValueProvider` options being stringified in Python Dataflow Flex Templates ([#39499](https://github.com/apache/beam/issues/39499)). +* Fixed unbounded checkpoint state growth for splittable DoFns that self-checkpoint on the portable Flink runner (Java) ([#27648](https://github.com/apache/beam/issues/27648)). +* Improved Java pipeline performance by avoiding repeated `DoFn` type descriptor resolution when creating cached invokers ([#39309](https://github.com/apache/beam/issues/39309)). +* (Python) Fixed a memory leak in Python SDK caused by storing exceptions with potentially large stack frames in a cache ([#39406](https://github.com/apache/beam/issues/39406)). + +# [2.75.0] - 2026-07-08 + +## Highlights + +* 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)). +* 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)). +* SpannerIO: Added support for Cloud Spanner Directed Reads (Java) ([#X](https://github.com/apache/beam/issues/X)). + +## 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)). +* (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)). + +## Breaking Changes + +* (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. + +## 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)). + +## Known Issues + +* (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)). +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). + +# [2.74.0] - 2026-06-02 + +## 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))). +* IcebergIO: support writing with hash distribution mode, and with autosharding ([#38061](https://github.com/apache/beam/issues/38061)). ## New Features / Improvements @@ -83,15 +223,14 @@ * 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 * (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)). @@ -100,14 +239,9 @@ * 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)). -## 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)). +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). # [2.73.0] - 2026-04-29 @@ -145,6 +279,10 @@ * 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)). +## Known Issues + +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). + # [2.72.0] - 2026-03-30 ## Highlights @@ -178,6 +316,10 @@ * Fixed [CVE-2024-28397](https://www.cve.org/CVERecord?id=CVE-2024-28397) by switching from js2py to pythonmonkey (Yaml) ([#37560](https://github.com/apache/beam/issues/37560)). +## Known Issues + +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). + # [2.71.0] - 2026-01-22 ## I/Os @@ -199,6 +341,7 @@ ## Known Issues +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). # [2.70.0] - 2025-12-16 @@ -221,6 +364,10 @@ Now Beam has full support for Milvus integration including Milvus enrichment and * (Python) Python 3.9 reached EOL in October 2025 and support for the language version has been removed. ([#36665](https://github.com/apache/beam/issues/36665)). +## Known Issues + +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). + # [2.69.0] - 2025-10-28 ## Highlights @@ -280,6 +427,10 @@ Now Beam has full support for Milvus integration including Milvus enrichment and ([#36141](https://github.com/apache/beam/issues/36141)). * Fixed Spanner Change Stream reading stuck issue due to watermark of partition moving backwards ([#36470](https://github.com/apache/beam/issues/36470)). +## Known Issues + +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). + # [2.68.0] - 2025-09-22 ## Highlights @@ -334,6 +485,7 @@ Now Beam has full support for Milvus integration including Milvus enrichment and ## Known Issues * ([#36470](https://github.com/apache/beam/issues/36470)). Spanner Change Stream reading stuck issue due to watermark of partition moving backwards. This issue exists in 2.67.0 and 2.68.0. To mitigate the issue, either use old version 2.66.0 or go to 2.69.0. +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). # [2.67.0] - 2025-08-12 @@ -382,6 +534,7 @@ Now Beam has full support for Milvus integration including Milvus enrichment and * ([#35666](https://github.com/apache/beam/issues/35666)). YAML Flatten incorrectly drops fields when input PCollections' schema are different. This issue exists for all versions since 2.52.0. * ([#36470](https://github.com/apache/beam/issues/36470)). Spanner Change Stream reading stuck issue due to watermark of partition moving backwards. This issue exists in 2.67.0 and 2.68.0. To mitigate the issue, either use old version 2.66.0 or go to 2.69.0. +* (Python) Long-running Python pipelines might experience memory growth and periodic OOMs ([#39406](https://github.com/apache/beam/issues/39406)). # [2.66.0] - 2025-07-01 @@ -2439,4 +2592,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/build.gradle.kts b/build.gradle.kts index 3f5d39d99347..d0cc5fbb32b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -343,7 +343,9 @@ tasks.register("javaPreCommit") { // a precommit task build multiple IOs (except those splitting into single jobs) tasks.register("javaioPreCommit") { dependsOn(":sdks:java:io:amqp:build") - dependsOn(":sdks:java:io:cassandra:build") + dependsOn(":sdks:java:io:arrow-flight:build") + // CassandraIO, HBaseIO and HCatalogIO do not support Java17+, test ran separately + // dependsOn(":sdks:java:io:cassandra:build") dependsOn(":sdks:java:io:csv:build") dependsOn(":sdks:java:io:cdap:build") dependsOn(":sdks:java:io:clickhouse:build") @@ -353,8 +355,8 @@ tasks.register("javaioPreCommit") { dependsOn(":sdks:java:io:elasticsearch:build") dependsOn(":sdks:java:io:file-schema-transform:build") dependsOn(":sdks:java:io:google-ads:build") - dependsOn(":sdks:java:io:hbase:build") - dependsOn(":sdks:java:io:hcatalog:build") + // dependsOn(":sdks:java:io:hbase:build") + // dependsOn(":sdks:java:io:hcatalog:build") dependsOn(":sdks:java:io:influxdb:build") dependsOn(":sdks:java:io:jdbc:build") dependsOn(":sdks:java:io:jms:build") 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/BeamDockerPlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerPlugin.groovy index 6963f96d7313..574e34118905 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerPlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerPlugin.groovy @@ -61,6 +61,7 @@ class BeamDockerPlugin implements Plugin { boolean push = false String builder = null String target = null + String compression = 'zstd' File resolvedDockerfile = null File resolvedDockerComposeTemplate = null @@ -72,6 +73,9 @@ class BeamDockerPlugin implements Plugin { DockerExtension(Project project) { this.project = project this.copySpec = project.copySpec() + if (project.hasProperty('docker-compression')) { + this.compression = project.property('docker-compression') + } } void resolvePathsAndValidate() { @@ -235,13 +239,23 @@ class BeamDockerPlugin implements Plugin { if (!ext.platform.isEmpty()) { buildCommandLine.addAll('--platform', String.join(',', ext.platform)) } - if (ext.load) { - buildCommandLine.add '--load' + if (ext.load && ext.push) { + throw new Exception("cannot combine 'push' and 'load' options") } - if (ext.push) { - buildCommandLine.add '--push' + if (ext.compression != null && !ext.compression.isEmpty()) { + if (ext.push) { + buildCommandLine.add "--output=type=registry,compression=${ext.compression},force-compression=true,oci-mediatypes=true" + } else if (ext.load) { + buildCommandLine.add '--load' + } else { + buildCommandLine.add "--output=type=image,compression=${ext.compression},force-compression=true,oci-mediatypes=true" + } + } else { if (ext.load) { - throw new Exception("cannot combine 'push' and 'load' options") + buildCommandLine.add '--load' + } + if (ext.push) { + buildCommandLine.add '--push' } } if (ext.builder != null) { 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..26b1c4738323 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -96,6 +96,17 @@ class BeamModulePlugin implements Plugin { } } + // Serializes pipeline options to JSON for the beamTestPipelineOptions system property. + // On Windows each option is wrapped in literal quotes so the JSON survives the quote + // stripping applied to the forked test JVM's command line. + // See https://github.com/apache/beam/issues/39332. + static def toBeamTestPipelineOptionsJson(def options) { + if (System.properties['os.name'].toLowerCase().contains('windows')) { + return JsonOutput.toJson(options.collect { "\"$it\"" }) + } + return JsonOutput.toJson(options) + } + /** A class defining the set of configurable properties accepted by applyJavaNature. */ static class JavaNatureConfiguration { /** Controls whether the spotbugs plugin is enabled and configured. */ @@ -597,25 +608,26 @@ class BeamModulePlugin implements Plugin { // // There are a few versions are determined by the BOMs by running scripts/tools/bomupgrader.py // marked as [bomupgrader]. See the documentation of that script for detail. - def activemq_version = "5.19.2" + def activemq_version = "5.19.5" def autovalue_version = "1.9" def autoservice_version = "1.0.1" def aws_java_sdk2_version = "2.20.162" def cassandra_driver_version = "3.10.2" - def cdap_version = "6.5.1" + def cdap_version = "6.11.4" 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 - def gax_version = "2.79.0" + def gax_version = "2.82.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" @@ -623,23 +635,21 @@ 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.18.6" + def jackson_version = "2.18.8" def jaxb_api_version = "2.3.3" def jsr305_version = "3.0.2" def everit_json_version = "1.14.2" - def kafka_version = "2.4.1" + def kafka_version = "3.9.2" 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.62.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" - // 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" @@ -729,6 +739,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", @@ -741,31 +753,31 @@ class BeamModulePlugin implements Plugin { google_api_client_gson : "com.google.api-client:google-api-client-gson:$google_clients_version", google_api_client_java6 : "com.google.api-client:google-api-client-java6:$google_clients_version", 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_bigquery : "com.google.apis:google-api-services-bigquery:v2-rev20260612-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 + google_api_services_storage : "com.google.apis:google-api-services-storage:v1-rev20260524-2.0.0", // [bomupgrader] sets version google_auth_library_credentials : "com.google.auth:google-auth-library-credentials", // google_cloud_platform_libraries_bom sets version 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 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.2.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.85.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 @@ -800,6 +812,7 @@ class BeamModulePlugin implements Plugin { guava : "com.google.guava:guava:$guava_version", guava_testlib : "com.google.guava:guava-testlib:$guava_version", hadoop_auth : "org.apache.hadoop:hadoop-auth:$hadoop_version", + hadoop_aws : "org.apache.hadoop:hadoop-aws:$hadoop_version", hadoop_client : "org.apache.hadoop:hadoop-client:$hadoop_version", hadoop_common : "org.apache.hadoop:hadoop-common:$hadoop_version", hadoop_mapreduce_client_core : "org.apache.hadoop:hadoop-mapreduce-client-core:$hadoop_version", @@ -838,8 +851,10 @@ class BeamModulePlugin implements Plugin { jupiter_api : "org.junit.jupiter:junit-jupiter-api:$jupiter_version", jupiter_engine : "org.junit.jupiter:junit-jupiter-engine:$jupiter_version", jupiter_params : "org.junit.jupiter:junit-jupiter-params:$jupiter_version", - kafka : "org.apache.kafka:kafka_2.11:$kafka_version", + kafka_scala_2_12 : "org.apache.kafka:kafka_2.12:$kafka_version", + kafka_scala_2_13 : "org.apache.kafka:kafka_2.13:$kafka_version", kafka_clients : "org.apache.kafka:kafka-clients:$kafka_version", + kafka_server : "org.apache.kafka:kafka-server:$kafka_version", log4j : "log4j:log4j:1.2.17", log4j_over_slf4j : "org.slf4j:log4j-over-slf4j:$slf4j_version", log4j2_api : "org.apache.logging.log4j:log4j-api:$log4j2_version", @@ -850,6 +865,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", @@ -864,8 +880,11 @@ class BeamModulePlugin implements Plugin { 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", // opentelemetry-bom sets version + opentelemetry_sdk_testing : "io.opentelemetry:opentelemetry-sdk-testing", // 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", @@ -934,6 +953,7 @@ class BeamModulePlugin implements Plugin { arrow_vector : "org.apache.arrow:arrow-vector:$arrow_version", arrow_memory_core : "org.apache.arrow:arrow-memory-core:$arrow_version", arrow_memory_netty : "org.apache.arrow:arrow-memory-netty:$arrow_version", + arrow_flight_core : "org.apache.arrow:flight-core:$arrow_version", ], groovy: [ groovy_all: "org.codehaus.groovy:groovy-all:2.4.13", @@ -950,6 +970,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 -> @@ -974,6 +1006,16 @@ 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 + } + // TODO(yathu): remove this once generated code (antlr) no longer trigger "this-escape", see above + // Unpin global opts when ver is lower than current + if (JavaVersion.current().compareTo(JavaVersion.VERSION_21) >= 0 && JavaVersion.toVersion(ver) < JavaVersion.VERSION_21) { + options.compilerArgs -= [ + '-Xlint:-this-escape', + ] + } if (ver == '8') { def java8Home = project.findProperty("java8Home") options.fork = true @@ -1157,16 +1199,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 " + @@ -1217,20 +1259,26 @@ class BeamModulePlugin implements Plugin { maxHeapSize = '2g' } + // NOTE: Use the character class "[.]" instead of an escaped "\\." to match a literal dot in + // these Checker Framework -AskipDefs/-AskipUses regexes. When a module is compiled on an older + // host JDK and forked to a newer JDK via javaXXHome (e.g. iceberg's requireJavaVersion 17 on a + // Java 11 CI host), Gradle passes the javac arguments through an @argfile. Backslash escapes do + // not survive that round-trip intact, so "\\." becomes a literal-backslash regex that matches + // nothing and the suppression is silently dropped. "[.]" is backslash-free and survives. List skipDefRegexes = [] skipDefRegexes << "AutoValue_.*" skipDefRegexes << "AutoBuilder_.*" skipDefRegexes << "AutoOneOf_.*" - skipDefRegexes << ".*\\.jmh_generated\\..*" + skipDefRegexes << ".*[.]jmh_generated[.].*" skipDefRegexes += configuration.generatedClassPatterns skipDefRegexes += configuration.classesTriggerCheckerBugs.keySet() String skipDefCombinedRegex = skipDefRegexes.collect({ regex -> "(${regex})"}).join("|") List skipUsesRegexes = [] // zstd-jni is not annotated, handles Zstd(De)CompressCtx.loadDict(null) just fine - skipUsesRegexes << "^com\\.github\\.luben\\.zstd\\..*" + skipUsesRegexes << "^com[.]github[.]luben[.]zstd[.].*" // SLF4J logger handles null log message parameters - skipUsesRegexes << "^org\\.slf4j\\.Logger.*" + skipUsesRegexes << "^org[.]slf4j[.]Logger.*" String skipUsesCombinedRegex = skipUsesRegexes.collect({ regex -> "(${regex})"}).join("|") project.apply plugin: 'org.checkerframework' @@ -1624,7 +1672,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 { @@ -1642,6 +1690,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) { @@ -2217,12 +2271,8 @@ class BeamModulePlugin implements Plugin { } } - // Windows handles quotation marks differently - if (pipelineOptionsString && System.properties['os.name'].toLowerCase().contains('windows')) { - def allOptionsListFormatted = allOptionsList.collect { "\"$it\"" } - pipelineOptionsStringFormatted = JsonOutput.toJson(allOptionsListFormatted) - } else if (pipelineOptionsString) { - pipelineOptionsStringFormatted = JsonOutput.toJson(allOptionsList) + if (pipelineOptionsString) { + pipelineOptionsStringFormatted = toBeamTestPipelineOptionsJson(allOptionsList) } systemProperties.beamTestPipelineOptions = pipelineOptionsStringFormatted ?: pipelineOptionsString @@ -2659,7 +2709,7 @@ class BeamModulePlugin implements Plugin { if (config.jobServerConfig) { beamTestPipelineOptions.add("--jobServerConfig=${config.jobServerConfig}") } - config.systemProperties.put("beamTestPipelineOptions", JsonOutput.toJson(beamTestPipelineOptions)) + config.systemProperties.put("beamTestPipelineOptions", toBeamTestPipelineOptionsJson(beamTestPipelineOptions)) project.tasks.register(name, Test) { group = "Verification" description = "Validates the PortableRunner with JobServer ${config.jobServerDriver}" @@ -2700,7 +2750,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") { @@ -2780,7 +2831,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' @@ -2823,7 +2875,7 @@ class BeamModulePlugin implements Plugin { def javaTask = project.tasks.register(config.name+"JavaUsing"+sdk, Test) { group = "Verification" description = "Validates runner for cross-language capability of using ${sdk} transforms from Java SDK" - systemProperty "beamTestPipelineOptions", JsonOutput.toJson(config.javaPipelineOptions) + systemProperty "beamTestPipelineOptions", toBeamTestPipelineOptionsJson(config.javaPipelineOptions) systemProperty "expansionJar", expansionJar systemProperty "expansionPort", port systemProperty "semiPersistDir", config.semiPersistDir @@ -2969,10 +3021,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) { @@ -3134,15 +3187,15 @@ 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}]" + } + def uvCacheDir = "${project.ext.envdir}/.uv_cache" 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}]" - } + // 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}\"" } } } 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..e154fc6ea7ac 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/Repositories.groovy @@ -19,13 +19,19 @@ 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 } + maven { url project.rootProject.file(project.offlineRepositoryRoot) } // To run gradle in offline mode, one must first invoke // 'updateOfflineRepository' to create an offline repo @@ -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,11 +78,15 @@ 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/" } - maven { url project.offlineRepositoryRoot } + maven { url project.rootProject.file(project.offlineRepositoryRoot) } } includeSources = false includeJavadocs = false diff --git a/contributor-docs/README.md b/contributor-docs/README.md index 1087a74f05c7..7e4381893c93 100644 --- a/contributor-docs/README.md +++ b/contributor-docs/README.md @@ -22,6 +22,7 @@ This directory contains documentation for contributors to the Apache Beam projec - [Committer Guide](committer-guide.md): Guidelines for Beam committers regarding code review, pull request objectives, merging processes, and post-merge tasks. - [Committer Onboarding](committer-onboarding.md): A checklist for new Beam committers to set up their accounts and permissions. - [Java Dependency Upgrades](java-dependency-upgrades.md): Instructions for upgrading Java dependencies in Beam, including running linkage checkers and verification tests. +- [Local Flink Python Validation](local-flink-python.md): Instructions for running Python pipelines on a local Flink standalone cluster. - [Python Tips](python-tips.md): Tips and instructions for developing the Python SDK, including environment setup, running tests, and handling dependencies. - [RC Testing Guide](rc-testing-guide.md): A guide for testing Beam Release Candidates (RCs) against downstream projects for Python, Java, and Go SDKs. - [Release Guide](release-guide.md): A comprehensive guide for the Release Manager on how to perform a Beam release, from preparation to promotion. diff --git a/contributor-docs/local-flink-python.md b/contributor-docs/local-flink-python.md new file mode 100644 index 000000000000..6ab30435c68b --- /dev/null +++ b/contributor-docs/local-flink-python.md @@ -0,0 +1,204 @@ + + +# Running Python pipelines on a local Flink cluster + +This guide describes a contributor workflow for validating Python Beam pipelines +against a real local Flink standalone cluster. It is useful when embedded Flink +is not enough, for example when validating streaming source behavior, checkpoint +boundaries, or runner-visible job state in the Flink dashboard. + +The commands assume a Unix shell (Linux, macOS, or WSL2 on Windows) with `curl`, +`tar`, and `java` on the `PATH`. + +* [What this setup validates](#what-this-setup-validates) +* [Prerequisites](#prerequisites) +* [Start a local Flink cluster](#start-a-local-flink-cluster) +* [Run a Beam Python pipeline](#run-a-beam-python-pipeline) +* [Troubleshooting](#troubleshooting) +* [Stop the cluster](#stop-the-cluster) + +## What this setup validates + +This setup runs three components: + +1. A Flink standalone cluster, consisting of a JobManager and a TaskManager. +1. A Beam Flink Job Server, started by the Python `FlinkRunner`. +1. A Python SDK harness, using `--environment_type=LOOPBACK` for local + development. + +The Flink dashboard at `http://localhost:8081` shows the submitted Beam jobs. +This is different from embedded Flink mode, where the cluster is started only +for the lifetime of one job and is not useful for manual dashboard inspection. + +## Prerequisites + +Install or prepare the following: + +* Docker Desktop (optional), only for the alternative method of obtaining the + Flink distribution. +* A Unix shell: Linux, macOS, or WSL2 on Windows. +* Java 11 on the `PATH`. +* A Python environment with the Beam SDK dependencies installed. +* A Beam source checkout for the Python code under test. +* A Flink 1.20 Job Server jar built from the same Beam checkout when validating + unreleased Beam changes. + +For a source-built Job Server jar, run this command from the Beam checkout: + +```sh +./gradlew :runners:flink:1.20:job-server:shadowJar +``` + +The jar is written under: + +```text +runners/flink/1.20/job-server/build/libs/ +``` + +## Start a local Flink cluster + +Use a Flink distribution whose minor version matches a Flink version supported +by your Beam version. See the [Flink Version Compatibility](https://beam.apache.org/documentation/runners/flink/#flink-version-compatibility) +table in the Flink Runner documentation, and confirm the exact patch version on +the [Flink downloads page](https://flink.apache.org/downloads.html). This guide +uses Flink 1.20. + +Download and unpack the binary distribution: + +```sh +FLINK_VERSION=1.20.1 +curl -fLO "https://archive.apache.org/dist/flink/flink-${FLINK_VERSION}/flink-${FLINK_VERSION}-bin-scala_2.12.tgz" +tar -xzf "flink-${FLINK_VERSION}-bin-scala_2.12.tgz" -C "$HOME" +export FLINK_HOME="$HOME/flink-${FLINK_VERSION}" +``` + +Ensure these settings exist in `$FLINK_HOME/conf/config.yaml`: + +```yaml +jobmanager.rpc.address: localhost +rest.address: localhost +taskmanager.numberOfTaskSlots: 2 +``` + +Start the cluster. The JobManager and TaskManager run as background daemons: + +```sh +"$FLINK_HOME/bin/start-cluster.sh" +``` + +Verify that the JobManager and TaskManager are available: + +```sh +curl -fsS http://localhost:8081/overview +``` + +Expected output includes one TaskManager and two slots: + +```json +{"taskmanagers":1,"slots-total":2,"slots-available":2,"jobs-running":0} +``` + +You can also open the Flink dashboard in a browser: + +```text +http://localhost:8081 +``` + +### Alternative: extract Flink from the Docker image + +If a direct download is not available, copy the distribution out of the Flink +Docker image with `docker cp`: + +```sh +docker create --name flink-dist flink:1.20 +docker cp flink-dist:/opt/flink "$HOME/flink-1.20" +docker rm flink-dist +export FLINK_HOME="$HOME/flink-1.20" +``` + +A distribution copied out of a Docker image can contain the container hostname in +`conf/config.yaml`; see [Troubleshooting](#troubleshooting). + +## Run a Beam Python pipeline + +For local Python development, use `FlinkRunner`, point it at the standalone +cluster, and use `LOOPBACK` so the Python SDK harness runs in the local process. + +Use a source checkout on `PYTHONPATH` when validating unreleased Python changes. +Set paths for your environment: + +```sh +export BEAM_CHECKOUT="$HOME/beam" +export PYTHON="$HOME/beamenv/bin/python" +export FLINK_JOB_SERVER_JAR="$(find "$BEAM_CHECKOUT/runners/flink/1.20/job-server/build/libs" \ + -name 'beam-runners-flink-1.20-job-server-*.jar' | head -n 1)" +``` + +Run a small pipeline: + +```sh +printf 'to be or not to be\nbeam runs on flink\n' > /tmp/beam-flink-input.txt + +PYTHONPATH="$BEAM_CHECKOUT/sdks/python" "$PYTHON" -m apache_beam.examples.wordcount \ + --runner=FlinkRunner \ + --flink_master=localhost:8081 \ + --flink_version=1.20 \ + --flink_job_server_jar="$FLINK_JOB_SERVER_JAR" \ + --environment_type=LOOPBACK \ + --input=/tmp/beam-flink-input.txt \ + --output=/tmp/beam-flink-counts +``` + +For released Beam, omit `--flink_job_server_jar` and the `PYTHONPATH` prefix; the +`FlinkRunner` downloads a Job Server matching `--flink_version` automatically. The +source checkout and built jar are only needed to test unreleased changes. + +Check the dashboard or REST API after the run: + +```sh +curl -fsS http://localhost:8081/jobs/overview +``` + +The job should be `FINISHED`. + +## Troubleshooting + +If the TaskManager does not register, check `$FLINK_HOME/conf/config.yaml`. +When a distribution is copied out of a Docker image, the file might contain the +container hostname. Replace it with: + +```yaml +jobmanager.rpc.address: localhost +``` + +If a Python job fails on native Windows with an invalid path containing `:`, +run the Python driver and Job Server from WSL2. Some staged artifact names used +by the portable runner are valid on Linux but invalid as native Windows file +names. + +On WSL2, keep at least one shell open in the distribution while the cluster runs. +Closing the last shell can stop the distribution and its background daemons. + +If the job starts but the Python transforms do not execute, check the +environment type. `LOOPBACK` is intended for local development. For a remote +or multi-machine Flink cluster, use a containerized environment instead. + +## Stop the cluster + +Stop the local cluster when you finish collecting results: + +```sh +"$FLINK_HOME/bin/stop-cluster.sh" +``` diff --git a/contributor-docs/release-guide.md b/contributor-docs/release-guide.md index 2024f1017975..7c96465c722b 100644 --- a/contributor-docs/release-guide.md +++ b/contributor-docs/release-guide.md @@ -1164,10 +1164,16 @@ At the end of the release, go to the GitHub milestones page and mark the recentl #### Update the Java BOM Google releases a BOM that pins compatible versions of their Java libraries. -After the release, try updating the BOM to the latest version. +After the release, an automated upgrade BOM PR is created or can be triggered manually. -To do so, create a draft PR and run test suites following the instructions at -https://github.com/apache/beam/blob/master/contributor-docs/java-dependency-upgrades.md. +A PR should have already been created (and possibly merged) by github-actions bot, you should verify that this was done correctly +by looking at open PRs from that bot - https://github.com/apache/beam/pulls/app%2Fgithub-actions + +If a PR has not been merged, drive it to completion. +If no PR was created, triage any failures in https://github.com/apache/beam/actions/workflows/beam_Upgrade_GCP_BOM.yml and manually update the BOM, +following https://github.com/apache/beam/blob/master/contributor-docs/java-dependency-upgrades.md or simply trigger the workflow again. + +##### Troubleshooting Triage the test failures and rerun any tests that seem potentially unrelated to the upgrade. If there are no test failures due to the BOM upgrade, request review and merge the PR as normal. diff --git a/examples/java/build.gradle b/examples/java/build.gradle index cf168293968f..84ace728362e 100644 --- a/examples/java/build.gradle +++ b/examples/java/build.gradle @@ -44,8 +44,8 @@ dependencies { if (project.findProperty('testJavaVersion') == '21' || JavaVersion.current().compareTo(JavaVersion.VERSION_21) >= 0) { // this dependency is a provided dependency for kafka-avro-serializer. It is not needed to compile with Java<=17 // but needed for compile only under Java21, specifically, required for extending from AbstractKafkaAvroDeserializer - compileOnly library.java.kafka - permitUnusedDeclared library.java.kafka + compileOnly library.java.kafka_scala_2_12 + permitUnusedDeclared library.java.kafka_scala_2_12 } implementation library.java.kafka_clients implementation project(path: ":sdks:java:core", configuration: "shadow") @@ -140,6 +140,13 @@ for (String runner : preCommitRunners) { forkEvery 1 maxParallelForks 4 systemProperty "beamTestPipelineOptions", JsonOutput.toJson(preCommitBeamTestPipelineOptions) + def testJavaVer = project.findProperty('testJavaVersion') ? (project.property('testJavaVersion') as int) : JavaVersion.current().majorVersion.toInteger() + if ("sparkRunner".equals(runner) && testJavaVer >= 17) { + 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" + } } } diff --git a/examples/java/common.gradle b/examples/java/common.gradle index 10ea43628bc8..91d07aff76ba 100644 --- a/examples/java/common.gradle +++ b/examples/java/common.gradle @@ -35,6 +35,9 @@ configurations.sparkRunnerPreCommit { exclude group: "org.slf4j", module: "jul-to-slf4j" exclude group: "org.slf4j", module: "slf4j-jdk14" } +resolveCapabilitiesConflict(configurations.flinkRunnerPreCommit, 'org.lz4:lz4-java', 'at.yawk.lz4') +resolveCapabilitiesConflict(configurations.sparkRunnerPreCommit, 'org.lz4:lz4-java', 'at.yawk.lz4') + dependencies { directRunnerPreCommit project(path: ":runners:direct-java", configuration: "shadow") diff --git a/examples/java/iceberg/build.gradle b/examples/java/iceberg/build.gradle index 4d4a1fb44413..2cff15c5fb0d 100644 --- a/examples/java/iceberg/build.gradle +++ b/examples/java/iceberg/build.gradle @@ -26,8 +26,8 @@ plugins { applyJavaNature( exportJavadoc: false, automaticModuleName: 'org.apache.beam.examples.iceberg', - // iceberg requires Java11+ - requireJavaVersion: JavaVersion.VERSION_11 + // iceberg requires Java17+ + requireJavaVersion: JavaVersion.VERSION_17 ) description = "Apache Beam :: Examples :: Java :: Iceberg" 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/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..f0719932d5fd 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 @@ -214,7 +214,7 @@ public interface Options extends PipelineOptions { day's worth (roughly) of data. Note: You may want to use a small sample dataset to test it locally/quickly : gs://apache-beam-samples/game/small/gaming_data.csv - You can also download it via the command line gsutil cp gs://apache-beam-samples/game/small/gaming_data.csv ./destination_folder/gaming_data.csv */ + You can also download it via the command line gcloud storage cp gs://apache-beam-samples/game/small/gaming_data.csv ./destination_folder/gaming_data.csv */ @Default.String("gs://apache-beam-samples/game/gaming_data*.csv") String getInput(); diff --git a/examples/java/src/main/java/org/apache/beam/examples/subprocess/utils/FileUtils.java b/examples/java/src/main/java/org/apache/beam/examples/subprocess/utils/FileUtils.java index d0244d233722..eff913b9f4fb 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/subprocess/utils/FileUtils.java +++ b/examples/java/src/main/java/org/apache/beam/examples/subprocess/utils/FileUtils.java @@ -28,6 +28,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.UUID; import org.apache.beam.examples.subprocess.configuration.SubProcessConfiguration; import org.apache.beam.sdk.io.FileSystems; import org.apache.beam.sdk.io.fs.ResolveOptions.StandardResolveOptions; @@ -77,30 +79,36 @@ public static String copyFileFromWorkerToGCS( } } - public static String copyFileFromGCSToWorker(ExecutableFile execuableFile) throws Exception { + public static String copyFileFromGCSToWorker(ExecutableFile executableFile) throws Exception { ResourceId sourceFile = - FileSystems.matchNewResource(execuableFile.getSourceGCSLocation(), false); - ResourceId destinationFile = - FileSystems.matchNewResource(execuableFile.getDestinationLocation(), false); + FileSystems.matchNewResource(executableFile.getSourceGCSLocation(), false); try { LOG.info( "Moving File {} to {} ", - execuableFile.getSourceGCSLocation(), - execuableFile.getDestinationLocation()); - Path path = Paths.get(execuableFile.getDestinationLocation()); + executableFile.getSourceGCSLocation(), + executableFile.getDestinationLocation()); + Path path = Paths.get(executableFile.getDestinationLocation()); if (path.toFile().exists()) { LOG.warn( "Overwriting file {}, should only see this once per worker.", - execuableFile.getDestinationLocation()); + executableFile.getDestinationLocation()); + } + Path stagedFile = path.resolveSibling(".beam-executable-" + UUID.randomUUID() + ".tmp"); + try { + ResourceId stagedResource = FileSystems.matchNewResource(stagedFile.toString(), false); + copyFile(sourceFile, stagedResource); + stagedFile.toFile().setExecutable(true); + Files.move( + stagedFile, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } finally { + Files.deleteIfExists(stagedFile); } - copyFile(sourceFile, destinationFile); - path.toFile().setExecutable(true); return path.toString(); } catch (Exception ex) { - LOG.error("Error moving file : {} ", execuableFile.fileName, ex); + LOG.error("Error moving file : {} ", executableFile.fileName, ex); throw ex; } } diff --git a/examples/java/src/test/java/org/apache/beam/examples/subprocess/utils/FileUtilsTest.java b/examples/java/src/test/java/org/apache/beam/examples/subprocess/utils/FileUtilsTest.java new file mode 100644 index 000000000000..02c23d705c27 --- /dev/null +++ b/examples/java/src/test/java/org/apache/beam/examples/subprocess/utils/FileUtilsTest.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.subprocess.utils; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.WRITE; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import java.io.File; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.beam.examples.subprocess.configuration.SubProcessConfiguration; +import org.apache.commons.lang3.SystemUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FileUtilsTest { + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void copyFileFromGCSToWorkerAtomicallyReplacesExecutable() throws Exception { + assumeTrue(SystemUtils.IS_OS_LINUX); + + File sourceDirectory = temporaryFolder.newFolder("source"); + File workerDirectory = temporaryFolder.newFolder("worker"); + String fileName = "echo.sh"; + Path source = sourceDirectory.toPath().resolve(fileName); + Path destination = workerDirectory.toPath().resolve(fileName); + Files.write(source, "#!/bin/sh\nexit 0\n".getBytes(UTF_8)); + Files.write(destination, "#!/bin/sh\nexit 1\n".getBytes(UTF_8)); + assertTrue(destination.toFile().setExecutable(true)); + Set destinationPermissions = Files.getPosixFilePermissions(destination); + + SubProcessConfiguration configuration = new SubProcessConfiguration(); + configuration.setSourcePath(sourceDirectory.getAbsolutePath()); + configuration.setWorkerPath(workerDirectory.getAbsolutePath()); + + try (FileChannel ignored = FileChannel.open(destination, WRITE)) { + String copiedFile = + FileUtils.copyFileFromGCSToWorker(new ExecutableFile(configuration, fileName)); + + assertEquals(destination.toString(), copiedFile); + assertArrayEquals(Files.readAllBytes(source), Files.readAllBytes(destination)); + assertEquals(destinationPermissions, Files.getPosixFilePermissions(destination)); + assertTrue(Files.isExecutable(destination)); + try (Stream files = Files.list(workerDirectory.toPath())) { + assertEquals(1, files.count()); + } + + Process process = new ProcessBuilder(destination.toString()).start(); + assertEquals(0, process.waitFor()); + } + } +} diff --git a/examples/kotlin/build.gradle b/examples/kotlin/build.gradle index 829aefd44745..60ddc1c52d7f 100644 --- a/examples/kotlin/build.gradle +++ b/examples/kotlin/build.gradle @@ -113,6 +113,13 @@ for (String runner : preCommitRunners) { forkEvery 1 maxParallelForks 4 systemProperty "beamTestPipelineOptions", JsonOutput.toJson(preCommitBeamTestPipelineOptions) + def testJavaVer = project.findProperty('testJavaVersion') ? (project.property('testJavaVersion') as int) : JavaVersion.current().majorVersion.toInteger() + if ("sparkRunner".equals(runner) && testJavaVer >= 17) { + 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" + } } } diff --git a/examples/multi-language/README.md b/examples/multi-language/README.md index f9905ca310e6..9b01e4f8eba2 100644 --- a/examples/multi-language/README.md +++ b/examples/multi-language/README.md @@ -124,7 +124,7 @@ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.multilanguage.Sk the digit. The second item is the predicted label of the digit. ``` -gsutil cat gs://$GCP_BUCKET/multi-language-beam/output* +gcloud storage cat gs://$GCP_BUCKET/multi-language-beam/output* ``` #### Instructions for running the Java pipeline at HEAD (Beam 2.41.0 and 2.42.0). @@ -171,7 +171,7 @@ export GCP_REGION= export EXPANSION_SERVICE_PORT= # This removes any existing output. -gsutil rm gs://$GCP_BUCKET/multi-language-beam/output* +gcloud storage rm gs://$GCP_BUCKET/multi-language-beam/output* ./gradlew :examples:multi-language:sklearnMinstClassification --args=" \ --runner=DataflowRunner \ @@ -188,7 +188,7 @@ gsutil rm gs://$GCP_BUCKET/multi-language-beam/output* of the digit. The second item is the predicted label of the digit. ``` -gsutil cat gs://$GCP_BUCKET/multi-language-beam/output* +gcloud storage cat gs://$GCP_BUCKET/multi-language-beam/output* ``` ### Python Dataframe Wordcount diff --git a/examples/notebooks/beam-ml/automatic_model_refresh.ipynb b/examples/notebooks/beam-ml/automatic_model_refresh.ipynb index c29881ea72fd..46d2d7e1eaa6 100644 --- a/examples/notebooks/beam-ml/automatic_model_refresh.ipynb +++ b/examples/notebooks/beam-ml/automatic_model_refresh.ipynb @@ -298,7 +298,7 @@ "model = tf.keras.applications.resnet.ResNet101()\n", "model.save('resnet101_weights_tf_dim_ordering_tf_kernels.keras')\n", "# After saving the model locally, upload the model to GCS bucket and provide that gcs bucket `URI` as `model_uri` to the `TFModelHandler`\n", - "!gsutil cp resnet101_weights_tf_dim_ordering_tf_kernels.keras gs://${BUCKET_NAME}/dataflow/resnet101_weights_tf_dim_ordering_tf_kernels.keras" + "!gcloud storage cp resnet101_weights_tf_dim_ordering_tf_kernels.keras gs://${BUCKET_NAME}/dataflow/resnet101_weights_tf_dim_ordering_tf_kernels.keras" ] }, { @@ -603,7 +603,7 @@ "source": [ "model = tf.keras.applications.resnet.ResNet152()\n", "model.save('resnet152_weights_tf_dim_ordering_tf_kernels.keras')\n", - "!gsutil cp resnet152_weights_tf_dim_ordering_tf_kernels.keras gs://${BUCKET_NAME}/resnet152_weights_tf_dim_ordering_tf_kernels.keras" + "!gcloud storage cp resnet152_weights_tf_dim_ordering_tf_kernels.keras gs://${BUCKET_NAME}/resnet152_weights_tf_dim_ordering_tf_kernels.keras" ] }, { diff --git a/examples/notebooks/beam-ml/rag_usecase/opensearch_connector.py b/examples/notebooks/beam-ml/rag_usecase/opensearch_connector.py index fc83c8d443cc..e9f07daef101 100644 --- a/examples/notebooks/beam-ml/rag_usecase/opensearch_connector.py +++ b/examples/notebooks/beam-ml/rag_usecase/opensearch_connector.py @@ -88,8 +88,8 @@ def __init__(self, """ self.host = host self.port = port - self.username = username | os.getenv("OPENSEARCH_USERNAME") - self.password = password | os.getenv("OPENSEARCH_PASSWORD") + self.username = username or os.getenv("OPENSEARCH_USERNAME") + self.password = password or os.getenv("OPENSEARCH_PASSWORD") self._batch_size = batch_size if not self.username or not self.password: @@ -247,8 +247,8 @@ def __init__(self, """ self.host = host self.port = port - self.username = username | os.getenv("OPENSEARCH_USERNAME") - self.password = password | os.getenv("OPENSEARCH_PASSWORD") + self.username = username or os.getenv("OPENSEARCH_USERNAME") + self.password = password or os.getenv("OPENSEARCH_PASSWORD") self.batch_size = batch_size self.embedded_columns = embedded_columns diff --git a/examples/notebooks/beam-ml/rag_usecase/opensearch_enrichment.py b/examples/notebooks/beam-ml/rag_usecase/opensearch_enrichment.py index 70397550241f..c43d9af181ba 100644 --- a/examples/notebooks/beam-ml/rag_usecase/opensearch_enrichment.py +++ b/examples/notebooks/beam-ml/rag_usecase/opensearch_enrichment.py @@ -77,8 +77,8 @@ def __init__( """ self.opensearch_host = opensearch_host self.opensearch_port = opensearch_port - self.username = username | os.getenv("OPENSEARCH_USERNAME") - self.password = password | os.getenv("OPENSEARCH_PASSWORD") + self.username = username or os.getenv("OPENSEARCH_USERNAME") + self.password = password or os.getenv("OPENSEARCH_PASSWORD") self.index_name = index_name self.vector_field = vector_field self.k = k diff --git a/examples/notebooks/beam-ml/run_inference_gemini.ipynb b/examples/notebooks/beam-ml/run_inference_gemini.ipynb new file mode 100644 index 000000000000..2a9731a56a35 --- /dev/null +++ b/examples/notebooks/beam-ml/run_inference_gemini.ipynb @@ -0,0 +1,609 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "fFjof1NgAJwu" + }, + "outputs": [], + "source": [ + "# @title ###### Licensed to the Apache Software Foundation (ASF), Version 2.0 (the \"License\")\n", + "\n", + "# Licensed to the Apache Software Foundation (ASF) under one\n", + "# or more contributor license agreements. See the NOTICE file\n", + "# distributed with this work for additional information\n", + "# regarding copyright ownership. The ASF licenses this file\n", + "# to you under the Apache License, Version 2.0 (the\n", + "# \"License\"); you may not use this file except in compliance\n", + "# with the License. You may obtain a copy of the License at\n", + "#\n", + "# http://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing,\n", + "# software distributed under the License is distributed on an\n", + "# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n", + "# KIND, either express or implied. See the License for the\n", + "# specific language governing permissions and limitations\n", + "# under the License" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A8xNRyZMW1yK" + }, + "source": [ + "# Apache Beam RunInference with Gemini\n", + "\n", + "\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 diff --git a/examples/notebooks/get-started/learn_beam_basics_by_doing.ipynb b/examples/notebooks/get-started/learn_beam_basics_by_doing.ipynb index 0a47a38197ba..4259bad8239d 100644 --- a/examples/notebooks/get-started/learn_beam_basics_by_doing.ipynb +++ b/examples/notebooks/get-started/learn_beam_basics_by_doing.ipynb @@ -324,7 +324,7 @@ "source": [ "# Creates a data directory with our dataset SMSSpamCollection\n", "!mkdir -p data\n", - "!gsutil cp gs://apachebeamdt/SMSSpamCollection data/" + "!gcloud storage cp gs://apachebeamdt/SMSSpamCollection data/" ] }, { @@ -995,7 +995,7 @@ "source": [ "!pip install --quiet apache-beam\n", "!mkdir -p data\n", - "!gsutil cp gs://apachebeamdt/SMSSpamCollection data/" + "!gcloud storage cp gs://apachebeamdt/SMSSpamCollection data/" ] }, { diff --git a/examples/notebooks/get-started/learn_beam_transforms_by_doing.ipynb b/examples/notebooks/get-started/learn_beam_transforms_by_doing.ipynb index 4b5dc5d15991..15b2fa6f18a1 100644 --- a/examples/notebooks/get-started/learn_beam_transforms_by_doing.ipynb +++ b/examples/notebooks/get-started/learn_beam_transforms_by_doing.ipynb @@ -324,7 +324,7 @@ "outputs": [], "source": [ "!mkdir -p data\n", - "!gsutil cp gs://dataflow-samples/shakespeare/kinglear.txt data/" + "!gcloud storage cp gs://dataflow-samples/shakespeare/kinglear.txt data/" ] }, { diff --git a/examples/notebooks/get-started/learn_beam_windowing_by_doing.ipynb b/examples/notebooks/get-started/learn_beam_windowing_by_doing.ipynb index 014a9ca7ff1a..b716516c71b3 100644 --- a/examples/notebooks/get-started/learn_beam_windowing_by_doing.ipynb +++ b/examples/notebooks/get-started/learn_beam_windowing_by_doing.ipynb @@ -173,7 +173,7 @@ "source": [ "# Copy the dataset file into the local file system from Google Cloud Storage.\n", "!mkdir -p data\n", - "!gsutil cp gs://batch-processing-example/air-quality-india.csv data/" + "!gcloud storage cp gs://batch-processing-example/air-quality-india.csv data/" ] }, { diff --git a/examples/notebooks/get-started/try-apache-beam-go.ipynb b/examples/notebooks/get-started/try-apache-beam-go.ipynb index 7e049af14c38..87130b59538b 100644 --- a/examples/notebooks/get-started/try-apache-beam-go.ipynb +++ b/examples/notebooks/get-started/try-apache-beam-go.ipynb @@ -85,7 +85,7 @@ "\n", "# Copy the input file into the local filesystem.\n", "run('mkdir -p data')\n", - "run('gsutil cp gs://dataflow-samples/shakespeare/kinglear.txt data/')" + "run('gcloud storage cp gs://dataflow-samples/shakespeare/kinglear.txt data/')" ], "cell_type": "code", "execution_count": 1, @@ -98,7 +98,7 @@ "\n", ">> mkdir -p data\n", "\n", - ">> gsutil cp gs://dataflow-samples/shakespeare/kinglear.txt data/\n", + ">> gcloud storage cp gs://dataflow-samples/shakespeare/kinglear.txt data/\n", "Copying gs://dataflow-samples/shakespeare/kinglear.txt...\n", "/ [1 files][153.6 KiB/153.6 KiB] \n", "Operation completed over 1 objects/153.6 KiB. \n", diff --git a/examples/notebooks/get-started/try-apache-beam-java.ipynb b/examples/notebooks/get-started/try-apache-beam-java.ipynb index 46ab413083cf..733d813e9cfc 100644 --- a/examples/notebooks/get-started/try-apache-beam-java.ipynb +++ b/examples/notebooks/get-started/try-apache-beam-java.ipynb @@ -104,7 +104,7 @@ "\n", "# Copy the input file into the local filesystem.\n", "run('mkdir -p data')\n", - "run('gsutil cp gs://dataflow-samples/shakespeare/kinglear.txt data/')" + "run('gcloud storage cp gs://dataflow-samples/shakespeare/kinglear.txt data/')" ], "execution_count": 1, "outputs": [ @@ -113,7 +113,7 @@ "text": [ ">> mkdir -p data\n", "\n", - ">> gsutil cp gs://dataflow-samples/shakespeare/kinglear.txt data/\n", + ">> gcloud storage cp gs://dataflow-samples/shakespeare/kinglear.txt data/\n", "Copying gs://dataflow-samples/shakespeare/kinglear.txt...\n", "/ [1 files][153.6 KiB/153.6 KiB] \n", "Operation completed over 1 objects/153.6 KiB. \n", diff --git a/examples/notebooks/get-started/try-apache-beam-py.ipynb b/examples/notebooks/get-started/try-apache-beam-py.ipynb index f243547979d7..656f87a2ebdf 100644 --- a/examples/notebooks/get-started/try-apache-beam-py.ipynb +++ b/examples/notebooks/get-started/try-apache-beam-py.ipynb @@ -107,7 +107,7 @@ "\n", "# Copy the input file into the local file system.\n", "run('mkdir -p data')\n", - "run('gsutil cp gs://dataflow-samples/shakespeare/kinglear.txt data/')" + "run('gcloud storage cp gs://dataflow-samples/shakespeare/kinglear.txt data/')" ], "execution_count": 1, "outputs": [ @@ -118,7 +118,7 @@ "\n", ">> mkdir -p data\n", "\n", - ">> gsutil cp gs://dataflow-samples/shakespeare/kinglear.txt data/\n", + ">> gcloud storage cp gs://dataflow-samples/shakespeare/kinglear.txt data/\n", "Copying gs://dataflow-samples/shakespeare/kinglear.txt...\n", "/ [1 files][153.6 KiB/153.6 KiB] \n", "Operation completed over 1 objects/153.6 KiB. \n", diff --git a/gradle.properties b/gradle.properties index 1d40c7ed4669..9503a0933dd3 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.77.0-SNAPSHOT +sdk_version=2.77.0.dev javaVersion=11 @@ -39,8 +39,11 @@ 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.19,1.20,2.0,2.1,2.2 # supported spark versions 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/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 diff --git a/infra/enforcement/account_keys.py b/infra/enforcement/account_keys.py index 4c3a8190d23f..56ccbf654b03 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. @@ -117,14 +164,15 @@ def _get_all_live_service_accounts(self) -> List[str]: request.name = f"projects/{self.project_id}" try: - accounts = self.service_account_client.list_service_accounts(request=request) - self.logger.debug(f"Retrieved {len(accounts.accounts)} service accounts for project {self.project_id}") + accounts_pager = self.service_account_client.list_service_accounts(request=request) + accounts = list(accounts_pager) + self.logger.debug(f"Retrieved {len(accounts)} service accounts for project {self.project_id}") if not accounts: self.logger.warning(f"No service accounts found in project {self.project_id}.") return [] - return [self._normalize_account_email(account.email) for account in accounts.accounts if not account.disabled] + return [self._normalize_account_email(account.email) for account in accounts if not account.disabled] except Exception as e: self.logger.error(f"Failed to retrieve service accounts for project {self.project_id}: {e}") raise @@ -259,21 +307,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 +369,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 +455,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 +588,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..bd9787b6ce87 100644 --- a/infra/enforcement/sending.py +++ b/infra/enforcement/sending.py @@ -59,25 +59,71 @@ def __init__(self, logger: logging.Logger, github_token: str, github_repo: str, self.logger = logger self.github_api_url = "https://api.github.com" - def _make_github_request(self, method: str, endpoint: str, json: Optional[dict] = None) -> requests.Response: + def _make_github_request(self, method: str, endpoint: str, json: Optional[dict] = None, params: Optional[dict] = None) -> requests.Response: """ - Makes a request to the GitHub API. + Makes a request to the GitHub API with retry logic for transient errors and rate limiting. Args: method (str): The HTTP method to use (e.g., "GET", "POST", "PATCH"). endpoint (str): The API endpoint to call. json (Optional[dict]): The JSON payload to send with the request. + params (Optional[dict]): The URL parameters to send with the request. Returns: requests.Response: The response from the API. """ + import time url = f"{self.github_api_url}/{endpoint}" - response = requests.request(method, url, headers=self.headers, json=json) + max_retries = 5 + backoff = 2 - if not response.ok: - self.logger.error(f"Failed GitHub API request to {endpoint}: {response.status_code} - {response.text}") - response.raise_for_status() - + for attempt in range(max_retries): + try: + response = requests.request(method, url, headers=self.headers, json=json, params=params) + + # Check for rate limiting / secondary rate limit (403, 429) + if response.status_code in [403, 429]: + retry_after = response.headers.get("Retry-After") + if retry_after: + sleep_seconds = int(retry_after) + else: + sleep_seconds = backoff + + self.logger.warning( + f"GitHub API rate limit hit ({response.status_code}) on {endpoint}. " + f"Retrying in {sleep_seconds} seconds... (Attempt {attempt + 1}/{max_retries})" + ) + time.sleep(sleep_seconds) + backoff *= 2 + continue + + # Check for transient server errors (500, 502, 503, 504) + if response.status_code in [500, 502, 503, 504]: + self.logger.warning( + f"GitHub API server error ({response.status_code}) on {endpoint}. " + f"Retrying in {backoff} seconds... (Attempt {attempt + 1}/{max_retries})" + ) + time.sleep(backoff) + backoff *= 2 + continue + + if not response.ok: + self.logger.error(f"Failed GitHub API request to {endpoint}: {response.status_code} - {response.text}") + response.raise_for_status() + + return response + except requests.exceptions.RequestException as e: + if attempt == max_retries - 1: + raise + self.logger.warning( + f"GitHub API request exception on {endpoint}: {e}. " + f"Retrying in {backoff} seconds... (Attempt {attempt + 1}/{max_retries})" + ) + time.sleep(backoff) + backoff *= 2 + + self.logger.error(f"Failed GitHub API request to {endpoint} after {max_retries} attempts.") + response.raise_for_status() return response def _send_email(self, title: str, body: str, recipient: str) -> None: @@ -97,15 +143,26 @@ def _send_email(self, title: str, body: str, recipient: str) -> None: def _get_open_issues(self, title: str) -> List[GitHubIssue]: """ - Retrieves the number of open GitHub issues with a given title. + Retrieves the open GitHub issues with a given title. 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" - response = self._make_github_request("GET", endpoint) + q = f'is:issue repo:{self.github_repo} in:title "{title}" is:open' + response = self._make_github_request("GET", "search/issues", params={"q": q}) 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 +176,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 +200,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. diff --git a/infra/enforcement/test_sending.py b/infra/enforcement/test_sending.py new file mode 100644 index 000000000000..26d4080adec5 --- /dev/null +++ b/infra/enforcement/test_sending.py @@ -0,0 +1,103 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.mock import patch, MagicMock +import logging +import requests +from sending import SendingClient + +class TestSendingClient(unittest.TestCase): + def setUp(self): + self.logger = logging.getLogger("TestLogger") + self.logger.setLevel(logging.DEBUG) + # Create a SendingClient with dummy values + self.client = SendingClient( + logger=self.logger, + github_token="dummy-token", + github_repo="apache/beam", + smtp_server="smtp.example.com", + smtp_port=587, + email="sender@example.com", + password="password" + ) + + @patch("requests.request") + def test_get_open_issues_flaky_retry(self, mock_request): + # We simulate a 403 secondary rate limit error on the first request, + # and a successful 200 response on the second request. + mock_response_fail = MagicMock() + mock_response_fail.status_code = 403 + mock_response_fail.ok = False + mock_response_fail.headers = {"Retry-After": "1"} + mock_response_fail.raise_for_status.side_effect = requests.exceptions.HTTPError("403 Client Error") + + mock_response_success = MagicMock() + mock_response_success.status_code = 200 + mock_response_success.ok = True + mock_response_success.json.return_value = { + "items": [ + { + "number": 1234, + "title": "[SECURITY] Action Required: Unmanaged Service Account Keys Detected", + "body": "Test body", + "state": "open", + "html_url": "https://github.com/apache/beam/issues/1234", + "created_at": "2026-07-02T00:00:00Z", + "updated_at": "2026-07-02T00:00:00Z" + } + ] + } + + mock_request.side_effect = [mock_response_fail, mock_response_success] + + # Call get_open_issues + issues = self.client._get_open_issues("[SECURITY] Action Required: Unmanaged Service Account Keys Detected") + + # Verify that two requests were made (one retry) + self.assertEqual(mock_request.call_count, 2) + self.assertEqual(len(issues), 1) + self.assertEqual(issues[0].number, 1234) + + @patch("requests.request") + def test_get_open_issues_query_format(self, mock_request): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.ok = True + mock_response.json.return_value = {"items": []} + mock_request.return_value = mock_response + + title = "[SECURITY] Action Required: Unmanaged Service Account Keys Detected" + self.client._get_open_issues(title) + + # Verify that the query parameter was passed correctly to requests + mock_request.assert_called_once() + args, kwargs = mock_request.call_args + + # Check that we passed params dict containing 'q' + self.assertIn("params", kwargs) + self.assertIn("q", kwargs["params"]) + + q = kwargs["params"]["q"] + # The query should specify the repo, title (quoted), state and type + self.assertIn('repo:apache/beam', q) + self.assertIn('is:issue', q) + self.assertIn('is:open', q) + self.assertIn('in:title', q) + self.assertIn(f'"{title}"', q) + + +if __name__ == "__main__": + unittest.main() diff --git a/infra/iam/roles/roles_config.yaml b/infra/iam/roles/roles_config.yaml index 453a3b07d977..1e94cdc2ccbd 100644 --- a/infra/iam/roles/roles_config.yaml +++ b/infra/iam/roles/roles_config.yaml @@ -52,7 +52,7 @@ roles: - iap - meshconfig - monitoring - - pubsub. # TODO: Remove '.' after Pubsublite GCP service is fully deprecated. + - pubsub - redis - resourcemanager - secretmanager diff --git a/infra/iam/users.yml b/infra/iam/users.yml index 42b56a6de53d..6c37b4c1fef0 100644 --- a/infra/iam/users.yml +++ b/infra/iam/users.yml @@ -125,11 +125,6 @@ member_type: user permissions: - role: roles/editor -- username: andres.vervaecke - email: andres.vervaecke@ml6.eu - member_type: user - permissions: - - role: roles/viewer - username: andrey.devyatkin email: andrey.devyatkin@akvelon.com member_type: user @@ -486,6 +481,11 @@ - 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_infra_manager - username: harrisonlim email: harrisonlim@google.com member_type: user @@ -517,11 +517,6 @@ - role: roles/pubsub.subscriber - role: roles/pubsub.viewer - role: roles/storage.admin -- username: jasper.van.den.bossche - email: jasper.van.den.bossche@ml6.eu - member_type: user - permissions: - - role: roles/editor - username: jeffreylwang email: jeffreylwang@google.com member_type: user @@ -1241,4 +1236,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 diff --git a/it/build.gradle b/it/build.gradle index 42a9ad9f4ee8..ad6ba19f383d 100644 --- a/it/build.gradle +++ b/it/build.gradle @@ -34,4 +34,8 @@ tasks.register('BigTablePerformanceTest') { tasks.register('BigQueryStorageApiStreamingPerformanceTest') { dependsOn(":it:google-cloud-platform:BigQueryStorageApiStreamingPerformanceTest") -} \ No newline at end of file +} + +tasks.register('IcebergPerformanceTest') { + dependsOn(":it:iceberg:IcebergPerformanceTest") +} diff --git a/it/clickhouse/build.gradle b/it/clickhouse/build.gradle index 858d0dea9207..0727f0328e45 100644 --- a/it/clickhouse/build.gradle +++ b/it/clickhouse/build.gradle @@ -29,6 +29,12 @@ ext.summary = "Integration test utilities for ClickHouse." def clickhouse_java_client_version = "0.9.6" +configurations.configureEach { + resolutionStrategy.capabilitiesResolution.withCapability("org.lz4:lz4-java") { + select("at.yawk.lz4:lz4-java:1.10.2") + } +} + dependencies { implementation project(path: ":it:common") implementation project(path: ":it:testcontainers") @@ -52,4 +58,4 @@ dependencies { permitUnusedDeclared "com.clickhouse:client-v2:$clickhouse_java_client_version:all" -} \ No newline at end of file +} diff --git a/it/common/build.gradle b/it/common/build.gradle index c92ec551cb25..62dd45ddacf5 100644 --- a/it/common/build.gradle +++ b/it/common/build.gradle @@ -28,6 +28,19 @@ ext.summary = "Code used by all integration test utilities." dependencies { implementation enforcedPlatform(library.java.google_cloud_platform_libraries_bom) implementation project(path: ":sdks:java:core", configuration: "shadow") + implementation project(path: ":runners:google-cloud-dataflow-java") + implementation 'com.google.cloud:google-cloud-bigquery' + implementation 'com.google.cloud:google-cloud-monitoring' + implementation 'com.google.cloud:google-cloud-storage' + implementation 'org.apache.hadoop:hadoop-common:3.3.6' + implementation 'org.apache.avro:avro:1.11.1' + implementation 'org.apache.parquet:parquet-avro:1.15.2' + implementation 'org.apache.parquet:parquet-common:1.15.2' + implementation 'org.apache.parquet:parquet-hadoop:1.15.2' + provided 'com.google.api.grpc:proto-google-cloud-monitoring-v3' + implementation library.java.jackson_core + implementation library.java.jackson_databind + implementation library.java.gax implementation library.java.google_api_services_dataflow implementation library.java.google_auth_library_credentials implementation library.java.google_auth_library_oauth2_http @@ -36,14 +49,20 @@ dependencies { implementation library.java.commons_lang3 implementation library.java.failsafe implementation library.java.google_code_gson + implementation library.java.google_cloud_core + implementation library.java.google_api_client implementation library.java.google_http_client implementation library.java.guava - + implementation library.java.protobuf_java_util + implementation library.java.protobuf_java + implementation library.java.junit + testImplementation library.java.mockito_inline + testRuntimeOnly library.java.slf4j_simple // TODO: excluding Guava until Truth updates it to >32.1.x testImplementation(library.java.truth) { - exclude group: 'com.google.guava', module: 'guava' + exclude group: 'com.google.guava', module: 'guava' } - testImplementation library.java.junit - testImplementation library.java.mockito_inline - testRuntimeOnly library.java.slf4j_simple + testImplementation project(path: ":runners:direct-java", configuration: "shadow") + testImplementation project(path: ":sdks:java:testing:test-utils") + testImplementation project(path: ":sdks:java:extensions:protobuf", configuration: "testRuntimeMigration") } diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/Artifact.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/Artifact.java similarity index 97% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/Artifact.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/Artifact.java index 830f44598be7..6371d6eedb1e 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/Artifact.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/Artifact.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts; +package org.apache.beam.it.common.artifacts; /** * Represents a single artifact. diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/ArtifactClient.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/ArtifactClient.java similarity index 99% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/ArtifactClient.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/ArtifactClient.java index e463baffeaf1..b275d35703e1 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/ArtifactClient.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/ArtifactClient.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts; +package org.apache.beam.it.common.artifacts; import java.io.IOException; import java.nio.file.Path; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/GcsArtifact.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/GcsArtifact.java similarity index 96% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/GcsArtifact.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/GcsArtifact.java index c60e3a0fe53a..5f4360ddbb2b 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/GcsArtifact.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/GcsArtifact.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts; +package org.apache.beam.it.common.artifacts; import com.google.cloud.storage.Blob; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/package-info.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/package-info.java similarity index 95% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/package-info.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/package-info.java index de2a2541d558..44b54f82aeba 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/package-info.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/package-info.java @@ -17,4 +17,4 @@ */ /** Package for working with test artifacts. */ -package org.apache.beam.it.gcp.artifacts; +package org.apache.beam.it.common.artifacts; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/ArtifactUtils.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/ArtifactUtils.java similarity index 98% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/ArtifactUtils.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/ArtifactUtils.java index 854651ae1f14..3a3bcb2d7ec4 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/ArtifactUtils.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/ArtifactUtils.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts.utils; +package org.apache.beam.it.common.artifacts.utils; import static java.util.Arrays.stream; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/AvroTestUtil.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/AvroTestUtil.java similarity index 98% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/AvroTestUtil.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/AvroTestUtil.java index 5cfe4c137e35..58241f9a1db3 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/AvroTestUtil.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/AvroTestUtil.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts.utils; +package org.apache.beam.it.common.artifacts.utils; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/JsonTestUtil.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/JsonTestUtil.java similarity index 99% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/JsonTestUtil.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/JsonTestUtil.java index 9a83558f7bfc..08c3cdfb0c5a 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/JsonTestUtil.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/JsonTestUtil.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts.utils; +package org.apache.beam.it.common.artifacts.utils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/ParquetTestUtil.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/ParquetTestUtil.java similarity index 98% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/ParquetTestUtil.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/ParquetTestUtil.java index d6ebf825e2c1..fb4326e1b4a7 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/ParquetTestUtil.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/ParquetTestUtil.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts.utils; +package org.apache.beam.it.common.artifacts.utils; import java.io.ByteArrayInputStream; import java.io.File; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/package-info.java b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/package-info.java similarity index 94% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/package-info.java rename to it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/package-info.java index 53cb4ed1d3af..aaf820b25103 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/utils/package-info.java +++ b/it/common/src/main/java/org/apache/beam/it/common/artifacts/utils/package-info.java @@ -17,4 +17,4 @@ */ /** Package for artifact utilities. */ -package org.apache.beam.it.gcp.artifacts.utils; +package org.apache.beam.it.common.artifacts.utils; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java b/it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManager.java similarity index 97% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java rename to it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManager.java index 45c22b1f0d55..728a47b224b4 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java +++ b/it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManager.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.bigquery; +package org.apache.beam.it.common.bigquery; import com.google.api.gax.paging.Page; import com.google.auth.Credentials; @@ -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 '{}'.", diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerException.java b/it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerException.java similarity index 96% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerException.java rename to it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerException.java index 61ef27e45260..ce95dc891551 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerException.java +++ b/it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerException.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.bigquery; +package org.apache.beam.it.common.bigquery; /** Custom exception for {@link BigQueryResourceManager} implementations. */ public class BigQueryResourceManagerException extends RuntimeException { diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerUtils.java b/it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerUtils.java similarity index 98% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerUtils.java rename to it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerUtils.java index f2b6849caa61..56cb82013885 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerUtils.java +++ b/it/common/src/main/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerUtils.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.bigquery; +package org.apache.beam.it.common.bigquery; import static org.apache.beam.it.common.utils.ResourceManagerUtils.generateResourceId; diff --git a/it/common/src/main/java/org/apache/beam/it/common/bigquery/package-info.java b/it/common/src/main/java/org/apache/beam/it/common/bigquery/package-info.java new file mode 100644 index 000000000000..f2d5978fb4eb --- /dev/null +++ b/it/common/src/main/java/org/apache/beam/it/common/bigquery/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. + */ + +/** Package for managing BigQuery common resources within integration tests. */ +package org.apache.beam.it.common.bigquery; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/AbstractPipelineLauncher.java b/it/common/src/main/java/org/apache/beam/it/common/dataflow/AbstractPipelineLauncher.java similarity index 99% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/AbstractPipelineLauncher.java rename to it/common/src/main/java/org/apache/beam/it/common/dataflow/AbstractPipelineLauncher.java index 9d00fa67f145..a6efaeca5afc 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/AbstractPipelineLauncher.java +++ b/it/common/src/main/java/org/apache/beam/it/common/dataflow/AbstractPipelineLauncher.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.dataflow; +package org.apache.beam.it.common.dataflow; import static org.apache.beam.it.common.PipelineLauncher.JobState.ACTIVE_STATES; import static org.apache.beam.it.common.PipelineLauncher.JobState.FAILED; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/DefaultPipelineLauncher.java b/it/common/src/main/java/org/apache/beam/it/common/dataflow/DefaultPipelineLauncher.java similarity index 92% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/DefaultPipelineLauncher.java rename to it/common/src/main/java/org/apache/beam/it/common/dataflow/DefaultPipelineLauncher.java index 11a09c4ba749..af6438a561aa 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/DefaultPipelineLauncher.java +++ b/it/common/src/main/java/org/apache/beam/it/common/dataflow/DefaultPipelineLauncher.java @@ -15,10 +15,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.dataflow; +package org.apache.beam.it.common.dataflow; import static org.apache.beam.it.common.logging.LogStrings.formatForLogging; -import static org.apache.beam.sdk.testing.TestPipeline.PROPERTY_BEAM_TEST_PIPELINE_OPTIONS; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import com.fasterxml.jackson.databind.ObjectMapper; @@ -41,7 +40,6 @@ import java.util.stream.StreamSupport; import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.utils.PipelineUtils; -import org.apache.beam.it.gcp.IOLoadTestBase; import org.apache.beam.runners.dataflow.DataflowPipelineJob; import org.apache.beam.sdk.PipelineResult; import org.apache.beam.sdk.metrics.DistributionResult; @@ -73,6 +71,8 @@ public class DefaultPipelineLauncher extends AbstractPipelineLauncher { private static final String READ_PIPELINE_NAME_OVERWRITE = "readPipelineNameOverride"; private static final String WRITE_PIPELINE_NAME_OVERWRITE = "writePipelineNameOverride"; private static final Pattern JOB_ID_PATTERN = Pattern.compile("Submitted job: (\\S+)"); + /** Namespace for Beam provided pipeline metrics (set up by Metrics transform). */ + public static final String BEAM_METRICS_NAMESPACE = "BEAM_METRICS"; // For unsupported runners (other than dataflow), implement launcher methods by operating with // PipelineResult. @@ -101,6 +101,18 @@ public class DefaultPipelineLauncher extends AbstractPipelineLauncher { .put(PipelineResult.State.UNRECOGNIZED, JobState.UNKNOWN) .build(); + // To make PipelineLauncher.getMetric work in a unified way for both runner provided metrics and + // pipeline defined + // metrics, here we wrap Beam provided metrics as a pre-defined metrics name + // [name_space:metric_type:metric_name + // which will be recognized by getMetric method + public enum PipelineMetricsType { + COUNTER, + STARTTIME, + ENDTIME, + RUNTIME, + } + private DefaultPipelineLauncher(Builder builder) { super( new Dataflow( @@ -169,7 +181,7 @@ private static void checkIfMetricResultIsUnique( resultCount <= 1, "More than one metric result matches name: %s in namespace %s. Metric results count: %s", name, - IOLoadTestBase.BEAM_METRICS_NAMESPACE, + BEAM_METRICS_NAMESPACE, resultCount); } @@ -180,15 +192,13 @@ private static Iterable> getDistributions( .metrics() .queryMetrics( MetricsFilter.builder() - .addNameFilter( - MetricNameFilter.named(IOLoadTestBase.BEAM_METRICS_NAMESPACE, metricName)) + .addNameFilter(MetricNameFilter.named(BEAM_METRICS_NAMESPACE, metricName)) .build()); return metrics.getDistributions(); } /** Pull Beam pipeline defined metrics given the jobId. */ - public Long getBeamMetric( - String jobId, IOLoadTestBase.PipelineMetricsType metricType, String metricName) { + public Long getBeamMetric(String jobId, PipelineMetricsType metricType, String metricName) { PipelineResult pipelineResult = MANAGED_JOBS.getOrDefault(jobId, UNMANAGED_JOBS.getOrDefault(jobId, null)); if (pipelineResult != null) { @@ -197,8 +207,7 @@ public Long getBeamMetric( .metrics() .queryMetrics( MetricsFilter.builder() - .addNameFilter( - MetricNameFilter.named(IOLoadTestBase.BEAM_METRICS_NAMESPACE, metricName)) + .addNameFilter(MetricNameFilter.named(BEAM_METRICS_NAMESPACE, metricName)) .build()); switch (metricType) { @@ -210,9 +219,7 @@ public Long getBeamMetric( return metricResult.getAttempted(); } catch (NoSuchElementException e) { LOG.error( - "Failed to get metric {}, from namespace {}", - metricName, - IOLoadTestBase.BEAM_METRICS_NAMESPACE); + "Failed to get metric {}, from namespace {}", metricName, BEAM_METRICS_NAMESPACE); } return UNKNOWN_METRIC_VALUE; case STARTTIME: @@ -230,9 +237,9 @@ public Long getBeamMetric( .map(element -> Objects.requireNonNull(element.getAttempted()).getMax()) .max(Long::compareTo) .orElse(UNKNOWN_METRIC_VALUE); - if (metricType == IOLoadTestBase.PipelineMetricsType.STARTTIME) { + if (metricType == PipelineMetricsType.STARTTIME) { return lowestMin; - } else if (metricType == IOLoadTestBase.PipelineMetricsType.ENDTIME) { + } else if (metricType == PipelineMetricsType.ENDTIME) { return greatestMax; } else { if (lowestMin != UNKNOWN_METRIC_VALUE && greatestMax != UNKNOWN_METRIC_VALUE) { @@ -254,15 +261,14 @@ public Long getBeamMetric( @Override public Double getMetric(String project, String region, String jobId, String metricName) throws IOException { - if (metricName.startsWith(IOLoadTestBase.BEAM_METRICS_NAMESPACE)) { + if (metricName.startsWith(BEAM_METRICS_NAMESPACE)) { String[] nameSpacedMetrics = metricName.split(":", 3); Preconditions.checkState( nameSpacedMetrics.length == 3, String.format( "Invalid Beam metrics name: %s, expected: '%s:metric_type:metric_name'", - metricName, IOLoadTestBase.BEAM_METRICS_NAMESPACE)); - IOLoadTestBase.PipelineMetricsType metricType = - IOLoadTestBase.PipelineMetricsType.valueOf(nameSpacedMetrics[1]); + metricName, BEAM_METRICS_NAMESPACE)); + PipelineMetricsType metricType = PipelineMetricsType.valueOf(nameSpacedMetrics[1]); // Pipeline defined metrics are long values. Have to cast to double that is what the base // class defined. @@ -437,16 +443,18 @@ private List extractOptions(String project, String region, LaunchConfig // add pipeline options from beamTestPipelineOptions system property to preserve the // pipeline options already set in TestPipeline. @Nullable - String beamTestPipelineOptions = System.getProperty(PROPERTY_BEAM_TEST_PIPELINE_OPTIONS); + String beamTestPipelineOptions = + System.getProperty( + org.apache.beam.sdk.testing.TestPipeline.PROPERTY_BEAM_TEST_PIPELINE_OPTIONS); if (!Strings.isNullOrEmpty(beamTestPipelineOptions)) { try { additionalOptions.addAll(MAPPER.readValue(beamTestPipelineOptions, List.class)); } catch (IOException e) { throw new RuntimeException( "Unable to instantiate test options from system property " - + PROPERTY_BEAM_TEST_PIPELINE_OPTIONS + + org.apache.beam.sdk.testing.TestPipeline.PROPERTY_BEAM_TEST_PIPELINE_OPTIONS + ":" - + System.getProperty(PROPERTY_BEAM_TEST_PIPELINE_OPTIONS), + + beamTestPipelineOptions, e); } } diff --git a/it/common/src/main/java/org/apache/beam/it/common/dataflow/package-info.java b/it/common/src/main/java/org/apache/beam/it/common/dataflow/package-info.java new file mode 100644 index 000000000000..eb10897523bc --- /dev/null +++ b/it/common/src/main/java/org/apache/beam/it/common/dataflow/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. + */ + +/** Package for managing Dataflow jobs from integration tests. */ +package org.apache.beam.it.common.dataflow; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/monitoring/MonitoringClient.java b/it/common/src/main/java/org/apache/beam/it/common/monitoring/MonitoringClient.java similarity index 99% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/monitoring/MonitoringClient.java rename to it/common/src/main/java/org/apache/beam/it/common/monitoring/MonitoringClient.java index 66550b5a5073..6b447d84512d 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/monitoring/MonitoringClient.java +++ b/it/common/src/main/java/org/apache/beam/it/common/monitoring/MonitoringClient.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.monitoring; +package org.apache.beam.it.common.monitoring; import com.google.api.gax.core.CredentialsProvider; import com.google.cloud.monitoring.v3.MetricServiceClient; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/monitoring/package-info.java b/it/common/src/main/java/org/apache/beam/it/common/monitoring/package-info.java similarity index 94% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/monitoring/package-info.java rename to it/common/src/main/java/org/apache/beam/it/common/monitoring/package-info.java index 9cd79ea9cf12..731bbc3a6b25 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/monitoring/package-info.java +++ b/it/common/src/main/java/org/apache/beam/it/common/monitoring/package-info.java @@ -17,4 +17,4 @@ */ /** Package for querying metrics from cloud monitoring. */ -package org.apache.beam.it.gcp.monitoring; +package org.apache.beam.it.common.monitoring; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/storage/GcsResourceManager.java b/it/common/src/main/java/org/apache/beam/it/common/storage/GcsResourceManager.java similarity index 97% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/storage/GcsResourceManager.java rename to it/common/src/main/java/org/apache/beam/it/common/storage/GcsResourceManager.java index 730ca23ee54b..85f2c8b0b018 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/storage/GcsResourceManager.java +++ b/it/common/src/main/java/org/apache/beam/it/common/storage/GcsResourceManager.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.storage; +package org.apache.beam.it.common.storage; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; @@ -39,10 +39,10 @@ import java.util.function.Consumer; import java.util.regex.Pattern; import org.apache.beam.it.common.ResourceManager; -import org.apache.beam.it.gcp.artifacts.Artifact; -import org.apache.beam.it.gcp.artifacts.ArtifactClient; -import org.apache.beam.it.gcp.artifacts.GcsArtifact; -import org.apache.beam.it.gcp.artifacts.utils.ArtifactUtils; +import org.apache.beam.it.common.artifacts.Artifact; +import org.apache.beam.it.common.artifacts.ArtifactClient; +import org.apache.beam.it.common.artifacts.GcsArtifact; +import org.apache.beam.it.common.artifacts.utils.ArtifactUtils; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.junit.rules.TestName; import org.slf4j.Logger; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/storage/package-info.java b/it/common/src/main/java/org/apache/beam/it/common/storage/package-info.java similarity index 95% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/storage/package-info.java rename to it/common/src/main/java/org/apache/beam/it/common/storage/package-info.java index efcea3aba267..1c5b3b3ce21e 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/storage/package-info.java +++ b/it/common/src/main/java/org/apache/beam/it/common/storage/package-info.java @@ -17,4 +17,4 @@ */ /** Package for managing Google Cloud Storage resources within integration tests. */ -package org.apache.beam.it.gcp.storage; +package org.apache.beam.it.common.storage; diff --git a/it/google-cloud-platform/src/main/resources/test-artifact.json b/it/common/src/main/resources/test-artifact.json similarity index 100% rename from it/google-cloud-platform/src/main/resources/test-artifact.json rename to it/common/src/main/resources/test-artifact.json diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/artifacts/GcsArtifactTest.java b/it/common/src/test/java/org/apache/beam/it/common/artifacts/GcsArtifactTest.java similarity index 94% rename from it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/artifacts/GcsArtifactTest.java rename to it/common/src/test/java/org/apache/beam/it/common/artifacts/GcsArtifactTest.java index 59b7931b603a..470741b399ad 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/artifacts/GcsArtifactTest.java +++ b/it/common/src/test/java/org/apache/beam/it/common/artifacts/GcsArtifactTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts; +package org.apache.beam.it.common.artifacts; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; @@ -30,7 +30,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -/** Unit tests for {@link GcsArtifact}. */ +/** Unit tests for {@link org.apache.beam.it.common.artifacts.GcsArtifact}. */ @RunWith(JUnit4.class) public class GcsArtifactTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/artifacts/utils/ArtifactUtilsTest.java b/it/common/src/test/java/org/apache/beam/it/common/artifacts/utils/ArtifactUtilsTest.java similarity index 94% rename from it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/artifacts/utils/ArtifactUtilsTest.java rename to it/common/src/test/java/org/apache/beam/it/common/artifacts/utils/ArtifactUtilsTest.java index 491d32d0ce6a..137c35aaf4bb 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/artifacts/utils/ArtifactUtilsTest.java +++ b/it/common/src/test/java/org/apache/beam/it/common/artifacts/utils/ArtifactUtilsTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts.utils; +package org.apache.beam.it.common.artifacts.utils; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -24,7 +24,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** Artifacts for {@link ArtifactUtils}. */ +/** Artifacts for {@link org.apache.beam.it.common.artifacts.utils.ArtifactUtils}. */ @RunWith(JUnit4.class) public final class ArtifactUtilsTest { diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerTest.java b/it/common/src/test/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerTest.java similarity index 99% rename from it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerTest.java rename to it/common/src/test/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerTest.java index 53987dbe2670..62aac93b4eed 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerTest.java +++ b/it/common/src/test/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.bigquery; +package org.apache.beam.it.common.bigquery; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerUtilsTest.java b/it/common/src/test/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerUtilsTest.java similarity index 93% rename from it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerUtilsTest.java rename to it/common/src/test/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerUtilsTest.java index 432b7ef7362f..7fc848b45292 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManagerUtilsTest.java +++ b/it/common/src/test/java/org/apache/beam/it/common/bigquery/BigQueryResourceManagerUtilsTest.java @@ -15,9 +15,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.bigquery; +package org.apache.beam.it.common.bigquery; -import static org.apache.beam.it.gcp.bigquery.BigQueryResourceManagerUtils.checkValidTableId; +import static org.apache.beam.it.common.bigquery.BigQueryResourceManagerUtils.checkValidTableId; import static org.junit.Assert.assertThrows; import java.util.Arrays; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/AbstractPipelineLauncherTest.java b/it/common/src/test/java/org/apache/beam/it/common/dataflow/AbstractPipelineLauncherTest.java similarity index 99% rename from it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/AbstractPipelineLauncherTest.java rename to it/common/src/test/java/org/apache/beam/it/common/dataflow/AbstractPipelineLauncherTest.java index f2c33d70116b..72d5514a6e3c 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/AbstractPipelineLauncherTest.java +++ b/it/common/src/test/java/org/apache/beam/it/common/dataflow/AbstractPipelineLauncherTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.dataflow; +package org.apache.beam.it.common.dataflow; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/DefaultPipelineLauncherTest.java b/it/common/src/test/java/org/apache/beam/it/common/dataflow/DefaultPipelineLauncherTest.java similarity index 91% rename from it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/DefaultPipelineLauncherTest.java rename to it/common/src/test/java/org/apache/beam/it/common/dataflow/DefaultPipelineLauncherTest.java index b6c0f8cdc58f..35510a0f1d1c 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/DefaultPipelineLauncherTest.java +++ b/it/common/src/test/java/org/apache/beam/it/common/dataflow/DefaultPipelineLauncherTest.java @@ -15,16 +15,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.dataflow; +package org.apache.beam.it.common.dataflow; +import static org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.BEAM_METRICS_NAMESPACE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.time.Instant; import org.apache.beam.it.common.PipelineLauncher; -import org.apache.beam.it.gcp.IOLoadTestBase; -import org.apache.beam.it.gcp.IOLoadTestBase.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; import org.apache.beam.sdk.io.GenerateSequence; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.testutils.metrics.TimeMonitor; @@ -48,7 +48,7 @@ public void testPipelineMetrics() throws IOException { pipeline .apply(GenerateSequence.from(0).to(numElements)) - .apply(ParDo.of(new TimeMonitor<>(IOLoadTestBase.BEAM_METRICS_NAMESPACE, timeMetrics))) + .apply(ParDo.of(new TimeMonitor<>(BEAM_METRICS_NAMESPACE, timeMetrics))) .apply(ParDo.of(new IOLoadTestBase.CountingFn<>(counterMetrics))); PipelineLauncher.LaunchConfig options = diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/IOLoadTestBase.java b/it/common/src/test/java/org/apache/beam/it/common/dataflow/IOLoadTestBase.java similarity index 87% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/IOLoadTestBase.java rename to it/common/src/test/java/org/apache/beam/it/common/dataflow/IOLoadTestBase.java index 14770a429731..c35f402bd8ac 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/IOLoadTestBase.java +++ b/it/common/src/test/java/org/apache/beam/it/common/dataflow/IOLoadTestBase.java @@ -15,7 +15,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp; +package org.apache.beam.it.common.dataflow; + +import static org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.BEAM_METRICS_NAMESPACE; import com.google.cloud.Timestamp; import java.io.IOException; @@ -25,7 +27,7 @@ import java.util.UUID; import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.TestProperties; -import org.apache.beam.it.gcp.dataflow.DefaultPipelineLauncher; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Metrics; import org.apache.beam.sdk.testutils.NamedTestResult; @@ -90,21 +92,6 @@ public void processElement(ProcessContext ctx) { } } - // To make PipelineLauncher.getMetric work in a unified way for both runner provided metrics and - // pipeline defined - // metrics, here we wrap Beam provided metrics as a pre-defined metrics name - // [name_space:metric_type:metric_name - // which will be recognized by getMetric method - public enum PipelineMetricsType { - COUNTER, - STARTTIME, - ENDTIME, - RUNTIME, - } - - /** Namespace for Beam provided pipeline metrics (set up by Metrics transform). */ - public static final String BEAM_METRICS_NAMESPACE = "BEAM_METRICS"; - /** Given a metrics name, return Beam metrics name. */ public static String getBeamMetricsName(PipelineMetricsType metricstype, String metricsName) { return BEAM_METRICS_NAMESPACE + ":" + metricstype + ":" + metricsName; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/IOStressTestBase.java b/it/common/src/test/java/org/apache/beam/it/common/dataflow/IOStressTestBase.java similarity index 99% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/IOStressTestBase.java rename to it/common/src/test/java/org/apache/beam/it/common/dataflow/IOStressTestBase.java index 5c2fb74cd2fb..e08eec031eb5 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/IOStressTestBase.java +++ b/it/common/src/test/java/org/apache/beam/it/common/dataflow/IOStressTestBase.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp; +package org.apache.beam.it.common.dataflow; import java.io.Serializable; import java.time.Duration; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/LoadTestBase.java b/it/common/src/test/java/org/apache/beam/it/common/dataflow/LoadTestBase.java similarity index 98% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/LoadTestBase.java rename to it/common/src/test/java/org/apache/beam/it/common/dataflow/LoadTestBase.java index cd9ef52ed835..cd1b71dc7552 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/LoadTestBase.java +++ b/it/common/src/test/java/org/apache/beam/it/common/dataflow/LoadTestBase.java @@ -15,10 +15,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp; +package org.apache.beam.it.common.dataflow; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.RUNNER_V2; import static org.apache.beam.it.common.logging.LogStrings.formatForLogging; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.RUNNER_V2; import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider; @@ -44,8 +44,8 @@ import org.apache.beam.it.common.PipelineLauncher.LaunchInfo; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; -import org.apache.beam.it.gcp.bigquery.BigQueryResourceManager; -import org.apache.beam.it.gcp.monitoring.MonitoringClient; +import org.apache.beam.it.common.bigquery.BigQueryResourceManager; +import org.apache.beam.it.common.monitoring.MonitoringClient; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.After; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/storage/GcsResourceManagerTest.java b/it/common/src/test/java/org/apache/beam/it/common/storage/GcsResourceManagerTest.java similarity index 95% rename from it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/storage/GcsResourceManagerTest.java rename to it/common/src/test/java/org/apache/beam/it/common/storage/GcsResourceManagerTest.java index 0153573feaed..70dc6dcbc9c4 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/storage/GcsResourceManagerTest.java +++ b/it/common/src/test/java/org/apache/beam/it/common/storage/GcsResourceManagerTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.storage; +package org.apache.beam.it.common.storage; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -35,6 +35,7 @@ import com.google.cloud.storage.Storage; import com.google.cloud.storage.Storage.BlobListOption; import com.google.cloud.storage.Storage.BucketListOption; +import com.google.common.truth.Truth; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; @@ -43,8 +44,8 @@ import java.util.List; import java.util.UUID; import java.util.regex.Pattern; -import org.apache.beam.it.gcp.artifacts.Artifact; -import org.apache.beam.it.gcp.artifacts.GcsArtifact; +import org.apache.beam.it.common.artifacts.Artifact; +import org.apache.beam.it.common.artifacts.GcsArtifact; 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.io.Resources; import org.junit.After; @@ -128,7 +129,7 @@ public void testCreateArtifactInRunDir() { verify(client).create(blobInfoCaptor.capture(), contentsCaptor.capture()); BlobInfo actualInfo = blobInfoCaptor.getValue(); - assertThat(actual.blob).isSameInstanceAs(blob); + Truth.assertThat(actual.blob).isSameInstanceAs(blob); assertThat(actualInfo.getBucket()).isEqualTo(BUCKET); assertThat(actualInfo.getName()) .isEqualTo(String.format("%s/%s/%s", TEST_CLASS, gcsClient.runId(), artifactName)); @@ -144,7 +145,7 @@ public void testUploadArtifact() throws IOException { verify(client).create(blobInfoCaptor.capture(), contentsCaptor.capture()); BlobInfo actualInfo = blobInfoCaptor.getValue(); - assertThat(actual.blob).isSameInstanceAs(blob); + Truth.assertThat(actual.blob).isSameInstanceAs(blob); assertThat(actualInfo.getBucket()).isEqualTo(BUCKET); assertThat(actualInfo.getName()) .isEqualTo(String.format("%s/%s/%s", TEST_CLASS, gcsClient.runId(), ARTIFACT_NAME)); @@ -185,8 +186,8 @@ public void testListArtifactsInMethodDirSinglePage() { BlobListOption actualOptions = listOptionsCaptor.getValue(); assertThat(actual).hasSize(2); - assertThat(actual.get(0).name()).isEqualTo(name1); - assertThat(actual.get(1).name()).isEqualTo(name3); + Truth.assertThat(actual.get(0).name()).isEqualTo(name1); + Truth.assertThat(actual.get(1).name()).isEqualTo(name3); assertThat(actualBucket).isEqualTo(BUCKET); assertThat(actualOptions) .isEqualTo( @@ -221,8 +222,8 @@ public void testListArtifactsInMethodDirMultiplePages() { BlobListOption actualOptions = listOptionsCaptor.getValue(); assertThat(actual).hasSize(2); - assertThat(actual.get(0).name()).isEqualTo(name1); - assertThat(actual.get(1).name()).isEqualTo(name3); + Truth.assertThat(actual.get(0).name()).isEqualTo(name1); + Truth.assertThat(actual.get(1).name()).isEqualTo(name3); assertThat(actualBucket).isEqualTo(BUCKET); assertThat(actualOptions) .isEqualTo( diff --git a/it/google-cloud-platform/build.gradle b/it/google-cloud-platform/build.gradle index 3a46f2b94d83..164a75c06ba0 100644 --- a/it/google-cloud-platform/build.gradle +++ b/it/google-cloud-platform/build.gradle @@ -27,49 +27,33 @@ applyJavaNature( description = "Apache Beam :: IT :: Google Cloud Platform" ext.summary = "Integration test utilities for Google Cloud Platform." +evaluationDependsOn(":it:common") + dependencies { implementation enforcedPlatform(library.java.google_cloud_platform_libraries_bom) - implementation project(path: ":sdks:java:core", configuration: "shadow") - implementation project(path: ":runners:google-cloud-dataflow-java") implementation project(path: ":it:common") implementation project(path: ":it:conditions") implementation project(path: ":it:truthmatchers") - implementation project(path: ":sdks:java:testing:test-utils") implementation library.java.failsafe implementation library.java.google_api_services_dataflow implementation library.java.junit implementation library.java.slf4j_api - implementation(library.java.truth) { - exclude group: 'com.google.guava', module: 'guava' - } implementation library.java.vendored_guava_32_1_2_jre implementation library.java.jackson_core implementation library.java.jackson_databind - implementation 'org.apache.hadoop:hadoop-common:3.3.5' - implementation 'org.apache.avro:avro:1.11.1' - implementation 'org.apache.parquet:parquet-avro:1.15.2' - implementation 'org.apache.parquet:parquet-common:1.15.2' - implementation 'org.apache.parquet:parquet-hadoop:1.15.2' implementation 'org.apache.commons:commons-lang3:3.9' implementation library.java.gax implementation library.java.google_api_common implementation library.java.google_api_client - implementation library.java.google_api_services_bigquery implementation library.java.google_auth_library_credentials implementation library.java.google_auth_library_oauth2_http - implementation library.java.google_http_client implementation library.java.guava - implementation library.java.protobuf_java_util implementation library.java.protobuf_java implementation library.java.threetenbp implementation 'org.awaitility:awaitility:4.2.0' - implementation 'joda-time:joda-time:2.10.10' // Google Cloud Dependencies implementation library.java.google_cloud_core - implementation 'com.google.cloud:google-cloud-storage' implementation 'com.google.cloud:google-cloud-bigquery' - implementation 'com.google.cloud:google-cloud-monitoring' - provided 'com.google.api.grpc:proto-google-cloud-monitoring-v3' implementation 'com.google.cloud:google-cloud-bigtable' implementation 'com.google.cloud:google-cloud-spanner' implementation 'com.google.cloud:google-cloud-pubsub' @@ -84,6 +68,7 @@ dependencies { implementation 'com.google.cloud:google-cloud-secretmanager' provided 'com.google.api.grpc:proto-google-cloud-secretmanager-v1' + testImplementation project(path: ":it:common", configuration: "testRuntimeMigration") testImplementation project(path: ":sdks:java:io:google-cloud-platform") testImplementation project(path: ":sdks:java:extensions:protobuf", configuration: "testRuntimeMigration") testImplementation project(path: ":sdks:java:io:synthetic") diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/conditions/BigQueryRowsCheck.java b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/conditions/BigQueryRowsCheck.java index 28626f7edd18..da0cf5e55dbe 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/conditions/BigQueryRowsCheck.java +++ b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/conditions/BigQueryRowsCheck.java @@ -19,8 +19,8 @@ import com.google.auto.value.AutoValue; import com.google.cloud.bigquery.TableId; +import org.apache.beam.it.common.bigquery.BigQueryResourceManager; import org.apache.beam.it.conditions.ConditionCheck; -import org.apache.beam.it.gcp.bigquery.BigQueryResourceManager; import org.checkerframework.checker.nullness.qual.Nullable; /** ConditionCheck to validate if BigQuery has received a certain number of rows. */ diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/ClassicTemplateClient.java b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/ClassicTemplateClient.java index 00aa4003817f..7908246dce13 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/ClassicTemplateClient.java +++ b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/ClassicTemplateClient.java @@ -30,6 +30,7 @@ import com.google.auth.http.HttpCredentialsAdapter; import dev.failsafe.Failsafe; import java.io.IOException; +import org.apache.beam.it.common.dataflow.AbstractPipelineLauncher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/FlexTemplateClient.java b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/FlexTemplateClient.java index 0a9731b13c32..496c4b0e980e 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/FlexTemplateClient.java +++ b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/dataflow/FlexTemplateClient.java @@ -32,6 +32,7 @@ import com.google.auth.http.HttpCredentialsAdapter; import dev.failsafe.Failsafe; import java.io.IOException; +import org.apache.beam.it.common.dataflow.AbstractPipelineLauncher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/spanner/matchers/SpannerAsserts.java b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/spanner/matchers/SpannerAsserts.java index 5a101e08d375..1778d70cf60a 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/spanner/matchers/SpannerAsserts.java +++ b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/spanner/matchers/SpannerAsserts.java @@ -17,7 +17,7 @@ */ package org.apache.beam.it.gcp.spanner.matchers; -import static org.apache.beam.it.gcp.artifacts.utils.JsonTestUtil.parseJsonString; +import static org.apache.beam.it.common.artifacts.utils.JsonTestUtil.parseJsonString; import static org.apache.beam.it.truthmatchers.PipelineAsserts.assertThatRecords; import com.google.cloud.spanner.Mutation; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/WordCountIT.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/WordCountIT.java index b561c0c71fb6..16cf2b8f4d31 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/WordCountIT.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/WordCountIT.java @@ -29,6 +29,7 @@ import org.apache.beam.it.common.PipelineLauncher.LaunchInfo; import org.apache.beam.it.common.PipelineLauncher.Sdk; import org.apache.beam.it.common.PipelineOperator.Result; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; import org.apache.beam.runners.dataflow.DataflowRunner; import org.apache.beam.sdk.io.TextIO; import org.apache.beam.sdk.options.PipelineOptions; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryIOLT.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryIOLT.java index f123ffa5954c..9ac03db1c741 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryIOLT.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryIOLT.java @@ -43,8 +43,10 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.bigquery.BigQueryResourceManager; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOLoadTestBase; import org.apache.beam.sdk.io.Read; import org.apache.beam.sdk.io.gcp.bigquery.AvroWriteRequest; import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryIOST.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryIOST.java index 14c83e48308a..495a4d4485d8 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryIOST.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryIOST.java @@ -41,8 +41,10 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.bigquery.BigQueryResourceManager; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOStressTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOStressTestBase; import org.apache.beam.runners.dataflow.options.DataflowPipelineWorkerPoolOptions; import org.apache.beam.sdk.io.Read; import org.apache.beam.sdk.io.gcp.bigquery.AvroWriteRequest; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryStreamingLT.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryStreamingLT.java index 6e511bd8e5c6..75a7be50a508 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryStreamingLT.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigquery/BigQueryStreamingLT.java @@ -41,7 +41,7 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; -import org.apache.beam.it.gcp.IOLoadTestBase; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; import org.apache.beam.runners.dataflow.DataflowRunner; import org.apache.beam.sdk.io.GenerateSequence; import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigtable/BigTableIOLT.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigtable/BigTableIOLT.java index 410c992fe2d6..27773121e79d 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigtable/BigTableIOLT.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigtable/BigTableIOLT.java @@ -34,8 +34,9 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOLoadTestBase; import org.apache.beam.sdk.io.GenerateSequence; import org.apache.beam.sdk.io.gcp.bigtable.BigtableIO; import org.apache.beam.sdk.testing.TestPipeline; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigtable/BigTableIOST.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigtable/BigTableIOST.java index 052a0839adf5..b86f13a0de10 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigtable/BigTableIOST.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/bigtable/BigTableIOST.java @@ -37,8 +37,9 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOStressTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOStressTestBase; import org.apache.beam.runners.dataflow.options.DataflowPipelineWorkerPoolOptions; import org.apache.beam.sdk.io.Read; import org.apache.beam.sdk.io.gcp.bigtable.BigtableIO; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/ClassicTemplateClientTest.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/ClassicTemplateClientTest.java index 88c35589f2be..a50c0951005f 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/ClassicTemplateClientTest.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/ClassicTemplateClientTest.java @@ -18,10 +18,10 @@ package org.apache.beam.it.gcp.dataflow; import static com.google.common.truth.Truth.assertThat; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.LEGACY_RUNNER; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.PARAM_JOB_ID; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.PARAM_JOB_TYPE; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.PARAM_RUNNER; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.LEGACY_RUNNER; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.PARAM_JOB_ID; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.PARAM_JOB_TYPE; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.PARAM_RUNNER; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -49,6 +49,7 @@ import org.apache.beam.it.common.PipelineLauncher.JobState; import org.apache.beam.it.common.PipelineLauncher.LaunchConfig; import org.apache.beam.it.common.PipelineLauncher.LaunchInfo; +import org.apache.beam.it.common.dataflow.AbstractPipelineLauncher; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/FlexTemplateClientTest.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/FlexTemplateClientTest.java index 06f44437414a..b94d52e45afb 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/FlexTemplateClientTest.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/dataflow/FlexTemplateClientTest.java @@ -18,10 +18,10 @@ package org.apache.beam.it.gcp.dataflow; import static com.google.common.truth.Truth.assertThat; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.LEGACY_RUNNER; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.PARAM_JOB_ID; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.PARAM_JOB_TYPE; -import static org.apache.beam.it.gcp.dataflow.AbstractPipelineLauncher.PARAM_RUNNER; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.LEGACY_RUNNER; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.PARAM_JOB_ID; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.PARAM_JOB_TYPE; +import static org.apache.beam.it.common.dataflow.AbstractPipelineLauncher.PARAM_RUNNER; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/datagenerator/DataGenerator.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/datagenerator/DataGenerator.java similarity index 99% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/datagenerator/DataGenerator.java rename to it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/datagenerator/DataGenerator.java index 99016b5dd3a4..0b838bddeb6d 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/datagenerator/DataGenerator.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/datagenerator/DataGenerator.java @@ -17,7 +17,7 @@ */ package org.apache.beam.it.gcp.datagenerator; -import static org.apache.beam.it.gcp.LoadTestBase.createConfig; +import static org.apache.beam.it.common.dataflow.LoadTestBase.createConfig; import static org.apache.beam.it.truthmatchers.PipelineAsserts.assertThatPipeline; import static org.apache.beam.it.truthmatchers.PipelineAsserts.assertThatResult; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/pubsub/PubSubIOST.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/pubsub/PubSubIOST.java index cb96db40b749..47058c75e399 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/pubsub/PubSubIOST.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/pubsub/PubSubIOST.java @@ -37,8 +37,9 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOStressTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOStressTestBase; import org.apache.beam.runners.dataflow.options.DataflowPipelineWorkerPoolOptions; import org.apache.beam.sdk.extensions.protobuf.Proto3SchemaMessages.Primitive; import org.apache.beam.sdk.io.Read; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/pubsub/PubsubIOLT.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/pubsub/PubsubIOLT.java index 477c79dd3bc7..da43fca2eaa3 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/pubsub/PubsubIOLT.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/pubsub/PubsubIOLT.java @@ -37,8 +37,9 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOLoadTestBase; import org.apache.beam.runners.direct.DirectOptions; import org.apache.beam.sdk.extensions.protobuf.Proto3SchemaMessages.Primitive; import org.apache.beam.sdk.io.Read; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/spanner/SpannerIOLT.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/spanner/SpannerIOLT.java index 8f3de1b58f40..4441b8dcd6b6 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/spanner/SpannerIOLT.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/spanner/SpannerIOLT.java @@ -37,8 +37,9 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOLoadTestBase; import org.apache.beam.sdk.io.GenerateSequence; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; import org.apache.beam.sdk.io.synthetic.SyntheticSourceOptions; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/spanner/SpannerIOST.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/spanner/SpannerIOST.java index 76312b3e10fb..4eb7030c23c3 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/spanner/SpannerIOST.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/spanner/SpannerIOST.java @@ -37,8 +37,9 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOStressTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOStressTestBase; import org.apache.beam.runners.dataflow.options.DataflowPipelineWorkerPoolOptions; import org.apache.beam.sdk.io.Read; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; diff --git a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/storage/FileBasedIOLT.java b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/storage/FileBasedIOLT.java index 58ba88d900e8..ac1a7fc103c6 100644 --- a/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/storage/FileBasedIOLT.java +++ b/it/google-cloud-platform/src/test/java/org/apache/beam/it/gcp/storage/FileBasedIOLT.java @@ -33,8 +33,10 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; +import org.apache.beam.it.common.storage.GcsResourceManager; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOLoadTestBase; import org.apache.beam.sdk.io.Compression; import org.apache.beam.sdk.io.Read; import org.apache.beam.sdk.io.TextIO; diff --git a/it/iceberg/build.gradle b/it/iceberg/build.gradle new file mode 100644 index 000000000000..3fa42fab8a79 --- /dev/null +++ b/it/iceberg/build.gradle @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT 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 org.apache.beam.gradle.IoPerformanceTestUtilities + +plugins { id 'org.apache.beam.module' } +applyJavaNature( + automaticModuleName: 'org.apache.beam.it.iceberg', + exportJavadoc: false, + // iceberg ended support for Java 11 in 1.11.0 + requireJavaVersion: JavaVersion.VERSION_17, +) + +description = "Apache Beam :: IT :: Iceberg" +ext.summary = "Integration test utilities for Iceberg." + +def iceberg_version = "1.10.0" +def parquet_version = "1.16.0" +def hive_version = "3.1.3" + +def hadoopVersions = [ + "336": "3.3.6", +] + +hadoopVersions.each {kv -> configurations.create("hadoopVersion$kv.key")} + +evaluationDependsOn(":it:common") + +dependencies { + provided "org.immutables:value:2.8.8" + permitUnusedDeclared "org.immutables:value:2.8.8" + runtimeOnly "org.apache.iceberg:iceberg-gcp:$iceberg_version" + runtimeOnly "org.apache.iceberg:iceberg-aws:$iceberg_version" + runtimeOnly "org.apache.iceberg:iceberg-aws-bundle:$iceberg_version" + 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") + testImplementation library.java.bigdataoss_gcsio + testImplementation library.java.bigdataoss_util_hadoop + testImplementation "org.apache.parquet:parquet-avro:$parquet_version" + testImplementation "org.apache.iceberg:iceberg-data:$iceberg_version" + testImplementation project(path: ":sdks:java:core", configuration: "shadowTest") + testImplementation library.java.junit + testImplementation library.java.hamcrest + testImplementation project(path: ":sdks:java:extensions:avro") + testImplementation 'org.awaitility:awaitility:4.2.0' + + // Hive catalog test dependencies + testImplementation project(path: ":sdks:java:io:iceberg:hive") + testImplementation "org.apache.iceberg:iceberg-common:$iceberg_version" + testImplementation ("org.apache.iceberg:iceberg-hive-metastore:$iceberg_version") + testImplementation ("org.apache.hive:hive-metastore:$hive_version") + testImplementation "org.assertj:assertj-core:3.11.1" + testRuntimeOnly ("org.apache.hive.hcatalog:hive-hcatalog-core:$hive_version") { + exclude group: "org.apache.hive", module: "hive-exec" + exclude group: "org.apache.parquet", module: "parquet-hadoop-bundle" + } + + // BigQueryMetastore catalog dep + testImplementation project(path: ":sdks:java:io:iceberg:bqms", configuration: "shadow") + testImplementation project(":sdks:java:io:google-cloud-platform") + testImplementation library.java.google_api_services_bigquery + testImplementation 'com.google.cloud:google-cloud-storage' + + testImplementation library.java.google_auth_library_oauth2_http + testRuntimeOnly library.java.slf4j_jdk14 + testImplementation project(path: ":runners:direct-java", configuration: "shadow") + testRuntimeOnly project(path: ":runners:google-cloud-dataflow-java") + testRuntimeOnly project(path: ":sdks:java:harness") + hadoopVersions.each {kv -> + "hadoopVersion$kv.key" "org.apache.hadoop:hadoop-client:$kv.value" + "hadoopVersion$kv.key" "org.apache.hadoop:hadoop-minicluster:$kv.value" + "hadoopVersion$kv.key" "org.apache.hadoop:hadoop-hdfs-client:$kv.value" + "hadoopVersion$kv.key" "org.apache.hadoop:hadoop-mapreduce-client-core:$kv.value" + } + + testImplementation project(path: ":sdks:java:extensions:protobuf", configuration: "testRuntimeMigration") + testImplementation project(path: ":sdks:java:io:synthetic") + testImplementation project(path: ":sdks:java:io:iceberg") + testImplementation project(path: ":it:truthmatchers") + testImplementation project(path: ":it:common", configuration: "testRuntimeMigration") + testImplementation library.java.mockito_inline + testImplementation project(path: ":sdks:java:extensions:google-cloud-platform-core", configuration: "testRuntimeMigration") + testRuntimeOnly library.java.slf4j_simple +} + +configurations.all { + // iceberg-core needs avro:1.12.0 + resolutionStrategy.force 'org.apache.avro:avro:1.12.0' + // 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' + resolutionStrategy.force 'org.apache.hadoop:hadoop-hdfs-client:3.3.6' +} + +hadoopVersions.each {kv -> + configurations."hadoopVersion$kv.key" { + resolutionStrategy { + force "org.apache.hadoop:hadoop-client:$kv.value" + force "org.apache.hadoop:hadoop-common:$kv.value" + force "org.apache.hadoop:hadoop-mapreduce-client-core:$kv.value" + force "org.apache.hadoop:hadoop-minicluster:$kv.value" + force "org.apache.hadoop:hadoop-hdfs:$kv.value" + force "org.apache.hadoop:hadoop-hdfs-client:$kv.value" + } + } +} + +tasks.register( + "IcebergPerformanceTest", IoPerformanceTestUtilities.IoPerformanceTest, project, 'iceberg', 'IcebergIOLT', + ['configuration':'large','project':'apache-beam-testing', 'artifactBucket':'io-performance-temp'] + + System.properties +) diff --git a/it/iceberg/src/test/java/org/apache/beam/it/iceberg/IcebergIOLT.java b/it/iceberg/src/test/java/org/apache/beam/it/iceberg/IcebergIOLT.java new file mode 100644 index 000000000000..2a6098a1518e --- /dev/null +++ b/it/iceberg/src/test/java/org/apache/beam/it/iceberg/IcebergIOLT.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.it.iceberg; + +import static org.apache.beam.it.truthmatchers.PipelineAsserts.assertThatResult; +import static org.junit.Assert.assertEquals; + +import com.google.auto.value.AutoValue; +import java.io.IOException; +import java.text.ParseException; +import java.time.Duration; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import org.apache.beam.it.common.PipelineLauncher; +import org.apache.beam.it.common.PipelineOperator; +import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; +import org.apache.beam.it.common.storage.GcsResourceManager; +import org.apache.beam.it.common.utils.ResourceManagerUtils; +import org.apache.beam.sdk.io.GenerateSequence; +import org.apache.beam.sdk.io.iceberg.IcebergCatalogConfig; +import org.apache.beam.sdk.io.iceberg.IcebergIO; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.testing.TestPipelineOptions; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.Row; +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.collect.ImmutableMap; +import org.apache.iceberg.catalog.TableIdentifier; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; + +/** + * IcebergIO performance tests using a Hadoop catalog backed by GCS. + * + *

The test writes generated rows to an Iceberg table, reads the table back, validates the number + * of rows, and exports write and read performance metrics. + * + *

Example trigger command: + * + *

+ * ./gradlew :it:iceberg:integrationTest \
+ *   --tests "org.apache.beam.it.iceberg.IcebergIOLT" \
+ *   -Dconfiguration=local \
+ *   -Dproject=[gcpProject] \
+ *   -DartifactBucket=[artifactBucket]
+ * 
+ */ +public final class IcebergIOLT extends IOLoadTestBase { + + private static final String READ_ELEMENT_METRIC_NAME = "read_count"; + private static final String NAMESPACE = "default"; + + private static final Schema ROW_SCHEMA = + Schema.builder().addInt64Field("id").addByteArrayField("data").build(); + + private static final Map TEST_CONFIGS = + ImmutableMap.of( + "local", + Configuration.of(1_000L, 5, "DirectRunner", 1_000), // approximately 1 MB + "small", + Configuration.of(1_000_000L, 20, "DataflowRunner", 1_000), // approximately 1 GB + "medium", + Configuration.of(10_000_000L, 40, "DataflowRunner", 1_000), // approximately 10 GB + "large", + Configuration.of(100_000_000L, 100, "DataflowRunner", 1_000) // approximately 100 GB + ); + + private static GcsResourceManager resourceManager; + + private Configuration configuration; + private IcebergCatalogConfig catalogConfig; + private TableIdentifier tableIdentifier; + + @Rule public TestPipeline writePipeline = TestPipeline.create(); + + @Rule public TestPipeline readPipeline = TestPipeline.create(); + + @BeforeClass + public static void beforeClass() { + resourceManager = + GcsResourceManager.builder(TestProperties.artifactBucket(), "icebergiolt", CREDENTIALS) + .build(); + } + + @AfterClass + public static void tearDownClass() { + ResourceManagerUtils.cleanResources(resourceManager); + } + + @Before + public void setup() { + configuration = getTestConfiguration(); + + String uniqueSuffix = + DateTimeFormatter.ofPattern("MMddHHmmssSSS") + .withZone(ZoneOffset.UTC) + .format(java.time.Instant.now()) + + "-" + + UUID.randomUUID().toString().substring(0, 10); + + String warehouseDirectory = "icebergiolt-" + uniqueSuffix; + resourceManager.registerTempDir(warehouseDirectory); + + String warehouseLocation = + String.format("gs://%s/%s", TestProperties.artifactBucket(), warehouseDirectory); + + tableIdentifier = TableIdentifier.of(NAMESPACE, "table_" + uniqueSuffix.replace("-", "_")); + + Map catalogProperties = + ImmutableMap.of("type", "hadoop", "warehouse", warehouseLocation); + + catalogConfig = + IcebergCatalogConfig.builder() + .setCatalogName("iceberg-load-test") + .setCatalogProperties(catalogProperties) + .build(); + + setTempLocation(writePipeline); + setTempLocation(readPipeline); + } + + /** Writes generated rows to Iceberg and verifies that all rows can be read back. */ + @Test + public void testIcebergWriteAndRead() throws IOException { + PipelineLauncher.LaunchInfo writeInfo = testWrite(); + + PipelineOperator.Result writeResult = + pipelineOperator.waitUntilDone( + createConfig(writeInfo, Duration.ofMinutes(configuration.getPipelineTimeout()))); + + assertThatResult(writeResult).isLaunchFinished(); + assertEquals( + PipelineLauncher.JobState.DONE, + pipelineLauncher.getJobStatus(project, region, writeInfo.jobId())); + + PipelineLauncher.LaunchInfo readInfo = testRead(); + + PipelineOperator.Result readResult = + pipelineOperator.waitUntilDone( + createConfig(readInfo, Duration.ofMinutes(configuration.getPipelineTimeout()))); + + assertThatResult(readResult).isLaunchFinished(); + assertEquals( + PipelineLauncher.JobState.DONE, + pipelineLauncher.getJobStatus(project, region, readInfo.jobId())); + + double numRecords = + pipelineLauncher.getMetric( + project, + region, + readInfo.jobId(), + getBeamMetricsName(PipelineMetricsType.COUNTER, READ_ELEMENT_METRIC_NAME)); + + assertEquals((double) configuration.getNumRows(), numRecords, 0.5); + + exportMetrics(writeInfo, readInfo); + } + + private PipelineLauncher.LaunchInfo testWrite() throws IOException { + writePipeline + .apply("Generate records", GenerateSequence.from(0).to(configuration.getNumRows())) + .apply("Map records", ParDo.of(new MapToIcebergFormat(configuration.getValueSizeBytes()))) + .setRowSchema(ROW_SCHEMA) + .apply("Write to Iceberg", IcebergIO.writeRows(catalogConfig).to(tableIdentifier)); + + PipelineLauncher.LaunchConfig options = + PipelineLauncher.LaunchConfig.builder("write-iceberg") + .setSdk(PipelineLauncher.Sdk.JAVA) + .setPipeline(writePipeline) + .addParameter("runner", configuration.getRunner()) + .addParameter( + "maxNumWorkers", + TestProperties.getProperty("maxNumWorkers", "10", TestProperties.Type.PROPERTY)) + .build(); + + return pipelineLauncher.launch(project, region, options); + } + + private PipelineLauncher.LaunchInfo testRead() throws IOException { + readPipeline + .apply("Read from Iceberg", IcebergIO.readRows(catalogConfig).from(tableIdentifier)) + .apply("Counting element", ParDo.of(new CountingFn<>(READ_ELEMENT_METRIC_NAME))); + + PipelineLauncher.LaunchConfig options = + PipelineLauncher.LaunchConfig.builder("read-iceberg") + .setSdk(PipelineLauncher.Sdk.JAVA) + .setPipeline(readPipeline) + .addParameter("runner", configuration.getRunner()) + .addParameter( + "maxNumWorkers", + TestProperties.getProperty("maxNumWorkers", "10", TestProperties.Type.PROPERTY)) + .build(); + + return pipelineLauncher.launch(project, region, options); + } + + private void exportMetrics( + PipelineLauncher.LaunchInfo writeInfo, PipelineLauncher.LaunchInfo readInfo) { + + /* + * Only use PCollection names belonging to transforms defined directly in this test. + * Internal IcebergIO transform names may change when the implementation changes. + */ + MetricsConfiguration writeMetricsConfig = + MetricsConfiguration.builder() + .setInputPCollection("Map records.out0") + .setInputPCollectionV2("Map records/ParMultiDo(MapToIcebergFormat).out0") + .build(); + + MetricsConfiguration readMetricsConfig = + MetricsConfiguration.builder() + .setOutputPCollection("Counting element.out0") + .setOutputPCollectionV2("Counting element/ParMultiDo(Counting).out0") + .build(); + + try { + exportMetricsToBigQuery(writeInfo, getMetrics(writeInfo, writeMetricsConfig)); + + exportMetricsToBigQuery(readInfo, getMetrics(readInfo, readMetricsConfig)); + } catch (IOException | ParseException | InterruptedException e) { + throw new RuntimeException("Unable to collect or export IcebergIO load-test metrics.", e); + } + } + + private Configuration getTestConfiguration() { + String testConfig = + TestProperties.getProperty("configuration", "local", TestProperties.Type.PROPERTY); + + Configuration selectedConfiguration = TEST_CONFIGS.get(testConfig); + + if (selectedConfiguration == null) { + throw new IllegalArgumentException( + String.format( + "Unknown test configuration: [%s]. Known configurations: %s", + testConfig, TEST_CONFIGS.keySet())); + } + + return selectedConfiguration; + } + + private static void setTempLocation(TestPipeline pipeline) { + String tempBucketName = TestProperties.artifactBucket(); + + if (Strings.isNullOrEmpty(tempBucketName)) { + return; + } + + String tempLocation = String.format("gs://%s/temp/", tempBucketName); + + pipeline.getOptions().as(TestPipelineOptions.class).setTempRoot(tempLocation); + + pipeline.getOptions().setTempLocation(tempLocation); + } + + /** Maps a generated sequence value to a deterministic Iceberg-compatible Beam row. */ + private static class MapToIcebergFormat extends DoFn { + + private final int valueSizeBytes; + private transient Random random; + + private MapToIcebergFormat(int valueSizeBytes) { + if (valueSizeBytes <= 0) { + throw new IllegalArgumentException("valueSizeBytes must be positive."); + } + + this.valueSizeBytes = valueSizeBytes; + } + + @Setup + public void setup() { + random = new Random(); + } + + @ProcessElement + public void processElement(@Element Long index, OutputReceiver output) { + + byte[] value = new byte[valueSizeBytes]; + + /* + * Using the row index as the seed makes retries deterministic. Random data also prevents + * compression from making the generated payload significantly smaller than configured. + */ + random.setSeed(index); + random.nextBytes(value); + + output.output(Row.withSchema(ROW_SCHEMA).addValues(index, value).build()); + } + } + + /** Options for the IcebergIO load test. */ + @AutoValue + abstract static class Configuration { + + abstract long getNumRows(); + + abstract int getPipelineTimeout(); + + abstract String getRunner(); + + abstract int getValueSizeBytes(); + + static Configuration of(long numRows, int pipelineTimeout, String runner, int valueSizeBytes) { + + if (numRows <= 0) { + throw new IllegalArgumentException("numRows must be positive."); + } + + if (pipelineTimeout <= 0) { + throw new IllegalArgumentException("pipelineTimeout must be positive."); + } + + if (valueSizeBytes <= 0) { + throw new IllegalArgumentException("valueSizeBytes must be positive."); + } + + return new AutoValue_IcebergIOLT_Configuration.Builder() + .setNumRows(numRows) + .setPipelineTimeout(pipelineTimeout) + .setRunner(runner) + .setValueSizeBytes(valueSizeBytes) + .build(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setNumRows(long numRows); + + abstract Builder setPipelineTimeout(int pipelineTimeout); + + abstract Builder setRunner(String runner); + + abstract Builder setValueSizeBytes(int valueSizeBytes); + + abstract Configuration build(); + } + } +} diff --git a/it/kafka/build.gradle b/it/kafka/build.gradle index 0496c423bebd..32ef0623fea9 100644 --- a/it/kafka/build.gradle +++ b/it/kafka/build.gradle @@ -42,8 +42,7 @@ dependencies { permitUsedUndeclared library.java.guava testImplementation library.java.mockito_core testRuntimeOnly library.java.slf4j_simple - // TODO: Remove the below dependencies after KafkaIOLT has been moved to the right directory. - testImplementation project(path: ":it:google-cloud-platform") + testImplementation project(path: ":it:common", configuration: "testRuntimeMigration") testImplementation project(path: ":sdks:java:io:kafka") testImplementation project(path: ":sdks:java:io:synthetic") testImplementation project(path: ":runners:direct-java") diff --git a/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaIOLT.java b/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaIOLT.java index c20699d61a55..1607ca1cd674 100644 --- a/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaIOLT.java +++ b/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaIOLT.java @@ -30,8 +30,9 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOLoadTestBase; import org.apache.beam.it.common.utils.ResourceManagerUtils; -import org.apache.beam.it.gcp.IOLoadTestBase; import org.apache.beam.runners.direct.DirectOptions; import org.apache.beam.sdk.io.kafka.KafkaIO; import org.apache.beam.sdk.io.synthetic.SyntheticOptions; diff --git a/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaIOST.java b/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaIOST.java index bb1cb37a11b6..d6231a69f1e7 100644 --- a/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaIOST.java +++ b/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaIOST.java @@ -34,7 +34,8 @@ import org.apache.beam.it.common.PipelineLauncher; import org.apache.beam.it.common.PipelineOperator; import org.apache.beam.it.common.TestProperties; -import org.apache.beam.it.gcp.IOStressTestBase; +import org.apache.beam.it.common.dataflow.DefaultPipelineLauncher.PipelineMetricsType; +import org.apache.beam.it.common.dataflow.IOStressTestBase; import org.apache.beam.runners.dataflow.options.DataflowPipelineWorkerPoolOptions; import org.apache.beam.sdk.io.Read; import org.apache.beam.sdk.io.kafka.KafkaIO; diff --git a/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaResourceManagerTest.java b/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaResourceManagerTest.java index 8c870815efc4..f25bca06c226 100644 --- a/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaResourceManagerTest.java +++ b/it/kafka/src/test/java/org/apache/beam/it/kafka/KafkaResourceManagerTest.java @@ -166,7 +166,8 @@ public void testCleanupShouldDropNonStaticTopic() throws IOException { KafkaResourceManager tm = new KafkaResourceManager(kafkaClient, container, builder); tm.cleanupAll(); - verify(kafkaClient).deleteTopics(argThat(list -> list.size() == numTopics)); + verify(kafkaClient) + .deleteTopics(argThat((Collection list) -> list.size() == numTopics)); } @Test diff --git a/it/mongodb/build.gradle b/it/mongodb/build.gradle index 960e15af8394..0a78bdda7724 100644 --- a/it/mongodb/build.gradle +++ b/it/mongodb/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation library.java.google_code_gson implementation library.java.mongo_java_driver implementation library.java.mongo_bson + implementation library.java.mongodb_driver_core implementation library.java.vendored_guava_32_1_2_jre testImplementation library.java.mockito_core diff --git a/it/mongodb/src/main/java/org/apache/beam/it/mongodb/MongoDBResourceManager.java b/it/mongodb/src/main/java/org/apache/beam/it/mongodb/MongoDBResourceManager.java index 8a4f116c8436..0e11f4b8e293 100644 --- a/it/mongodb/src/main/java/org/apache/beam/it/mongodb/MongoDBResourceManager.java +++ b/it/mongodb/src/main/java/org/apache/beam/it/mongodb/MongoDBResourceManager.java @@ -20,6 +20,8 @@ import static org.apache.beam.it.mongodb.MongoDBResourceManagerUtils.checkValidCollectionName; import static org.apache.beam.it.mongodb.MongoDBResourceManagerUtils.generateDatabaseName; +import com.mongodb.ConnectionString; +import com.mongodb.MongoDriverInformation; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; @@ -57,6 +59,10 @@ public class MongoDBResourceManager extends TestContainerResourceManager32.1.x implementation(library.java.truth) { exclude group: 'com.google.guava', module: 'guava' } + implementation library.java.jackson_core + implementation library.java.jackson_databind implementation library.java.guava + implementation library.java.vendored_guava_32_1_2_jre permitUsedUndeclared library.java.guava implementation library.java.google_code_gson -} \ No newline at end of file +} diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/ArtifactAsserts.java b/it/truthmatchers/src/main/java/org/apache/beam/it/truthmatchers/ArtifactAsserts.java similarity index 79% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/ArtifactAsserts.java rename to it/truthmatchers/src/main/java/org/apache/beam/it/truthmatchers/ArtifactAsserts.java index 3b1251307d0d..92407e5a2fc9 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/ArtifactAsserts.java +++ b/it/truthmatchers/src/main/java/org/apache/beam/it/truthmatchers/ArtifactAsserts.java @@ -15,15 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts.matchers; - -import static com.google.common.truth.Truth.assertAbout; -import static org.apache.beam.it.truthmatchers.PipelineAsserts.assertThatRecords; +package org.apache.beam.it.truthmatchers; +import com.google.common.truth.Truth; import java.util.List; import org.apache.avro.generic.GenericRecord; -import org.apache.beam.it.gcp.artifacts.Artifact; -import org.apache.beam.it.truthmatchers.RecordsSubject; +import org.apache.beam.it.common.artifacts.Artifact; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; public class ArtifactAsserts { @@ -36,7 +33,7 @@ public class ArtifactAsserts { * @return Truth Subject to chain assertions. */ public static ArtifactsSubject assertThatArtifacts(List artifacts) { - return assertAbout(ArtifactsSubject.records()).that(artifacts); + return Truth.assertAbout(ArtifactsSubject.records()).that(artifacts); } /** @@ -47,7 +44,7 @@ public static ArtifactsSubject assertThatArtifacts(List artifacts) { * @return Truth Subject to chain assertions. */ public static ArtifactsSubject assertThatArtifact(Artifact artifact) { - return assertAbout(ArtifactsSubject.records()).that(ImmutableList.of(artifact)); + return Truth.assertAbout(ArtifactsSubject.records()).that(ImmutableList.of(artifact)); } /** @@ -57,6 +54,6 @@ public static ArtifactsSubject assertThatArtifact(Artifact artifact) { * @return Truth Subject to chain assertions. */ public static RecordsSubject assertThatGenericRecords(List records) { - return assertThatRecords(ArtifactsSubject.genericRecordToRecords(records)); + return PipelineAsserts.assertThatRecords(ArtifactsSubject.genericRecordToRecords(records)); } } diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/ArtifactsSubject.java b/it/truthmatchers/src/main/java/org/apache/beam/it/truthmatchers/ArtifactsSubject.java similarity index 87% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/ArtifactsSubject.java rename to it/truthmatchers/src/main/java/org/apache/beam/it/truthmatchers/ArtifactsSubject.java index ab5b49699484..e2518280345d 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/ArtifactsSubject.java +++ b/it/truthmatchers/src/main/java/org/apache/beam/it/truthmatchers/ArtifactsSubject.java @@ -15,11 +15,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.it.gcp.artifacts.matchers; +package org.apache.beam.it.truthmatchers; -import static org.apache.beam.it.gcp.artifacts.matchers.ArtifactAsserts.assertThatGenericRecords; -import static org.apache.beam.it.truthmatchers.PipelineAsserts.assertThatRecords; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.hash.Hashing.sha256; +import static org.apache.beam.it.truthmatchers.ArtifactAsserts.assertThatGenericRecords; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -31,11 +29,11 @@ import java.util.Map; import org.apache.avro.Schema; import org.apache.avro.generic.GenericRecord; -import org.apache.beam.it.gcp.artifacts.Artifact; -import org.apache.beam.it.gcp.artifacts.utils.AvroTestUtil; -import org.apache.beam.it.gcp.artifacts.utils.JsonTestUtil; -import org.apache.beam.it.gcp.artifacts.utils.ParquetTestUtil; -import org.apache.beam.it.truthmatchers.RecordsSubject; +import org.apache.beam.it.common.artifacts.Artifact; +import org.apache.beam.it.common.artifacts.utils.AvroTestUtil; +import org.apache.beam.it.common.artifacts.utils.JsonTestUtil; +import org.apache.beam.it.common.artifacts.utils.ParquetTestUtil; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.hash.Hashing; /** * Subject that has assertion operations for artifact lists (GCS files), usually coming from the @@ -98,7 +96,8 @@ public void hasContent(String content) { */ public void hasHash(String hash) { if (actual.stream() - .noneMatch(artifact -> sha256().hashBytes(artifact.contents()).toString().equals(hash))) { + .noneMatch( + artifact -> Hashing.sha256().hashBytes(artifact.contents()).toString().equals(hash))) { failWithActual("expected to contain hash", hash); } } @@ -167,6 +166,6 @@ public RecordsSubject asJsonRecords() { throw new RuntimeException("Error reading " + artifact.name() + " as JSON.", e); } } - return assertThatRecords(allRecords); + return PipelineAsserts.assertThatRecords(allRecords); } } diff --git a/learning/tour-of-beam/backend/function.go b/learning/tour-of-beam/backend/function.go index 90ed8642580f..eaa1dcad076a 100644 --- a/learning/tour-of-beam/backend/function.go +++ b/learning/tour-of-beam/backend/function.go @@ -213,7 +213,7 @@ func postUnitComplete(w http.ResponseWriter, r *http.Request) { return } - fmt.Fprint(w, "{}") + _, _ = fmt.Fprint(w, "{}") } // Save user code for unit @@ -245,7 +245,7 @@ func postUserCode(w http.ResponseWriter, r *http.Request) { return } - fmt.Fprint(w, "{}") + _, _ = fmt.Fprint(w, "{}") } // Delete user progress @@ -259,5 +259,5 @@ func postDeleteProgress(w http.ResponseWriter, r *http.Request) { return } - fmt.Fprint(w, "{}") + _, _ = fmt.Fprint(w, "{}") } diff --git a/learning/tour-of-beam/backend/integration_tests/client.go b/learning/tour-of-beam/backend/integration_tests/client.go index 956c67dde5d6..9b169f08536c 100644 --- a/learning/tour-of-beam/backend/integration_tests/client.go +++ b/learning/tour-of-beam/backend/integration_tests/client.go @@ -165,7 +165,9 @@ func Do(dst interface{}, method, url string, queryParams, headers map[string]str if err != nil { return err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if err := verifyServerHeaders(resp.Header); err != nil { return err @@ -180,7 +182,9 @@ func Do(dst interface{}, method, url string, queryParams, headers map[string]str } tee := io.TeeReader(resp.Body, os.Stdout) - defer os.Stdout.WriteString("\n") + defer func() { + _, _ = os.Stdout.WriteString("\n") + }() if err := json.NewDecoder(tee).Decode(dst); err != nil { return fmt.Errorf("response decode err: %w", err) } diff --git a/learning/tour-of-beam/backend/internal/fs_content/yaml.go b/learning/tour-of-beam/backend/internal/fs_content/yaml.go index 0546637a0d55..ebb8d97e11f0 100644 --- a/learning/tour-of-beam/backend/internal/fs_content/yaml.go +++ b/learning/tour-of-beam/backend/internal/fs_content/yaml.go @@ -16,8 +16,8 @@ package fs_content import ( - "io/ioutil" "log" + "os" "gopkg.in/yaml.v3" ) @@ -25,7 +25,7 @@ import ( // Could have done it in generics if 1.18 was supported in GCF // Fatals on error. func loadLearningPathInfo(path string) (info learningPathInfo) { - buf, err := ioutil.ReadFile(path) + buf, err := os.ReadFile(path) if err != nil { log.Fatal(err) } @@ -39,7 +39,7 @@ func loadLearningPathInfo(path string) (info learningPathInfo) { } func loadLearningModuleInfo(path string) (info learningModuleInfo) { - buf, err := ioutil.ReadFile(path) + buf, err := os.ReadFile(path) if err != nil { log.Fatal(err) } @@ -53,7 +53,7 @@ func loadLearningModuleInfo(path string) (info learningModuleInfo) { } func loadLearningGroupInfo(path string) (info learningGroupInfo) { - buf, err := ioutil.ReadFile(path) + buf, err := os.ReadFile(path) if err != nil { log.Fatal(err) } @@ -67,7 +67,7 @@ func loadLearningGroupInfo(path string) (info learningGroupInfo) { } func loadLearningUnitInfo(path string) (info learningUnitInfo) { - buf, err := ioutil.ReadFile(path) + buf, err := os.ReadFile(path) if err != nil { log.Fatal(err) } diff --git a/learning/tour-of-beam/backend/internal/storage/datastore.go b/learning/tour-of-beam/backend/internal/storage/datastore.go index 4c95384a9a0b..cdf633211610 100644 --- a/learning/tour-of-beam/backend/internal/storage/datastore.go +++ b/learning/tour-of-beam/backend/internal/storage/datastore.go @@ -159,7 +159,7 @@ func (d *DatastoreDb) saveContentTree(tx *datastore.Transaction, tree *tob.Conte // could have used numericID keys, if there was no transaction: // incomplete keys are resolved after Tx commit, and // we need to reference them in child nodes - var groupId int = 0 + groupId := 0 genGroupKey := func(parentKey *datastore.Key) *datastore.Key { groupId++ return datastoreKey(TbLearningNodeKind, @@ -181,13 +181,14 @@ func (d *DatastoreDb) saveContentTree(tx *datastore.Transaction, tree *tob.Conte } saveNode = func(node tob.Node, order, level int, parentKey *datastore.Key) error { - if node.Type == tob.NODE_UNIT { + switch node.Type { + case tob.NODE_UNIT: return saveUnit(node.Unit, order, level, parentKey) - } else if node.Type == tob.NODE_GROUP { + case tob.NODE_GROUP: return saveGroup(node.Group, order, level, parentKey) + default: + return fmt.Errorf("unknown datastore node type: %v", node.Type) } - - return fmt.Errorf("unknown datastore node type: %v", node.Type) } rootKey := pgNameKey(TbLearningPathKind, tree.Sdk.StorageID(), nil) diff --git a/learning/tour-of-beam/backend/internal/storage/mock.go b/learning/tour-of-beam/backend/internal/storage/mock.go index 2a39c774bd2b..3a28ef979c1c 100644 --- a/learning/tour-of-beam/backend/internal/storage/mock.go +++ b/learning/tour-of-beam/backend/internal/storage/mock.go @@ -19,7 +19,7 @@ import ( "context" "encoding/json" "errors" - "io/ioutil" + "os" "path" "runtime" "strings" @@ -42,7 +42,7 @@ func (d *Mock) GetContentTree(_ context.Context, sdk tob.Sdk) (ct tob.ContentTre if sdk == tob.SDK_SCIO { return ct, errors.New("empty sdk tree") } - content, _ := ioutil.ReadFile(path.Join(getSamplesPath(), "get_content_tree.json")) + content, _ := os.ReadFile(path.Join(getSamplesPath(), "get_content_tree.json")) _ = json.Unmarshal(content, &ct) return ct, nil } @@ -55,7 +55,7 @@ func (d *Mock) GetUnitContent(_ context.Context, sdk tob.Sdk, unitId string) (u if strings.HasPrefix(unitId, "unknown_") { return u, tob.ErrNoUnit } - content, _ := ioutil.ReadFile(path.Join(getSamplesPath(), "get_unit_content.json")) + content, _ := os.ReadFile(path.Join(getSamplesPath(), "get_unit_content.json")) err = json.Unmarshal(content, &u) return u, err } @@ -74,7 +74,7 @@ func (d *Mock) SaveUser(ctx context.Context, uid string) error { } func (d *Mock) GetUserProgress(_ context.Context, sdk tob.Sdk, userId string) (sp *tob.SdkProgress, err error) { - content, _ := ioutil.ReadFile(path.Join(getSamplesPath(), "get_user_progress.json")) + content, _ := os.ReadFile(path.Join(getSamplesPath(), "get_user_progress.json")) _ = json.Unmarshal(content, &sp) return sp, nil } 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..9b8493f1e499 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,12 +133,16 @@ 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) {} } // Requests the ProcessBundleDescriptor with the given id. message GetProcessBundleDescriptorRequest { + // (Required) The id of the ProcessBundleDescriptor to retrieve. string process_bundle_descriptor_id = 1; } @@ -416,16 +423,26 @@ 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; } +// The response to a ProcessBundleRequest, returned by the SDK harness after +// bundle processing completes (or is partially completed with residual roots). message ProcessBundleResponse { // (Optional) Specifies that the bundle has not been completed and the // following applications need to be scheduled and executed in the future. @@ -495,6 +512,8 @@ message MonitoringInfosMetadataRequest { repeated string monitoring_info_id = 1; } +// A response containing progress information for a currently active bundle. +// This is returned in response to a ProcessBundleProgressRequest. message ProcessBundleProgressResponse { // DEPRECATED (Required) The list of metrics or other MonitoredState // collected while processing this bundle. @@ -705,12 +724,17 @@ message ProcessBundleSplitResponse { } +// A request sent by the runner to finalize a bundle that previously indicated +// it requires finalization (via requires_finalization in ProcessBundleResponse). +// This is sent after the runner has committed the output of the bundle, allowing +// the SDK to perform any post-commit cleanup. message FinalizeBundleRequest { // (Required) A reference to a completed process bundle request with the given // instruction id. string instruction_id = 1; } +// A response to a FinalizeBundleRequest, indicating that finalization is complete. message FinalizeBundleResponse { // Empty } @@ -749,10 +773,16 @@ message Elements { bool is_last = 4; } + // Indicates whether the bundle is being drained. When draining, sources + // should stop producing new data and the SDK should finish processing + // in-flight elements. message DrainMode { enum Enum { + // Drain mode has not been specified. UNSPECIFIED = 0; + // The bundle is not being drained; normal processing continues. NOT_DRAINING = 1; + // The bundle is being drained; sources should stop producing new data. DRAINING = 2; } } @@ -783,6 +813,8 @@ message Elements { // Element metadata passed as part of WindowedValue to make WindowedValue // extensible and backward compatible message ElementMetadata { + // (Optional) Indicates whether the bundle is being drained, allowing the + // SDK to handle drain semantics for this element. optional DrainMode.Enum drain = 1; // (Optional) As part of https://www.w3.org/TR/trace-context/ we are forwarding a trace and participating in it. // Traceparent header represents the incoming request in a tracing system in a common format. @@ -834,7 +866,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) @@ -847,6 +887,9 @@ service BeamFnData { * State API */ +// A request sent by the SDK harness to the runner to manipulated +// state associated with a particular StateKey. State requests are scoped to +// the instruction (bundle) that is currently being processed. message StateRequest { // (Required) A unique identifier provided by the SDK which represents this // requests execution. The StateResponse corresponding with this request @@ -874,6 +917,8 @@ message StateRequest { } } +// A response from the runner to a StateRequest, containing the result of +// the requested state operation. message StateResponse { // (Required) A reference provided by the SDK which represents a requests // execution. The StateResponse must have the matching id when responding @@ -898,8 +943,15 @@ message StateResponse { } } +// An API for the SDK harness to access state stored by the runner on behalf +// of the SDK. State is scoped to the currently processing bundle and can be +// used for user state, side inputs, and runner-managed references. +// Stable 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) @@ -909,6 +961,9 @@ service BeamFnState { stream StateResponse) {} } +// A key identifying which state to access. Exactly one of the oneof type +// fields must be set, determining the kind of state being requested +// (e.g. side input data, user state, or runner-managed references). message StateKey { message Runner { // (Required) Opaque information supplied by the runner. Used to support @@ -1184,18 +1239,24 @@ message StateAppendRequest { bytes data = 1; } -// A response to append state. +// A response to a StateAppendRequest. Currently empty as append operations +// do not return data. message StateAppendResponse {} -// A request to clear state. +// A request to clear all state associated with the given StateKey. message StateClearRequest {} -// A response to clear state. +// A response to a StateClearRequest. Currently empty as clear operations +// do not return data. message StateClearResponse {} -// A message describes a sort key range [start, end). +// A message describing a sort key range [start, end) for ordered list state. +// Used in OrderedListUserState to specify which portion of the ordered list +// to retrieve. message OrderedListRange { + // (Required) The inclusive start of the sort key range. int64 start = 1; + // (Required) The exclusive end of the sort key range. int64 end = 2; } @@ -1289,12 +1350,17 @@ message LogEntry { google.protobuf.Struct custom_data = 9; } +// A control message sent from the runner to the SDK harness to configure +// logging behavior. Currently empty, reserved for future log-level control. message LogControl {} // Stable 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) @@ -1303,27 +1369,44 @@ service BeamFnLogging { stream LogControl) {} } +// A request from the runner to start a new SDK worker process with the given +// configuration, including endpoint addresses and parameters. message StartWorkerRequest { + // (Required) A unique identifier for the worker to start. string worker_id = 1; + // (Required) The control endpoint the worker should connect to. org.apache.beam.model.pipeline.v1.ApiServiceDescriptor control_endpoint = 2; + // (Optional) The logging endpoint the worker should use. org.apache.beam.model.pipeline.v1.ApiServiceDescriptor logging_endpoint = 3; + // (Optional) The artifact retrieval endpoint the worker should use. org.apache.beam.model.pipeline.v1.ApiServiceDescriptor artifact_endpoint = 4; + // (Optional) The provisioning endpoint the worker should use. org.apache.beam.model.pipeline.v1.ApiServiceDescriptor provision_endpoint = 5; + // (Optional) Additional runner-specific parameters to pass to the worker. map params = 10; } +// A response from a StartWorkerRequest. If the worker failed to start, +// the error field will contain a human-readable error message. message StartWorkerResponse { + // (Optional) Error message if the worker failed to start. string error = 1; } +// A request from the runner to stop a running SDK worker process. message StopWorkerRequest { + // (Required) The unique identifier of the worker to stop. string worker_id = 1; } message StopWorkerResponse { + // (Optional) Human-readable error message if the worker failed to stop cleanly. string error = 1; } +// An API for the runner to manage external SDK worker processes. +// Allows the runner to start and stop SDK worker instances, typically used +// for containerized or process-based execution environments. service BeamFnExternalWorkerPool { // Start the SDK worker with the given ID. rpc StartWorker (StartWorkerRequest) returns (StartWorkerResponse) {} @@ -1356,6 +1439,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/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_provision_api.proto b/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_provision_api.proto index 5ba8ab21d3ff..3143608f9dc3 100644 --- a/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_provision_api.proto +++ b/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_provision_api.proto @@ -75,7 +75,7 @@ message ProvisionInfo { // (optional) The control endpoint this SDK should use. org.apache.beam.model.pipeline.v1.ApiServiceDescriptor control_endpoint = 10; - // The set of dependencies that should be staged into this environment. + // (optional) The set of dependencies that should be staged into this environment. repeated org.apache.beam.model.pipeline.v1.ArtifactInformation dependencies = 11; // (optional) A set of capabilities that this SDK is allowed to use in its diff --git a/model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml b/model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml index 8ce9d2751545..eb7999680074 100644 --- a/model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml +++ b/model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml @@ -626,3 +626,34 @@ examples: "\u0001\u0003\u0061\u0062\u0063" : "abc" "\u0001\u000a\u006d\u006f\u0072\u0065\u0020\u0062\u0079\u0074\u0065\u0073" : "more bytes" "\u0000" : null + +--- + +coder: + urn: "beam:coder:row:v1" + # f_timestamp: logical(timestamp:v1, precision=3, representation row) + payload: "\x0a\x88\x01\x0a\x0bf_timestamp\x1ay:w\x0a\x1ebeam:logical_type:timestamp:v1\x1aK2I\x0aG\x0a\x0d\x0a\x07seconds\x1a\x02\x10\x04\x0a\x10\x0a\x0asubseconds\x1a\x02\x10\x02\x12$5f2fbf6c-8b8e-4a30-a9be-e0f65f0f79c3\"\x02\x10\x03*\x04\x0a\x02\x18\x03\x12$9c7c1b6c-2a5e-4c53-9b32-91b71c1f8ac3" +examples: + "\x01\x00\x02\x00\xb6\x95\xd5\xf9\x05\x03\xe7": {f_timestamp: {seconds: 1597328054, subseconds: 999}} + "\x01\x00\x02\x00\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01\x01\xf4": {f_timestamp: {seconds: -2, subseconds: 500}} + "\x01\x00\x02\x00\xb6\x95\xd5\xf9\x05\x00\x00": {f_timestamp: {seconds: 1597328054, subseconds: 0}} + +--- + +coder: + urn: "beam:coder:row:v1" + # f_timestamp: logical(timestamp:v1, precision=6, representation row) + payload: "\x0a\x88\x01\x0a\x0bf_timestamp\x1ay:w\x0a\x1ebeam:logical_type:timestamp:v1\x1aK2I\x0aG\x0a\x0d\x0a\x07seconds\x1a\x02\x10\x04\x0a\x10\x0a\x0asubseconds\x1a\x02\x10\x03\x12$b8b3faf8-63f8-4b40-9f4d-3c1e83d4e07b\"\x02\x10\x03*\x04\x0a\x02\x18\x06\x12$e0a72e10-b214-4fd2-a4dc-fca0e7f7d924" +examples: + "\x01\x00\x02\x00\xb6\x95\xd5\xf9\x05\xc0\xc4\x07": {f_timestamp: {seconds: 1597328054, subseconds: 123456}} + "\x01\x00\x02\x00\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01\xa0\xc2\x1e": {f_timestamp: {seconds: -2, subseconds: 500000}} + +--- + +coder: + urn: "beam:coder:row:v1" + # f_timestamp: logical(timestamp:v1, precision=9, representation row) + payload: "\x0a\x88\x01\x0a\x0bf_timestamp\x1ay:w\x0a\x1ebeam:logical_type:timestamp:v1\x1aK2I\x0aG\x0a\x0d\x0a\x07seconds\x1a\x02\x10\x04\x0a\x10\x0a\x0asubseconds\x1a\x02\x10\x03\x12$3d6f7b81-2f13-4a41-8be3-4d8cd8f30f42\"\x02\x10\x03*\x04\x0a\x02\x18\x09\x12$cf03a3c1-4a3a-4f77-8e2b-6e5b19c19f27" +examples: + "\x01\x00\x02\x00\xb6\x95\xd5\xf9\x05\x95\x9a\xef:": {f_timestamp: {seconds: 1597328054, subseconds: 123456789}} + "\x01\x00\x02\x00\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01\xff\x93\xeb\xdc\x03": {f_timestamp: {seconds: -2, subseconds: 999999999}} diff --git a/model/interactive/src/main/proto/org/apache/beam/model/interactive/v1/beam_interactive_api.proto b/model/interactive/src/main/proto/org/apache/beam/model/interactive/v1/beam_interactive_api.proto index 6351b2472579..faae80261402 100644 --- a/model/interactive/src/main/proto/org/apache/beam/model/interactive/v1/beam_interactive_api.proto +++ b/model/interactive/src/main/proto/org/apache/beam/model/interactive/v1/beam_interactive_api.proto @@ -50,11 +50,15 @@ message TestStreamFileRecord { org.apache.beam.model.pipeline.v1.TestStreamPayload.Event recorded_event = 1; } +// A gRPC service that serves cached TestStream events to a TestStream source. +// Used by Interactive Beam to replay recorded pipeline elements from a streaming cache. service TestStreamService { // A TestStream will request for events using this RPC. rpc Events(EventsRequest) returns (stream org.apache.beam.model.pipeline.v1.TestStreamPayload.Event) {} } +// A request for TestStream events from a streaming cache. +// The client specifies which PCollection output tags to read events for. message EventsRequest { // The set of PCollections to read from. These are the PTransform outputs // local names. These are a subset of the TestStream's outputs. This allows diff --git a/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_artifact_api.proto b/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_artifact_api.proto index 35cd4f0c73d1..a64352c9289e 100644 --- a/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_artifact_api.proto +++ b/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_artifact_api.proto @@ -78,18 +78,23 @@ message ResolveArtifactsResponse { // A request to get an artifact. message GetArtifactRequest { + // (Required) The artifact to retrieve, typically a resolved artifact from ResolveArtifacts. org.apache.beam.model.pipeline.v1.ArtifactInformation artifact = 1; } // Part of a response to getting an artifact. message GetArtifactResponse { + // A chunk of the artifact's binary content. bytes data = 1; } // Wraps an ArtifactRetrievalService request for use in ReverseArtifactRetrievalService. +// The server sends these requests to the client during the reverse staging flow. message ArtifactRequestWrapper { oneof request { + // Request to resolve a set of artifacts into concrete artifact references. ResolveArtifactsRequest resolve_artifact = 1000; + // Request to retrieve the bytes of a specific artifact. GetArtifactRequest get_artifact = 1001; } } @@ -151,16 +156,22 @@ message ArtifactMetadata { // A collection of artifacts. message Manifest { + // (Required) The list of artifacts staged for a job. repeated ArtifactMetadata artifact = 1; } // A manifest with location information. message ProxyManifest { + // (Required) The manifest of staged artifacts. Manifest manifest = 1; + // A location (name + URI) at which a staged artifact can be retrieved. message Location { - string name = 1; - string uri = 2; + // (Required) The name of the artifact, matching the name in the Manifest. + string name = 1; + // (Required) The URI at which the artifact content can be retrieved. + string uri = 2; } + // (Required) The list of artifact locations for retrieval. repeated Location location = 2; } @@ -173,6 +184,7 @@ message GetManifestRequest { // A response containing a job manifest. message GetManifestResponse { + // (Required) The manifest of staged artifacts for the job. Manifest manifest = 1; } @@ -187,9 +199,11 @@ message LegacyGetArtifactRequest { // Part of an artifact. message ArtifactChunk { + // A chunk of the artifact's binary content. bytes data = 1; } +// Metadata for an artifact being staged, sent as the first message in a PutArtifact stream. message PutArtifactMetadata { // (Required) A token for artifact staging session. This token can be obtained // from PrepareJob request in JobService @@ -211,6 +225,7 @@ message PutArtifactRequest { } } +// A response to staging an artifact. An empty response indicating the artifact was staged successfully. message PutArtifactResponse { } diff --git a/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_expansion_api.proto b/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_expansion_api.proto index a4736f8b4938..0e3b1adbffd6 100644 --- a/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_expansion_api.proto +++ b/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_expansion_api.proto @@ -33,6 +33,8 @@ import "google/protobuf/struct.proto"; import "org/apache/beam/model/pipeline/v1/beam_runner_api.proto"; import "org/apache/beam/model/pipeline/v1/schema.proto"; +// A request to expand a single PTransform in a remote SDK. The expansion service +// resolves the transform into its constituent subtransforms and outputs. message ExpansionRequest { // Set of components needed to interpret the transform, or which // may be useful for its expansion. This includes the input @@ -64,6 +66,8 @@ message ExpansionRequest { google.protobuf.Struct pipeline_options = 6; } +// The result of expanding a PTransform, containing the expanded transform and +// all newly created components. message ExpansionResponse { // Set of components needed to execute the expanded transform, // including the (original) inputs, outputs, and subtransforms. @@ -82,9 +86,13 @@ message ExpansionResponse { string error = 10; } +// A request to discover all SchemaTransformProviders registered with the expansion service. +// This is used by cross-language pipeline construction to enumerate available transforms. message DiscoverSchemaTransformRequest { } +// Configuration metadata for a SchemaTransform, describing its inputs, outputs, +// and configuration schema. Used to enable cross-language transform discovery. message SchemaTransformConfig { // Config schema of the SchemaTransform org.apache.beam.model.pipeline.v1.Schema config_schema = 1; @@ -102,6 +110,7 @@ message SchemaTransformConfig { string description = 4; } +// A response containing all discovered SchemaTransform configurations. message DiscoverSchemaTransformResponse { // A mapping from SchemaTransform ID to schema transform config of discovered // SchemaTransforms diff --git a/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_job_api.proto b/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_job_api.proto index 8e196d5bedd3..353eaec57994 100644 --- a/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_job_api.proto +++ b/model/job-management/src/main/proto/org/apache/beam/model/job_management/v1/beam_job_api.proto @@ -112,6 +112,7 @@ message RunJobRequest { } +// The response from submitting a job for execution. message RunJobResponse { string job_id = 1; // (required) The ID for the executing job } @@ -144,6 +145,7 @@ message DrainJobResponse { } // A subset of info provided by ProvisionApi.ProvisionInfo +// Represents metadata about a submitted job. message JobInfo { string job_id = 1; // (required) string job_name = 2; // (required) @@ -155,6 +157,7 @@ message JobInfo { // Throws error GRPC_STATUS_UNAVAILABLE if server is down message GetJobsRequest { } +// The response from requesting a list of all invoked jobs. message GetJobsResponse { repeated JobInfo job_info = 1; // (required) } @@ -168,6 +171,7 @@ message GetJobStateRequest { } +// An event representing a job state transition. Emitted by GetState and GetStateStream. message JobStateEvent { JobState.Enum state = 1; // (required) google.protobuf.Timestamp timestamp = 2; // (required) @@ -182,6 +186,7 @@ message GetJobPipelineRequest { } +// The response from requesting a job's pipeline representation. message GetJobPipelineResponse { org.apache.beam.model.pipeline.v1.Pipeline pipeline = 1; // (required) } @@ -195,25 +200,41 @@ message JobMessagesRequest { string job_id = 1; // (required) } +// A single log message or diagnostic event from a running job. message JobMessage { + // (Required) A unique identifier for this message. string message_id = 1; + // (Required) The time at which the message was emitted, as a string representation. string time = 2; + // (Required) The importance level of this message. MessageImportance importance = 3; + // (Required) The text content of the message. string message_text = 4; + // Importance levels for job messages, ordered from least to most severe. enum MessageImportance { + // The importance was not specified. MESSAGE_IMPORTANCE_UNSPECIFIED = 0; + // Debug-level messages, typically very verbose. JOB_MESSAGE_DEBUG = 1; + // Detailed informational messages. JOB_MESSAGE_DETAILED = 2; + // Basic informational messages. JOB_MESSAGE_BASIC = 3; + // Warning messages indicating potential issues. JOB_MESSAGE_WARNING = 4; + // Error messages indicating failures. JOB_MESSAGE_ERROR = 5; } } +// A streaming response from GetMessageStream, containing either a job message +// or a job state change event. message JobMessagesResponse { oneof response { + // A log message or diagnostic event from the job. JobMessage message_response = 1; + // A job state transition event. JobStateEvent state_response = 2; } } @@ -270,17 +291,22 @@ message JobState { } +// A request to fetch metrics for a given job. message GetJobMetricsRequest { string job_id = 1; // (required) } +// A response containing metrics for a given job. message GetJobMetricsResponse { + // (Required) The metrics results containing both attempted and committed values. MetricResults metrics = 1; } // All metrics for a given job. Runners may support one or the other or both. message MetricResults { + // Metrics reflecting the result of attempted (non-committed) computations. repeated org.apache.beam.model.pipeline.v1.MonitoringInfo attempted = 1; + // Metrics reflecting the result of committed computations. repeated org.apache.beam.model.pipeline.v1.MonitoringInfo committed = 2; } @@ -296,12 +322,17 @@ message DescribePipelineOptionsRequest { // Types mirror those of JSON, since that's how pipeline options are serialized. message PipelineOptionType { enum Enum { + // A string value. STRING = 0; + // A boolean (true/false) value. BOOLEAN = 1; // whole numbers, see https://json-schema.org/understanding-json-schema/reference/numeric.html INTEGER = 2; + // A floating-point number. NUMBER = 3; + // An array of values. ARRAY = 4; + // A nested object. OBJECT = 5; }; } @@ -324,6 +355,7 @@ message PipelineOptionDescriptor { string group = 5; } +// The response from describing pipeline options, containing a list of option descriptors. message DescribePipelineOptionsResponse { // List of pipeline option descriptors. repeated PipelineOptionDescriptor options = 1; 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..ceea6ee2c17b 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 @@ -586,11 +586,17 @@ message StateSpec { FunctionSpec protocol = 7; } +// Specification for a read-modify-write state cell, which holds a single value +// per key and window. The value can be read, modified, and written back atomically. message ReadModifyWriteStateSpec { + // (Required) The id of the Coder for the values stored in this state cell. string coder_id = 1; } +// Specification for a bag state cell, which holds an unordered collection of elements +// per key and window. Elements can be appended and read as an iterable. message BagStateSpec { + // (Required) The id of the Coder for elements stored in this bag. string element_coder_id = 1; } @@ -601,30 +607,57 @@ message OrderedListStateSpec { string element_coder_id = 1; } +// Specification for a combining state cell, which incrementally combines values +// using a CombineFn. Rather than storing all input values, it stores a single +// accumulator that is updated as new values arrive. message CombiningStateSpec { + // (Required) The id of the Coder for the accumulator. string accumulator_coder_id = 1; + // (Required) The FunctionSpec of the CombineFn used to combine values. FunctionSpec combine_fn = 2; } +// Specification for a map state cell, which holds a key-value mapping per key +// and window. Keys and values can be read, written, and deleted individually. +// Note: This is distinct from MultimapStateSpec in that MapState keys are unique. +// In practice, Beam SDKs use Multimap state for both map and multimap semantics. message MapStateSpec { + // (Required) The id of the Coder for map keys. string key_coder_id = 1; + // (Required) The id of the Coder for map values. string value_coder_id = 2; } +// Specification for a multimap state cell, which holds a key-to-multiple-values +// mapping per key and window. A single key can be associated with multiple values, +// analogous to a multimap. message MultimapStateSpec { + // (Required) The id of the Coder for map keys. string key_coder_id = 1; + // (Required) The id of the Coder for map values. string value_coder_id = 2; } +// Specification for a set state cell, which holds an unordered collection of +// unique elements per key and window. Elements can be added, removed, and checked +// for existence. message SetStateSpec { + // (Required) The id of the Coder for elements stored in this set. string element_coder_id = 1; } +// Specification for a timer family, which groups timers that share the same +// time domain and encoding. Timer families allow timers to be managed together, +// including batch processing of expired timers. message TimerFamilySpec { + // (Required) The time domain for this timer family (event time or processing time). TimeDomain.Enum time_domain = 1; + // (Required) The id of the Coder for timers in this family. string timer_family_coder_id = 2; } +// Indicates whether a PCollection is bounded (finite, known size) or unbounded (infinite, unknown size). +// This affects how runners execute the pipeline and which features are available. message IsBounded { enum Enum { UNSPECIFIED = 0; @@ -811,7 +844,13 @@ message GroupIntoBatchesPayload { int64 max_buffering_duration_millis = 2; } +// Payload for the RedistributeByKey and RedistributeArbitrarily composite transforms. +// These transforms redistribute elements across workers, optionally allowing duplicates +// to improve throughput at the cost of additional processing. message RedistributePayload { + // (Optional) If true, the redistribution may produce duplicate elements. + // This can improve performance by avoiding costly deduplication, but requires + // downstream transforms to be tolerant of duplicates. bool allow_duplicates = 1; } @@ -1330,8 +1369,9 @@ message Trigger { message Default { } - // Ready whenever the requisite number of input elements have arrived + // Ready whenever the requisite number of input elements have arrived. message ElementCount { + // (Required) The number of elements that must arrive before this trigger fires. int32 element_count = 1; } @@ -1477,11 +1517,12 @@ message StandardArtifacts { } } +// Payload for a file-based artifact, referencing a locally-accessible file on disk. message ArtifactFilePayload { - // a string for an artifact file path e.g. "/tmp/foo.jar" + // (Required) A file path for an artifact e.g. "/tmp/foo.jar" string path = 1; - // The hex-encoded sha256 checksum of the artifact. + // (Optional) The hex-encoded sha256 checksum of the artifact. string sha256 = 2; } @@ -1498,11 +1539,12 @@ message EmbeddedFilePayload { bytes data = 1; } +// Payload for a PyPI artifact, describing a Python package dependency. message PyPIPayload { - // Pypi compatible artifact id e.g. "apache-beam" + // (Required) A PyPI-compatible artifact id e.g. "apache-beam" string artifact_id = 1; - // Pypi compatible version string. + // (Required) A PyPI-compatible version string, e.g. "2.59.0" string version = 2; } @@ -1515,29 +1557,40 @@ message MavenPayload { string repository_url = 2; } +// Payload for a deferred artifact, where the interpretation of the data is +// deferred to the creator. This allows custom artifact types not covered by +// the standard artifact types. message DeferredArtifactPayload { - // A unique string identifier assigned by the creator of this payload. The creator may use this key to confirm + // (Required) A unique string identifier assigned by the creator of this payload. The creator may use this key to confirm // whether they can parse the data. string key = 1; - // Data for deferred artifacts. Interpretation of bytes is delegated to the creator of this payload. + // (Required) Data for deferred artifacts. Interpretation of bytes is delegated to the creator of this payload. bytes data = 2; } +// Payload for the staging_to role, used when an artifact needs to be staged +// to a particular location. The staged name is relative to the staging directory. message ArtifactStagingToRolePayload { - // A generated staged name (relative path under staging directory). + // (Required) A generated staged name (relative path under staging directory). string staged_name = 1; } +// Full descriptor of an artifact, including its type and role. +// Used to describe dependencies of an Environment. message ArtifactInformation { - // A URN that describes the type of artifact + // (Required) A URN that describes the type of artifact (e.g. file, URL, embedded, PyPI, Maven). + // See StandardArtifacts.Types for well-known URNs. string type_urn = 1; + // (Optional) The payload for the artifact type, as determined by type_urn. bytes type_payload = 2; - // A URN that describes the role of artifact + // (Required) A URN that describes the role of the artifact (e.g. staging_to, go_worker_binary). + // See StandardArtifacts.Roles for well-known URNs. string role_urn = 3; + // (Optional) The payload for the artifact role, as determined by role_urn. bytes role_payload = 4; } @@ -1598,24 +1651,41 @@ message StandardEnvironments { } } -// The payload of a Docker image +// Payload for a Docker container environment. message DockerPayload { + // (Required) The Docker container image URL, e.g. "apache/beam_java_sdk:2.59.0" string container_image = 1; // implicitly linux_amd64. } +// Payload for a native process environment, where the SDK harness is launched as +// a subprocess directly on the worker machine (not inside a container). message ProcessPayload { + // (Required) The operating system, e.g. "linux", "darwin". string os = 1; // "linux", "darwin", .. + // (Required) The CPU architecture, e.g. "amd64", "arm64". string arch = 2; // "amd64", .. + // (Required) The command to execute to start the SDK harness process. string command = 3; // process to execute + // (Optional) Environment variables to set for the SDK harness process. map env = 4; // Environment variables } +// Payload for an external process environment, where the SDK harness is managed +// by an external service rather than directly by the runner. The runner communicates +// with the external worker pool via the provided gRPC endpoint. message ExternalPayload { + // (Required) The gRPC endpoint serving the BeamFnExternalWorkerPool API. ApiServiceDescriptor endpoint = 1; // Serving BeamFnExternalWorkerPool API. + // (Optional) Arbitrary extra parameters to pass to the external worker pool. map params = 2; // Arbitrary extra parameters to pass } +// Payload for an AnyOf environment, which presents a set of equivalent environments +// that a runner may choose from. Each alternative is fully specified with its own +// dependencies, capabilities, etc. This allows SDKs to express flexibility in +// execution environment while preserving compatibility across runners. message AnyOfEnvironmentPayload { + // (Required) The list of fully-specified environments the runner may choose from. // Each is fully contained (with their own dependencies, capabilities, etc.) repeated Environment environments = 1; } @@ -1689,6 +1759,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"]; } } @@ -1848,6 +1922,8 @@ message StandardDisplayData { } } +// A labelled display data payload, associating a human-readable label with a value. +// Used as the payload for the StandardDisplayData.LABELLED URN. message LabelledPayload { // (Required) A human readable label for the value. string label = 1; diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/endpoints.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/endpoints.proto index 46fb3d5d3b1c..202d8d6181b9 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/endpoints.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/endpoints.proto @@ -39,6 +39,9 @@ message ApiServiceDescriptor { AuthenticationSpec authentication = 2; } +// Describes the authentication mechanism for connecting to a Beam API endpoint. +// Uses a URN-based approach to allow different authentication methods (e.g. OAuth2, +// bearer tokens) to be specified in an extensible manner. message AuthenticationSpec { // (Required) A URN that describes the accompanying payload. // For any URN that is not recognized (by whomever is inspecting 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..debacc245d60 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 @@ -60,34 +60,55 @@ message ExpansionMethods { } // Defines the URNs for managed transforms. +// Managed transforms are high-level, pre-configured transforms that map to +// specific SchemaTransform implementations. They provide a way to +// to use common I/O patterns without worrying about Beam version. +// See https://beam.apache.org/documentation/io/managed-io/ message ManagedTransforms { enum Urns { + // Reads from an Iceberg table. ICEBERG_READ = 0 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:iceberg_read:v1"]; + // Writes to an Iceberg table. ICEBERG_WRITE = 1 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:iceberg_write:v1"]; + // Reads from a Kafka topic. KAFKA_READ = 2 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:kafka_read:v1"]; + // Writes to a Kafka topic. KAFKA_WRITE = 3 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:kafka_write:v1"]; + // Reads from BigQuery using the Storage API. BIGQUERY_READ = 4 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:bigquery_storage_read:v1"]; + // Writes to BigQuery. BIGQUERY_WRITE = 5 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:bigquery_write:v1"]; + // Reads from Iceberg in CDC (Change Data Capture) mode. ICEBERG_CDC_READ = 6 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:iceberg_cdc_read:v1"]; + // Reads from a PostgreSQL database. POSTGRES_READ = 7 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:postgres_read:v1"]; + // Writes to a PostgreSQL database. POSTGRES_WRITE = 8 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:postgres_write:v1"]; + // Reads from a MySQL database. MYSQL_READ = 9 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:mysql_read:v1"]; + // Writes to a MySQL database. MYSQL_WRITE = 10 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:mysql_write:v1"]; + // Reads from a SQL Server database. SQL_SERVER_READ = 11 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:sql_server_read:v1"]; + // Writes to a SQL Server database. 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"]; + DELTA_LAKE_CDC_READ = 14 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:schematransform:org.apache.beam:delta_lake_cdc_read:v1"]; } } @@ -143,6 +164,9 @@ message BuilderMethod { bytes payload = 3; } +// Defines annotation keys used to attach metadata to transforms in the +// portable pipeline representation. These annotations allow SDKs to +// recover configuration information that was used to construct a transform. message Annotations { enum Enum { // The annotation key for the encoded configuration Row used to build a transform diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/metrics.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/metrics.proto index fcce35394b91..49e0c7b3ea82 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/metrics.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/metrics.proto @@ -58,7 +58,9 @@ message MonitoringInfoSpec { // The key name and value string of MonitoringInfo annotations. message Annotation { + // The annotation key, e.g. "description", "units". string key = 1; + // The annotation value as a string. string value = 2; } @@ -198,7 +200,8 @@ message MonitoringInfoSpecs { }] }]; - // Represents a set of strings seen across bundles. + // Represents a bounded trie of strings seen across bundles. The trie has a + // maximum number of elements it will store before truncating. USER_BOUNDED_TRIE = 22 [(monitoring_info_spec) = { urn: "beam:metric:user:bounded_trie:v1", type: "beam:metrics:bounded_trie:v1", @@ -225,6 +228,7 @@ message MonitoringInfoSpecs { // }] // }]; + // The total number of elements output to a PCollection by a PTransform. ELEMENT_COUNT = 10 [(monitoring_info_spec) = { urn: "beam:metric:element_count:v1", type: "beam:metrics:sum_int64:v1", @@ -235,6 +239,8 @@ message MonitoringInfoSpecs { } ] }]; + // The distribution of byte sizes of a sampled set of elements in a PCollection. + // Sampling is used because serializing elements to compute byte size is CPU-intensive. SAMPLED_BYTE_SIZE = 11 [(monitoring_info_spec) = { urn: "beam:metric:sampled_byte_size:v1", type: "beam:metrics:distribution_int64:v1", @@ -248,6 +254,8 @@ message MonitoringInfoSpecs { } ] }]; + // The total estimated execution time (in milliseconds) of the startBundle + // function in a ParDo transform. START_BUNDLE_MSECS = 12 [(monitoring_info_spec) = { urn: "beam:metric:pardo_execution_time:start_bundle_msecs:v1", type: "beam:metrics:sum_int64:v1", @@ -259,6 +267,8 @@ message MonitoringInfoSpecs { } ] }]; + // The total estimated execution time (in milliseconds) of the processElement + // function in a ParDo transform. PROCESS_BUNDLE_MSECS = 13 [(monitoring_info_spec) = { urn: "beam:metric:pardo_execution_time:process_bundle_msecs:v1", type: "beam:metrics:sum_int64:v1", @@ -270,6 +280,8 @@ message MonitoringInfoSpecs { } ] }]; + // The total estimated execution time (in milliseconds) of the finishBundle + // function in a ParDo transform. FINISH_BUNDLE_MSECS = 14 [(monitoring_info_spec) = { urn: "beam:metric:pardo_execution_time:finish_bundle_msecs:v1", type: "beam:metrics:sum_int64:v1", @@ -281,6 +293,7 @@ message MonitoringInfoSpecs { } ] }]; + // The total estimated execution time (in milliseconds) of the entire PTransform. TOTAL_MSECS = 15 [(monitoring_info_spec) = { urn: "beam:metric:ptransform_execution_time:total_msecs:v1", type: "beam:metrics:sum_int64:v1", @@ -330,6 +343,8 @@ message MonitoringInfoSpecs { }] }]; + // The total number of API requests made to an I/O service, grouped by + // service, method, resource, PTransform, and status. API_REQUEST_COUNT = 19 [(monitoring_info_spec) = { urn: "beam:metric:io:api_request_count:v1", type: "beam:metrics:sum_int64:v1", @@ -353,6 +368,8 @@ message MonitoringInfoSpecs { ] }]; + // A histogram of API request latencies (in milliseconds) made to I/O service + // APIs for batching reads or writes. API_REQUEST_LATENCIES = 20 [(monitoring_info_spec) = { urn: "beam:metric:io:api_request_latencies:v1", type: "beam:metrics:histogram_int64:v1", @@ -379,6 +396,8 @@ message MonitoringInfoSpecs { ] }]; + // Represents a user-defined histogram metric tracking the distribution of + // integer values across bundles. USER_HISTOGRAM = 23 [(monitoring_info_spec) = { urn: "beam:metric:user:histogram_int64:v1", type: "beam:metrics:histogram_int64:v1", @@ -400,13 +419,23 @@ message MonitoringInfoLabelProps { // Enum extension to store MonitoringInfo related // specifications, constants, etc. +// This extension mechanism allows enum values in MonitoringInfoSpecs and +// MonitoringInfoLabels to carry structured metadata (specs and label names) +// directly on the enum value definition. extend google.protobuf.EnumValueOptions { + // Properties for MonitoringInfoLabel enum values, specifying the label name + // to use in the MonitoringInfo labels map. MonitoringInfoLabelProps label_props = 127337796; // From: commit 0x7970544. - // Enum extension to store the MonitoringInfoSpecs. + // Enum extension to store the MonitoringInfoSpec. MonitoringInfoSpec monitoring_info_spec = 207174266; } +// A monitoring info represents a single data point or aggregated metric reported +// by the SDK or runner. It contains the URN identifying the metric, the type +// encoding, the payload (encoded metric value), labels providing context (such +// as which PTransform or PCollection the metric is associated with), and an +// optional start_time for cumulative metrics. message MonitoringInfo { // (Required) Defines the semantic meaning of the metric or monitored state. // @@ -429,33 +458,62 @@ message MonitoringInfo { // refer to them. For actively processed bundles, these should match the // values within the ProcessBundleDescriptor. For job management APIs, // these should match values within the original pipeline representation. + // Label referencing the PTransform that this metric is associated with. TRANSFORM = 0 [(label_props) = { name: "PTRANSFORM" }]; + // Label referencing the PCollection that this metric is associated with. PCOLLECTION = 1 [(label_props) = { name: "PCOLLECTION" }]; + // Label referencing the WindowingStrategy that this metric is associated with. WINDOWING_STRATEGY = 2 [(label_props) = { name: "WINDOWING_STRATEGY" }]; + // Label referencing the Coder used for encoding elements. CODER = 3 [(label_props) = { name: "CODER" }]; + // Label referencing the Environment that this metric is associated with. ENVIRONMENT = 4 [(label_props) = { name: "ENVIRONMENT" }]; + // Label for the namespace of a user metric, used to avoid name collisions + // between user-defined metrics. NAMESPACE = 5 [(label_props) = { name: "NAMESPACE" }]; + // Label for the name of a user metric within its namespace. NAME = 6 [(label_props) = { name: "NAME" }]; + // Label for the I/O service name (e.g. "bigquery", "pubsub"). SERVICE = 7 [(label_props) = { name: "SERVICE" }]; + // Label for the I/O API method name (e.g. "read", "write"). METHOD = 8 [(label_props) = { name: "METHOD" }]; + // Label for the I/O resource being accessed (e.g. a table or topic name). RESOURCE = 9 [(label_props) = { name: "RESOURCE" }]; + // Label for the HTTP status code of an I/O API request. STATUS = 10 [(label_props) = { name: "STATUS" }]; + // Label for the BigQuery project ID associated with this metric. BIGQUERY_PROJECT_ID = 11 [(label_props) = { name: "BIGQUERY_PROJECT_ID" }]; + // Label for the BigQuery dataset associated with this metric. BIGQUERY_DATASET = 12 [(label_props) = { name: "BIGQUERY_DATASET" }]; + // Label for the BigQuery table associated with this metric. BIGQUERY_TABLE = 13 [(label_props) = { name: "BIGQUERY_TABLE" }]; + // Label for the BigQuery view associated with this metric. BIGQUERY_VIEW = 14 [(label_props) = { name: "BIGQUERY_VIEW" }]; + // Label for a user-provided query name used to identify a BigQuery query. BIGQUERY_QUERY_NAME = 15 [(label_props) = { name: "BIGQUERY_QUERY_NAME" }]; + // Label for the GCS bucket associated with this metric. GCS_BUCKET = 16 [(label_props) = { name: "GCS_BUCKET"}]; + // Label for the GCP project ID associated with a GCS operation. GCS_PROJECT_ID = 17 [(label_props) = { name: "GCS_PROJECT_ID"}]; + // Label for the Datastore project associated with this metric. DATASTORE_PROJECT = 18 [(label_props) = { name: "DATASTORE_PROJECT" }]; + // Label for the Datastore namespace associated with this metric. DATASTORE_NAMESPACE = 19 [(label_props) = { name: "DATASTORE_NAMESPACE" }]; + // Label for the Bigtable project ID associated with this metric. BIGTABLE_PROJECT_ID = 20 [(label_props) = { name: "BIGTABLE_PROJECT_ID"}]; + // Label for the instance ID of a Bigtable or other service. INSTANCE_ID = 21 [(label_props) = { name: "INSTANCE_ID"}]; + // Label for the table ID of a Bigtable or other service. TABLE_ID = 22 [(label_props) = { name: "TABLE_ID"}]; + // Label for the Spanner project ID associated with this metric. SPANNER_PROJECT_ID = 23 [(label_props) = { name: "SPANNER_PROJECT_ID" }]; + // Label for the Spanner database ID associated with this metric. SPANNER_DATABASE_ID = 24 [(label_props) = { name: "SPANNER_DATABASE_ID" }]; + // Label for the Spanner table ID associated with this metric. SPANNER_TABLE_ID = 25 [(label_props) = { name: "SPANNER_TABLE_ID" }]; + // Label for the Spanner instance ID associated with this metric. SPANNER_INSTANCE_ID = 26 [(label_props) = { name: "SPANNER_INSTANCE_ID" }]; + // Label for a user-provided query name used to identify a Spanner query. SPANNER_QUERY_NAME = 27 [(label_props) = { name: "SPANNER_QUERY_NAME" }]; // Label which if has a "true" value indicates that the metric is intended // to be aggregated per-worker. @@ -489,7 +547,10 @@ message MonitoringInfo { google.protobuf.Timestamp start_time = 5; } -// A set of well known URNs that specify the encoding and aggregation method. +// A set of well known URNs that specify the encoding and aggregation method +// for metric payloads. Each URN defines how the payload bytes of a MonitoringInfo +// should be decoded and how values from multiple bundles should be aggregated +// together by the runner. message MonitoringInfoTypeUrns { enum Enum { // Represents an integer counter where values are summed across bundles. @@ -594,6 +655,9 @@ message MonitoringInfoTypeUrns { BOTTOM_N_DOUBLE_TYPE = 9 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:metrics:bottom_n_double:v1"]; + // Represents progress information for a PTransform, containing both completed + // and remaining work as separate values. + // // Encoding: ... // - iter: beam:coder:iterable:v1 // - valueX: beam:coder:double:v1 @@ -614,7 +678,8 @@ message MonitoringInfoTypeUrns { BOUNDED_TRIE_TYPE = 12 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:metrics:bounded_trie:v1"]; - // Represents a histogram. + // Represents a histogram of integer values. The payload is encoded as a + // HistogramValue protobuf message. HISTOGRAM = 13 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:metrics:histogram_int64:v1"]; diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto index 58d16b83749d..c114c35f2d90 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto @@ -37,6 +37,8 @@ message Schema { repeated Field fields = 1; // REQUIRED. An RFC 4122 UUID. string id = 2; + // (Optional) Schema-level options that provide additional metadata + // or configuration for this schema. repeated Option options = 3; // Indicates that encoding positions have been overridden. bool encoding_positions_set = 4; @@ -47,8 +49,11 @@ message Field { string name = 1; // OPTIONAL. Human readable description of this field, such as the query that generated it. string description = 2; + // (Required) The type of this field. FieldType type = 3; + // (Optional) An integer identifier for this field. Used for schema evolution + // to track fields across schema changes even when field names or positions change. int32 id = 4; // OPTIONAL. The position of this field's data when encoded, e.g. with beam:coder:row:v1. // Either no fields in a given row are have encoding position populated, @@ -58,56 +63,104 @@ message Field { // If this Field is part of a Schema where encoding_positions_set is True then encoding_position must be // defined, otherwise this field is ignored. int32 encoding_position = 5; + // (Optional) Field-level options that provide additional metadata + // or configuration for this field. repeated Option options = 6; } +// Represents the type of a field in a Beam Schema. A field type can be one of +// several categories: atomic types (primitives like INT32, STRING, etc.), +// collection types (arrays, iterables, maps), row types (nested schemas), +// or logical types (custom types with a URN-based representation). message FieldType { + // Whether this field can hold null values. bool nullable = 1; oneof type_info { + // A primitive (atomic) type such as INT32, STRING, BYTES, etc. AtomicType atomic_type = 2; + // An ordered array of elements of a uniform element type. ArrayType array_type = 3; + // An unordered iterable of elements of a uniform element type. IterableType iterable_type = 4; + // A map with typed keys and typed values. MapType map_type = 5; + // A nested row with its own schema. RowType row_type = 6; + // A user-defined logical type, identified by URN. LogicalType logical_type = 7; } } +// The set of primitive (atomic) types supported by Beam Schemas. enum AtomicType { + // Unspecified type; used as the default/placeholder value. UNSPECIFIED = 0; + // A single signed byte (8-bit integer). BYTE = 1; + // A 16-bit signed integer. INT16 = 2; + // A 32-bit signed integer. INT32 = 3; + // A 64-bit signed integer. INT64 = 4; + // A single-precision (32-bit) IEEE 754 floating-point number. FLOAT = 5; + // A double-precision (64-bit) IEEE 754 floating-point number. DOUBLE = 6; + // A UTF-8 encoded string. STRING = 7; + // A boolean (true/false) value. BOOLEAN = 8; + // A sequence of bytes. BYTES = 9; } +// Represents an ordered, variable-length array type. All elements share +// the same element type. message ArrayType { + // The type of elements in this array. FieldType element_type = 1; } +// Represents an unordered, variable-length iterable type. Similar to ArrayType +// but without guaranteed ordering of elements. message IterableType { + // The type of elements in this iterable. FieldType element_type = 1; } +// Represents a map (key-value association) type. Keys and values may each +// have their own distinct types. message MapType { + // The type of the map keys. FieldType key_type = 1; + // The type of the map values. FieldType value_type = 2; } +// Represents a nested row type, which contains its own Schema. This enables +// hierarchical/structured data within a single schema field. message RowType { + // The schema describing the structure of nested rows. Schema schema = 1; } +// Represents a user-defined logical type. Logical types extend the type system +// by mapping a URN-identified type to an underlying representation type. +// This allows SDKs to define custom types (e.g. Decimal, Date) while encoding +// them using one of the built-in atomic or collection types. message LogicalType { + // (Required) URN uniquely identifying this logical type. string urn = 1; + // (Optional) Payload providing additional configuration for this logical type. bytes payload = 2; + // (Required) The underlying FieldType used to encode/decode values of this + // logical type on the wire. FieldType representation = 3; + // (Optional) The type of the argument passed when constructing an instance + // of this logical type (e.g. the length argument for FixedBytes). FieldType argument_type = 4; + // (Optional) The actual argument value for this logical type instance. FieldValue argument = 5; } @@ -181,9 +234,25 @@ message LogicalTypes { // since the epoch. DATE = 8 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:logical_type:date:v1"]; + + // A URN for Timestamp type + // - Representation type: ROW for + // precision < 5, otherwise ROW + // - Argument type: INT32. + // A timestamp without a timezone, represented as seconds since the + // epoch plus a non-negative fraction of a second. The argument is + // the precision: the number of decimal digits of the subseconds + // field (e.g. 3 for milliseconds, 6 for micros, 9 for nanos). + // subseconds stays non-negative even for timestamps before the epoch, + // e.g. -1.5s at precision 6 is {seconds: -2, subseconds: 500000}. + TIMESTAMP = 9 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:logical_type:timestamp:v1"]; } } +// Represents a schema option, which is a named, typed value attached to a +// Schema or Field. Options provide additional metadata or configuration +// that is not part of the core type system. message Option { // REQUIRED. Identifier for the option. string name = 1; @@ -191,57 +260,93 @@ message Option { // Conventionally, options that don't require additional configuration should // use a boolean type, with the value set to true. FieldType type = 2; + // The typed value of this option. FieldValue value = 3; } +// A concrete instance of a row, containing the values for each field as defined +// by the associated Schema. The order of values matches the field order in the +// Schema. message Row { + // The values for each field in the row, in schema field order. repeated FieldValue values = 1; } +// Represents the value of a single field within a Row. The oneof corresponds +// to the FieldType categories: atomic, array, iterable, map, row, or logical. message FieldValue { // If none of these are set, value is considered null. oneof field_value { + // The value for an atomic (primitive) field type. AtomicTypeValue atomic_value = 1; + // The value for an array field type. ArrayTypeValue array_value = 2; + // The value for an iterable field type. IterableTypeValue iterable_value = 3; + // The value for a map field type. MapTypeValue map_value = 4; + // The value for a nested row field type. Row row_value = 5; + // The value for a logical type field. LogicalTypeValue logical_type_value = 6; } } +// The concrete value of an atomic (primitive) field. The oneof member must +// correspond to the AtomicType of the field's FieldType. message AtomicTypeValue { oneof value { + // Value for AtomicType.BYTE. int32 byte = 1; + // Value for AtomicType.INT16. int32 int16 = 2; + // Value for AtomicType.INT32. int32 int32 = 3; + // Value for AtomicType.INT64. int64 int64 = 4; + // Value for AtomicType.FLOAT. float float = 5; + // Value for AtomicType.DOUBLE. double double = 6; + // Value for AtomicType.STRING. string string = 7; + // Value for AtomicType.BOOLEAN. bool boolean = 8; + // Value for AtomicType.BYTES. bytes bytes = 9; } } +// The concrete value of an array field, containing a list of element values. message ArrayTypeValue { + // The element values in this array, in order. repeated FieldValue element = 1; } +// The concrete value of an iterable field, containing a list of element values. message IterableTypeValue { + // The element values in this iterable. repeated FieldValue element = 1; } +// The concrete value of a map field, represented as a list of key-value entries. message MapTypeValue { + // The key-value entries in this map. repeated MapTypeEntry entries = 1; } +// A single key-value entry within a MapTypeValue. message MapTypeEntry { + // The key of this map entry. FieldValue key = 1; + // The value of this map entry. FieldValue value = 2; } +// The concrete value of a logical type field. Contains the underlying +// representation value that encodes the logical type's data. message LogicalTypeValue { + // The underlying representation value for this logical type. FieldValue value = 1; } @@ -260,7 +365,9 @@ message SchemaCoderPayload { repeated AdditionalCoderInfo additional_coder_infos = 4; message AdditionalCoderInfo { + // URN identifying the additional coder information. string urn = 1; + // Payload for the additional coder information. bytes payload = 2; } } diff --git a/playground/backend/containers/scio/Dockerfile b/playground/backend/containers/scio/Dockerfile index 3d448b45906a..526789d75dc5 100644 --- a/playground/backend/containers/scio/Dockerfile +++ b/playground/backend/containers/scio/Dockerfile @@ -52,11 +52,21 @@ COPY --from=build /go/src/playground/backend/new_scio_project.sh /opt/playground COPY --from=build /go/src/playground/backend/internal/fs_tool/ExampleData.scala /opt/playground/backend/ RUN chmod +x /opt/playground/backend/new_scio_project.sh -# Install sbt -RUN echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | tee /etc/apt/sources.list.d/sbt.list &&\ -echo "deb https://repo.scala-sbt.org/scalasbt/debian /" | tee /etc/apt/sources.list.d/sbt_old.list &&\ -curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | apt-key add -RUN apt-get update && apt-get install -y sbt +# Install sbt 1.x +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gnupg \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL \ + "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" \ + | gpg --dearmor -o /etc/apt/keyrings/scalasbt.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/scalasbt.gpg] https://repo.scala-sbt.org/scalasbt/debian all main" \ + > /etc/apt/sources.list.d/sbt.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends sbt=1.10.11 \ + && rm -rf /var/lib/apt/lists/* COPY src/properties.yaml /opt/playground/backend/properties.yaml COPY entrypoint.sh / @@ -83,7 +93,7 @@ RUN chown -R appuser:appgroup /opt/sbt-template ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update \ && apt-get install -y --no-install-recommends unzip \ - && rm -rf /var/lib/apt/lists/* \ + && rm -rf /var/lib/apt/lists/* #Download spotify g8 template at specific commit #ARG g8_commit=7c1ba7c1651dfd70976028842e721da4107c0d6d diff --git a/playground/backend/go.mod b/playground/backend/go.mod index bcb3a78fbd4e..b1aa1f4aeef2 100644 --- a/playground/backend/go.mod +++ b/playground/backend/go.mod @@ -15,7 +15,7 @@ module beam.apache.org/playground/backend -go 1.20 +go 1.23.0 require ( cloud.google.com/go/datastore v1.11.0 @@ -40,8 +40,7 @@ require ( require ( cloud.google.com/go v0.110.0 // indirect - cloud.google.com/go/compute v1.19.1 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/functions v1.13.0 // indirect cloud.google.com/go/longrunning v0.4.1 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect @@ -76,7 +75,7 @@ require ( go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/net v0.9.0 // indirect - golang.org/x/oauth2 v0.7.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/text v0.9.0 // indirect diff --git a/playground/backend/go.sum b/playground/backend/go.sum index 516388aeb276..b634f9597e2a 100644 --- a/playground/backend/go.sum +++ b/playground/backend/go.sum @@ -169,13 +169,12 @@ cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvj 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 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= -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 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 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= @@ -970,6 +969,7 @@ github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= @@ -1975,8 +1975,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri 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 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 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= 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" diff --git a/playground/kafka-emulator/build.gradle b/playground/kafka-emulator/build.gradle index 17bb9de948d7..62f6001c76c8 100644 --- a/playground/kafka-emulator/build.gradle +++ b/playground/kafka-emulator/build.gradle @@ -23,6 +23,17 @@ plugins { applyJavaNature(exportJavadoc: false, publish: false) +// embedded-kafka 2.4.1.1 requires kafka-clients 2.4.1. BeamModulePlugin forces +// kafka-clients to the Beam-wide kafka_version (3.9.2+ after #39284), which +// prevents the emulator from binding and blocks every Playground RunCode call. +// TODO: Migrate embedded-kafka to 3.9.2. +// https://mvnrepository.com/artifact/io.github.embeddedkafka/embedded-kafka_2.12/3.9.2 +configurations.all { + resolutionStrategy { + force 'org.apache.kafka:kafka-clients:2.4.1' + } +} + distZip { archiveFileName = "${archiveBaseName.get()}.zip" } diff --git a/release/build.gradle.kts b/release/build.gradle.kts index 5be707428605..6cf3540a7f88 100644 --- a/release/build.gradle.kts +++ b/release/build.gradle.kts @@ -39,9 +39,9 @@ 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")) { + if ((project.findProperty("ver")?.toString()?.isNotEmpty() == true && project.findProperty("ver") != "2.XX.0") || !project.version.toString().endsWith("SNAPSHOT")) { // only run one variant of MobileGaming on Dataflow for nightly dependsOn(":runners:google-cloud-dataflow-java:runMobileGamingJavaDataflow") } diff --git a/release/src/main/groovy/TestScripts.groovy b/release/src/main/groovy/TestScripts.groovy index dc2438007ac1..e0e9cf454495 100644 --- a/release/src/main/groovy/TestScripts.groovy +++ b/release/src/main/groovy/TestScripts.groovy @@ -25,6 +25,10 @@ import groovy.util.CliBuilder */ class TestScripts { + class BackgroundProcessInfo { + String cmd + } + // Global state to maintain when running the steps class var { static File startDir @@ -37,6 +41,8 @@ class TestScripts { static String bqDataset static String pubsubTopic static String mavenLocalPath + static List backgroundProcesses = Collections.synchronizedList(new ArrayList()) + static Map backgroundProcessInfo = Collections.synchronizedMap(new HashMap()) } def TestScripts(String[] args) { @@ -79,6 +85,10 @@ class TestScripts { var.mavenLocalPath = options.mavenLocalPath println "Maven local path: ${var.mavenLocalPath}" } + + Runtime.getRuntime().addShutdownHook(new Thread({ + stopAllBackgroundProcesses() + })) } def ver() { @@ -135,6 +145,75 @@ class TestScripts { } } + // Run a command in the background, returning the Process object. + public Process runBackground(String cmd) { + println cmd + if (cmd.startsWith("mvn ")) { + return _mvnBackground(cmd.substring(4)) + } else { + return _executeBackground(cmd) + } + } + + // Check whether any background processes exited unexpectedly with a non-zero exit code + public void checkBackgroundProcesses() { + def procs = new ArrayList<>(var.backgroundProcesses) + for (Process proc : procs) { + if (proc != null && !proc.isAlive()) { + int exitVal = proc.exitValue() + if (exitVal != 0) { + def info = var.backgroundProcessInfo.get(proc) + String cmd = info ? info.cmd : "unknown command" + error("Background command failed with exit code ${exitVal}: ${cmd}") + } + } + } + } + + // Stop/kill a background process and all its descendants. + public void stopProcess(Process proc) { + if (proc != null) { + if (!proc.isAlive()) { + int exitVal = proc.exitValue() + var.backgroundProcesses.remove(proc) + def info = var.backgroundProcessInfo.remove(proc) + if (exitVal != 0) { + String cmd = info ? info.cmd : "unknown command" + error("Background command failed with exit code ${exitVal}: ${cmd}") + } + } else { + try { + proc.descendants().forEach { it.destroyForcibly() } + } catch (Throwable ignored) { + } + proc.destroyForcibly() + proc.waitFor(10, java.util.concurrent.TimeUnit.SECONDS) + var.backgroundProcesses.remove(proc) + var.backgroundProcessInfo.remove(proc) + } + } + } + + // Stop all active background processes. + public void stopAllBackgroundProcesses() { + def procs = new ArrayList<>(var.backgroundProcesses) + procs.each { proc -> + if (proc != null && proc.isAlive()) { + try { + proc.descendants().forEach { it.destroyForcibly() } + } catch (Throwable ignored) { + } + proc.destroyForcibly() + try { + proc.waitFor(10, java.util.concurrent.TimeUnit.SECONDS) + } catch (Throwable ignored) { + } + } + var.backgroundProcesses.remove(proc) + var.backgroundProcessInfo.remove(proc) + } + } + // Check for expected results in actual stdout from previous command, if fails, log errors then exit. public void see(String expected, String actual) { if (!actual.contains(expected)) { @@ -159,6 +238,8 @@ class TestScripts { // Cleanup and print success public void done() { + checkBackgroundProcesses() + stopAllBackgroundProcesses() var.startDir.deleteDir() println "[SUCCESS]" System.exit(0) @@ -166,6 +247,7 @@ class TestScripts { // Run a single command, capture output, verify return code is 0 private String _execute(String cmd) { + checkBackgroundProcesses() def shell = "sh -c cmd".split(' ') shell[2] = cmd def pb = new ProcessBuilder(shell) @@ -187,6 +269,27 @@ class TestScripts { return output_text } + // Run a single command asynchronously in the background + private Process _executeBackground(String cmd) { + def shell = "sh -c cmd".split(' ') + shell[2] = cmd + def pb = new ProcessBuilder(shell) + pb.directory(var.curDir) + pb.redirectErrorStream(true) + def proc = pb.start() + var.backgroundProcesses.add(proc) + var.backgroundProcessInfo.put(proc, new BackgroundProcessInfo(cmd: cmd)) + Thread.startDaemon { + try { + proc.inputStream.eachLine { + println it + } + } catch (Throwable ignored) { + } + } + return proc + } + // Change directory private void _chdir(String subdir) { var.curDir = new File(var.curDir.absolutePath, subdir) @@ -195,8 +298,8 @@ class TestScripts { } } - // Run a maven command, setting up a new local repository and a settings.xml with a custom repository if needed - private String _mvn(String args) { + // Build the maven command string with custom repository and settings.xml + private String _buildMvnCmd(String args) { String mvnlocalPath = var.mavenLocalPath if (!(var.mavenLocalPath)) { mvnlocalPath = var.startDir @@ -204,37 +307,48 @@ class TestScripts { def m2 = new File(mvnlocalPath, ".m2/repository") m2.mkdirs() def settings = new File(mvnlocalPath, "settings.xml") - if(!settings.exists()) { - settings.write """ - - ${m2.absolutePath} - - - testrel - - - test.release - ${var.repoUrl} - - - - - - """ + if (!settings.exists()) { + settings.write """ + + ${m2.absolutePath} + + + testrel + + + test.release + ${var.repoUrl} + + + + + + """ } def cmd = "mvn ${args} -s ${settings.absolutePath} -Ptestrel -B" - String path = System.getenv("PATH"); + String path = System.getenv("PATH") // Set the path on jenkins executors to use a recent maven // MAVEN_HOME is not set on some executors, so default to 3.5.2 String maven_home = System.getenv("MAVEN_HOME") ?: '/usr/local/maven' println "Using maven ${maven_home}" def mvnPath = "${maven_home}/bin" def setPath = "export PATH=\"${mvnPath}:${path}\" && " - return _execute(setPath + cmd) + return setPath + cmd + } + + // Run a maven command, setting up a new local repository and a settings.xml with a custom repository if needed + private String _mvn(String args) { + return _execute(_buildMvnCmd(args)) + } + + // Run a maven command in the background + private Process _mvnBackground(String args) { + return _executeBackground(_buildMvnCmd(args)) } // Clean up and report error public void error(String text) { + stopAllBackgroundProcesses() var.startDir.deleteDir() println "[ERROR] $text" System.exit(1) diff --git a/release/src/main/groovy/mobilegaming-java-dataflow.groovy b/release/src/main/groovy/mobilegaming-java-dataflow.groovy index a2164aa6a019..31b4f0670f98 100644 --- a/release/src/main/groovy/mobilegaming-java-dataflow.groovy +++ b/release/src/main/groovy/mobilegaming-java-dataflow.groovy @@ -45,7 +45,7 @@ int waitTime = 15 // seconds def outputPath = "gs://${t.gcsBucket()}/${mobileGamingCommands.getUserScoreOutputName(runner)}" def outputFound = false for (int i = 0; i < retries; i++) { - def files = t.run("gsutil ls ${outputPath}*") + def files = t.run("gcloud storage ls ${outputPath}*") if (files?.trim()) { outputFound = true break @@ -58,10 +58,10 @@ if (!outputFound) { throw new RuntimeException("No output files found for HourlyTeamScore after ${retries * waitTime} seconds.") } -command_output_text = t.run "gsutil cat ${outputPath}* | grep user19_BananaWallaby" +command_output_text = t.run "gcloud storage cat ${outputPath}* | grep user19_BananaWallaby" t.see "total_score: 231, user: user19_BananaWallaby", command_output_text t.success("UserScore successfully run on DataflowRunner.") -t.run "gsutil rm gs://${t.gcsBucket()}/${mobileGamingCommands.getUserScoreOutputName(runner)}*" +t.run "gcloud storage rm gs://${t.gcsBucket()}/${mobileGamingCommands.getUserScoreOutputName(runner)}*" /** @@ -76,7 +76,7 @@ t.run(mobileGamingCommands.createPipelineCommand("HourlyTeamScore", runner)) outputPath = "gs://${t.gcsBucket()}/${mobileGamingCommands.getHourlyTeamScoreOutputName(runner)}" outputFound = false for (int i = 0; i < retries; i++) { - def files = t.run("gsutil ls ${outputPath}*") + def files = t.run("gcloud storage ls ${outputPath}*") if (files?.trim()) { outputFound = true break @@ -89,10 +89,10 @@ if (!outputFound) { throw new RuntimeException("No output files found for UserScore after ${retries * waitTime} seconds.") } -command_output_text = t.run "gsutil cat ${outputPath}* | grep AzureBilby " +command_output_text = t.run "gcloud storage cat ${outputPath}* | grep AzureBilby " t.see "total_score: 2788, team: AzureBilby", command_output_text t.success("HourlyTeamScore successfully run on DataflowRunner.") -t.run "gsutil rm gs://${t.gcsBucket()}/${mobileGamingCommands.getHourlyTeamScoreOutputName(runner)}*" +t.run "gcloud storage rm gs://${t.gcsBucket()}/${mobileGamingCommands.getHourlyTeamScoreOutputName(runner)}*" /** @@ -120,37 +120,53 @@ class LeaderBoardRunner { ].join(",") String tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") - - if (!tables.contains(userTable)) { - t.intent("Creating table: ${userTable}") - t.run("bq mk --table ${dataset}.${userTable} ${userSchema}") + if (tables.contains(userTable)) { + t.run("bq rm -f -t ${dataset}.${userTable}") + } + if (tables.contains(teamTable)) { + t.run("bq rm -f -t ${dataset}.${teamTable}") } - if (!tables.contains(teamTable)) { - t.intent("Creating table: ${teamTable}") - t.run("bq mk --table ${dataset}.${teamTable} ${teamSchema}") + int retries = 10 + boolean deleted = false + for (int i = 0; i < retries; i++) { + tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") + if (!tables.contains(userTable) && !tables.contains(teamTable)) { + deleted = true + break + } + sleep(3000) } + if (!deleted) { + t.error("Timed out waiting for tables ${userTable} / ${teamTable} to be deleted.") + } + + t.intent("Creating table: ${userTable}") + t.run("bq mk --table ${dataset}.${userTable} ${userSchema}") + t.intent("Creating table: ${teamTable}") + t.run("bq mk --table ${dataset}.${teamTable} ${teamSchema}") // Verify that the tables have been created successfully - tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") - while (!tables.contains(userTable) || !tables.contains(teamTable)) { - sleep(3000) + boolean created = false + for (int i = 0; i < retries; i++) { tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") + if (tables.contains(userTable) && tables.contains(teamTable)) { + created = true + break + } + sleep(3000) + } + if (!created) { + t.error("Timed out waiting for tables ${userTable} / ${teamTable} to be created.") } println "Tables ${userTable} and ${teamTable} created successfully." - def InjectorThread = Thread.start() { - t.run(mobileGamingCommands.createInjectorCommand()) - } + def injectorProcess = t.runBackground(mobileGamingCommands.createInjectorCommand()) String jobName = "leaderboard-validation-" + new Date().getTime() + "-" + new Random().nextInt(1000) - def LeaderBoardThread = Thread.start() { - if (useStreamingEngine) { - t.run(mobileGamingCommands.createPipelineCommand( - "LeaderBoardWithStreamingEngine", runner, jobName, "LeaderBoard")) - } else { - t.run(mobileGamingCommands.createPipelineCommand("LeaderBoard", runner, jobName)) - } - } + def leaderBoardProcess = useStreamingEngine ? + t.runBackground(mobileGamingCommands.createPipelineCommand( + "LeaderBoardWithStreamingEngine", runner, jobName, "LeaderBoard")) : + t.runBackground(mobileGamingCommands.createPipelineCommand("LeaderBoard", runner, jobName)) t.run("gcloud dataflow jobs list | grep pyflow-wordstream-candidate | grep Running | cut -d' ' -f1") @@ -175,8 +191,8 @@ class LeaderBoardRunner { println "Waiting for pipeline to produce more results..." sleep(60000) // wait for 1 min } - InjectorThread.stop() - LeaderBoardThread.stop() + t.stopProcess(injectorProcess) + t.stopProcess(leaderBoardProcess) t.run("""RUNNING_JOB=`gcloud dataflow jobs list | grep ${jobName} | grep Running | cut -d' ' -f1` if [ ! -z "\${RUNNING_JOB}" ] then @@ -202,10 +218,17 @@ fi // It will take couple seconds to clean up tables. // This loop makes sure tables are completely deleted before running the pipeline - tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") - while (tables.contains(userTable) || tables.contains(teamTable)) { - sleep(3000) + deleted = false + for (int i = 0; i < retries; i++) { tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") + if (!tables.contains(userTable) && !tables.contains(teamTable)) { + deleted = true + break + } + sleep(3000) + } + if (!deleted) { + println "Warning: Timed out waiting for tables ${userTable} / ${teamTable} to be deleted." } } } diff --git a/release/src/main/groovy/mobilegaming-java-dataflowbom.groovy b/release/src/main/groovy/mobilegaming-java-dataflowbom.groovy index e156357c19dc..b3f2adea1f63 100644 --- a/release/src/main/groovy/mobilegaming-java-dataflowbom.groovy +++ b/release/src/main/groovy/mobilegaming-java-dataflowbom.groovy @@ -45,7 +45,7 @@ int waitTime = 15 // seconds def outputPath = "gs://${t.gcsBucket()}/${mobileGamingCommands.getUserScoreOutputName(runner)}" def outputFound = false for (int i = 0; i < retries; i++) { - def files = t.run("gsutil ls ${outputPath}*") + def files = t.run("gcloud storage ls ${outputPath}*") if (files?.trim()) { outputFound = true break @@ -58,10 +58,10 @@ if (!outputFound) { throw new RuntimeException("No output files found for UserScore after ${retries * waitTime} seconds.") } -command_output_text = t.run "gsutil cat ${outputPath}* | grep user19_BananaWallaby" +command_output_text = t.run "gcloud storage cat ${outputPath}* | grep user19_BananaWallaby" t.see "total_score: 231, user: user19_BananaWallaby", command_output_text t.success("UserScore successfully run on DataflowRunner.") -t.run "gsutil rm gs://${t.gcsBucket()}/${mobileGamingCommands.getUserScoreOutputName(runner)}*" +t.run "gcloud storage rm gs://${t.gcsBucket()}/${mobileGamingCommands.getUserScoreOutputName(runner)}*" /** @@ -76,7 +76,7 @@ t.run(mobileGamingCommands.createPipelineCommand("HourlyTeamScore", runner)) outputPath = "gs://${t.gcsBucket()}/${mobileGamingCommands.getHourlyTeamScoreOutputName(runner)}" outputFound = false for (int i = 0; i < retries; i++) { - def files = t.run("gsutil ls ${outputPath}*") + def files = t.run("gcloud storage ls ${outputPath}*") if (files?.trim()) { outputFound = true break @@ -89,10 +89,10 @@ if (!outputFound) { throw new RuntimeException("No output files found for HourlyTeamScore after ${retries * waitTime} seconds.") } -command_output_text = t.run "gsutil cat ${outputPath}* | grep AzureBilby " +command_output_text = t.run "gcloud storage cat ${outputPath}* | grep AzureBilby " t.see "total_score: 2788, team: AzureBilby", command_output_text t.success("HourlyTeamScore successfully run on DataflowRunner.") -t.run "gsutil rm gs://${t.gcsBucket()}/${mobileGamingCommands.getHourlyTeamScoreOutputName(runner)}*" +t.run "gcloud storage rm gs://${t.gcsBucket()}/${mobileGamingCommands.getHourlyTeamScoreOutputName(runner)}*" new LeaderBoardRunner().run(runner, t, mobileGamingCommands, false) new LeaderBoardRunner().run(runner, t, mobileGamingCommands, true) diff --git a/release/src/main/groovy/mobilegaming-java-direct.groovy b/release/src/main/groovy/mobilegaming-java-direct.groovy index 34eab4c00768..398822a9a2ce 100644 --- a/release/src/main/groovy/mobilegaming-java-direct.groovy +++ b/release/src/main/groovy/mobilegaming-java-direct.groovy @@ -80,32 +80,51 @@ def teamSchema = [ ].join(",") String tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") +if (tables.contains(userTable)) { + t.run("bq rm -f -t ${dataset}.${userTable}") +} +if (tables.contains(teamTable)) { + t.run("bq rm -f -t ${dataset}.${teamTable}") +} -if (!tables.contains(userTable)) { - t.intent("Creating table: ${userTable}") - t.run("bq mk --table ${dataset}.${userTable} ${userSchema}") +int retries = 10 +boolean deleted = false +for (int i = 0; i < retries; i++) { + tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") + if (!tables.contains(userTable) && !tables.contains(teamTable)) { + deleted = true + break + } + sleep(3000) } -if (!tables.contains(teamTable)) { - t.intent("Creating table: ${teamTable}") - t.run("bq mk --table ${dataset}.${teamTable} ${teamSchema}") +if (!deleted) { + t.error("Timed out waiting for tables ${userTable} / ${teamTable} to be deleted.") } +t.intent("Creating table: ${userTable}") +t.run("bq mk --table ${dataset}.${userTable} ${userSchema}") +t.intent("Creating table: ${teamTable}") +t.run("bq mk --table ${dataset}.${teamTable} ${teamSchema}") + // Verify that the tables have been created -tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") -while (!tables.contains(userTable) || !tables.contains(teamTable)) { - sleep(3000) +boolean created = false +for (int i = 0; i < retries; i++) { tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") + if (tables.contains(userTable) && tables.contains(teamTable)) { + created = true + break + } + sleep(3000) +} +if (!created) { + t.error("Timed out waiting for tables ${userTable} / ${teamTable} to be created.") } println "Tables ${userTable} and ${teamTable} created successfully." -def InjectorThread = Thread.start() { - t.run(mobileGamingCommands.createInjectorCommand()) -} +def injectorProcess = t.runBackground(mobileGamingCommands.createInjectorCommand()) jobName = "leaderboard-validation-" + new Date().getTime() + "-" + new Random().nextInt(1000) -def LeaderBoardThread = Thread.start() { - t.run(mobileGamingCommands.createPipelineCommand("LeaderBoard", runner, jobName)) -} +def leaderBoardProcess = t.runBackground(mobileGamingCommands.createPipelineCommand("LeaderBoard", runner, jobName)) // verify outputs in BQ tables def startTime = System.currentTimeMillis() @@ -128,12 +147,32 @@ while ((System.currentTimeMillis() - startTime)/60000 < mobileGamingCommands.EXE println "Waiting for pipeline to produce more results..." sleep(60000) // wait for 1 min } -InjectorThread.stop() -LeaderBoardThread.stop() +t.stopProcess(injectorProcess) +t.stopProcess(leaderBoardProcess) if(!isSuccess){ t.error("FAILED: Failed running LeaderBoard on DirectRunner") } t.success("LeaderBoard successfully run on DirectRunner.") +tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") +if (tables.contains(userTable)) { + t.run("bq rm -f -t ${dataset}.${userTable}") +} +if (tables.contains(teamTable)) { + t.run("bq rm -f -t ${dataset}.${teamTable}") +} +deleted = false +for (int i = 0; i < retries; i++) { + tables = t.run("bq query --use_legacy_sql=false 'SELECT table_name FROM ${dataset}.INFORMATION_SCHEMA.TABLES'") + if (!tables.contains(userTable) && !tables.contains(teamTable)) { + deleted = true + break + } + sleep(3000) +} +if (!deleted) { + println "Warning: Timed out waiting for tables ${userTable} / ${teamTable} to be deleted." +} + t.done() diff --git a/release/src/main/groovy/quickstart-java-dataflow.groovy b/release/src/main/groovy/quickstart-java-dataflow.groovy index fb84fedca0b3..5776185f8f11 100644 --- a/release/src/main/groovy/quickstart-java-dataflow.groovy +++ b/release/src/main/groovy/quickstart-java-dataflow.groovy @@ -31,7 +31,7 @@ t.describe 'Run Apache Beam Java SDK Quickstart - Dataflow' t.intent 'Runs the WordCount Code with Dataflow runner' // Remove any count files - t.run """gsutil rm gs://${t.gcsBucket()}/count* || echo 'No files'""" + t.run """gcloud storage rm gs://${t.gcsBucket()}/count* || echo 'No files'""" // Run the wordcount example with the Dataflow runner t.run """mvn compile exec:java -q \ @@ -54,7 +54,7 @@ t.describe 'Run Apache Beam Java SDK Quickstart - Dataflow' def outputPath = "gs://${t.gcsBucket()}/count" def outputFound = false for (int i = 0; i < retries; i++) { - def files = t.run("gsutil ls ${outputPath}*") + def files = t.run("gcloud storage ls ${outputPath}*") if (files?.trim()) { outputFound = true break @@ -68,11 +68,11 @@ t.describe 'Run Apache Beam Java SDK Quickstart - Dataflow' } // Verify wordcount text - String result = t.run """gsutil cat ${outputPath}* | grep Montague:""" + String result = t.run """gcloud storage cat ${outputPath}* | grep Montague:""" t.see "Montague: 47", result // Remove count files - t.run """gsutil rm gs://${t.gcsBucket()}/count*""" + t.run """gcloud storage rm gs://${t.gcsBucket()}/count*""" // Clean up t.done() diff --git a/release/src/main/groovy/quickstart-java-flinklocal.groovy b/release/src/main/groovy/quickstart-java-flinklocal.groovy index 36c6ddd38354..3cd59270c04d 100644 --- a/release/src/main/groovy/quickstart-java-flinklocal.groovy +++ b/release/src/main/groovy/quickstart-java-flinklocal.groovy @@ -41,9 +41,9 @@ t.describe 'Run Apache Beam Java SDK Quickstart - Flink Local' -Dhttp.keepAlive=false \ -Pflink-runner""" - def cp = "target/classes:${deps}" + def cp = "target/classes:${deps.trim()}" t.run """mvn exec:exec -q -Dexec.executable=java \ - -Dexec.args="-cp ${cp} org.apache.beam.examples.WordCount \ + -Dexec.args="-cp '${cp}' org.apache.beam.examples.WordCount \ --inputFile=pom.xml --output=counts --runner=FlinkRunner" """ // Verify text from the pom.xml input file diff --git a/release/src/main/groovy/quickstart-java-spark.groovy b/release/src/main/groovy/quickstart-java-spark.groovy index 3c5be754daab..248e85dbcc59 100644 --- a/release/src/main/groovy/quickstart-java-spark.groovy +++ b/release/src/main/groovy/quickstart-java-spark.groovy @@ -30,10 +30,22 @@ t.describe 'Run Apache Beam Java SDK Quickstart - Spark' t.intent 'Runs the WordCount Code with Spark runner' // Run the wordcount example with the spark runner - t.run """mvn compile exec:java -q \ - -Dexec.mainClass=org.apache.beam.examples.WordCount \ - -Dexec.args="--inputFile=pom.xml --output=counts \ - --runner=SparkRunner" -Pspark-runner""" + + // Retrieve classpath + def deps = t.run """mvn compile dependency:build-classpath -q \ + -Dmdep.outputFile=/dev/stdout \ + -Dmaven.wagon.http.retryHandler.class=default \ + -Dmaven.wagon.http.retryHandler.count=5 \ + -Dmaven.wagon.http.pool=false \ + -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 \ + -Dhttp.keepAlive=false \ + -Pspark-runner""" + + def cp = "target/classes:${deps.trim()}" + def 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 --add-opens=java.base/java.lang=ALL-UNNAMED" + t.run """mvn exec:exec -q -Dexec.executable=java \ + -Dexec.args="${jvmArgs} -cp '${cp}' org.apache.beam.examples.WordCount \ + --inputFile=pom.xml --output=counts --runner=SparkRunner" """ // Verify text from the pom.xml input file String result = t.run "grep Foundation counts*" diff --git a/release/src/main/python-release/python_release_automation_utils.sh b/release/src/main/python-release/python_release_automation_utils.sh index 337ece8ba643..3884b02c40f5 100644 --- a/release/src/main/python-release/python_release_automation_utils.sh +++ b/release/src/main/python-release/python_release_automation_utils.sh @@ -82,23 +82,25 @@ function get_version() { ####################################### function download_files() { if [[ $1 = *"wheel"* ]]; then - if [[ $2 == "python3.7" ]]; then - BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - elif [[ $2 == "python3.8" ]]; then - BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - elif [[ $2 == "python3.9" ]]; then - BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - elif [[ $2 == "python3.10" ]]; then - BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + if [[ $2 == "python3.10" ]]; then + BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + elif [[ $2 == "python3.11" ]]; then + BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + elif [[ $2 == "python3.12" ]]; then + BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + elif [[ $2 == "python3.13" ]]; then + BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + elif [[ $2 == "python3.14" ]]; then + BEAM_PYTHON_SDK_WHL="apache_beam-$VERSION*-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" else echo "Unable to determine a Beam wheel for interpreter version $2." exit 1 fi - wget -r -l2 --no-parent -nd -A "$BEAM_PYTHON_SDK_WHL*" $RC_STAGING_URL + wget -e robots=off -r -l2 --no-parent -nd -A "$BEAM_PYTHON_SDK_WHL*" "$RC_STAGING_URL" else - BEAM_PYTHON_SDK_ZIP="apache-beam-$VERSION.tar.gz" - wget -r -l2 --no-parent -nd -A "$BEAM_PYTHON_SDK_ZIP*" $RC_STAGING_URL + BEAM_PYTHON_SDK_ZIP="apache_beam-$VERSION.tar.gz" + wget -e robots=off -r -l2 --no-parent -nd -A "$BEAM_PYTHON_SDK_ZIP*" "$RC_STAGING_URL" fi } @@ -158,7 +160,7 @@ function get_asc_name() { function install_sdk() { sdk_file=$(get_sdk_name $1) print_separator "Creating new virtualenv with $2 interpreter and installing the SDK from $sdk_file." - gsutil version -l + gcloud version rm -rf ./temp_virtualenv_${2} $2 -m venv temp_virtualenv_${2} . ./temp_virtualenv_${2}/bin/activate @@ -267,7 +269,7 @@ function verify_user_score() { expected_output_file_name="$USERSCORE_OUTPUT_PREFIX-$1-runner.txt" actual_output_files=$(ls) if [[ $1 = *"dataflow"* ]]; then - actual_output_files=$(gsutil ls gs://$BUCKET_NAME) + actual_output_files=$(gcloud storage ls gs://$BUCKET_NAME) expected_output_file_name="gs://$BUCKET_NAME/$expected_output_file_name" fi echo $actual_output_files @@ -279,7 +281,7 @@ function verify_user_score() { fi if [[ $1 = *"dataflow"* ]]; then - gsutil rm $expected_output_file_name* + gcloud storage rm $expected_output_file_name* fi echo "SUCCEED: user_score successfully run on $1-runner." } diff --git a/release/src/main/python-release/run_release_candidate_python_quickstart.sh b/release/src/main/python-release/run_release_candidate_python_quickstart.sh index 3af527acfa2e..1cc815bcc412 100755 --- a/release/src/main/python-release/run_release_candidate_python_quickstart.sh +++ b/release/src/main/python-release/run_release_candidate_python_quickstart.sh @@ -75,7 +75,7 @@ function verify_hash() { wget https://dist.apache.org/repos/dist/dev/beam/KEYS gpg --import KEYS gpg --verify $ASC_FILE_NAME $BEAM_PYTHON_SDK - gsutil version -l + gcloud version } @@ -122,7 +122,7 @@ function verify_wordcount_dataflow() { # verify results. wordcount_output_in_gcs="gs://$BUCKET_NAME/$WORDCOUNT_OUTPUT" - gcs_pull_result=$(gsutil ls gs://$BUCKET_NAME) + gcs_pull_result=$(gcloud storage ls gs://$BUCKET_NAME) if [[ $gcs_pull_result != *$wordcount_output_in_gcs* ]]; then echo "ERROR: The wordcount example failed on DataflowRunner". complete "failed when running wordcount example with DataflowRunner." @@ -130,7 +130,7 @@ function verify_wordcount_dataflow() { fi # clean output files from GCS - gsutil rm gs://$BUCKET_NAME/$WORDCOUNT_OUTPUT-* + gcloud storage rm gs://$BUCKET_NAME/$WORDCOUNT_OUTPUT-* echo "SUCCEED: wordcount successfully run on DataflowRunner." } diff --git a/runners/core-java/build.gradle b/runners/core-java/build.gradle index 403cf4f2bc5a..cafb57c1552a 100644 --- a/runners/core-java/build.gradle +++ b/runners/core-java/build.gradle @@ -49,6 +49,7 @@ dependencies { implementation library.java.slf4j_api implementation library.java.jackson_core implementation library.java.jackson_databind + implementation library.java.opentelemetry_context implementation library.java.hamcrest testImplementation project(path: ":sdks:java:core", configuration: "shadowTest") testImplementation library.java.junit diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/DoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/DoFnRunner.java index 500aedecb5d6..d1278eddaef0 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/DoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/DoFnRunner.java @@ -63,6 +63,18 @@ public interface DoFnRunner void onWindowExpiration( BoundedWindow window, Instant timestamp, KeyT key); + /** + * Performs per-key cleanup or processing after all elements, timers for a key have been processed + * and before moving to the next key or before finishBundle for the last key. + * + *

This is an optional method that can be used by runners as a hook to reset any per key state + * before moving to a different key in the same bundle. Currently used only by the Dataflow + * Streaming runner. + * + * @param key current key to clean up or finish processing + */ + void finishKey(KeyT key); + /** * Returns the underlying fn instance. * diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/DoFnRunners.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/DoFnRunners.java index 2cab758d36e0..0608ea24b049 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/DoFnRunners.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/DoFnRunners.java @@ -85,9 +85,9 @@ public static DoFnRunner simpleRunner( public static DoFnRunner, KV> lateDataDroppingRunner( DoFnRunner, KV> wrappedRunner, - TimerInternals timerInternals, + StepContext stepContext, WindowingStrategy windowingStrategy) { - return new LateDataDroppingDoFnRunner<>(wrappedRunner, windowingStrategy, timerInternals); + return new LateDataDroppingDoFnRunner<>(wrappedRunner, windowingStrategy, stepContext); } /** diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/GroupAlsoByWindowViaWindowSetNewDoFn.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/GroupAlsoByWindowViaWindowSetNewDoFn.java index f242c7d10003..349e109930f0 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/GroupAlsoByWindowViaWindowSetNewDoFn.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/GroupAlsoByWindowViaWindowSetNewDoFn.java @@ -109,7 +109,7 @@ public void processElement(ProcessContext c) throws Exception { reduceFn, c.getPipelineOptions()); - reduceFnRunner.processElements(keyedWorkItem.elementsIterable()); + reduceFnRunner.processElements(keyedWorkItem); reduceFnRunner.onTimers(keyedWorkItem.timersIterable()); reduceFnRunner.persist(); } diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/KeyedWorkItem.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/KeyedWorkItem.java index 4901c5cbed5b..2be8b0790301 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/KeyedWorkItem.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/KeyedWorkItem.java @@ -35,4 +35,13 @@ public interface KeyedWorkItem { /** Returns an iterable containing the elements. */ Iterable> elementsIterable(); + + /** + * Returns an iterable containing windowed values without guaranteeing element payload decoding. + * Useful for lightweight inspection of windowing metadata without payload deserialization + * overhead. + */ + default Iterable> elementWindowsIterable() { + return (Iterable) elementsIterable(); + } } 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 41052a76f13e..1d5908c24975 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 @@ -31,6 +31,7 @@ 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.annotations.VisibleForTesting; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** @@ -57,9 +58,9 @@ public class LateDataDroppingDoFnRunner, KV> doFnRunner, WindowingStrategy windowingStrategy, - TimerInternals timerInternals) { + StepContext stepContext) { this.doFnRunner = doFnRunner; - lateDataFilter = new LateDataFilter(windowingStrategy, timerInternals); + lateDataFilter = new LateDataFilter(windowingStrategy, stepContext); } @Override @@ -101,6 +102,11 @@ public void finishBundle() { doFnRunner.finishBundle(); } + @Override + public void finishKey(KeyT key) { + doFnRunner.finishKey(key); + } + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { doFnRunner.onWindowExpiration(window, timestamp, key); @@ -110,13 +116,12 @@ public void onWindowExpiration(BoundedWindow window, Instant timestamp, K @VisibleForTesting static class LateDataFilter { private final WindowingStrategy windowingStrategy; - private final TimerInternals timerInternals; + private final StepContext stepContext; private final Counter droppedDueToLateness; - public LateDataFilter( - WindowingStrategy windowingStrategy, TimerInternals timerInternals) { + public LateDataFilter(WindowingStrategy windowingStrategy, StepContext stepContext) { this.windowingStrategy = windowingStrategy; - this.timerInternals = timerInternals; + this.stepContext = stepContext; this.droppedDueToLateness = Metrics.counter(LateDataDroppingDoFnRunner.class, DROPPED_DUE_TO_LATENESS); } @@ -140,8 +145,8 @@ public Iterable> filter( element.getTimestamp(), key, window, - timerInternals.currentInputWatermarkTime(), - timerInternals.currentOutputWatermarkTime()); + stepContext.timerInternals().currentInputWatermarkTime(), + stepContext.timerInternals().currentOutputWatermarkTime()); } else { nonLateElements.add( WindowedValues.of( @@ -167,7 +172,7 @@ public Iterable> filter( * @return True if element can be dropped. */ private boolean canDropDueToExpiredWindow(BoundedWindow window) { - Instant inputWM = timerInternals.currentInputWatermarkTime(); + Instant inputWM = stepContext.timerInternals().currentInputWatermarkTime(); return LateDataUtils.garbageCollectionTime(window, windowingStrategy).isBefore(inputWM); } } 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..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; @@ -130,114 +131,119 @@ 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 ValueKind valueKind(DoFn doFn) { + return processContext.valueKind(); + } + + @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 +284,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 @@ -407,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/ReduceFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java index 1ae0c52f853a..e49c858393f4 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; @@ -59,6 +60,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.collect.FluentIterable; 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.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; @@ -94,6 +96,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 +225,9 @@ public class ReduceFnRunner { */ private final NonEmptyPanes nonEmptyPanes; + private final boolean useNewWindowOptimization; + private final boolean disableWatermarkKnownEmptyOptimization; + public ReduceFnRunner( K key, WindowingStrategy windowingStrategy, @@ -244,6 +254,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 +282,8 @@ public ReduceFnRunner( new TriggerStateMachineRunner<>( triggerStateMachine, new TriggerStateMachineContextFactory<>( - windowingStrategy.getWindowFn(), stateInternals, activeWindows)); + windowingStrategy.getWindowFn(), stateInternals, activeWindows), + this.useNewWindowOptimization); } private ActiveWindowSet createActiveWindowSet() { @@ -282,6 +302,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) { @@ -332,13 +362,24 @@ private Collection windowsThatShouldFire(Set windows) throws Exception { * setting holds, and invoking {@link ReduceFn#onTrigger}. * */ + public void processElements(KeyedWorkItem keyedWorkItem) throws Exception { + processElementsInternal( + keyedWorkItem.elementWindowsIterable(), keyedWorkItem.elementsIterable()); + } + public void processElements(Iterable> values) throws Exception { - if (!values.iterator().hasNext()) { + processElementsInternal(values, values); + } + + private void processElementsInternal( + Iterable> elementWindows, Iterable> values) + throws Exception { + if (Iterables.isEmpty(elementWindows)) { return; } // Determine all the windows for elements. - Set windows = collectWindows(values); + Set windows = collectWindows(elementWindows); // If an incoming element introduces a new window, attempt to merge it into an existing // window eagerly. Map windowToMergeResult = mergeWindows(windows); @@ -397,7 +438,7 @@ public void persist() { } /** Extract the windows associated with the values. */ - private Set collectWindows(Iterable> values) throws Exception { + private Set collectWindows(Iterable> values) throws Exception { Set windows = new HashSet<>(); for (WindowedValue value : values) { for (BoundedWindow untypedWindow : value.getWindows()) { @@ -626,6 +667,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 +816,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 +996,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); } @@ -1013,7 +1068,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()); } /** 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..7f9cbf15e00d 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 @@ -21,6 +21,8 @@ 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 io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -61,6 +63,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; @@ -183,6 +186,17 @@ public void startBundle() { @Override public void processElement(WindowedValue compressedElem) { + Context openTelemetryContext = compressedElem.getOpenTelemetryContext(); + if (openTelemetryContext == null) { + processElementInternal(compressedElem); + } else { + try (Scope ignore = openTelemetryContext.makeCurrent()) { + processElementInternal(compressedElem); + } + } + } + + private void processElementInternal(WindowedValue compressedElem) { if (observesWindow) { for (WindowedValue elem : compressedElem.explodeWindows()) { invokeProcessElement(elem); @@ -229,6 +243,9 @@ public void finishBundle() { } } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { invoker.invokeOnWindowExpiration( @@ -506,6 +523,11 @@ public Instant timestamp() { return elem.getRecordOffset(); } + @Override + public ValueKind valueKind() { + return elem.getValueKind(); + } + public Collection windows() { return elem.getWindows(); } @@ -597,6 +619,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( @@ -638,6 +665,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."); @@ -859,8 +893,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 @@ -895,6 +938,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; @@ -933,6 +981,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."); @@ -1180,8 +1235,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 @@ -1228,6 +1292,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; @@ -1260,6 +1329,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/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..4eea2c0509dd 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; } @@ -463,8 +469,9 @@ public String getErrorContext() { restrictionState.readLater(); watermarkEstimatorState.readLater(); WindowedValue read = elementState.read(); + RestrictionT restriction = restrictionState.read(); if (timer.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN) { - read = + WindowedValue drainRead = WindowedValues.of( read.getValue(), read.getTimestamp(), @@ -475,8 +482,73 @@ public String getErrorContext() { CausedByDrain.CAUSED_BY_DRAIN, read.getOpenTelemetryContext(), read.getValueKind()); + RestrictionTracker.TruncateResult truncateResult = + invoker.invokeTruncateRestriction( + new BaseArgumentProvider() { + @Override + public InputT element(DoFn doFn) { + return drainRead.getValue(); + } + + @Override + public Object restriction() { + return restriction; + } + + @Override + public RestrictionTracker restrictionTracker() { + return invoker.invokeNewTracker(this); + } + + @Override + public Instant timestamp(DoFn doFn) { + return drainRead.getTimestamp(); + } + + @Override + public PipelineOptions pipelineOptions() { + return c.getPipelineOptions(); + } + + @Override + public PaneInfo paneInfo(DoFn doFn) { + return drainRead.getPaneInfo(); + } + + @Override + public BoundedWindow window() { + return Iterables.getOnlyElement(drainRead.getWindows()); + } + + @Override + public Object sideInput(String tagId) { + PCollectionView view = sideInputMapping.get(tagId); + if (view == null) { + throw new IllegalArgumentException( + "calling getSideInput() with unknown view"); + } + return sideInputReader.get( + view, view.getWindowMappingFn().getSideInputWindow(window())); + } + + @Override + public String getErrorContext() { + return ProcessFn.class.getSimpleName() + ".invokeTruncateRestriction"; + } + }); + if (truncateResult == null) { + elementState.clear(); + restrictionState.clear(); + watermarkEstimatorState.clear(); + holdState.clear(); + return; + } + RestrictionT truncatedRestriction = truncateResult.getTruncatedRestriction(); + elementAndRestriction = KV.of(drainRead, truncatedRestriction); + restrictionState.write(truncatedRestriction); + } else { + elementAndRestriction = KV.of(read, restriction); } - elementAndRestriction = KV.of(read, restrictionState.read()); watermarkEstimatorStateT = watermarkEstimatorState.read(); } @@ -624,6 +696,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/StatefulDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/StatefulDoFnRunner.java index 779138834669..f5de79652f23 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/StatefulDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/StatefulDoFnRunner.java @@ -43,6 +43,7 @@ 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.base.MoreObjects; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; @@ -131,6 +132,11 @@ public void finishBundle() { doFnRunner.finishBundle(); } + @Override + public void finishKey(KeyT key) { + doFnRunner.finishKey(key); + } + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { doFnRunner.onWindowExpiration(window, timestamp, key); 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/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/construction/package-info.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/construction/package-info.java index 45df64f968f9..a42940bbbc36 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/construction/package-info.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/construction/package-info.java @@ -17,8 +17,4 @@ */ /** Internal package related to runner construction. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.runners.core.construction; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/metrics/package-info.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/metrics/package-info.java index 523c612ad586..5cd4ef1b996e 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/metrics/package-info.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/metrics/package-info.java @@ -17,8 +17,4 @@ */ /** Utilities for runners to implement metrics. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.runners.core.metrics; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/package-info.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/package-info.java index ec674ff82422..10cb4d6f61bb 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/package-info.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/package-info.java @@ -17,8 +17,4 @@ */ /** Provides utilities for Beam runner authors. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.runners.core; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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/main/java/org/apache/beam/runners/core/triggers/package-info.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/package-info.java index af0c4d776b08..b1218616c7a4 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/package-info.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/package-info.java @@ -17,8 +17,4 @@ */ /** State machine implementations for triggers, called "triggers" because they react to events. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.runners.core.triggers; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunnerTest.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunnerTest.java index 5a6257ba0a1b..3ce098ee05ea 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunnerTest.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunnerTest.java @@ -49,6 +49,7 @@ public class LateDataDroppingDoFnRunnerTest { private static final FixedWindows WINDOW_FN = FixedWindows.of(Duration.millis(10)); @Mock private TimerInternals mockTimerInternals; + @Mock private StepContext mockStepContext; @Before public void setUp() { @@ -60,9 +61,10 @@ public void testLateDataFilter() throws Exception { MetricsContainerImpl container = new MetricsContainerImpl("any"); MetricsEnvironment.setCurrentContainer(container); when(mockTimerInternals.currentInputWatermarkTime()).thenReturn(new Instant(15L)); + when(mockStepContext.timerInternals()).thenReturn(mockTimerInternals); LateDataFilter lateDataFilter = - new LateDataFilter(WindowingStrategy.of(WINDOW_FN), mockTimerInternals); + new LateDataFilter(WindowingStrategy.of(WINDOW_FN), mockStepContext); Iterable> actual = lateDataFilter.filter( 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/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..6ff3b99b8605 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 @@ -47,6 +47,7 @@ import org.apache.beam.sdk.state.TimeDomain; import org.apache.beam.sdk.transforms.Combine.CombineFn; import org.apache.beam.sdk.transforms.CombineWithContext.CombineFnWithContext; +import org.apache.beam.sdk.transforms.DoFn.BundleFinalizer; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; @@ -94,6 +95,23 @@ public class ReduceFnTester { private final TestInMemoryStateInternals stateInternals = new TestInMemoryStateInternals<>(KEY); private final InMemoryTimerInternals timerInternals = new InMemoryTimerInternals(); + private final StepContext stepContext = + new StepContext() { + @Override + public StateInternals stateInternals() { + return stateInternals; + } + + @Override + public TimerInternals timerInternals() { + return timerInternals; + } + + @Override + public BundleFinalizer bundleFinalizer() { + throw new UnsupportedOperationException(); + } + }; private final WindowFn windowFn; private final TestWindowedValueReceiver testOutputter; @@ -129,6 +147,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 @@ -564,7 +595,7 @@ public final void injectElements(List> values) throws E ReduceFnRunner runner = createRunner(); runner.processElements( - new LateDataDroppingDoFnRunner.LateDataFilter(objectStrategy, timerInternals) + new LateDataDroppingDoFnRunner.LateDataFilter(objectStrategy, stepContext) .filter(KEY, inputs)); // Persist after each bundle. diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/SimplePushbackSideInputDoFnRunnerTest.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/SimplePushbackSideInputDoFnRunnerTest.java index 1ae937b7a836..aa61122a7547 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/SimplePushbackSideInputDoFnRunnerTest.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/SimplePushbackSideInputDoFnRunnerTest.java @@ -65,6 +65,7 @@ import org.apache.beam.sdk.values.WindowingStrategy; 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.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; import org.junit.Before; @@ -379,6 +380,9 @@ public void finishBundle() { finished = true; } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) {} } 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..247412a85dc4 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 @@ -36,6 +36,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.Executors; import org.apache.beam.runners.core.SplittableParDoViaKeyedWorkItems.ProcessFn; @@ -48,12 +49,15 @@ import org.apache.beam.sdk.state.TimeDomain; import org.apache.beam.sdk.testing.ResetDateTimeProvider; 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.DoFnTester; +import org.apache.beam.sdk.transforms.View; import org.apache.beam.sdk.transforms.splittabledofn.HasDefaultTracker; import org.apache.beam.sdk.transforms.splittabledofn.ManualWatermarkEstimator; import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker; import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker.IsBounded; import org.apache.beam.sdk.transforms.splittabledofn.SplitResult; import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimators; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; @@ -140,6 +144,8 @@ private static class ProcessFnTester< private InMemoryTimerInternals timerInternals; private TestInMemoryStateInternals stateInternals; private InMemoryBundleFinalizer bundleFinalizer; + private final ProcessFn + processFn; ProcessFnTester( Instant currentProcessingTime, @@ -150,47 +156,54 @@ private static class ProcessFnTester< int maxOutputsPerBundle, Duration maxBundleDuration) throws Exception { + this( + currentProcessingTime, + fn, + inputCoder, + restrictionCoder, + watermarkEstimatorStateCoder, + maxOutputsPerBundle, + maxBundleDuration, + Collections.emptyMap(), + NullSideInputReader.empty()); + } + + ProcessFnTester( + Instant currentProcessingTime, + final DoFn fn, + Coder inputCoder, + Coder restrictionCoder, + Coder watermarkEstimatorStateCoder, + int maxOutputsPerBundle, + Duration maxBundleDuration, + Map> sideInputMapping, + SideInputReader sideInputReader) + throws Exception { // The exact windowing strategy doesn't matter in this test, but it should be able to // 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, + sideInputMapping); this.tester = DoFnTester.of(processFn); this.timerInternals = new InMemoryTimerInternals(); this.stateInternals = new TestInMemoryStateInternals<>("dummy"); processFn.setStateInternalsFactory(key -> stateInternals); processFn.setTimerInternalsFactory(key -> timerInternals); - processFn.setSideInputReader(NullSideInputReader.empty()); + processFn.setSideInputReader(sideInputReader); processFn.setProcessElementInvoker( new OutputAndTimeBoundedSplittableProcessElementInvoker<>( fn, tester.getPipelineOptions(), new DoFnTesterWindowedValueReceiver(tester), tester.getMainOutputTag(), - new SideInputReader() { - @Override - public T get(PCollectionView view, BoundedWindow window) { - throw new NoSuchElementException(); - } - - @Override - public boolean contains(PCollectionView view) { - return false; - } - - @Override - public boolean isEmpty() { - return true; - } - }, + sideInputReader, Executors.newSingleThreadScheduledExecutor(Executors.defaultThreadFactory()), maxOutputsPerBundle, maxBundleDuration, @@ -386,6 +399,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 +752,289 @@ 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); + } + } + + private static class TruncateFn extends DoFn { + private final boolean truncateToNull; + private final List calls = new ArrayList<>(); + + public TruncateFn(boolean truncateToNull) { + this.truncateToNull = truncateToNull; + } + + @ProcessElement + public ProcessContinuation process( + ProcessContext c, RestrictionTracker tracker) { + for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) { + c.output(c.element() + ":" + 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); + } + + @TruncateRestriction + public RestrictionTracker.TruncateResult truncate( + @Restriction OffsetRange restriction, @Element Integer element) { + calls.add("truncate:" + element + ":" + restriction); + if (truncateToNull) { + return null; + } + // Truncate so that we only process one more element. + return RestrictionTracker.TruncateResult.of( + new OffsetRange(restriction.getFrom(), restriction.getFrom() + 1)); + } + } + + @Test + public void testTruncateRestrictionOnDrain() throws Exception { + TruncateFn fn = new TruncateFn(false); + Instant base = Instant.now(); + + try (ProcessFnTester tester = + new ProcessFnTester<>( + base, + fn, + BigEndianIntegerCoder.of(), + SerializableCoder.of(OffsetRange.class), + VoidCoder.of(), + MAX_OUTPUTS_PER_BUNDLE, + MAX_BUNDLE_DURATION)) { + tester.startElement(42, new OffsetRange(0, 10)); + assertThat(tester.takeOutputElements(), contains("42:0", "42:1", "42:2")); + + assertTrue(tester.advanceDrain()); + assertThat(tester.takeOutputElements(), contains("42:3")); + assertEquals(Collections.singletonList("truncate:42:[3, 10)"), fn.calls); + assertEquals(null, tester.getWatermarkHold()); + } + } + + @Test + public void testTruncateRestrictionReturnsNullOnDrain() throws Exception { + TruncateFn fn = new TruncateFn(true); + Instant base = Instant.now(); + + try (ProcessFnTester tester = + new ProcessFnTester<>( + base, + fn, + BigEndianIntegerCoder.of(), + SerializableCoder.of(OffsetRange.class), + VoidCoder.of(), + MAX_OUTPUTS_PER_BUNDLE, + MAX_BUNDLE_DURATION)) { + tester.startElement(42, new OffsetRange(0, 10)); + assertThat(tester.takeOutputElements(), contains("42:0", "42:1", "42:2")); + + assertTrue(tester.advanceDrain()); + assertTrue(tester.takeOutputElements().isEmpty()); + assertEquals(Collections.singletonList("truncate:42:[3, 10)"), fn.calls); + assertEquals(null, tester.getWatermarkHold()); + } + } + + private static class TruncateWithSideInputFn extends DoFn { + private final List calls = new ArrayList<>(); + + @ProcessElement + public ProcessContinuation process( + ProcessContext c, RestrictionTracker tracker) { + for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) { + c.output(c.element() + ":" + 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); + } + + @TruncateRestriction + public RestrictionTracker.TruncateResult truncate( + @Restriction OffsetRange restriction, + @Element Integer element, + @SideInput("sideInput") String sideInput) { + calls.add("truncate:" + element + ":" + sideInput + ":" + restriction); + return RestrictionTracker.TruncateResult.of( + new OffsetRange(restriction.getFrom(), restriction.getFrom() + 1)); + } + } + + @Test + public void testTruncateRestrictionWithSideInputOnDrain() throws Exception { + TruncateWithSideInputFn fn = new TruncateWithSideInputFn(); + Instant base = Instant.now(); + PCollectionView view = + TestPipeline.create().apply(Create.of("sideValue")).apply(View.asSingleton()); + Map> sideInputMapping = Collections.singletonMap("sideInput", view); + SideInputReader sideInputReader = + new SideInputReader() { + @Override + public T get(PCollectionView v, BoundedWindow window) { + if (v.equals(view)) { + return (T) "sideValue"; + } + throw new NoSuchElementException(); + } + + @Override + public boolean contains(PCollectionView v) { + return v.equals(view); + } + + @Override + public boolean isEmpty() { + return false; + } + }; + + try (ProcessFnTester tester = + new ProcessFnTester<>( + base, + fn, + BigEndianIntegerCoder.of(), + SerializableCoder.of(OffsetRange.class), + VoidCoder.of(), + MAX_OUTPUTS_PER_BUNDLE, + MAX_BUNDLE_DURATION, + sideInputMapping, + sideInputReader)) { + tester.startElement(42, new OffsetRange(0, 10)); + assertThat(tester.takeOutputElements(), contains("42:0", "42:1", "42:2")); + + assertTrue(tester.advanceDrain()); + assertThat(tester.takeOutputElements(), contains("42:3")); + assertEquals(Collections.singletonList("truncate:42:sideValue:[3, 10)"), fn.calls); + } + } + + private static class UnboundedOffsetRangeTracker extends OffsetRangeTracker { + public UnboundedOffsetRangeTracker(OffsetRange range) { + super(range); + } + + @Override + public IsBounded isBounded() { + return IsBounded.UNBOUNDED; + } + } + + // Tests that if we don't override TruncateRestriction, the default TruncateRestriction + // implementation is used (which for unbounded restrictions stops processing immediately). + private static class DefaultTruncateUnboundedFn extends DoFn { + @ProcessElement + public ProcessContinuation process( + ProcessContext c, RestrictionTracker tracker) { + for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) { + c.output(c.element() + ":" + i); + if (i == 2) { + return resume(); + } + } + return stop(); + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction() { + return new OffsetRange(0, 10); + } + + @NewTracker + public RestrictionTracker newTracker(@Restriction OffsetRange range) { + return new UnboundedOffsetRangeTracker(range); + } + } + + @Test + public void testDefaultTruncateRestrictionUnboundedStopsOnDrain() throws Exception { + DefaultTruncateUnboundedFn fn = new DefaultTruncateUnboundedFn(); + Instant base = Instant.now(); + + try (ProcessFnTester tester = + new ProcessFnTester<>( + base, + fn, + BigEndianIntegerCoder.of(), + SerializableCoder.of(OffsetRange.class), + VoidCoder.of(), + MAX_OUTPUTS_PER_BUNDLE, + MAX_BUNDLE_DURATION)) { + tester.startElement(42, new OffsetRange(0, 10)); + assertThat(tester.takeOutputElements(), contains("42:0", "42:1", "42:2")); + + assertTrue(tester.advanceDrain()); + assertTrue(tester.takeOutputElements().isEmpty()); + assertEquals(null, tester.getWatermarkHold()); + } + } } 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/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..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 @@ -348,17 +348,23 @@ private void shutdownIfNecessary(State newState) { } catch (final Exception e) { errors.add(e); } - pipelineState.compareAndSet(State.RUNNING, newState); // ensure we hit a terminal node - if (!errors.isEmpty()) { - final IllegalStateException exception = - new IllegalStateException( - "Error" - + (errors.size() == 1 ? "" : "s") - + " during executor shutdown:\n" - + errors.stream() - .map(Exception::getMessage) - .collect(Collectors.joining("\n- ", "- ", ""))); - visibleUpdates.failed(exception); + IllegalStateException exception = null; + try { + if (!errors.isEmpty()) { + exception = + new IllegalStateException( + "Error" + + (errors.size() == 1 ? "" : "s") + + " occurred during executor shutdown:\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 + } + if (exception != null) { throw exception; } } diff --git a/runners/direct-java/src/main/java/org/apache/beam/runners/direct/UnboundedReadEvaluatorFactory.java b/runners/direct-java/src/main/java/org/apache/beam/runners/direct/UnboundedReadEvaluatorFactory.java index e45990a760ea..c4a4fe86bf90 100644 --- a/runners/direct-java/src/main/java/org/apache/beam/runners/direct/UnboundedReadEvaluatorFactory.java +++ b/runners/direct-java/src/main/java/org/apache/beam/runners/direct/UnboundedReadEvaluatorFactory.java @@ -173,16 +173,17 @@ public void processElement( } else { Instant watermark = reader.getWatermark(); if (watermark.isBefore(BoundedWindow.TIMESTAMP_MAX_VALUE)) { - // If the reader had no elements available, but the shard is not done, reuse it later - // Might be better to finalize old checkpoint. + // If the reader had no elements available, but the shard is not done, reuse it later. + // Finalize old checkpoint now. + final CheckpointMarkT checkpoint = shard.getCheckpoint(); + if (checkpoint != null) { + checkpoint.finalizeCheckpoint(); + } resultBuilder.addUnprocessedElements( Collections.>singleton( WindowedValues.timestampedValueInGlobalWindow( UnboundedSourceShard.of( - shard.getSource(), - shard.getDeduplicator(), - reader, - shard.getCheckpoint()), + shard.getSource(), shard.getDeduplicator(), reader, null), watermark))); } else { // End of input. Close the reader after finalizing old checkpoint. diff --git a/runners/extensions-java/metrics/src/main/java/org/apache/beam/runners/extensions/metrics/package-info.java b/runners/extensions-java/metrics/src/main/java/org/apache/beam/runners/extensions/metrics/package-info.java index 00e6a5b3efeb..a09f72599d9a 100644 --- a/runners/extensions-java/metrics/src/main/java/org/apache/beam/runners/extensions/metrics/package-info.java +++ b/runners/extensions-java/metrics/src/main/java/org/apache/beam/runners/extensions/metrics/package-info.java @@ -16,8 +16,4 @@ * limitations under the License. */ /** Provides extensions to be used with the runners such as metrics sinks. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.runners.extensions.metrics; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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.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.19/build.gradle b/runners/flink/1.19/build.gradle index 1545da258477..4116b817eec4 100644 --- a/runners/flink/1.19/build.gradle +++ b/runners/flink/1.19/build.gradle @@ -23,3 +23,9 @@ project.ext { // Load the main build script which contains all build logic. apply from: "../flink_runner.gradle" + +// Flink 1.19 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/flink/1.19/job-server/build.gradle b/runners/flink/1.19/job-server/build.gradle index 332f04e08ceb..c9e09a5de8c4 100644 --- a/runners/flink/1.19/job-server/build.gradle +++ b/runners/flink/1.19/job-server/build.gradle @@ -29,3 +29,9 @@ project.ext { // Load the main build script which contains all build logic. apply from: "$basePath/flink_job_server.gradle" + +// Flink 1.19 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} 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/1.20/build.gradle b/runners/flink/1.20/build.gradle index 4c148321ed49..86a1cb7ef54d 100644 --- a/runners/flink/1.20/build.gradle +++ b/runners/flink/1.20/build.gradle @@ -23,3 +23,9 @@ project.ext { // Load the main build script which contains all build logic. apply from: "../flink_runner.gradle" + +// Flink 1.20 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/flink/1.20/job-server/build.gradle b/runners/flink/1.20/job-server/build.gradle index e5fdd1febf92..9f129b2c021c 100644 --- a/runners/flink/1.20/job-server/build.gradle +++ b/runners/flink/1.20/job-server/build.gradle @@ -29,3 +29,9 @@ project.ext { // Load the main build script which contains all build logic. apply from: "$basePath/flink_job_server.gradle" + +// Flink 1.20 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/flink/2.0/build.gradle b/runners/flink/2.0/build.gradle index 490bc593f40c..4e034173fb93 100644 --- a/runners/flink/2.0/build.gradle +++ b/runners/flink/2.0/build.gradle @@ -41,3 +41,9 @@ project.ext { // Load the main build script which contains all build logic. apply from: "../flink_runner.gradle" + +// Flink 2.0 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/flink/2.0/job-server/build.gradle b/runners/flink/2.0/job-server/build.gradle index 6d068f839491..ebac0a8e039c 100644 --- a/runners/flink/2.0/job-server/build.gradle +++ b/runners/flink/2.0/job-server/build.gradle @@ -29,3 +29,9 @@ project.ext { // Load the main build script which contains all build logic. apply from: "$basePath/flink_job_server.gradle" + +// Flink 2.0 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkStreamingPortablePipelineTranslator.java b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkStreamingPortablePipelineTranslator.java index 647297394f52..efffa4fdbc4a 100644 --- a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkStreamingPortablePipelineTranslator.java +++ b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkStreamingPortablePipelineTranslator.java @@ -733,7 +733,6 @@ private void translateStreamingImpulse( private void translateExecutableStage( String id, RunnerApi.Pipeline pipeline, StreamingTranslationContext context) { - // TODO: Fail on splittable DoFns. // TODO: Special-case single outputs to avoid multiplexing PCollections. RunnerApi.Components components = pipeline.getComponents(); RunnerApi.PTransform transform = components.getTransformsOrThrow(id); diff --git a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/translation/functions/FlinkExecutableStageFunction.java b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/translation/functions/FlinkExecutableStageFunction.java index 1298fd3105aa..3f8b5bdfe473 100644 --- a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/translation/functions/FlinkExecutableStageFunction.java +++ b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/translation/functions/FlinkExecutableStageFunction.java @@ -31,7 +31,6 @@ import org.apache.beam.runners.core.InMemoryStateInternals; import org.apache.beam.runners.core.InMemoryTimerInternals; import org.apache.beam.runners.core.StateInternals; -import org.apache.beam.runners.core.StateTags; import org.apache.beam.runners.core.TimerInternals; import org.apache.beam.runners.core.construction.SerializablePipelineOptions; import org.apache.beam.runners.flink.FlinkPipelineOptions; @@ -56,6 +55,7 @@ import org.apache.beam.sdk.fn.data.FnDataReceiver; import org.apache.beam.sdk.io.FileSystems; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.state.MapState; import org.apache.beam.sdk.transforms.join.RawUnionValue; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.util.construction.PTransformTranslation; @@ -284,12 +284,17 @@ public void mapPartition( List> residuals = new ArrayList<>(); TimerInternals.TimerData timer; while ((timer = sdfTimerInternals.removeNextProcessingTimer()) != null) { - WindowedValue stateValue = - sdfStateInternals - .state(timer.getNamespace(), StateTags.value(timer.getTimerId(), inputCoder)) - .read(); - - residuals.add(stateValue); + MapState> residualState = + sdfStateInternals.state( + timer.getNamespace(), + BundleCheckpointHandlers.StateAndTimerBundleCheckpointHandler.residualStateTag( + inputCoder)); + WindowedValue stateValue = residualState.get(timer.getTimerId()).read(); + residualState.remove(timer.getTimerId()); + // A pre-#27648 savepoint has no entry under the new tag, so the read is null; drop it. + if (stateValue != null) { + residuals.add(stateValue); + } } processElements(residuals, bundle); } diff --git a/runners/flink/2.1/build.gradle b/runners/flink/2.1/build.gradle new file mode 100644 index 000000000000..32c71eb96483 --- /dev/null +++ b/runners/flink/2.1/build.gradle @@ -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. + */ + +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" diff --git a/runners/flink/1.17/job-server-container/build.gradle b/runners/flink/2.1/job-server-container/build.gradle similarity index 100% rename from runners/flink/1.17/job-server-container/build.gradle rename to runners/flink/2.1/job-server-container/build.gradle diff --git a/runners/flink/1.17/job-server/build.gradle b/runners/flink/2.1/job-server/build.gradle similarity index 95% rename from runners/flink/1.17/job-server/build.gradle rename to runners/flink/2.1/job-server/build.gradle index 89915349ae9a..3135abcb19b7 100644 --- a/runners/flink/1.17/job-server/build.gradle +++ b/runners/flink/2.1/job-server/build.gradle @@ -24,7 +24,7 @@ project.ext { 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' + archives_base_name = 'beam-runners-flink-2.1-job-server' } // Load the main build script which contains all build logic. diff --git a/runners/flink/2.2/build.gradle b/runners/flink/2.2/build.gradle new file mode 100644 index 000000000000..1e1d7fdd5e75 --- /dev/null +++ b/runners/flink/2.2/build.gradle @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT 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" diff --git a/runners/flink/1.18/job-server-container/build.gradle b/runners/flink/2.2/job-server-container/build.gradle similarity index 100% rename from runners/flink/1.18/job-server-container/build.gradle rename to runners/flink/2.2/job-server-container/build.gradle diff --git a/runners/flink/1.18/job-server/build.gradle b/runners/flink/2.2/job-server/build.gradle similarity index 95% rename from runners/flink/1.18/job-server/build.gradle rename to runners/flink/2.2/job-server/build.gradle index e70fdcc0c581..527245329211 100644 --- a/runners/flink/1.18/job-server/build.gradle +++ b/runners/flink/2.2/job-server/build.gradle @@ -24,7 +24,7 @@ project.ext { 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' + archives_base_name = 'beam-runners-flink-2.2-job-server' } // Load the main build script which contains all build logic. 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/runners/flink/flink_runner.gradle b/runners/flink/flink_runner.gradle index 837561ec71b7..165d5c2d93d7 100644 --- a/runners/flink/flink_runner.gradle +++ b/runners/flink/flink_runner.gradle @@ -180,6 +180,22 @@ if (use_override) { } } +def flinkTestJvmArgs() { + def testJavaVer = project.findProperty('testJavaVersion') ? (project.property('testJavaVersion') as int) : JavaVersion.current().majorVersion.toInteger() + if (testJavaVer >= 17) { + return [ + "--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", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "-Djava.security.manager=allow", + ] + } else { + return [] + } +} + test { systemProperty "log4j.configuration", "log4j-test.properties" // Change log level to debug: @@ -187,6 +203,7 @@ test { // Change log level to debug only for the package and nested packages: // systemProperty "org.slf4j.simpleLogger.log.org.apache.beam.runners.flink.translation.wrappers.streaming", "debug" jvmArgs "-XX:-UseGCOverheadLimit" + jvmArgs += flinkTestJvmArgs() if (System.getProperty("beamSurefireArgline")) { jvmArgs System.getProperty("beamSurefireArgline") } diff --git a/runners/flink/job-server/flink_job_server.gradle b/runners/flink/job-server/flink_job_server.gradle index b85f8fc98aaa..46b1b5a607fe 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' @@ -239,6 +239,27 @@ tasks.register("validatesPortableRunner") { dependsOn validatesPortableRunnerStreamingCheckpoint } +def flinkJobServerJvmArgs() { + def testJavaVer = project.findProperty('testJavaVersion') ? (project.property('testJavaVersion') as int) : JavaVersion.current().majorVersion.toInteger() + if (testJavaVer >= 17) { + return [ + "--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", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "-Djava.security.manager=allow", + ] + } + return [] +} + +[validatesPortableRunnerDocker, validatesPortableRunnerBatchDataSet, validatesPortableRunnerBatch, validatesPortableRunnerStreaming, validatesPortableRunnerStreamingCheckpoint].each { task -> + task.configure { + jvmArgs += flinkJobServerJvmArgs() + } +} + def jobPort = BeamModulePlugin.getRandomPort() def artifactPort = BeamModulePlugin.getRandomPort() @@ -248,13 +269,31 @@ 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}\"" + def jvmArgs = flinkJobServerJvmArgs().join(' ') + def jvmArgsOpt = jvmArgs ? "--jvm_args \"${jvmArgs}\"" : "" + 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}\" ${jvmArgsOpt}" } def cleanupTask = project.tasks.register("flinkJobServerCleanup", Exec) { 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..d7c8912ded01 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.DRAINING; + } + try { + drainFuture.get(); + return State.DRAINED; + } 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/main/java/org/apache/beam/runners/flink/FlinkStreamingPortablePipelineTranslator.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkStreamingPortablePipelineTranslator.java index 77dc4d795167..e4abae4c751d 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkStreamingPortablePipelineTranslator.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkStreamingPortablePipelineTranslator.java @@ -732,7 +732,6 @@ private void translateStreamingImpulse( private void translateExecutableStage( String id, RunnerApi.Pipeline pipeline, StreamingTranslationContext context) { - // TODO: Fail on splittable DoFns. // TODO: Special-case single outputs to avoid multiplexing PCollections. RunnerApi.Components components = pipeline.getComponents(); RunnerApi.PTransform transform = components.getTransformsOrThrow(id); diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/metrics/DoFnRunnerWithMetricsUpdate.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/metrics/DoFnRunnerWithMetricsUpdate.java index 56e077253ae6..c327c8d91bea 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/metrics/DoFnRunnerWithMetricsUpdate.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/metrics/DoFnRunnerWithMetricsUpdate.java @@ -27,6 +27,7 @@ import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.WindowedValue; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** @@ -105,6 +106,9 @@ public void finishBundle() { container.updateMetrics(stepName); } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { delegate.onWindowExpiration(window, timestamp, key); diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/functions/FlinkExecutableStageFunction.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/functions/FlinkExecutableStageFunction.java index 6c13f8872e9d..f513bda9935a 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/functions/FlinkExecutableStageFunction.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/functions/FlinkExecutableStageFunction.java @@ -31,7 +31,6 @@ import org.apache.beam.runners.core.InMemoryStateInternals; import org.apache.beam.runners.core.InMemoryTimerInternals; import org.apache.beam.runners.core.StateInternals; -import org.apache.beam.runners.core.StateTags; import org.apache.beam.runners.core.TimerInternals; import org.apache.beam.runners.core.construction.SerializablePipelineOptions; import org.apache.beam.runners.flink.FlinkPipelineOptions; @@ -56,6 +55,7 @@ import org.apache.beam.sdk.fn.data.FnDataReceiver; import org.apache.beam.sdk.io.FileSystems; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.state.MapState; import org.apache.beam.sdk.transforms.join.RawUnionValue; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.util.construction.PTransformTranslation; @@ -284,12 +284,17 @@ public void mapPartition( List> residuals = new ArrayList<>(); TimerInternals.TimerData timer; while ((timer = sdfTimerInternals.removeNextProcessingTimer()) != null) { - WindowedValue stateValue = - sdfStateInternals - .state(timer.getNamespace(), StateTags.value(timer.getTimerId(), inputCoder)) - .read(); - - residuals.add(stateValue); + MapState> residualState = + sdfStateInternals.state( + timer.getNamespace(), + BundleCheckpointHandlers.StateAndTimerBundleCheckpointHandler.residualStateTag( + inputCoder)); + WindowedValue stateValue = residualState.get(timer.getTimerId()).read(); + residualState.remove(timer.getTimerId()); + // A pre-#27648 savepoint has no entry under the new tag, so the read is null; drop it. + if (stateValue != null) { + residuals.add(stateValue); + } } processElements(residuals, bundle); } diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/ExecutableStageDoFnOperator.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/ExecutableStageDoFnOperator.java index 4ebb359fceae..b4d5e43557c1 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/ExecutableStageDoFnOperator.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/ExecutableStageDoFnOperator.java @@ -88,6 +88,7 @@ import org.apache.beam.sdk.function.ThrowingFunction; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.state.BagState; +import org.apache.beam.sdk.state.MapState; import org.apache.beam.sdk.state.State; import org.apache.beam.sdk.state.StateContext; import org.apache.beam.sdk.state.TimeDomain; @@ -1009,9 +1010,20 @@ public void onTimer( Preconditions.checkNotNull(remoteBundle, "Call to onTimer outside of a bundle"); if (StateAndTimerBundleCheckpointHandler.isSdfTimer(timerId)) { StateNamespace namespace = StateNamespaces.window(windowCoder, window); - WindowedValue stateValue = - keyedStateInternals.state(namespace, StateTags.value(timerId, residualCoder)).read(); - processElement(stateValue); + // Residuals from pre-#27648 savepoints used per-residual ValueState tags and are not + // migrated; an older savepoint has no entry under the new tag, so the read is null and the + // residual is dropped. + MapState> residualState = + keyedStateInternals.state( + namespace, StateAndTimerBundleCheckpointHandler.residualStateTag(residualCoder)); + WindowedValue stateValue = residualState.get(timerId).read(); + // Drop the consumed residual so a polling SDF's keyed state stays bounded across + // self-checkpoints (apache/beam#27648). Flink commits this removal only with the bundle's + // checkpoint, so on failure the timer re-fires and the residual is re-read. + residualState.remove(timerId); + if (stateValue != null) { + processElement(stateValue); + } } else { KV transformAndTimerFamilyId = TimerReceiverFactory.decodeTimerDataTimerId(timerFamilyId); @@ -1064,6 +1076,9 @@ public void finishBundle() { } } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) {} diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperator.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperator.java index fad97e2d7dfd..5043ed9e5303 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperator.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperator.java @@ -118,7 +118,7 @@ protected DoFnRunner, KV> createWrappingDoF // for some K, V return DoFnRunners.lateDataDroppingRunner( - (DoFnRunner) doFnRunner, timerInternals, windowingStrategy); + (DoFnRunner) doFnRunner, stepContext, windowingStrategy); } @Override diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/stableinput/BufferingDoFnRunner.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/stableinput/BufferingDoFnRunner.java index 73b20238ef05..6d1d4085f0bc 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/stableinput/BufferingDoFnRunner.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/stableinput/BufferingDoFnRunner.java @@ -255,6 +255,9 @@ public void finishBundle() { Optional.ofNullable(finishBundleCallback).ifPresent(Runnable::run); } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) {} 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..09ceede9d586 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 testDetachedDrainReturnsDrainingThenDrained() 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.DRAINING)); + assertThat(result.getState(), is(PipelineResult.State.DRAINING)); + + drainFuture.complete("savepoint"); + assertThat(result.getState(), is(PipelineResult.State.DRAINED)); + 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.DRAINING)); + } + + retryDrainFuture.complete("savepoint"); + assertThat(result.getState(), is(PipelineResult.State.DRAINED)); + verify(jobClient, times(2)).stopWithSavepoint(true, null, SavepointFormatType.DEFAULT); + } } diff --git a/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkSubmissionTest.java b/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkSubmissionTest.java index 8e4c3255fac5..8e5ef8a3445a 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkSubmissionTest.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkSubmissionTest.java @@ -19,11 +19,12 @@ import java.io.File; import java.lang.reflect.Field; -import java.lang.reflect.Modifier; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.Permission; import java.util.Collection; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -230,21 +231,49 @@ private static void restoreEnvironment() throws Exception { * We modify the JVM's environment variables here. This is necessary for the end-to-end test * because Flink's CliFrontend requires a Flink configuration file for which the location can only * be set using the {@code ConfigConstants.ENV_FLINK_CONF_DIR} environment variable. + * + *

On Unix, {@code theEnvironment} uses {@code Variable}/{@code Value} keys; {@code putAll} + * with {@code String} keys does not work for {@code System.getenv(String)} lookups. */ + @SuppressWarnings("unchecked") private static void modifyEnv(Map env) throws Exception { Class processEnv = Class.forName("java.lang.ProcessEnvironment"); - Field envField = processEnv.getDeclaredField("theUnmodifiableEnvironment"); + Field theEnvironmentField = processEnv.getDeclaredField("theEnvironment"); + theEnvironmentField.setAccessible(true); + Map envMap = (Map) theEnvironmentField.get(null); + envMap.clear(); - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(envField, envField.getModifiers() & ~Modifier.FINAL); - - envField.setAccessible(true); - envField.set(null, env); - envField.setAccessible(false); + Class variableClass = null; + Class valueClass = null; + try { + variableClass = Class.forName("java.lang.ProcessEnvironment$Variable"); + valueClass = Class.forName("java.lang.ProcessEnvironment$Value"); + } catch (ClassNotFoundException e) { + // Windows: theEnvironment uses String keys. + } + if (variableClass != null && valueClass != null) { + Method valueOfVariable = variableClass.getDeclaredMethod("valueOf", String.class); + Method valueOfValue = valueClass.getDeclaredMethod("valueOf", String.class); + valueOfVariable.setAccessible(true); + valueOfValue.setAccessible(true); + for (Map.Entry entry : env.entrySet()) { + envMap.put( + valueOfVariable.invoke(null, entry.getKey()), + valueOfValue.invoke(null, entry.getValue())); + } + } else { + envMap.putAll(env); + } - modifiersField.setInt(envField, envField.getModifiers() & Modifier.FINAL); - modifiersField.setAccessible(false); + if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).startsWith("windows")) { + Field ciEnvField = processEnv.getDeclaredField("theCaseInsensitiveEnvironment"); + ciEnvField.setAccessible(true); + Map ciEnvMap = (Map) ciEnvField.get(null); + if (ciEnvMap != null) { + ciEnvMap.clear(); + ciEnvMap.putAll(env); + } + } } /** Prevents the CliFrontend from calling System.exit. */ 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/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperatorTest.java b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperatorTest.java index 5c4975ffab01..cce8f808c242 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperatorTest.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperatorTest.java @@ -523,6 +523,9 @@ public void finishBundle() { wrappedRunner.finishBundle(); } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration( BoundedWindow window, Instant timestamp, KeyT key) { diff --git a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/UnboundedSourceWrapperTest.java b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/UnboundedSourceWrapperTest.java index f57198e08e3e..0033243d9255 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/UnboundedSourceWrapperTest.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/io/UnboundedSourceWrapperTest.java @@ -666,10 +666,7 @@ private static void testSourceDoesNotShutdown(boolean shouldHaveReaders) throws if (!shouldHaveReaders) { // The expected state is for finalizeSource to sleep instead of exiting while (true) { - StackTraceElement[] callStack = thread.getStackTrace(); - if (callStack.length >= 2 - && "sleep".equals(callStack[0].getMethodName()) - && "finalizeSource".equals(callStack[1].getMethodName())) { + if (isInFinalizeSource(thread.getStackTrace())) { break; } Thread.sleep(10); @@ -694,6 +691,15 @@ private static void testSourceDoesNotShutdown(boolean shouldHaveReaders) throws assertThat(thread.isAlive(), is(false)); } + private static boolean isInFinalizeSource(StackTraceElement[] callStack) { + for (StackTraceElement frame : callStack) { + if ("finalizeSource".equals(frame.getMethodName())) { + return true; + } + } + return false; + } + @Test public void testSequentialReadingFromBoundedSource() throws Exception { UnboundedReadFromBoundedSource.BoundedToUnboundedSourceAdapter source = 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..ec075a4faf7d 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-20260731' +ext.dataflowFnapiContainerVersion = 'beam-master-20260731' ext.dataflowContainerBaseRepository = 'gcr.io/cloud-dataflow/v1beta3' processResources { @@ -113,6 +113,9 @@ dependencies { implementation library.java.google_cloud_logging permitUnusedDeclared library.java.google_cloud_logging // BEAM-11761 implementation library.java.opentelemetry_context + runtimeOnly library.java.opentelemetry_exporter_otlp + runtimeOnly library.java.opentelemetry_extension_autoconfigure + runtimeOnly project(":sdks:java:extensions:opentelemetry-gcp-auth-extension") implementation library.java.hamcrest implementation library.java.jackson_annotations implementation library.java.jackson_core @@ -167,6 +170,7 @@ def legacyPipelineOptions = [ "--region=${gcpRegion}", "--tempRoot=${dataflowValidatesTempRoot}", "--dataflowWorkerJar=${dataflowLegacyWorkerJar}", + "--usePublicIps=false", "--experiments=enable_lineage" ] @@ -184,6 +188,7 @@ def runnerV2CommonPipelineOptions = [ "--tempRoot=${dataflowValidatesTempRoot}", "--experiments=use_unified_worker,use_runner_v2", "--firestoreDb=${firestoreDb}", + "--usePublicIps=false", "--experiments=enable_lineage" ] @@ -431,7 +436,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 +476,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', @@ -484,6 +489,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', ] ] @@ -570,7 +578,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 +618,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 +888,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/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/DataflowPipelineJob.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java index 400f161dee2f..278dace6ef48 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 @@ -365,6 +365,7 @@ private void logTerminalState(State state) { switch (state) { case DONE: case CANCELLED: + case DRAINED: LOG.info("Job {} finished with status {}.", getJobId(), state); break; case UPDATED: @@ -432,55 +433,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 +503,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/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..1b03cc10351e 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; @@ -101,6 +102,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; @@ -1242,12 +1244,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 +1262,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 @@ -1310,6 +1312,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)); } @@ -1321,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 " @@ -1374,10 +1396,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 +1413,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 +1569,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 @@ -2510,8 +2535,9 @@ private String getJobIdFromName(String jobName) { listResult = dataflowClient.listJobs(token); token = listResult.getNextPageToken(); for (Job job : listResult.getJobs()) { + State state = MonitoringUtil.toState(job.getCurrentState()); if (job.getName().equals(jobName) - && MonitoringUtil.toState(job.getCurrentState()).equals(State.RUNNING)) { + && (state.equals(State.RUNNING) || state.equals(State.DRAINING))) { return job.getId(); } } 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/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..58521d9b1263 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 @@ -190,12 +190,19 @@ 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) + @Description("Necessary duration for a commit to be considered stuck and mark worker unhealthy.") + @Default.Integer(60 * 60 * 1000) int getStuckCommitDurationMillis(); 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/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); /** diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/MonitoringUtil.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/MonitoringUtil.java index d117cf786129..0e25bbd265ae 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/MonitoringUtil.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/util/MonitoringUtil.java @@ -221,17 +221,19 @@ public static State toState(@Nullable String stateName) { return State.CANCELLED; case "JOB_STATE_UPDATED": return State.UPDATED; + case "JOB_STATE_DRAINING": + return State.DRAINING; + case "JOB_STATE_DRAINED": + return State.DRAINED; case "JOB_STATE_RUNNING": case "JOB_STATE_PENDING": // Job has not yet started; closest mapping is RUNNING - case "JOB_STATE_DRAINING": // Job is still active; the closest mapping is RUNNING case "JOB_STATE_CANCELLING": // Job is still active; the closest mapping is RUNNING case "JOB_STATE_PAUSING": // Job is still active; the closest mapping is RUNNING case "JOB_STATE_RESOURCE_CLEANING_UP": // Job is still active; the closest mapping is RUNNING return State.RUNNING; case "JOB_STATE_DONE": - case "JOB_STATE_DRAINED": // Job has successfully terminated; closest mapping is DONE return State.DONE; default: LOG.warn( 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/DataflowPipelineJobTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineJobTest.java index 54ba10df9d1c..be7ad2e6e110 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.DRAINING, 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/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..6c32dd749d0b 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; @@ -277,6 +285,11 @@ private static Pipeline buildDataflowPipelineWithLargeGraph(DataflowPipelineOpti } static Dataflow buildMockDataflow(Dataflow.Projects.Locations.Jobs mockJobs) throws IOException { + return buildMockDataflow(mockJobs, "JOB_STATE_RUNNING"); + } + + static Dataflow buildMockDataflow(Dataflow.Projects.Locations.Jobs mockJobs, String currentState) + throws IOException { Dataflow mockDataflowClient = mock(Dataflow.class); Dataflow.Projects mockProjects = mock(Dataflow.Projects.class); Dataflow.Projects.Locations mockLocations = mock(Dataflow.Projects.Locations.class); @@ -300,7 +313,7 @@ static Dataflow buildMockDataflow(Dataflow.Projects.Locations.Jobs mockJobs) thr new Job() .setName("oldjobname") .setId("oldJobId") - .setCurrentState("JOB_STATE_RUNNING")))); + .setCurrentState(currentState)))); Job resultJob = new Job(); resultJob.setId("newid"); @@ -367,6 +380,10 @@ static GcsUtil buildMockGcsUtil() throws IOException { } private DataflowPipelineOptions buildPipelineOptions() throws IOException { + return buildPipelineOptions("JOB_STATE_RUNNING"); + } + + private DataflowPipelineOptions buildPipelineOptions(String currentState) throws IOException { DataflowPipelineOptions options = PipelineOptionsFactory.as(DataflowPipelineOptions.class); options.setRunner(DataflowRunner.class); options.setProject(PROJECT_ID); @@ -374,7 +391,7 @@ private DataflowPipelineOptions buildPipelineOptions() throws IOException { options.setRegion(REGION_ID); // Set FILES_PROPERTY to empty to prevent a default value calculated from classpath. options.setFilesToStage(new ArrayList<>()); - options.setDataflowClient(buildMockDataflow(mockJobs)); + options.setDataflowClient(buildMockDataflow(mockJobs, currentState)); options.setGcsUtil(mockGcsUtil); options.setGcpCredential(new TestCredential()); @@ -785,6 +802,19 @@ public void testUpdate() throws IOException { assertValidJob(jobCaptor.getValue()); } + @Test + public void testUpdateDrainingJob() throws IOException { + DataflowPipelineOptions options = buildPipelineOptions("JOB_STATE_DRAINING"); + options.setUpdate(true); + options.setJobName("oldJobName"); + Pipeline p = buildDataflowPipeline(options); + p.run(); + + ArgumentCaptor jobCaptor = ArgumentCaptor.forClass(Job.class); + Mockito.verify(mockJobs).create(eq(PROJECT_ID), eq(REGION_ID), jobCaptor.capture()); + assertEquals("oldJobId", jobCaptor.getValue().getReplaceJobId()); + } + @Test public void testUploadGraph() throws IOException { DataflowPipelineOptions options = buildPipelineOptions(); @@ -1847,7 +1877,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); } } } @@ -2777,4 +2810,183 @@ 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(); + } + + @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")); + } } diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/MonitoringUtilTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/MonitoringUtilTest.java index 5f76b6750ffa..35af1be4cdfc 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/MonitoringUtilTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/util/MonitoringUtilTest.java @@ -100,9 +100,9 @@ public void testToStateNormal() { // Non-trivially mapped cases assertEquals(State.STOPPED, MonitoringUtil.toState("JOB_STATE_PAUSED")); - assertEquals(State.RUNNING, MonitoringUtil.toState("JOB_STATE_DRAINING")); + assertEquals(State.DRAINING, MonitoringUtil.toState("JOB_STATE_DRAINING")); assertEquals(State.RUNNING, MonitoringUtil.toState("JOB_STATE_PAUSING")); - assertEquals(State.DONE, MonitoringUtil.toState("JOB_STATE_DRAINED")); + assertEquals(State.DRAINED, MonitoringUtil.toState("JOB_STATE_DRAINED")); } @Test 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)); diff --git a/runners/google-cloud-dataflow-java/worker/build.gradle b/runners/google-cloud-dataflow-java/worker/build.gradle index 21879861e9d6..2ca5d77a6f00 100644 --- a/runners/google-cloud-dataflow-java/worker/build.gradle +++ b/runners/google-cloud-dataflow-java/worker/build.gradle @@ -154,6 +154,22 @@ applyJavaNature( /******************************************************************************/ // Configure the worker root project +tasks.withType(Test).configureEach { + // WindmillStateTestUtils.assertNoReference walks every object reachable from the Windmill + // state cache (Guava Cache / ConcurrentHashMap) to ensure no per-work-item WindmillStateReader + // leaks into the global cache. It uses reflection (Field.setAccessible) into JDK internals + // (e.g. Integer.value, AtomicReferenceArray.array, ReentrantLock.sync, ReferenceQueue.head), + // which requires --add-opens on Java 17+. + jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.lang.ref=ALL-UNNAMED', + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', + '--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED', + '--add-opens=java.base/java.util.concurrent.locks=ALL-UNNAMED' +} + configurations { sourceFile @@ -213,6 +229,8 @@ dependencies { implementation library.java.jackson_databind implementation library.java.joda_time implementation library.java.opentelemetry_context + testImplementation library.java.opentelemetry_sdk_testing + implementation library.java.opentelemetry_api 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/AssignWindowsParDoFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/AssignWindowsParDoFnFactory.java index 83cbc3aa62c7..673130a6048a 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/AssignWindowsParDoFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/AssignWindowsParDoFnFactory.java @@ -119,6 +119,9 @@ public void processTimers() throws Exception { // Nothing. } + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() throws Exception { receiver = null; diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/BatchModeUngroupingParDoFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/BatchModeUngroupingParDoFn.java index 8e2b325b580a..c3cf6d9e67e2 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/BatchModeUngroupingParDoFn.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/BatchModeUngroupingParDoFn.java @@ -73,6 +73,9 @@ public void processTimers() throws Exception { // The timers for the underlying ParDoFn are processed at the end of each element } + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() throws Exception { underlyingParDoFn.finishBundle(); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/CreateIsmShardKeyAndSortKeyDoFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/CreateIsmShardKeyAndSortKeyDoFnFactory.java index bd991560c186..afe2e28b2a67 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/CreateIsmShardKeyAndSortKeyDoFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/CreateIsmShardKeyAndSortKeyDoFnFactory.java @@ -114,6 +114,9 @@ public void processElement(Object untypedElem) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() {} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowExecutionContext.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowExecutionContext.java index 6ff05b4b4452..888e954c1c9f 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowExecutionContext.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowExecutionContext.java @@ -150,6 +150,10 @@ boolean isSinkFullHintSet() { // the state size might grow unbounded. } + protected final long getBytesSinked() { + return bytesSinked; + } + /** * Sets a flag to indicate that a sink has enough data written to it. This hint is read by * upstream producers to stop producing if they can. Mainly used in streaming. diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounter.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounter.java index 7c5859a9d324..1a927c03c61c 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounter.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounter.java @@ -18,12 +18,14 @@ package org.apache.beam.runners.dataflow.worker; import org.apache.beam.runners.core.ElementByteSizeObservable; +import org.apache.beam.runners.core.KeyedWorkItem; 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.counters.NameContext; import org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter; import org.apache.beam.runners.dataflow.worker.util.common.worker.OutputObjectAndByteCounter; +import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; @@ -33,6 +35,7 @@ @SuppressWarnings({ "nullness" // TODO(https://github.com/apache/beam/issues/20497) }) +@Internal public class DataflowOutputCounter implements ElementCounter { /** Number of logical element and single window pairs that were processed. */ private static final String ELEMENT_COUNTER_NAME = "-ElementCount"; @@ -41,20 +44,36 @@ public class DataflowOutputCounter implements ElementCounter { private OutputObjectAndByteCounter objectAndByteCounter; private Counter elementCount; + private final boolean isStreaming; - public DataflowOutputCounter( - String outputName, CounterFactory counterFactory, NameContext nameContext) { - this(outputName, null, counterFactory, nameContext); + public static DataflowOutputCounter create( + String outputName, + ElementByteSizeObservable elementByteSizeObservable, + CounterFactory counterFactory, + NameContext nameContext, + boolean isStreaming) { + return new DataflowOutputCounter( + outputName, elementByteSizeObservable, counterFactory, nameContext, isStreaming); + } + + public static DataflowOutputCounter create( + String outputName, + CounterFactory counterFactory, + NameContext nameContext, + boolean isStreaming) { + return new DataflowOutputCounter(outputName, null, counterFactory, nameContext, isStreaming); } - public DataflowOutputCounter( + private DataflowOutputCounter( String outputName, ElementByteSizeObservable elementByteSizeObservable, CounterFactory counterFactory, - NameContext nameContext) { - objectAndByteCounter = + NameContext nameContext, + boolean isStreaming) { + this.isStreaming = isStreaming; + this.objectAndByteCounter = new OutputObjectAndByteCounter(elementByteSizeObservable, counterFactory, nameContext); - objectAndByteCounter.countMeanByte(outputName + MEAN_BYTE_COUNTER_NAME); + this.objectAndByteCounter.countMeanByte(outputName + MEAN_BYTE_COUNTER_NAME); createElementCounter(counterFactory, outputName + ELEMENT_COUNTER_NAME); } @@ -63,15 +82,42 @@ public void update(Object elem) throws Exception { objectAndByteCounter.update(elem); long windowsSize = ((WindowedValue) elem).getWindows().size(); if (windowsSize == 0) { - // GroupingShuffleReader produces ValueInEmptyWindows. - // For now, we count the element at least once to keep the current counter - // behavior. - elementCount.addValue(1L); + updateEmptyWindows((WindowedValue) elem); } else { + // Standard WindowedValue. elementCount.addValue(windowsSize); } } + private void updateEmptyWindows(WindowedValue elem) { + if (isStreaming) { + Object value = elem.getValue(); + if (value instanceof KeyedWorkItem) { + // KeyedWorkItem wrapped in ValueInEmptyWindows + // (e.g. WindowingWindmillReader for Streaming GBK) + KeyedWorkItem keyedWorkItem = (KeyedWorkItem) value; + long totalElementCount = 0; + // Iterate through elementWindowsIterable and ignore timers in KeyedWorkItem. + // Uses lightweight metadata-only iteration without payload deserialization overhead. + for (WindowedValue element : keyedWorkItem.elementWindowsIterable()) { + long elementWindowsSize = element.getWindows().size(); + // Fan out for windows. + totalElementCount += (elementWindowsSize == 0 ? 1L : elementWindowsSize); + } + elementCount.addValue(totalElementCount); + } else { + // NOTE: in streaming mode, this should not normally happen. + // Counting as 1 element serves as a fallback to maintain counter behavior without failing + // execution. + elementCount.addValue(1L); + } + } else { + // Non-KeyedWorkItem wrapped in ValueInEmptyWindows + // (e.g. GroupingShuffleReader KV output for Batch GBK) + elementCount.addValue(1L); + } + } + @Override public void finishLazyUpdate(Object elem) { objectAndByteCounter.finishLazyUpdate(elem); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowProcessFnRunner.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowProcessFnRunner.java index ec1fcd6c8432..400dbc047443 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowProcessFnRunner.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowProcessFnRunner.java @@ -35,6 +35,7 @@ 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.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** @@ -128,6 +129,11 @@ public void finishBundle() { simpleRunner.finishBundle(); } + @Override + public void finishKey(KeyT key) { + simpleRunner.finishKey(key); + } + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { simpleRunner.onWindowExpiration(window, timestamp, key); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowWorkUnitClient.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowWorkUnitClient.java index af8e7dd50c95..39d35d5ac94b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowWorkUnitClient.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowWorkUnitClient.java @@ -135,18 +135,18 @@ public Optional getWorkItem() throws IOException { final String stage; if (work.getMapTask() != null) { - stage = work.getMapTask().getStageName(); + stage = work.getMapTask().getSystemName(); logger.info("Starting MapTask stage {}", stage); } else if (work.getSeqMapTask() != null) { - stage = work.getSeqMapTask().getStageName(); + stage = work.getSeqMapTask().getSystemName(); logger.info("Starting SeqMapTask stage {}", stage); } else if (work.getSourceOperationTask() != null) { - stage = work.getSourceOperationTask().getStageName(); + stage = work.getSourceOperationTask().getSystemName(); logger.info("Starting SourceOperationTask stage {}", stage); } else { stage = null; } - DataflowWorkerLoggingMDC.setStageName(stage); + DataflowWorkerLoggingMDC.setSystemStageName(stage); stageStartTime.set(DateTime.now()); DataflowWorkerLoggingMDC.setWorkId(Long.toString(work.getId())); @@ -227,7 +227,7 @@ public WorkItemServiceState reportWorkItemStatus(WorkItemStatus workItemStatus) // Log the stage execution time of finished stages that have a stage name. This will not be set // in the event this status is associated with a dummy work item. if (firstNonNull(workItemStatus.getCompleted(), Boolean.FALSE) - && DataflowWorkerLoggingMDC.getStageName() != null) { + && DataflowWorkerLoggingMDC.getSystemStageName() != null) { DateTime startTime = stageStartTime.get(); if (startTime != null) { // elapsed time can be negative by time correction @@ -236,7 +236,7 @@ public WorkItemServiceState reportWorkItemStatus(WorkItemStatus workItemStatus) // This thread should have been tagged with the stage start time during getWorkItem(), logger.info( "Finished processing stage {} with {} errors in {} seconds ", - DataflowWorkerLoggingMDC.getStageName(), + DataflowWorkerLoggingMDC.getSystemStageName(), numErrors, (double) elapsed / 1000); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ForwardingParDoFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ForwardingParDoFn.java index d55181559322..3a864d6caf2f 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ForwardingParDoFn.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ForwardingParDoFn.java @@ -19,6 +19,7 @@ import org.apache.beam.runners.dataflow.worker.util.common.worker.ParDoFn; import org.apache.beam.runners.dataflow.worker.util.common.worker.Receiver; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A base class for {@link ParDoFn} implementations for overriding particular methods while @@ -47,6 +48,11 @@ public void processTimers() throws Exception { delegate.processTimers(); } + @Override + public void finishKey(@Nullable Object key) throws Exception { + delegate.finishKey(key); + } + @Override public void finishBundle() throws Exception { delegate.finishBundle(); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/GroupAlsoByWindowFnRunner.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/GroupAlsoByWindowFnRunner.java index 4845bb0c98e4..aa60a61af8af 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/GroupAlsoByWindowFnRunner.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/GroupAlsoByWindowFnRunner.java @@ -27,6 +27,7 @@ import org.apache.beam.sdk.util.WindowedValueReceiver; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.WindowedValue; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** @@ -102,6 +103,9 @@ private void invokeProcessElement(WindowedValue elem) { @Override public void finishBundle() {} + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) {} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/GroupAlsoByWindowsParDoFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/GroupAlsoByWindowsParDoFn.java index 882dd497e3f5..6c92b7f10ea1 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/GroupAlsoByWindowsParDoFn.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/GroupAlsoByWindowsParDoFn.java @@ -142,6 +142,12 @@ public void processTimers() throws Exception { // it here to build a KeyedWorkItem } + @Override + public void finishKey(Object key) throws Exception { + checkState(fnRunner != null); + fnRunner.finishKey(key); + } + @Override public void finishBundle() throws Exception { checkState(fnRunner != null); @@ -210,7 +216,7 @@ private DoFnRunner>> createRunner() { } return (DoFnRunner>>) DoFnRunners., W>lateDataDroppingRunner( - streamingGABWRunner, stepContext.timerInternals(), windowingStrategy); + streamingGABWRunner, stepContext, windowingStrategy); } else { if (hasStreamingSideInput) { return new StreamingSideInputDoFnRunner<>( 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; } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactory.java index d3f2aacc74d0..3ea29787eb3b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactory.java @@ -63,6 +63,7 @@ import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.fn.IdGenerator; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.util.common.ElementByteSizeObserver; import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.WindowedValues.WindowedValueCoder; @@ -102,8 +103,9 @@ public DataflowMapTaskExecutor create( IdGenerator idGenerator) { // Swap out all the InstructionOutput nodes with OutputReceiver nodes + boolean isStreaming = options.as(StreamingOptions.class).isStreaming(); Networks.replaceDirectedNetworkNodes( - network, createOutputReceiversTransform(stageName, counterSet)); + network, createOutputReceiversTransform(stageName, counterSet, isStreaming)); // Swap out all the ParallelInstruction nodes with Operation nodes. While updating the network, // we keep track of @@ -345,7 +347,7 @@ OperationNode createFlattenOperation( * Returns a function which can convert {@link InstructionOutput}s into {@link OutputReceiver}s. */ static Function createOutputReceiversTransform( - final String stageName, final CounterFactory counterFactory) { + final String stageName, final CounterFactory counterFactory, final boolean isStreaming) { return new TypeSafeNodeFunction(InstructionOutputNode.class) { @Override public Node typedApply(InstructionOutputNode input) { @@ -355,7 +357,7 @@ public Node typedApply(InstructionOutputNode input) { CloudObjects.coderFromCloudObject(CloudObject.fromSpec(cloudOutput.getCodec())); ElementCounter outputCounter = - new DataflowOutputCounter( + DataflowOutputCounter.create( cloudOutput.getName(), new ElementByteSizeObservableCoder<>(coder), counterFactory, @@ -363,7 +365,8 @@ public Node typedApply(InstructionOutputNode input) { stageName, cloudOutput.getOriginalName(), cloudOutput.getSystemName(), - cloudOutput.getName())); + cloudOutput.getName()), + isStreaming); outputReceiver.addOutputCounter(outputCounter); return OutputReceiverNode.create(outputReceiver, coder, input.getPcollectionId()); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/MultiKeyBundleOptions.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/MultiKeyBundleOptions.java new file mode 100644 index 000000000000..8a264dc57ef6 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/MultiKeyBundleOptions.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.auto.value.AutoValue; +import java.util.concurrent.TimeUnit; +import org.apache.beam.sdk.annotations.Internal; +import org.apache.beam.sdk.options.ExperimentalOptions; +import org.apache.beam.sdk.options.PipelineOptions; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@AutoValue +@Internal +public abstract class MultiKeyBundleOptions { + // TODO: consider moving this to be part of PipelineOptions after the feature is stable + + private static final Logger LOG = LoggerFactory.getLogger(MultiKeyBundleOptions.class); + + // Don't use. Experiment guarding multi key bundles. The feature is work in progress and + // incomplete. + public static final String UNSTABLE_ENABLE_MULTI_KEY_BUNDLE = "unstable_enable_multi_key_bundle"; + + private static final String WINDMILL_MAX_KEY_GROUP_BATCH_SIZE = + "windmill_max_key_group_batch_size"; + private static final String WINDMILL_MAX_KEY_GROUP_BATCH_TIME_MS = + "windmill_max_key_group_batch_time_ms"; + private static final String WINDMILL_MAX_KEY_GROUP_BATCH_SINK_BYTES = + "windmill_max_key_group_batch_sink_bytes"; + + public abstract int maxKeyGroupBatchSize(); + + public abstract long maxKeyGroupBatchTimeNanos(); + + public abstract boolean multiKeyBundleEnabled(); + + public abstract long maxKeyGroupBatchSinkBytes(); + + public static Builder builder() { + return new AutoValue_MultiKeyBundleOptions.Builder(); + } + + public static MultiKeyBundleOptions fromOptions(PipelineOptions options) { + int maxKeyGroupBatchSize = + tryParseInt( + ExperimentalOptions.getExperimentValue(options, WINDMILL_MAX_KEY_GROUP_BATCH_SIZE), + 100, + WINDMILL_MAX_KEY_GROUP_BATCH_SIZE); + + long batchTimeMs = + tryParseLong( + ExperimentalOptions.getExperimentValue(options, WINDMILL_MAX_KEY_GROUP_BATCH_TIME_MS), + 100, + WINDMILL_MAX_KEY_GROUP_BATCH_TIME_MS); + + boolean multiKeyBundleEnabled = + ExperimentalOptions.hasExperiment(options, UNSTABLE_ENABLE_MULTI_KEY_BUNDLE); + + long maxKeyGroupBatchSinkBytes = + tryParseLong( + ExperimentalOptions.getExperimentValue( + options, WINDMILL_MAX_KEY_GROUP_BATCH_SINK_BYTES), + StreamingDataflowWorker.MAX_SINK_BYTES, + WINDMILL_MAX_KEY_GROUP_BATCH_SINK_BYTES); + + return builder() + .setMaxKeyGroupBatchSize(maxKeyGroupBatchSize) + .setMaxKeyGroupBatchTimeNanos(TimeUnit.MILLISECONDS.toNanos(batchTimeMs)) + .setMultiKeyBundleEnabled(multiKeyBundleEnabled) + .setMaxKeyGroupBatchSinkBytes(maxKeyGroupBatchSinkBytes) + .build(); + } + + private static int tryParseInt(@Nullable String value, int defaultValue, String experimentName) { + if (value == null) { + return defaultValue; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + LOG.warn( + "Failed to parse experiment {} value '{}' as integer, falling back to default: {}", + experimentName, + value, + defaultValue, + e); + return defaultValue; + } + } + + private static long tryParseLong( + @Nullable String value, long defaultValue, String experimentName) { + if (value == null) { + return defaultValue; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + LOG.warn( + "Failed to parse experiment {} value '{}' as long, falling back to default: {}", + experimentName, + value, + defaultValue, + e); + return defaultValue; + } + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setMaxKeyGroupBatchSize(int size); + + public abstract Builder setMaxKeyGroupBatchTimeNanos(long nanos); + + public abstract Builder setMultiKeyBundleEnabled(boolean enabled); + + public abstract Builder setMaxKeyGroupBatchSinkBytes(long bytes); + + public abstract MultiKeyBundleOptions build(); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PairWithConstantKeyDoFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PairWithConstantKeyDoFnFactory.java index 6951e3a95b20..425184a4a126 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PairWithConstantKeyDoFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PairWithConstantKeyDoFnFactory.java @@ -98,6 +98,9 @@ public void processElement(Object untypedElem) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() {} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PartialGroupByKeyParDoFns.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PartialGroupByKeyParDoFns.java index 399258d7dbb9..a6d7810412a1 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PartialGroupByKeyParDoFns.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/PartialGroupByKeyParDoFns.java @@ -317,6 +317,9 @@ public void processElement(Object elem) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() throws Exception { groupingTable.flush(receiver); @@ -377,6 +380,9 @@ public void processElement(Object elem) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() throws Exception { groupingTable.flush(receiver); 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/ReifyTimestampAndWindowsParDoFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ReifyTimestampAndWindowsParDoFnFactory.java index 746c09404f6e..0438b525b6b9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ReifyTimestampAndWindowsParDoFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ReifyTimestampAndWindowsParDoFnFactory.java @@ -86,6 +86,9 @@ public void processElement(Object untypedElem) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() throws Exception { this.receiver = null; 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..e0f1e0f410cd 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,36 @@ 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()); - } + helpers.processTimers( + SimpleParDoFnHelpers.TimerType.USER, + helpers.userStepContext, + windowCoder, + this::onStartKey, + () -> sideInputProcessor); + helpers.processTimers( + SimpleParDoFnHelpers.TimerType.SYSTEM, + helpers.stepContext, + windowCoder, + this::onStartKey, + () -> sideInputProcessor); } - 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(); - } - } - } + @Override + public void finishKey(Object key) throws Exception {} @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 +218,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..15bfba9bbc49 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java @@ -0,0 +1,519 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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(); + + boolean isStreaming = options.as(StreamingOptions.class).isStreaming(); + ElementCounter outputCounter = + DataflowOutputCounter.create( + outputName, counterFactory, stepContext.getNameContext(), isStreaming); + 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 93c288fea9ea..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 { @@ -157,6 +157,7 @@ public DoFnRunner>, OutputT> crea 10000, Duration.standardSeconds(10), stepContext::bundleFinalizer)); + processFn.setBacklogBytesCallback(userStepContext::setBacklogBytes); DoFnRunner>, OutputT> simpleRunner = new SimpleDoFnRunner<>( options, @@ -173,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/StreamingDataflowWorker.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java index 4d070da995b3..c10e3c0c488c 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 @@ -35,6 +35,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -115,6 +116,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; @@ -207,7 +209,7 @@ public final class StreamingDataflowWorker { private StreamingDataflowWorker( WindmillServerStub windmillServer, long clientId, - ComputationConfig.Fetcher configFetcher, + Fetcher configFetcher, ComputationStateCache computationStateCache, WindmillStateCache windmillStateCache, BoundedQueueExecutor workUnitExecutor, @@ -224,7 +226,8 @@ private StreamingDataflowWorker( GrpcWindmillStreamFactory windmillStreamFactory, ScheduledExecutorService activeWorkRefreshExecutorFn, ConcurrentMap stageInfoMap, - @Nullable GrpcDispatcherClient dispatcherClient) { + @Nullable GrpcDispatcherClient dispatcherClient, + MultiKeyBundleOptions multiKeyBundleOptions) { // Register standard file systems. FileSystems.setDefaultPipelineOptions(options); this.configFetcher = configFetcher; @@ -253,6 +256,7 @@ private StreamingDataflowWorker( this.streamingWorkScheduler = StreamingWorkScheduler.create( options, + multiKeyBundleOptions, clock, readerCache, mapTaskExecutorFactory, @@ -308,9 +312,6 @@ private StreamingDataflowWorker( new ActiveWorkRefresher( clock, options.getActiveWorkRefreshPeriodMillis(), - options.isEnableStreamingEngine() - ? Math.max(options.getStuckCommitDurationMillis(), 0) - : 0, computationStateCache::getAllPresentComputations, sampler, activeWorkRefreshExecutorFn, @@ -318,7 +319,10 @@ private StreamingDataflowWorker( this.statusPages.set( createStatusPageBuilder( - this.options, this.windmillStreamFactory, this.memoryMonitor.memoryMonitor()) + this.options, + this.windmillStreamFactory, + this.memoryMonitor.memoryMonitor(), + this::isHealthy) .setClock(this.clock) .setClientId(this.clientId) .setIsRunning(this.running) @@ -570,7 +574,10 @@ private void switchStreamingWorkerHarness(ConnectivityType connectivityType) { } this.statusPages.set( createStatusPageBuilder( - this.options, this.windmillStreamFactory, this.memoryMonitor.memoryMonitor()) + this.options, + this.windmillStreamFactory, + this.memoryMonitor.memoryMonitor(), + this::isHealthy) .setClock(this.clock) .setClientId(this.clientId) .setIsRunning(this.running) @@ -587,12 +594,28 @@ private void switchStreamingWorkerHarness(ConnectivityType connectivityType) { LOG.info("Started new StreamingWorkerStatusPages instance."); } + @VisibleForTesting + boolean isHealthy() { + int stuckCommitDurationMillis = + options.isEnableStreamingEngine() ? Math.max(options.getStuckCommitDurationMillis(), 0) : 0; + if (stuckCommitDurationMillis > 0) { + Instant stuckCommitDeadline = clock.get().minus(Duration.millis(stuckCommitDurationMillis)); + for (ComputationState computationState : computationStateCache.getAllPresentComputations()) { + if (computationState.hasStuckCommits(stuckCommitDeadline)) { + return false; + } + } + } + return true; + } + private static StreamingWorkerStatusPages.Builder createStatusPageBuilder( DataflowWorkerHarnessOptions options, GrpcWindmillStreamFactory windmillStreamFactory, - MemoryMonitor memoryMonitor) { + MemoryMonitor memoryMonitor, + BooleanSupplier healthyIndicator) { WorkerStatusPages workerStatusPages = - WorkerStatusPages.create(DEFAULT_STATUS_PORT, memoryMonitor); + WorkerStatusPages.create(DEFAULT_STATUS_PORT, memoryMonitor, healthyIndicator); StreamingWorkerStatusPages.Builder streamingStatusPages = StreamingWorkerStatusPages.builder().setStatusPages(workerStatusPages); @@ -623,7 +646,8 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o ConcurrentMap stageInfo = new ConcurrentHashMap<>(); StreamingCounters streamingCounters = StreamingCounters.create(); WorkUnitClient dataflowServiceClient = new DataflowWorkUnitClient(options, LOG); - BoundedQueueExecutor workExecutor = createWorkUnitExecutor(options); + MultiKeyBundleOptions multiKeyBundleOptions = MultiKeyBundleOptions.fromOptions(options); + BoundedQueueExecutor workExecutor = createWorkUnitExecutor(options, multiKeyBundleOptions); ScheduledExecutorService commitFinalizerCleanupExecutor = Executors.newScheduledThreadPool( 1, @@ -722,7 +746,8 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o Executors.newSingleThreadScheduledExecutor( new ThreadFactoryBuilder().setNameFormat("RefreshWork").build()), stageInfo, - configFetcherComputationStateCacheAndWindmillClient.windmillDispatcherClient()); + configFetcherComputationStateCacheAndWindmillClient.windmillDispatcherClient(), + multiKeyBundleOptions); } /** @@ -752,6 +777,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) @@ -870,7 +897,8 @@ static StreamingDataflowWorker forTesting( StreamingCounters streamingCounters, WindmillStubFactoryFactory stubFactory) { ConcurrentMap stageInfo = new ConcurrentHashMap<>(); - BoundedQueueExecutor workExecutor = createWorkUnitExecutor(options); + MultiKeyBundleOptions multiKeyBundleOptions = MultiKeyBundleOptions.fromOptions(options); + BoundedQueueExecutor workExecutor = createWorkUnitExecutor(options, multiKeyBundleOptions); ScheduledExecutorService commitFinalizerCleanupExecutor = Executors.newScheduledThreadPool( 1, @@ -984,7 +1012,8 @@ static StreamingDataflowWorker forTesting( : windmillStreamFactory.build(), executorSupplier.apply("RefreshWork"), stageInfo, - grpcDispatcherClient); + grpcDispatcherClient, + multiKeyBundleOptions); } private static GrpcWindmillStreamFactory.Builder createGrpcwindmillStreamFactoryBuilder( @@ -1014,9 +1043,11 @@ private static JobHeader createJobHeader(DataflowWorkerHarnessOptions options, l .build(); } - private static BoundedQueueExecutor createWorkUnitExecutor(DataflowWorkerHarnessOptions options) { + private static BoundedQueueExecutor createWorkUnitExecutor( + DataflowWorkerHarnessOptions options, MultiKeyBundleOptions multiKeyBundleOptions) { boolean useFairMonitor = DataflowRunner.hasExperiment(options, BOUNDED_QUEUE_EXECUTOR_USE_FAIR_MONITOR_EXPERIMENT); + boolean useKeyGroupWorkQueue = multiKeyBundleOptions.multiKeyBundleEnabled(); return new BoundedQueueExecutor( chooseMaxThreads(options), THREAD_EXPIRATION_TIME_SEC, @@ -1024,7 +1055,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 { @@ -1042,6 +1074,24 @@ 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); + } 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); StreamingDataflowWorker worker = StreamingDataflowWorker.fromOptions(options); @@ -1179,9 +1229,14 @@ private void onCompleteCommit(CompleteCommit completeCommit) { computationStateCache .getIfPresent(completeCommit.computationId()) .ifPresent( - state -> + state -> { + if (completeCommit.retryableFailure()) { + state.reexecuteActiveWork(completeCommit.shardedKey(), completeCommit.workId()); + } else { state.completeWorkAndScheduleNextWorkForKey( - completeCommit.shardedKey(), completeCommit.workId())); + completeCommit.shardedKey(), completeCommit.workId()); + } + }); } @AutoValue diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowViaWindowSetFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowViaWindowSetFn.java index ec36644d1e68..a183df19b6e7 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowViaWindowSetFn.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowViaWindowSetFn.java @@ -93,7 +93,7 @@ public void processElement( reduceFn, options); - reduceFnRunner.processElements(keyedWorkItem.elementsIterable()); + reduceFnRunner.processElements(keyedWorkItem); reduceFnRunner.onTimers(keyedWorkItem.timersIterable()); reduceFnRunner.persist(); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputDoFnRunner.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputDoFnRunner.java index 0b9ccd1f37c6..3de7c0f3a9b9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputDoFnRunner.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputDoFnRunner.java @@ -39,6 +39,7 @@ 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.Sets; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** @@ -155,6 +156,11 @@ public void finishBundle() { sideInputFetcher.persist(); } + @Override + public void finishKey(KeyT key) { + simpleDoFnRunner.finishKey(key); + } + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { simpleDoFnRunner.onWindowExpiration(window, timestamp, key); 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..63de0b8d55db --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingKeyedWorkItemSideInputParDoFn.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 finishKey(Object key) throws Exception {} + + @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/StreamingModeExecutionContext.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContext.java index f75d452b211b..365ebbdc1f9d 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; @@ -24,7 +25,8 @@ import com.google.api.services.dataflow.model.SideInputInfo; import java.io.Closeable; import java.io.IOException; -import java.util.Collection; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -33,6 +35,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.concurrent.NotThreadSafe; import org.apache.beam.repackaged.core.org.apache.commons.lang3.tuple.Pair; @@ -45,21 +48,31 @@ import org.apache.beam.runners.core.metrics.ExecutionStateTracker; import org.apache.beam.runners.core.metrics.ExecutionStateTracker.ExecutionState; import org.apache.beam.runners.dataflow.worker.DataflowOperationContext.DataflowExecutionState; -import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.StepContext; import org.apache.beam.runners.dataflow.worker.counters.CounterFactory; import org.apache.beam.runners.dataflow.worker.counters.NameContext; import org.apache.beam.runners.dataflow.worker.profiler.ScopedProfiler.ProfileScope; +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.FailedWorkHandler; +import org.apache.beam.runners.dataflow.worker.streaming.KeyCommitTooLargeException; 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.streaming.config.StreamingGlobalConfig; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfigHandle; +import org.apache.beam.runners.dataflow.worker.streaming.harness.StreamingCounters; 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.streaming.sideinput.SideInputStateFetcherFactory; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor; +import org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter; +import org.apache.beam.runners.dataflow.worker.util.common.worker.OutputObjectAndByteCounter; +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; 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; @@ -68,8 +81,10 @@ import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTagEncodingV1; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTagEncodingV2; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTimerData; +import org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures.FailureTracker; import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.CoderException; import org.apache.beam.sdk.io.UnboundedSource; import org.apache.beam.sdk.io.UnboundedSource.UnboundedReader; import org.apache.beam.sdk.metrics.MetricsContainer; @@ -92,6 +107,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; @@ -103,14 +119,11 @@ * 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 { +public class StreamingModeExecutionContext + extends DataflowExecutionContext { private static final Logger LOG = LoggerFactory.getLogger(StreamingModeExecutionContext.class); @@ -128,7 +141,7 @@ public class StreamingModeExecutionContext extends DataflowExecutionContext, Map>> sideInputCache; - private WindmillTagEncoding windmillTagEncoding; + private final WindmillTagEncoding windmillTagEncoding; /** * The current user-facing key for this execution context. * @@ -141,22 +154,59 @@ public class StreamingModeExecutionContext extends DataflowExecutionContext activeReader; + private @Nullable WorkExecutor workExecutor; + private boolean finishKeyCalled = false; + + private @Nullable BoundedQueueExecutor workQueueExecutor; + private @Nullable BoundedQueueExecutorWorkHandle budgetHandle; + + private final HotKeyLogger hotKeyLogger; + private final boolean hotKeyLoggingEnabled; + private final String stepName; + private final String systemName; + private final StreamingCounters streamingCounters; + private final FailureTracker failureTracker; + private @Nullable Coder keyCoder; + + // Key switch listener to delegate MDC logging context and thread name updates + public interface KeyTransitionListener { + // oldWork is null when newWork is the first work for the bundle. + void onKeyTransition(@Nullable Work oldWork, Work newWork); + } + + private @Nullable KeyTransitionListener keyTransitionListener; + private @Nullable FailedWorkHandler onFailedWorkHandler; + + private List executedWorks = Collections.emptyList(); + private List outputBuilders = Collections.emptyList(); + + // Map> + private Map> finalizationCallbacks = Collections.emptyMap(); + private AtomicBoolean workBatchFailed = new AtomicBoolean(false); + private @Nullable WindmillStateReader activeStateReader; + private long stateBytesRead = 0; + private final String sourceBytesProcessCounterName; + + private final MultiKeyBundleOptions multiKeyBundleOptions; + private int workItemsPolled = 0; + private long bundleStartTimeNanos = 0; + public StreamingModeExecutionContext( CounterFactory counterFactory, String computationId, @@ -168,7 +218,16 @@ public StreamingModeExecutionContext( StreamingModeExecutionStateRegistry executionStateRegistry, StreamingGlobalConfigHandle globalConfigHandle, long sinkByteLimit, - boolean throwExceptionOnLargeOutput) { + boolean throwExceptionOnLargeOutput, + HotKeyLogger hotKeyLogger, + boolean hotKeyLoggingEnabled, + String stepName, + String systemName, + StreamingCounters streamingCounters, + FailureTracker failureTracker, + String sourceBytesProcessCounterName, + MultiKeyBundleOptions multiKeyBundleOptions, + SideInputStateFetcherFactory sideInputStateFetcherFactory) { super( counterFactory, metricsContainerRegistry, @@ -183,6 +242,23 @@ public StreamingModeExecutionContext( this.stateCache = stateCache; this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN; this.throwExceptionOnLargeOutput = throwExceptionOnLargeOutput; + this.hotKeyLogger = checkNotNull(hotKeyLogger); + this.hotKeyLoggingEnabled = hotKeyLoggingEnabled; + this.stepName = checkNotNull(stepName); + this.systemName = checkNotNull(systemName); + this.streamingCounters = checkNotNull(streamingCounters); + this.failureTracker = checkNotNull(failureTracker); + this.sourceBytesProcessCounterName = checkNotNull(sourceBytesProcessCounterName); + this.sideInputStateFetcherFactory = checkNotNull(sideInputStateFetcherFactory); + + this.multiKeyBundleOptions = checkNotNull(multiKeyBundleOptions); + + StreamingGlobalConfig config = globalConfigHandle.getConfig(); + this.operationalLimits = config.operationalLimits(); + this.windmillTagEncoding = + config.enableStateTagEncodingV2() + ? WindmillTagEncodingV2.instance() + : WindmillTagEncodingV1.instance(); } @VisibleForTesting @@ -190,6 +266,10 @@ public final long getBacklogBytes() { return backlogBytes; } + public String getSystemName() { + return systemName; + } + public long getMaxOutputKeyBytes() { return operationalLimits.getMaxOutputKeyBytes(); } @@ -203,7 +283,7 @@ public boolean throwExceptionsForLargeOutput() { } public boolean workIsFailed() { - return work != null && work.isFailed(); + return workBatchFailed.get(); } public boolean getDrainMode() { @@ -224,7 +304,7 @@ public byte[] getCurrentRecordId() { throw new RuntimeException( "Unexpected getCurrentRecordId() while offset-based deduplication is not enabled."); } - return activeReader.getCurrentRecordId(); + return checkStateNotNull(activeReader).getCurrentRecordId(); } public byte[] getCurrentRecordOffset() { @@ -232,42 +312,139 @@ public byte[] getCurrentRecordOffset() { throw new RuntimeException( "Unexpected getCurrentRecordOffset() while offset-based deduplication is not enabled."); } - return activeReader.getCurrentRecordOffset(); + return checkStateNotNull(activeReader).getCurrentRecordOffset(); + } + + /** Reset context before using it on a new bundle */ + public void reset() { + // these lists and maps are returned to callers after processing + // don't clear and reuse, instead reset the reference. + this.executedWorks = Collections.emptyList(); + this.outputBuilders = Collections.emptyList(); + this.finalizationCallbacks = Collections.emptyMap(); + // Work from prior bundles might have a reference to the old workBatchFailed. + // If the work gets retried it'll get the new workBatchFailed to notify failure. + this.workBatchFailed = new AtomicBoolean(false); + this.sideInputCache.clear(); + this.activeStateReader = null; + this.activeReader = null; + this.keyCoder = null; + this.workExecutor = null; + this.workQueueExecutor = null; + this.budgetHandle = null; + this.keyTransitionListener = null; + this.onFailedWorkHandler = null; + this.work = null; + this.key = null; + this.outputBuilder = null; + this.sideInputStateFetcher = null; + this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN; + clearSinkFullHint(); + this.stateBytesRead = 0; } public void start( - @Nullable Object key, Work work, - WindmillStateReader stateReader, - SideInputStateFetcher sideInputStateFetcher, - Windmill.WorkItemCommitRequest.Builder outputBuilder) { - this.key = key; - this.work = work; - this.computationKey = WindmillComputationKey.create(computationId, work.getShardedKey()); - this.sideInputStateFetcher = sideInputStateFetcher; + WorkExecutor workExecutor, + BoundedQueueExecutor workQueueExecutor, + BoundedQueueExecutorWorkHandle budgetHandle, + @Nullable Coder keyCoder, + KeyTransitionListener keyTransitionListener, + FailedWorkHandler onFailedWorkHandler) + throws CoderException { + reset(); + this.executedWorks = new ArrayList<>(); + this.outputBuilders = new ArrayList<>(); + this.finalizationCallbacks = new HashMap<>(); + this.keyCoder = keyCoder; + this.workExecutor = workExecutor; + this.workQueueExecutor = workQueueExecutor; + this.budgetHandle = budgetHandle; + this.keyTransitionListener = keyTransitionListener; + this.onFailedWorkHandler = checkStateNotNull(onFailedWorkHandler); + + this.workItemsPolled = 1; + this.bundleStartTimeNanos = System.nanoTime(); + StreamingGlobalConfig config = globalConfigHandle.getConfig(); // Snapshot the limits for entire bundle processing. this.operationalLimits = config.operationalLimits(); - this.windmillTagEncoding = - config.enableStateTagEncodingV2() - ? WindmillTagEncodingV2.instance() - : WindmillTagEncodingV1.instance(); - this.outputBuilder = outputBuilder; - this.sideInputCache.clear(); - clearSinkFullHint(); - Instant processingTime = computeProcessingTime(work.getWorkItem().getTimers().getTimersList()); + startForNewKey(work); + } - Collection stepContexts = getAllStepContexts(); - if (!stepContexts.isEmpty()) { - // This must be only created once for the workItem as token validation will fail if the same - // work token is reused. - WindmillStateCache.ForKey cacheForKey = - stateCache.forKey(getComputationKey(), getWorkItem().getCacheToken(), getWorkToken()); - for (StepContext stepContext : stepContexts) { - stepContext.start(stateReader, processingTime, cacheForKey, work.watermarks()); + private @Nullable Object decodeKey(Work work) throws CoderException { + // If the read output KVs, then we can decode Windmill's byte key into userland + // key object and provide it to the execution context for use with per-key state. + // Otherwise, we pass null. + // + // The coder type that will be present is: + // WindowedValueCoder(TimerOrElementCoder(KvCoder)) + if (keyCoder != null) { + try { + return keyCoder.decode(work.getWorkItem().getKey().newInput(), Coder.Context.OUTER); + } catch (CoderException e) { + throw e; + } catch (IOException e) { + throw new RuntimeException("Failed to decode key during processing", e); } } + return null; + } + + private Windmill.WorkItemCommitRequest.Builder createOutputBuilder(Work work) { + return Windmill.WorkItemCommitRequest.newBuilder() + .setKey(work.getWorkItem().getKey()) + .setShardingKey(work.getWorkItem().getShardingKey()) + .setWorkToken(work.getWorkItem().getWorkToken()) + .setCacheToken(work.getWorkItem().getCacheToken()); + } + + private void logHotKeyIfDetected(Work work, @Nullable Object decodedKey) { + if (work.getWorkItem().hasHotKeyInfo()) { + Windmill.HotKeyInfo hotKeyInfo = work.getWorkItem().getHotKeyInfo(); + Duration hotKeyAge = Duration.millis(hotKeyInfo.getHotKeyAgeUsec() / 1000); + if (decodedKey != null && hotKeyLoggingEnabled) { + hotKeyLogger.logHotKeyDetection(stepName, hotKeyAge, decodedKey); + } else { + hotKeyLogger.logHotKeyDetection(stepName, hotKeyAge); + } + } + } + + private void startStepContexts( + WindmillStateReader stateReader, + Instant processingTime, + WindmillStateCache.ForKey cacheForKey, + Watermarks watermarks) { + for (StepContext stepContext : getAllStepContexts()) { + stepContext.start(stateReader, processingTime, cacheForKey, watermarks); + } + } + + public void finishKey() { + WorkExecutor localExecutor = + checkStateNotNull(workExecutor, "workExecutor must be set before calling finishKey()"); + if (finishKeyCalled) { + return; + } + this.finishKeyCalled = true; + if (activeStateReader != null) { + this.stateBytesRead += activeStateReader.getBytesRead(); + } + if (sideInputStateFetcher != null) { + this.stateBytesRead += sideInputStateFetcher.getBytesRead(); + } + try { + localExecutor.finishKey(key); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void flushState() { + checkState(finishKeyCalled, "finishKey must be called before flushState"); + flushStateInternal(); } /** @@ -349,9 +526,9 @@ private SideInput fetchSideInput( return fetchSideInputFromWindmill( view, sideInputWindow, - checkNotNull(stateFamily), + checkStateNotNull(stateFamily), state, - checkNotNull(scopedReadStateSupplier), + checkStateNotNull(scopedReadStateSupplier), tagCache); } @@ -363,8 +540,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); @@ -381,28 +558,8 @@ private List getFiredTimers() { return getWorkItem().getTimers().getTimersList(); } - public @Nullable ByteString getSerializedKey() { - return work == null ? null : work.getWorkItem().getKey(); - } - - public WindmillComputationKey getComputationKey() { - return computationKey; - } - - public long getWorkToken() { - return getWorkItem().getWorkToken(); - } - - public Windmill.WorkItem getWorkItem() { - return checkNotNull( - work, - "work is null. A call to StreamingModeExecutionContext.start(...) is required to set" - + " work for execution.") - .getWorkItem(); - } - public Windmill.WorkItemCommitRequest.Builder getOutputBuilder() { - return outputBuilder; + return checkStateNotNull(outputBuilder); } /** @@ -421,20 +578,23 @@ public void setActiveReader(UnboundedReader reader) { /** Invalidate the state and reader caches for this computation and key. */ public void invalidateCache() { - ByteString key = getSerializedKey(); - if (key != null) { - readerCache.invalidateReader(getComputationKey()); - if (activeReader != null) { - try { - activeReader.close(); - } catch (IOException e) { - LOG.warn( - "Failed to close reader for {}-{}", computationId, getWorkItem().getShardingKey(), e); - } + for (Work w : executedWorks) { + WindmillComputationKey compKey = + WindmillComputationKey.create(computationId, w.getShardedKey()); + readerCache.invalidateReader(compKey); + stateCache.invalidate(w.getShardedKey()); + } + if (activeReader != null) { + try { + activeReader.close(); + } catch (IOException e) { + Windmill.WorkItem workItem = getWorkItem(); + long shardingKey = workItem != null ? workItem.getShardingKey() : -1L; + LOG.warn("Failed to close reader for {}-{}", systemName, shardingKey, e); } - activeReader = null; - stateCache.invalidate(key, getWorkItem().getShardingKey()); } + activeReader = null; + activeStateReader = null; } public UnboundedSource.@Nullable CheckpointMark getReaderCheckpoint( @@ -450,7 +610,7 @@ public void invalidateCache() { } } - public Map> flushState() { + private void flushStateInternal() { Map> callbacks = new HashMap<>(); for (StepContext stepContext : getAllStepContexts()) { @@ -469,15 +629,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( @@ -494,7 +655,7 @@ public Map> flushState() { @SuppressWarnings("unchecked") Coder checkpointCoder = - ((UnboundedSource) activeReader.getCurrentSource()) + ((UnboundedSource) reader.getCurrentSource()) .getCheckpointMarkCoder(); if (checkpointCoder != null) { ByteStringOutputStream stream = new ByteStringOutputStream(); @@ -504,7 +665,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."); @@ -512,26 +673,239 @@ 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. + getOutputBuilder().setSourceBacklogBytes(backlogBytes); + } + + this.finalizationCallbacks.putAll(callbacks); + + getOutputBuilder() + .setSourceBytesProcessed(computeSourceBytesProcessed(sourceBytesProcessCounterName)); + + validateCommitRequestSize(); + } + + private void validateCommitRequestSize() { + Windmill.WorkItemCommitRequest.Builder currentBuilder = getOutputBuilder(); + Work currentWork = getWork(); + long byteLimit = operationalLimits.getMaxWorkItemCommitBytes(); + Windmill.WorkItemCommitRequest commitRequest = currentBuilder.build(); + int commitSize = commitRequest.getSerializedSize(); + + // Detect overflow of integer serialized size or if the byte limit was exceeded. + // Commit is too large if overflow has occurred or the commitSize has exceeded the allowed + // commit byte limit. + int estimatedCommitSize = commitSize < 0 ? Integer.MAX_VALUE : commitSize; + streamingCounters.windmillMaxObservedWorkItemCommitBytes().addValue(estimatedCommitSize); + if (commitSize >= 0 && commitSize < byteLimit) { + return; } - return callbacks; + + KeyCommitTooLargeException e = + KeyCommitTooLargeException.causedBy( + systemName, byteLimit, commitRequest, key, hotKeyLoggingEnabled); + failureTracker.trackFailure(systemName, currentWork.getWorkItem(), e); + LOG.error("{}", e.toString()); + + // Drop the current request in favor of a new, minimal one requesting truncation. + // Messages, timers, counters, and other commit content will not be used by the service + // so, we're purposefully dropping them here + Windmill.WorkItemCommitRequest.Builder truncationBuilder = + buildWorkItemTruncationRequestBuilder(currentWork, estimatedCommitSize); + currentBuilder.clear(); + currentBuilder.mergeFrom(truncationBuilder.build()); + + // TODO: throw and retry when truncation is not on a single key bundle. + checkState( + !multiKeyBundleOptions.multiKeyBundleEnabled(), + "Commit truncation not implemented for multikey bundles"); } + private Windmill.WorkItemCommitRequest.Builder buildWorkItemTruncationRequestBuilder( + Work work, int estimatedCommitSize) { + Windmill.WorkItemCommitRequest.Builder outputBuilder = createOutputBuilder(work); + outputBuilder.setExceedsMaxWorkItemCommitBytes(true); + outputBuilder.setEstimatedWorkItemCommitBytes(estimatedCommitSize); + return outputBuilder; + } + + private final long computeSourceBytesProcessed(String sourceBytesCounterName) { + if (!(workExecutor instanceof DataflowMapTaskExecutor)) { + return 0L; + } + HashMap counters = + ((DataflowMapTaskExecutor) workExecutor) + .getReadOperation() + .receivers[0] + .getOutputCounters(); + + return Optional.ofNullable(counters.get(sourceBytesCounterName)) + .map(counter -> ((OutputObjectAndByteCounter) counter).getByteCount().getAndReset()) + .orElse(0L); + } + + public boolean advance() throws CoderException { + if (!multiKeyBundleOptions.multiKeyBundleEnabled()) { + return false; + } + + Work activeWork = checkStateNotNull(work); + BoundedQueueExecutor executor = checkStateNotNull(workQueueExecutor); + BoundedQueueExecutorWorkHandle handle = checkStateNotNull(budgetHandle); + + if (workIsFailed()) { + throw new WorkItemCancelledException(activeWork.getWorkItem().getShardingKey()); + } + + if (activeWork.getKeyGroup().equals(Work.KeyGroup.DEFAULT) || shouldStopBatching()) { + return false; + } + + @Nullable + ExecutableWork additionalWork = + executor.pollWork( + computationId, + activeWork.getKeyGroup(), + handle, + checkStateNotNull(onFailedWorkHandler)); + if (additionalWork != null) { + flushStateInternal(); + Work newWork = additionalWork.work(); + ++workItemsPolled; + startForNewKey(newWork); + return true; + } + + return false; + } + + private boolean shouldStopBatching() { + // TODO: stop batching if the previous work item requested truncation + if (workItemsPolled >= multiKeyBundleOptions.maxKeyGroupBatchSize()) { + return true; + } + long elapsedNanos = System.nanoTime() - bundleStartTimeNanos; + if (elapsedNanos >= multiKeyBundleOptions.maxKeyGroupBatchTimeNanos()) { + return true; + } + return getBytesSinked() >= multiKeyBundleOptions.maxKeyGroupBatchSinkBytes(); + } + + private void startForNewKey(Work newWork) throws CoderException { + newWork.setState(Work.State.PROCESSING); + if (keyTransitionListener != null && this.work != null && this.work != newWork) { + keyTransitionListener.onKeyTransition(this.work, newWork); + } + this.key = decodeKey(newWork); + this.work = newWork; + this.finishKeyCalled = false; + this.computationKey = WindmillComputationKey.create(computationId, newWork.getShardedKey()); + + this.outputBuilder = createOutputBuilder(newWork); + this.outputBuilders.add(this.outputBuilder); + newWork.setOnFailureListener(this.workBatchFailed); + this.executedWorks.add(newWork); + + logHotKeyIfDetected(newWork, this.key); + + this.sideInputStateFetcher = + sideInputStateFetcherFactory.createSideInputStateFetcher(newWork::fetchSideInput); + this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN; + this.activeReader = null; + + // Note: We do NOT clear sideInputCache here, allowing Key B to reuse warm side inputs! + + // Re-initialize state cache and state/timer internals across all step contexts + Instant processingTime = + computeProcessingTime(newWork.getWorkItem().getTimers().getTimersList()); + if (getAllStepContexts().isEmpty()) { + checkState(this.activeStateReader == null); + } else { + // This must be only created once for a workItem as token validation will fail if the same + // work token is reused. + WindmillStateCache.ForKey cacheForKey = + stateCache.forKey( + getComputationKey(), newWork.getWorkItem().getCacheToken(), getWorkToken()); + this.activeStateReader = newWork.createWindmillStateReader(this::workIsFailed); + startStepContexts(this.activeStateReader, processingTime, cacheForKey, newWork.watermarks()); + } + } + + // Returns state bytes read during the bundle execution + public long getStateBytesRead() { + return stateBytesRead; + } + + // Returns list of commit requests from the bundle + public List getWorkItemCommits() { + List commits = new ArrayList<>(outputBuilders.size()); + for (Windmill.WorkItemCommitRequest.Builder builder : outputBuilders) { + commits.add(builder.build()); + } + return commits; + } + + // Returns list of Work that was executed in the bundle + public List getExecutedWorks() { + return executedWorks; + } + + // Returns finalization callbacks recorded during the bundle execution + public Map> getFinalizationCallbacks() { + return finalizationCallbacks; + } + + // Returns the current key being processed or null if an unkeyed stage. + public @Nullable Object getKey() { + return key; + } + + // Returns the current Work being processed. + public Work getWork() { + return checkStateNotNull( + work, + "work is null. A call to StreamingModeExecutionContext.start(...) is required to set" + + " work for execution."); + } + + // Returns the serialized windmill key for the current Work + public @Nullable ByteString getSerializedKey() { + return work == null ? null : work.getWorkItem().getKey(); + } + + // Returns the serialized windmill key for the current Work + public WindmillComputationKey getComputationKey() { + return checkStateNotNull(computationKey); + } + + // Returns the windmill work token for the current Work + public long getWorkToken() { + return getWorkItem().getWorkToken(); + } + + // Returns the windmill WorkItem proto for the current Work + public Windmill.WorkItem getWorkItem() { + return getWork().getWorkItem(); + } + + @Nullable String getStateFamily(NameContext nameContext) { return nameContext.userName() == null ? null : stateNameMap.get(nameContext.userName()); } @@ -573,7 +947,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); @@ -616,14 +990,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; @@ -636,9 +1012,9 @@ private ScopedReadStateSupplier( } @Override - public @Nullable Closeable get() { + public Closeable get() { if (stateTracker == null) { - return null; + return NO_OP_CLOSEABLE; } return stateTracker.enterState(readState); } @@ -699,7 +1075,7 @@ public TimerInternals timerInternals() { } @Override - public TimerData getNextFiredTimer(Coder windowCoder) { + public @Nullable TimerData getNextFiredTimer(Coder windowCoder) { return wrapped.getNextFiredUserTimer(windowCoder); } @@ -726,6 +1102,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. */ @@ -746,7 +1127,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"); } @@ -779,31 +1160,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()); } @@ -814,52 +1196,86 @@ 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); + } + poisonStateAndTimerInternals(); + } + + /** + * Poisons the state and timer internals to prevent any subsequent (stale) usage. + * + *

This ensures that if these key-specific internals are incorrectly cached and used after + * the key's execution context has finished, it will fail fast with a clear error rather than + * silently corrupting state. + */ + private void poisonStateAndTimerInternals() { + if (stateInternals != null) { + stateInternals.poison(); + } + if (systemTimerInternals != null) { + systemTimerInternals.poison(); + } + if (userTimerInternals != null) { + userTimerInternals.poison(); + } } @Override - public TimerData getNextFiredTimer(Coder windowCoder) { - if (cachedFiredSystemTimers == null) { - cachedFiredSystemTimers = + public void setBacklogBytes(double backlogBytes) { + StreamingModeExecutionContext.this.backlogBytes = (long) backlogBytes; + } + + @Override + 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( @@ -871,16 +1287,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; } @@ -914,12 +1331,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)) @@ -933,17 +1357,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 @@ -951,15 +1378,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; } } @@ -993,12 +1420,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, @@ -1007,7 +1428,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"); } @@ -1017,7 +1439,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() @@ -1029,7 +1451,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. */ @@ -1044,11 +1466,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. */ @@ -1061,14 +1484,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 @@ -1077,8 +1498,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/main/java/org/apache/beam/runners/dataflow/worker/StreamingPCollectionViewWriterParDoFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingPCollectionViewWriterParDoFn.java index 61730b0c8d88..6b51427bb930 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingPCollectionViewWriterParDoFn.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingPCollectionViewWriterParDoFn.java @@ -73,6 +73,9 @@ public void processElement(Object element) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() throws Exception {} 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..ef1a5922fe5c 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,14 +17,14 @@ */ 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; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.WindowedValue; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** @@ -37,43 +37,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); } } @@ -91,10 +81,15 @@ public void onTimer( "Attempt to deliver a timer to a DoFn, but timers are not supported in Dataflow."); } + @Override + public void finishKey(KeyT key) { + simpleDoFnRunner.finishKey(key); + } + @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/ToIsmRecordForMultimapDoFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ToIsmRecordForMultimapDoFnFactory.java index f9e2d6de2461..261ed69fb5d7 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ToIsmRecordForMultimapDoFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ToIsmRecordForMultimapDoFnFactory.java @@ -149,6 +149,9 @@ public void processElement(Object untypedElem) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() {} 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..eade6a074439 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 @@ -21,6 +21,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; import java.io.IOException; import java.io.InputStream; import java.util.Collection; @@ -39,7 +40,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; @@ -111,20 +111,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 @@ -142,6 +134,7 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce */ CausedByDrain drainingValueFromUpstream = CausedByDrain.NORMAL; ValueKind valueKind = ValueKind.INSERT; + Context openTelemetryContext = null; if (WindowedValues.WindowedValueCoder.isMetadataSupported()) { BeamFnApi.Elements.ElementMetadata elementMetadata = WindmillSink.decodeAdditionalMetadata(windowsCoder, message.getMetadata()); @@ -149,7 +142,8 @@ 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()); + openTelemetryContext = WindmillOpenTelemetryContextPropagator.read(elementMetadata); } if (valueCoder instanceof KvCoder) { KvCoder kvCoder = (KvCoder) valueCoder; @@ -168,7 +162,7 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce null, null, drainingValueFromUpstream, - null, + openTelemetryContext, valueKind); } else { notifyElementRead(data.available() + metadata.available()); @@ -181,7 +175,7 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce null, null, drainingValueFromUpstream, - null, + openTelemetryContext, valueKind); } } 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/main/java/org/apache/beam/runners/dataflow/worker/ValuesDoFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ValuesDoFnFactory.java index 3ddb3c2003db..acf143a5467b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ValuesDoFnFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/ValuesDoFnFactory.java @@ -80,6 +80,9 @@ public void processElement(Object untypedElem) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(Object key) throws Exception {} + @Override public void finishBundle() {} 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..290c25836f63 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 @@ -19,6 +19,7 @@ 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.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -43,7 +44,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; @@ -140,6 +140,16 @@ public Iterable timersIterable() { } private @Nullable WindowedValue parseElem(Windmill.Message message) { + return parseElemInternal(message, true); + } + + private @Nullable WindowedValue parseElemWindowOnly(Windmill.Message message) { + return parseElemInternal(message, false); + } + + @SuppressWarnings("nullness") + private @Nullable WindowedValue parseElemInternal( + Windmill.Message message, boolean parseValue) { try { Instant timestamp = WindmillTimeUtils.windmillToHarnessTimestamp(message.getTimestamp()); Collection windows = @@ -151,6 +161,7 @@ public Iterable timersIterable() { */ CausedByDrain drainingValueFromUpstream = CausedByDrain.NORMAL; ValueKind valueKind = ValueKind.INSERT; + Context openTelemetryContext = null; if (WindowedValues.WindowedValueCoder.isMetadataSupported()) { BeamFnApi.Elements.ElementMetadata elementMetadata = WindmillSink.decodeAdditionalMetadata(windowsCoder, message.getMetadata()); @@ -158,10 +169,14 @@ 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()); + openTelemetryContext = WindmillOpenTelemetryContextPropagator.read(elementMetadata); + } + ElemT value = null; + if (parseValue) { + InputStream inputStream = message.getData().newInput(); + value = valueCoder.decode(inputStream, Coder.Context.OUTER); } - InputStream inputStream = message.getData().newInput(); - ElemT value = valueCoder.decode(inputStream, Coder.Context.OUTER); return WindowedValues.of( value, timestamp, @@ -170,7 +185,7 @@ public Iterable timersIterable() { null, null, drainingValueFromUpstream, - null, + openTelemetryContext, valueKind); } catch (RuntimeException | IOException e) { if (!skipUndecodableElements) { @@ -185,6 +200,17 @@ public Iterable timersIterable() { } } + @Override + @SuppressWarnings({"nullness", "unchecked"}) + public Iterable> elementWindowsIterable() { + return (Iterable>) + (Iterable) + FluentIterable.from(workItem.getMessageBundlesList()) + .transformAndConcat(Windmill.InputMessageBundle::getMessagesList) + .transform(this::parseElemWindowOnly) + .filter(Objects::nonNull); + } + @Override @SuppressWarnings("nullness") public Iterable> elementsIterable() { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillOpenTelemetryContextPropagator.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillOpenTelemetryContextPropagator.java new file mode 100644 index 000000000000..78543ce6a2ee --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillOpenTelemetryContextPropagator.java @@ -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. + */ +package org.apache.beam.runners.dataflow.worker; + +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.sdk.annotations.Internal; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.checkerframework.checker.nullness.qual.Nullable; + +@Internal +public class WindmillOpenTelemetryContextPropagator { + + public static final String TRACEPARENT = "traceparent"; + public static final String TRACESTATE = "tracestate"; + 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.equalsIgnoreCase(key)) { + return carrier.getTraceparent(); + } else if (TRACESTATE.equalsIgnoreCase(key)) { + return carrier.getTracestate(); + } + return null; + } + }; + + public static void set(Context from, BeamFnApi.Elements.ElementMetadata.Builder builder) { + W3CTraceContextPropagator.getInstance().inject(from, builder, SETTER); + } + + public static Context read(BeamFnApi.Elements.ElementMetadata from) { + return W3CTraceContextPropagator.getInstance().extract(Context.root(), from, GETTER); + } +} 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..134655a72a54 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,17 +34,21 @@ */ public abstract class WindmillReaderIteratorBase extends NativeReader.NativeReaderIterator> { - private final Windmill.WorkItem work; - private int bundleIndex = 0; - private int messageIndex = -1; + private final StreamingModeExecutionContext context; + private Windmill.WorkItem work; + private int bundleIndex; + private int messageIndex; private @Nullable WindowedValue current = null; private final ValueProvider skipUndecodableElements; 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(); + this.bundleIndex = 0; + this.messageIndex = -1; } @Override @@ -54,11 +58,26 @@ public boolean start() throws IOException { @Override public boolean advance() throws IOException { + if (context.workIsFailed()) { + throw new WorkItemCancelledException(checkNotNull(context.getWorkItem()).getShardingKey()); + } + while (true) { if (bundleIndex >= work.getMessageBundlesCount()) { + // If elements are exhausted, try advancing the execution context to the next key in the + // group + context.finishKey(); + if (context.advance()) { + // Transition succeeded! Update iterator references to the new work item + resetWorkFromContext(); + continue; + } + + // All work items are exhausted. current = null; return false; } + Windmill.InputMessageBundle bundle = work.getMessageBundles(bundleIndex); ++messageIndex; if (messageIndex >= bundle.getMessagesCount()) { @@ -66,6 +85,7 @@ public boolean advance() throws IOException { ++bundleIndex; continue; } + try { current = checkNotNull(decodeMessage(bundle.getMessages(messageIndex))); return true; @@ -84,6 +104,12 @@ public boolean advance() throws IOException { } } + private void resetWorkFromContext() { + this.work = context.getWorkItem(); + this.bundleIndex = 0; + this.messageIndex = -1; + } + protected abstract WindowedValue decodeMessage(Windmill.Message message) throws IOException; @Override 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..9d8a0f0da309 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 @@ -21,6 +21,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -220,16 +221,27 @@ public long add(WindowedValue data) throws IOException { ByteString key, value; ByteString id = ByteString.EMPTY; // todo #33176 specify additional metadata in the future - BeamFnApi.Elements.ElementMetadata additionalMetadata = - BeamFnApi.Elements.ElementMetadata.newBuilder() - .setDrain( - data.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN - ? BeamFnApi.Elements.DrainMode.Enum.DRAINING - : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) - .build(); + BeamFnApi.Elements.ElementMetadata.Builder additionalMetadataBuilder = + BeamFnApi.Elements.ElementMetadata.newBuilder(); + additionalMetadataBuilder + .setDrain( + data.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN + ? BeamFnApi.Elements.DrainMode.Enum.DRAINING + : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) + .setValueKind(WindmillValueKindHelper.toProto(data.getValueKind())); + Context openTelemetryContext = data.getOpenTelemetryContext(); + if (openTelemetryContext != null) { + // TODO replace with OpenTelemetryContextPropagator + WindmillOpenTelemetryContextPropagator.set(openTelemetryContext, additionalMetadataBuilder); + } + ByteString metadata = encodeMetadata( - stream, windowsCoder, data.getWindows(), data.getPaneInfo(), additionalMetadata); + stream, + windowsCoder, + data.getWindows(), + data.getPaneInfo(), + additionalMetadataBuilder.build()); if (valueCoder instanceof KvCoder) { KvCoder kvCoder = (KvCoder) valueCoder; KV kv = checkNotNull((KV) data.getValue()); @@ -253,20 +265,26 @@ public long add(WindowedValue data) throws IOException { } if (key.size() > context.getMaxOutputKeyBytes()) { if (context.throwExceptionsForLargeOutput()) { - throw new OutputTooLargeException("Key too large: " + key.size()); + throw new OutputTooLargeException( + String.format( + "Key for fused stage %s too large: %s", context.getSystemName(), key.size())); } else { LOG.error( - "Trying to output too large key with size {}. Limit is {}. See https://cloud.google.com/dataflow/docs/guides/common-errors#key-commit-too-large-exception. Running with --experiments=throw_exceptions_on_large_output will instead throw an OutputTooLargeException which may be caught in user code.", + "Trying to output too large key for fused stage {} with size {}. Limit is {}. See https://cloud.google.com/dataflow/docs/guides/common-errors#key-commit-too-large-exception. Running with --experiments=throw_exceptions_on_large_output will instead throw an OutputTooLargeException which may be caught in user code.", + context.getSystemName(), key.size(), context.getMaxOutputKeyBytes()); } } if (value.size() > context.getMaxOutputValueBytes()) { if (context.throwExceptionsForLargeOutput()) { - throw new OutputTooLargeException("Value too large: " + value.size()); + throw new OutputTooLargeException( + String.format( + "Value for fused stage %s too large: %s", context.getSystemName(), value.size())); } else { LOG.error( - "Trying to output too large value with size {}. Limit is {}. See https://cloud.google.com/dataflow/docs/guides/common-errors#key-commit-too-large-exception. Running with --experiments=throw_exceptions_on_large_output will instead throw an OutputTooLargeException which may be caught in user code.", + "Trying to output too large value for fused stage {} with size {}. Limit is {}. See https://cloud.google.com/dataflow/docs/guides/common-errors#key-commit-too-large-exception. Running with --experiments=throw_exceptions_on_large_output will instead throw an OutputTooLargeException which may be caught in user code.", + context.getSystemName(), value.size(), context.getMaxOutputValueBytes()); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillTimerInternals.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillTimerInternals.java index 07ce62d59339..9b8757769db0 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillTimerInternals.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillTimerInternals.java @@ -64,6 +64,19 @@ class WindmillTimerInternals implements TimerInternals { private final Consumer onTimerModified; private final WindmillTagEncoding windmillTagEncoding; + private boolean poisoned = false; + + public void poison() { + this.poisoned = true; + } + + private void checkNotPoisoned() { + if (poisoned) { + throw new IllegalStateException( + "WindmillTimerInternals is poisoned and cannot be used after flushState()."); + } + } + public WindmillTimerInternals( String stateFamily, // unique identifies a step WindmillTimerType type, @@ -80,12 +93,14 @@ public WindmillTimerInternals( } public WindmillTimerInternals withType(WindmillTimerType type) { + checkNotPoisoned(); return new WindmillTimerInternals( stateFamily, type, processingTime, watermarks, windmillTagEncoding, onTimerModified); } @Override public void setTimer(TimerData timerKey) { + checkNotPoisoned(); String timerDataKey = getTimerDataKey(timerKey.getTimerId(), timerKey.getTimerFamilyId()); timerMap.put( new SimpleEntry<>(timerDataKey, timerKey.getNamespace()), @@ -101,6 +116,7 @@ public void setTimer( Instant timestamp, Instant outputTimestamp, TimeDomain timeDomain) { + checkNotPoisoned(); TimerData timer = TimerData.of( timerId, @@ -124,6 +140,7 @@ private static String getTimerDataKey(String timerId, String timerFamilyId) { @Override public void deleteTimer(TimerData timerKey) { + checkNotPoisoned(); String timerDataKey = getTimerDataKey(timerKey.getTimerId(), timerKey.getTimerFamilyId()); timerMap.put( new SimpleEntry<>(timerDataKey, timerKey.getNamespace()), @@ -139,6 +156,7 @@ public void deleteTimer(StateNamespace namespace, String timerId, String timerFa @Override public void deleteTimer( StateNamespace namespace, String timerId, String timerFamilyId, TimeDomain timeDomain) { + checkNotPoisoned(); deleteTimer( TimerData.of( timerId, @@ -152,12 +170,14 @@ public void deleteTimer( @Override public Instant currentProcessingTime() { + checkNotPoisoned(); Instant now = Instant.now(); return processingTime.isAfter(now) ? processingTime : now; } @Override public @Nullable Instant currentSynchronizedProcessingTime() { + checkNotPoisoned(); return watermarks.synchronizedProcessingTime(); } @@ -172,6 +192,7 @@ public Instant currentProcessingTime() { */ @Override public Instant currentInputWatermarkTime() { + checkNotPoisoned(); return watermarks.inputDataWatermark(); } @@ -186,10 +207,12 @@ public Instant currentInputWatermarkTime() { */ @Override public @Nullable Instant currentOutputWatermarkTime() { + checkNotPoisoned(); return watermarks.outputDataWatermark(); } public void persistTo(Windmill.WorkItemCommitRequest.Builder outputBuilder) { + checkNotPoisoned(); for (Entry value : timerMap.values()) { // Regardless of whether it is set or not, it must have some TimerData stored so we // can know its time domain 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/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..c258e25146e4 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 @@ -30,7 +30,6 @@ import org.apache.beam.runners.dataflow.util.CloudObject; import org.apache.beam.runners.dataflow.worker.util.ValueInEmptyWindows; import org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader; -import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem; import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.options.PipelineOptions; @@ -49,7 +48,6 @@ @Internal class WindowingWindmillReader extends NativeReader>> { - private final Coder keyCoder; private final Coder valueCoder; private final Coder windowCoder; private final Coder> windowsCoder; @@ -66,7 +64,6 @@ class WindowingWindmillReader extends NativeReader keyedWorkItemCoder = (WindmillKeyedWorkItem.FakeKeyedWorkItemCoder) inputCoder.getValueCoder(); - this.keyCoder = keyedWorkItemCoder.getKeyCoder(); this.valueCoder = keyedWorkItemCoder.getElementCoder(); this.context = context; this.skipUndecodableElements = skipUndecodableElements; @@ -129,71 +126,77 @@ public static WindowingWindmillReader create( return new WindowingWindmillReader<>(coder, context, skipUndecodableElements); } + private KeyedWorkItem createKeyedWorkItem() { + @SuppressWarnings("unchecked") + @Nullable + K key = (K) context.getKey(); + return new WindmillKeyedWorkItem<>( + key, + context.getWorkItem(), + windowCoder, + windowsCoder, + valueCoder, + context.getWindmillTagEncoding(), + context.getDrainMode(), + skipUndecodableElements.isAccessible() + && Boolean.TRUE.equals(skipUndecodableElements.get())); + } + + private boolean isEmpty(KeyedWorkItem keyedWorkItem) { + return Iterables.isEmpty(keyedWorkItem.timersIterable()) + && Iterables.isEmpty(keyedWorkItem.elementsIterable()); + } + @Override public NativeReaderIterator>> iterator() throws IOException { - final K key = - keyCoder.decode( - checkStateNotNull(context.getSerializedKey()).newInput(), Coder.Context.OUTER); - final WorkItem workItem = context.getWorkItem(); - KeyedWorkItem keyedWorkItem = - new WindmillKeyedWorkItem<>( - key, - workItem, - windowCoder, - windowsCoder, - valueCoder, - context.getWindmillTagEncoding(), - context.getDrainMode(), - skipUndecodableElements.isAccessible() - && Boolean.TRUE.equals(skipUndecodableElements.get())); - final boolean isEmptyWorkItem = - (Iterables.isEmpty(keyedWorkItem.timersIterable()) - && Iterables.isEmpty(keyedWorkItem.elementsIterable())); - final WindowedValue> value = new ValueInEmptyWindows<>(keyedWorkItem); - - // Return a noop iterator when current workitem is an empty workitem. - if (isEmptyWorkItem) { - return new NativeReaderIterator>>() { - @Override - public boolean start() throws IOException { - return false; + return new NativeReaderIterator>>() { + private @Nullable WindowedValue> current = null; + + @Override + public boolean start() throws IOException { + if (context.workIsFailed()) { + throw new WorkItemCancelledException( + checkStateNotNull(context.getWorkItem()).getShardingKey()); } - - @Override - public boolean advance() throws IOException { - return false; + KeyedWorkItem firstKeyedWorkItem = createKeyedWorkItem(); + if (isEmpty(firstKeyedWorkItem)) { + return advance(); // Try to transition immediately if the first key is empty! } - - @Override - public WindowedValue> getCurrent() { - throw new NoSuchElementException(); - } - }; - } else { - return new NativeReaderIterator>>() { - private @Nullable WindowedValue> current = null; - - @Override - public boolean start() throws IOException { - current = value; - return true; + current = new ValueInEmptyWindows<>(firstKeyedWorkItem); + return true; + } + + @Override + public boolean advance() throws IOException { + if (context.workIsFailed()) { + throw new WorkItemCancelledException( + checkStateNotNull(context.getWorkItem()).getShardingKey()); } - @Override - public boolean advance() throws IOException { + while (true) { + context.finishKey(); + if (context.advance()) { + KeyedWorkItem newKeyedWorkItem = createKeyedWorkItem(); + if (isEmpty(newKeyedWorkItem)) { + continue; + } + current = new ValueInEmptyWindows<>(newKeyedWorkItem); + return true; + } + current = null; return false; } + } - @Override - public WindowedValue> getCurrent() { - if (current == null) { - throw new NoSuchElementException(); - } - return value; + @Override + public WindowedValue> getCurrent() { + if (current == null) { + throw new NoSuchElementException(); } - }; - } + return current; + } + }; } @Override diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/KeyTokenInvalidException.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkCancellingException.java similarity index 50% rename from runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/KeyTokenInvalidException.java rename to runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkCancellingException.java index 29b16b71883f..9cb4a1c0a5be 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/KeyTokenInvalidException.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkCancellingException.java @@ -17,21 +17,31 @@ */ package org.apache.beam.runners.dataflow.worker; -import javax.annotation.Nullable; +import org.checkerframework.checker.nullness.qual.Nullable; -/** Indicates that the key token was invalid when data was attempted to be fetched. */ -public class KeyTokenInvalidException extends RuntimeException { - public KeyTokenInvalidException(String key) { - super("Unable to fetch data due to token mismatch for key " + key); +/** + * Indicates that the work is no longer valid and should be canceled. It is thrown as a signal for + * upper layers to mark the work as failed. This is different from WorkItemCancelledException, which + * is thrown after marking the work as failed. + */ +public class WorkCancellingException extends RuntimeException { + + public WorkCancellingException(long sharding_key) { + super("Work cancelling exception for key " + sharding_key); + } + + public WorkCancellingException(Throwable cause) { + super(cause); } - /** Returns whether an exception was caused by a {@link KeyTokenInvalidException}. */ - public static boolean isKeyTokenInvalidException(@Nullable Throwable t) { - while (t != null) { - if (t instanceof KeyTokenInvalidException) { + /** Returns whether an exception was caused by a {@link WorkCancellingException}. */ + public static boolean isWorkCancellingException(Throwable t) { + @Nullable Throwable throwable = t; + while (throwable != null) { + if (throwable instanceof WorkCancellingException) { return true; } - t = t.getCause(); + throwable = throwable.getCause(); } return false; } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkItemCancelledException.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkItemCancelledException.java index a12a5075c5ee..8053767491bd 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkItemCancelledException.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkItemCancelledException.java @@ -17,31 +17,14 @@ */ package org.apache.beam.runners.dataflow.worker; -/** Indicates that the work item was cancelled and should not be retried. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) +/** + * Indicates that the work item was canceled. When this is thrown, the work is already marked as + * failed. This is different from WorkItemCancellingException which is thrown before marking work as + * failed. + */ public class WorkItemCancelledException extends RuntimeException { + public WorkItemCancelledException(long sharding_key) { super("Work item cancelled for key " + sharding_key); } - - public WorkItemCancelledException(String message, Throwable cause) { - super(message, cause); - } - - public WorkItemCancelledException(Throwable cause) { - super(cause); - } - - /** Returns whether an exception was caused by a {@link WorkItemCancelledException}. */ - public static boolean isWorkItemCancelledException(Throwable t) { - while (t != null) { - if (t instanceof WorkItemCancelledException) { - return true; - } - t = t.getCause(); - } - 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..3220dbade61c 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(Object key) 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/logging/DataflowWorkerLoggingHandler.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingHandler.java index 62057c22b8d4..23d93a551baa 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingHandler.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingHandler.java @@ -43,6 +43,8 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.protobuf.Struct; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -147,6 +149,8 @@ static ResourceBundle resourceBundleForNonDirectLogLevelHint(Level nonDirectLogL /** If true, add SLF4J MDC to custom_data of the log message. */ private final AtomicBoolean logCustomMdc = new AtomicBoolean(false); + private final AtomicBoolean logOpenTelemetryTraceSpanIdAndSampled = new AtomicBoolean(false); + // Only instantiated and set if enableDirectLogging is called. private static class DirectLoggingState { DirectLoggingState( @@ -250,6 +254,10 @@ public void setLogMdc(boolean enabled) { logCustomMdc.set(enabled); } + public void setLogOpenTelemetryTraceAndSpanId(boolean enabled) { + logOpenTelemetryTraceSpanIdAndSampled.set(enabled); + } + private static Pair, ImmutableMap> labelsFromOptionsAndMetadata(PipelineOptions options) { DataflowPipelineOptions dataflowOptions = options.as(DataflowPipelineOptions.class); @@ -350,7 +358,8 @@ LogEntry constructDirectLogEntry( addLogField( payloadBuilder, "exception", formatException(record.getThrown()), MESSAGE_MAX_LENGTH); addLogField(payloadBuilder, "thread", String.valueOf(record.getThreadID()), FIELD_MAX_LENGTH); - addLogField(payloadBuilder, "stage", DataflowWorkerLoggingMDC.getStageName(), FIELD_MAX_LENGTH); + addLogField( + payloadBuilder, "stage", DataflowWorkerLoggingMDC.getSystemStageName(), FIELD_MAX_LENGTH); addLogField(payloadBuilder, "worker", DataflowWorkerLoggingMDC.getWorkerId(), FIELD_MAX_LENGTH); addLogField(payloadBuilder, "work", DataflowWorkerLoggingMDC.getWorkId(), FIELD_MAX_LENGTH); addLogField(payloadBuilder, "job", DataflowWorkerLoggingMDC.getJobId(), FIELD_MAX_LENGTH); @@ -384,7 +393,15 @@ LogEntry constructDirectLogEntry( LogEntry.newBuilder(Payload.JsonPayload.of(payloadBuilder.build())) .setTimestamp(Instant.ofEpochMilli(record.getMillis())) .setSeverity(severityFor(record.getLevel())); - + if (logOpenTelemetryTraceSpanIdAndSampled.get()) { + SpanContext spanContext = Span.current().getSpanContext(); + if (spanContext.isValid()) { + builder = builder.setTrace(spanContext.getTraceId()).setSpanId(spanContext.getSpanId()); + if (spanContext.isSampled()) { + builder = builder.setTraceSampled(spanContext.isSampled()); + } + } + } if (stepId != null) { builder.setResource( MonitoredResource.newBuilder(RESOURCE_TYPE) @@ -593,7 +610,7 @@ public synchronized void publishToDisk( writeIfNotEmpty(generator, "message", getFormatter().formatMessage(record)); writeIfNotEmpty(generator, "thread", String.valueOf(record.getThreadID())); writeIfNotEmpty(generator, "job", DataflowWorkerLoggingMDC.getJobId()); - writeIfNotEmpty(generator, "stage", DataflowWorkerLoggingMDC.getStageName()); + writeIfNotEmpty(generator, "stage", DataflowWorkerLoggingMDC.getSystemStageName()); if (currentExecutionState != null) { NameContext nameContext = currentExecutionState.getStepName(); @@ -605,6 +622,18 @@ public synchronized void publishToDisk( writeIfNotEmpty(generator, "work", DataflowWorkerLoggingMDC.getWorkId()); writeIfNotEmpty(generator, "logger", record.getLoggerName()); writeIfNotEmpty(generator, "exception", formatException(record.getThrown())); + + if (logOpenTelemetryTraceSpanIdAndSampled.get()) { + SpanContext spanContext = Span.current().getSpanContext(); + if (spanContext.isValid()) { + generator.writeStringField("trace", spanContext.getTraceId()); + generator.writeStringField("spanId", spanContext.getSpanId()); + if (spanContext.isSampled()) { + generator.writeBooleanField("trace_sampled", spanContext.isSampled()); + } + } + } + if (logCustomMdc.get()) { @Nullable Map mdcMap = MDC.getCopyOfContextMap(); if (mdcMap != null && !mdcMap.isEmpty()) { 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..1627c96a3427 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)); } @@ -385,6 +383,10 @@ public static synchronized void configure(DataflowWorkerLoggingOptions options) loggingHandler.setLogMdc(true); } + if (harnessOptions.getLogOpenTelemetryTraceAndSpanId()) { + loggingHandler.setLogOpenTelemetryTraceAndSpanId(true); + } + if (usedDeprecated) { LOG.warn( "Deprecated DataflowWorkerLoggingOptions are used for log level settings." diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingMDC.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingMDC.java index 508ef6f4169f..38518a70ca6b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingMDC.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingMDC.java @@ -25,7 +25,8 @@ }) public class DataflowWorkerLoggingMDC { private static final InheritableThreadLocal jobId = new InheritableThreadLocal<>(); - private static final InheritableThreadLocal stageName = new InheritableThreadLocal<>(); + private static final InheritableThreadLocal systemStageName = + new InheritableThreadLocal<>(); private static final InheritableThreadLocal workerId = new InheritableThreadLocal<>(); private static final InheritableThreadLocal workId = new InheritableThreadLocal<>(); private static final InheritableThreadLocal sdkHarnessId = new InheritableThreadLocal<>(); @@ -35,9 +36,9 @@ public static void setJobId(String newJobId) { jobId.set(newJobId); } - /** Sets the Stage Name of the current thread, which will be inherited by child threads. */ - public static void setStageName(@Nullable String newStageName) { - stageName.set(newStageName); + /** Sets the System Stage Name of the current thread, which will be inherited by child threads. */ + public static void setSystemStageName(@Nullable String newSystemStageName) { + systemStageName.set(newSystemStageName); } /** Sets the Worker ID of the current thread, which will be inherited by child threads. */ @@ -60,9 +61,9 @@ public static String getJobId() { return jobId.get(); } - /** Gets the Stage Name of the current thread. */ - public static String getStageName() { - return stageName.get(); + /** Gets the System Stage Name of the current thread. */ + public static String getSystemStageName() { + return systemStageName.get(); } /** Gets the Worker ID of the current thread. */ diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkState.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkState.java index e430f6c8f638..1e9c8629b9ff 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkState.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkState.java @@ -27,7 +27,6 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Queue; -import java.util.function.BiConsumer; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; @@ -39,7 +38,6 @@ 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.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.slf4j.Logger; @@ -88,6 +86,11 @@ static ActiveWorkState create(WindmillStateCache.ForComputation computationState return new ActiveWorkState(new HashMap<>(), computationStateCache); } + synchronized @Nullable ExecutableWork getActiveWork(ShardedKey shardedKey, WorkId workId) { + LinkedHashMap workQueue = activeWork.get(shardedKey.shardingKey()); + return workQueue == null ? null : workQueue.get(workId); + } + @VisibleForTesting static ActiveWorkState forTesting( Map> activeWork, @@ -261,46 +264,28 @@ private synchronized Optional getNextWork( return nextWork; } - /** - * Invalidates all {@link Work} that is in the {@link Work.State#COMMITTING} state which started - * before the stuckCommitDeadline. - */ - synchronized void invalidateStuckCommits( - Instant stuckCommitDeadline, BiConsumer shardedKeyAndWorkTokenConsumer) { - for (Entry shardedKeyAndWorkId : - getStuckCommitsAt(stuckCommitDeadline).entrySet()) { - ShardedKey shardedKey = shardedKeyAndWorkId.getKey(); - WorkId workId = shardedKeyAndWorkId.getValue(); - computationStateCache.invalidate(shardedKey.key(), shardedKey.shardingKey()); - shardedKeyAndWorkTokenConsumer.accept(shardedKey, workId); - } - } - private static @Nullable ExecutableWork firstValue(Map map) { Iterator> iterator = map.entrySet().iterator(); return iterator.hasNext() ? iterator.next().getValue() : null; } - private synchronized ImmutableMap getStuckCommitsAt( - Instant stuckCommitDeadline) { - // Determine the stuck commit keys but complete them outside the loop iterating over - // activeWork as completeWork may delete the entry from activeWork. - ImmutableMap.Builder stuckCommits = ImmutableMap.builder(); + /** + * Returns true if there is any {@link Work} in the {@link Work.State#COMMITTING} state which + * started before the stuckCommitDeadline. + */ + synchronized boolean hasStuckCommits(Instant stuckCommitDeadline) { for (Entry> entry : activeWork.entrySet()) { @Nullable ExecutableWork executableWork = firstValue(entry.getValue()); - if (executableWork != null) { - Work work = executableWork.work(); - if (work.isStuckCommittingAt(stuckCommitDeadline)) { - LOG.error( - "Detected key {} stuck in COMMITTING state since {}, completing it with error.", - work.getShardedKey(), - work.getStateStartTime()); - stuckCommits.put(work.getShardedKey(), work.id()); - } + if (executableWork != null + && executableWork.work().isStuckCommittingAt(stuckCommitDeadline)) { + LOG.warn( + "Detected key {} stuck in COMMITTING state since {}", + executableWork.work().getShardedKey(), + executableWork.work().getStateStartTime()); + return true; } } - - return stuckCommits.build(); + return false; } /** 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..20661aae0a04 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java @@ -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. + */ +package org.apache.beam.runners.dataflow.worker.streaming; + +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; + +/** + * A handle to use when requesting pulling more work from @BoundedQueueExecutor + * via @BoundedQueueExecutor.pollWork + */ +public interface BoundedQueueExecutorWorkHandle { + // Returns all work that are tracked by the handle + ImmutableList getWorkBatch(); +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationState.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationState.java index 3886d4fbc01b..e2333e23a716 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationState.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationState.java @@ -69,6 +69,10 @@ public String getComputationId() { return computationId; } + public String getSystemName() { + return mapTask.getSystemName(); + } + public MapTask getMapTask() { return mapTask; } @@ -131,9 +135,15 @@ public void completeWorkAndScheduleNextWorkForKey(ShardedKey shardedKey, WorkId .ifPresent(this::forceExecute); } - public void invalidateStuckCommits(Instant stuckCommitDeadline) { - activeWorkState.invalidateStuckCommits( - stuckCommitDeadline, this::completeWorkAndScheduleNextWorkForKey); + public void reexecuteActiveWork(ShardedKey shardedKey, WorkId workId) { + ExecutableWork activeWork = activeWorkState.getActiveWork(shardedKey, workId); + if (activeWork != null) { + forceExecute(activeWork); + } + } + + public boolean hasStuckCommits(Instant stuckCommitDeadline) { + return activeWorkState.hasStuckCommits(stuckCommitDeadline); } private void execute(ExecutableWork executableWork) { 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..dabf72ba4eae 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 @@ -18,21 +18,15 @@ package org.apache.beam.runners.dataflow.worker.streaming; import com.google.auto.value.AutoValue; -import java.util.HashMap; import java.util.Optional; import javax.annotation.concurrent.NotThreadSafe; import org.apache.beam.runners.core.metrics.ExecutionStateTracker; -import org.apache.beam.runners.dataflow.worker.DataflowMapTaskExecutor; import org.apache.beam.runners.dataflow.worker.DataflowWorkExecutor; import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext; -import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher; -import org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter; -import org.apache.beam.runners.dataflow.worker.util.common.worker.OutputObjectAndByteCounter; -import org.apache.beam.runners.dataflow.worker.windmill.Windmill; -import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateReader; +import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.KeyTransitionListener; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor; import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.coders.Coder; -import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,15 +61,24 @@ public static ComputationWorkExecutor.Builder builder() { /** * Executes DoFns for the Work. Blocks the calling thread until DoFn(s) have completed execution. */ - public final void executeWork( - @Nullable Object key, + public final StreamingModeExecutionContext executeWork( Work work, - WindmillStateReader stateReader, - SideInputStateFetcher sideInputStateFetcher, - Windmill.WorkItemCommitRequest.Builder outputBuilder) + BoundedQueueExecutor workQueueExecutor, + BoundedQueueExecutorWorkHandle budgetHandle, + KeyTransitionListener keyTransitionListener, + FailedWorkHandler onFailedWorkHandler) throws Exception { - context().start(key, work, stateReader, sideInputStateFetcher, outputBuilder); + context() + .start( + work, + workExecutor(), + workQueueExecutor, + budgetHandle, + keyCoder().orElse(null), + keyTransitionListener, + onFailedWorkHandler); workExecutor().execute(); + return context(); } /** @@ -84,6 +87,7 @@ public final void executeWork( */ public final void invalidate() { context().invalidateCache(); + context().reset(); try { workExecutor().close(); } catch (Exception e) { @@ -91,18 +95,6 @@ public final void invalidate() { } } - public final long computeSourceBytesProcessed(String sourceBytesCounterName) { - HashMap counters = - ((DataflowMapTaskExecutor) workExecutor()) - .getReadOperation() - .receivers[0] - .getOutputCounters(); - - return Optional.ofNullable(counters.get(sourceBytesCounterName)) - .map(counter -> ((OutputObjectAndByteCounter) counter).getByteCount().getAndReset()) - .orElse(0L); - } - @AutoValue.Builder public abstract static class Builder { public abstract Builder setWorkExecutor(DataflowWorkExecutor workExecutor); 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..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 @@ -17,32 +17,69 @@ */ 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 final WorkId id() { + public void run(BoundedQueueExecutorWorkHandle handle) { + try { + executeWorkFn().accept(work(), handle); + } catch (Throwable t) { + throw ExceptionUtils.safeWrapThrowableAsException(t); + } + } + + public WorkId id() { return work().id(); } - public final Windmill.WorkItem getWorkItem() { + public Windmill.WorkItem getWorkItem() { return work().getWorkItem(); } + + @Override + 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/FailedWorkHandler.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/FailedWorkHandler.java new file mode 100644 index 000000000000..683ecf5d6600 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/FailedWorkHandler.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; + +/** Handler for failed {@link Work}. */ +@FunctionalInterface +public interface FailedWorkHandler { + void onFailedWork(Work work); +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/KeyCommitTooLargeException.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/KeyCommitTooLargeException.java index 4d6ae8a208c1..331b9a2a734f 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/KeyCommitTooLargeException.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/KeyCommitTooLargeException.java @@ -18,16 +18,25 @@ package org.apache.beam.runners.dataflow.worker.streaming; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; +import org.checkerframework.checker.nullness.qual.Nullable; public final class KeyCommitTooLargeException extends Exception { public static KeyCommitTooLargeException causedBy( - String computationId, long byteLimit, Windmill.WorkItemCommitRequest request) { + String stageName, + long byteLimit, + Windmill.WorkItemCommitRequest request, + @Nullable Object decodedKey, + boolean hotKeyLoggingEnabled) { StringBuilder message = new StringBuilder(); message.append("Commit request for stage "); - message.append(computationId); + message.append(stageName); message.append(" and sharding key "); - message.append(request.getShardingKey()); + message.append(Long.toUnsignedString(request.getShardingKey())); + if (decodedKey != null && hotKeyLoggingEnabled) { + message.append(" and key "); + message.append(decodedKey); + } if (request.getSerializedSize() > 0) { message.append( " has size " @@ -38,9 +47,8 @@ public static KeyCommitTooLargeException causedBy( message.append(" is larger than 2GB and cannot be processed"); } message.append( - ". This may be caused by grouping a very " - + "large amount of data in a single window without using Combine," - + " or by producing a large amount of data from a single input element." + ". This may be caused by grouping a very large amount of data in a single window without" + + " using Combine, or by producing a large amount of data from a single input element." + " See https://cloud.google.com/dataflow/docs/guides/common-errors#key-commit-too-large-exception."); return new KeyCommitTooLargeException(message.toString()); } 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..4541a1c313a2 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,14 +25,18 @@ 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.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; import javax.annotation.concurrent.NotThreadSafe; import org.apache.beam.repackaged.core.org.apache.commons.lang3.tuple.Pair; import org.apache.beam.runners.dataflow.worker.ActiveMessageMetadata; import org.apache.beam.runners.dataflow.worker.DataflowExecutionStateSampler; +import org.apache.beam.runners.dataflow.worker.WorkCancellingException; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalData; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataRequest; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.KeyedGetDataRequest; @@ -41,7 +45,6 @@ import org.apache.beam.runners.dataflow.worker.windmill.Windmill.LatencyAttribution.ActiveLatencyBreakdown; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.LatencyAttribution.ActiveLatencyBreakdown.ActiveElementMetadata; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.LatencyAttribution.ActiveLatencyBreakdown.Distribution; -import org.apache.beam.runners.dataflow.worker.windmill.Windmill.LatencyAttribution.State; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItemCommitRequest; import org.apache.beam.runners.dataflow.worker.windmill.client.commits.Commit; @@ -52,6 +55,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,12 +78,16 @@ 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; private volatile boolean isFailed; private volatile String processingThreadName = ""; + private final AtomicReference<@Nullable AtomicBoolean> onFailureListener = + new AtomicReference<>(null); private final boolean drainMode; + private ImmutableList getWorkStreamLatencies; private Work( WorkItem workItem, @@ -87,7 +95,8 @@ private Work( Watermarks watermarks, ProcessingContext processingContext, boolean drainMode, - Supplier clock) { + Supplier clock, + ImmutableList getWorkStreamLatencies) { this.shardedKey = ShardedKey.create(workItem.getKey(), workItem.getShardingKey()); this.workItem = workItem; this.serializedWorkItemSize = serializedWorkItemSize; @@ -101,12 +110,19 @@ 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()) + '-' + Long.toHexString(workItem.getWorkToken()); this.currentState = TimedState.initialState(startTime); this.isFailed = false; + // We defer recordGetWorkStreamLatencies() to be called during bundle processing + // as these are constructed on the hot GetWork thread + this.getWorkStreamLatencies = getWorkStreamLatencies; } public static Work create( @@ -115,9 +131,16 @@ public static Work create( Watermarks watermarks, ProcessingContext processingContext, boolean drainMode, - Supplier clock) { + Supplier clock, + ImmutableList getWorkStreamLatencies) { return new Work( - workItem, serializedWorkItemSize, watermarks, processingContext, drainMode, clock); + workItem, + serializedWorkItemSize, + watermarks, + processingContext, + drainMode, + clock, + getWorkStreamLatencies); } public static ProcessingContext createProcessingContext( @@ -184,17 +207,41 @@ public long getSerializedWorkItemSize() { return serializedWorkItemSize; } + public String getComputationId() { + return processingContext.computationId(); + } + @Override public ShardedKey getShardedKey() { return shardedKey; } public Optional fetchKeyedState(KeyedGetDataRequest keyedGetDataRequest) { - return processingContext.fetchKeyedState(keyedGetDataRequest); + try { + Optional response = + processingContext.fetchKeyedState(keyedGetDataRequest); + if (response.isPresent() && response.get().getFailed()) { + // Work is not valid in backend anymore. + this.setFailed(); + } + return response; + } catch (RuntimeException e) { + if (WorkCancellingException.isWorkCancellingException(e)) { + this.setFailed(); + } + throw e; + } } public GlobalData fetchSideInput(GlobalDataRequest request) { - return processingContext.getDataClient().getSideInputData(request); + try { + return processingContext.getDataClient().getSideInputData(request); + } catch (RuntimeException e) { + if (WorkCancellingException.isWorkCancellingException(e)) { + this.setFailed(); + } + throw e; + } } public String backendWorkerToken() { @@ -237,6 +284,19 @@ public void setProcessingThreadName(String processingThreadName) { @Override public void setFailed() { this.isFailed = true; + AtomicBoolean listener = onFailureListener.get(); + if (listener != null) { + listener.set(true); + } + } + + // Sets the passed in boolean to true if the work fails + // Supports registering only one boolean at a time. + public void setOnFailureListener(@Nullable AtomicBoolean listener) { + onFailureListener.set(listener); + if (isFailed && listener != null) { + listener.set(true); + } } public boolean isCommitPending() { @@ -261,8 +321,12 @@ public void queueCommit(WorkItemCommitRequest commitRequest, ComputationState co processingContext.workCommitter().accept(Commit.create(commitRequest, computationState, this)); } - public WindmillStateReader createWindmillStateReader() { - return WindmillStateReader.forWork(this); + public Consumer workCommitter() { + return processingContext.workCommitter(); + } + + public WindmillStateReader createWindmillStateReader(Supplier workIsFailed) { + return WindmillStateReader.forWork(this, workIsFailed); } @Override @@ -270,11 +334,13 @@ public WorkId id() { return id; } - public void recordGetWorkStreamLatencies( - ImmutableList getWorkStreamLatencies) { - for (LatencyAttribution latency : getWorkStreamLatencies) { - totalDurationPerState.put( - latency.getState(), Duration.millis(latency.getTotalDurationMillis())); + public void recordGetWorkStreamLatencies() { + if (!getWorkStreamLatencies.isEmpty()) { + for (LatencyAttribution latency : getWorkStreamLatencies) { + totalDurationPerState.put( + latency.getState(), Duration.millis(latency.getTotalDurationMillis())); + } + this.getWorkStreamLatencies = ImmutableList.of(); } } @@ -383,6 +449,10 @@ private boolean isCommitPending() { abstract Instant startTime(); } + public KeyGroup getKeyGroup() { + return keyGroup; + } + @AutoValue public abstract static class ProcessingContext { @@ -416,4 +486,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/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(); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/MetricsDataProvider.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/MetricsDataProvider.java index 901e2d235f85..f2144fd906a2 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/MetricsDataProvider.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/MetricsDataProvider.java @@ -60,7 +60,9 @@ public void appendSummaryHtml(PrintWriter writer) { writer.println("Active Keys:
"); for (ComputationState computationState : allComputationStates.get()) { writer.print(computationState.getComputationId()); - writer.print(":
"); + writer.print(" ("); + writer.print(computationState.getSystemName()); + writer.print("):
"); computationState.printActiveWork(writer); writer.println("
"); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/StreamingWorkerStatusReporter.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/StreamingWorkerStatusReporter.java index 374dd97a1b16..4b65b263fb9b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/StreamingWorkerStatusReporter.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/harness/StreamingWorkerStatusReporter.java @@ -232,7 +232,7 @@ public void stop() { } private void reportHarnessStartup() { - DataflowWorkerLoggingMDC.setStageName("startup"); + DataflowWorkerLoggingMDC.setSystemStageName("startup"); CounterSet restartCounter = new CounterSet(); restartCounter .longSum( 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..2dd0f971168e 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,12 @@ */ 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.ArrayList; +import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; @@ -24,13 +30,18 @@ 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.runners.dataflow.worker.streaming.FailedWorkHandler; +import org.apache.beam.runners.dataflow.worker.streaming.Work; +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.collect.ImmutableList; 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; @@ -38,7 +49,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; @@ -59,6 +82,9 @@ public class BoundedQueueExecutor { @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, @@ -66,7 +92,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 = @@ -75,7 +103,7 @@ public BoundedQueueExecutor( initialMaximumPoolSize, keepAliveTime, unit, - new LinkedBlockingQueue<>(), + keyGroupWorkQueue != null ? keyGroupWorkQueue : new LinkedBlockingQueue<>(), threadFactory) { @Override protected void beforeExecute(Thread t, Runnable r) { @@ -106,7 +134,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 +147,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 +255,116 @@ 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 final List workBatch; + + @GuardedBy("this") + private long bytes; + + @GuardedBy("this") + private boolean closed = false; + + private BoundedQueueExecutorWorkHandleImpl(Work work, long bytes) { + checkArgument(bytes >= 0); + this.workBatch = new ArrayList<>(); + this.workBatch.add(checkArgumentNotNull(work)); + this.bytes = bytes; + } + + /** + * Merges the budget from another handle into this handle. + * + *

This transfers the budget (workBatch 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.workBatch.addAll(other.workBatch); + this.bytes += other.bytes; + other.closed = true; + other.workBatch.clear(); + other.bytes = 0; + } + } + } + + public synchronized boolean isClosed() { + return closed; + } + + @Override + public synchronized ImmutableList getWorkBatch() { + return ImmutableList.copyOf(workBatch); + } + + @VisibleForTesting + synchronized long bytes() { + return bytes; + } + + @Override + public synchronized void close() { + if (closed) return; + closed = true; + decrementCounters(this.workBatch.size(), this.bytes); + } + } + + 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(work.work(), 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 +374,54 @@ 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); + } + } + + @VisibleForTesting + BoundedQueueExecutorWorkHandleImpl createBudgetHandle(Work work, long bytes) { + return new BoundedQueueExecutorWorkHandleImpl(work, bytes); + } + + public @Nullable ExecutableWork pollWork( + String computationId, + Work.KeyGroup keyGroup, + BoundedQueueExecutorWorkHandle handle, + FailedWorkHandler onFailedWorkHandler) { + checkArgument( + computationId != null && keyGroup != null && !keyGroup.equals(Work.KeyGroup.DEFAULT)); + checkArgument(handle instanceof BoundedQueueExecutorWorkHandleImpl); + checkStateNotNull(onFailedWorkHandler); + BoundedQueueExecutorWorkHandleImpl internalHandle = (BoundedQueueExecutorWorkHandleImpl) handle; + if (keyGroupWorkQueue == null) { + return null; + } + while (true) { + @Nullable QueuedWork queuedWork = keyGroupWorkQueue.pollWork(computationId, keyGroup); + if (queuedWork == null) { + return null; + } + Work work = queuedWork.getWork().work(); + if (work.isFailed()) { + queuedWork.getHandle().close(); + onFailedWorkHandler.onFailedWork(work); + continue; + } + internalHandle.merge(queuedWork.getHandle()); + return queuedWork.getWork(); } } - private void decrementCounters(long workBytes) { + 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 +440,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/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/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..7ef011266507 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 @@ -19,6 +19,7 @@ import java.io.Closeable; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.checkerframework.checker.nullness.qual.Nullable; /** A flatten operation. */ public class FlattenOperation extends ReceivingOperation { @@ -43,6 +44,9 @@ public void process(Object elem) throws Exception { } } + @Override + public void finishKey(@Nullable Object key) 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..90fc76276940 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; } } @@ -112,6 +109,13 @@ public void execute() throws Exception { // TODO: support for success / failure ports? } + @Override + public void finishKey(@Nullable Object key) throws Exception { + for (Operation op : operations) { + op.finishKey(key); + } + } + @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..138ce525e26b 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 @@ -17,6 +17,8 @@ */ package org.apache.beam.runners.dataflow.worker.util.common.worker; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * The abstract base class for Operations, which correspond to Instructions in the original MapTask * InstructionGraph. @@ -137,6 +139,9 @@ public void finish() throws Exception { } } + /** Called when all elements for a specific key have been processed. */ + public abstract void finishKey(@Nullable Object key) 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/ParDoFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoFn.java index 84dbbd627b08..75ecdb67dd42 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoFn.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/ParDoFn.java @@ -17,6 +17,8 @@ */ package org.apache.beam.runners.dataflow.worker.util.common.worker; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Interface for functions invokable by {@link ParDoOperation} instances. * @@ -30,6 +32,8 @@ public interface ParDoFn { void processTimers() throws Exception; + void finishKey(@Nullable Object key) throws Exception; + void finishBundle() throws Exception; void abort() throws Exception; 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..a814b90cae21 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 @@ -19,6 +19,7 @@ import java.io.Closeable; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.checkerframework.checker.nullness.qual.Nullable; /** A ParDo mapping function. */ public class ParDoOperation extends ReceivingOperation { @@ -45,13 +46,19 @@ 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(@Nullable Object key) throws Exception { try (Closeable scope = context.enterProcessTimers()) { checkStarted(); fn.processTimers(); + fn.finishKey(key); } + } + @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..5d118626f18d 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(@Nullable Object key) 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/SimplePartialGroupByKeyParDoFn.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/SimplePartialGroupByKeyParDoFn.java index 7a9fcfdf0694..cce77f3aceb6 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/SimplePartialGroupByKeyParDoFn.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/common/worker/SimplePartialGroupByKeyParDoFn.java @@ -17,6 +17,8 @@ */ package org.apache.beam.runners.dataflow.worker.util.common.worker; +import org.checkerframework.checker.nullness.qual.Nullable; + /** A partial group-by-key {@link ParDoFn} implementation. */ public class SimplePartialGroupByKeyParDoFn implements ParDoFn { private final GroupingTable groupingTable; @@ -39,6 +41,9 @@ public void processElement(Object elem) throws Exception { @Override public void processTimers() {} + @Override + public void finishKey(@Nullable Object key) throws Exception {} + @Override public void finishBundle() throws Exception { groupingTable.flush(receiver); 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..8aab050fb22c 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(@Nullable Object key) 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..a97c9920b9a3 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 @@ -21,6 +21,7 @@ import org.apache.beam.runners.dataflow.worker.counters.Counter; import org.apache.beam.runners.dataflow.worker.counters.CounterName; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; +import org.checkerframework.checker.nullness.qual.Nullable; /** A write operation. */ @SuppressWarnings({ @@ -105,6 +106,9 @@ public void finish() throws Exception { } } + @Override + public void finishKey(@Nullable Object key) throws Exception {} + @Override public void abort() throws Exception { if (writer == null) { diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/WindmillStream.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/WindmillStream.java index 526b67890783..36001c151508 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/WindmillStream.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/WindmillStream.java @@ -108,6 +108,11 @@ boolean commitWorkItem( Windmill.WorkItemCommitRequest request, Consumer onDone); + boolean commitMultiKeyWorkItem( + String computation, + Windmill.MultiKeyWorkItemCommitRequest request, + Consumer onDone); + /** Flushes any pending work items to the wire. */ void flush(); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/Commit.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/Commit.java index b840d22a3434..78e74896ef18 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/Commit.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/Commit.java @@ -17,35 +17,93 @@ */ package org.apache.beam.runners.dataflow.worker.windmill.client.commits; -import com.google.auto.value.AutoValue; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + import org.apache.beam.runners.dataflow.worker.streaming.ComputationState; import org.apache.beam.runners.dataflow.worker.streaming.Work; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.MultiKeyWorkItemCommitRequest; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItemCommitRequest; 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; /** Value class for a queued commit. */ @Internal -@AutoValue -public abstract class Commit { +public class Commit { + + private final ComputationState computationState; + private final ImmutableList workBatch; + private final @Nullable WorkItemCommitRequest singleKeyRequest; + private final @Nullable MultiKeyWorkItemCommitRequest multiKeyRequest; public static Commit create( WorkItemCommitRequest request, ComputationState computationState, Work work) { Preconditions.checkArgument(request.getSerializedSize() > 0); - return new AutoValue_Commit(request, computationState, work); + return new Commit(computationState, ImmutableList.of(work), request, null); + } + + public static Commit createMultiKey( + MultiKeyWorkItemCommitRequest multiKeyRequest, + ComputationState computationState, + ImmutableList workBatch) { + Preconditions.checkArgument(!workBatch.isEmpty()); + return new Commit(computationState, workBatch, null, multiKeyRequest); + } + + private Commit( + ComputationState computationState, + ImmutableList workBatch, + @Nullable WorkItemCommitRequest singleKeyRequest, + @Nullable MultiKeyWorkItemCommitRequest multiKeyRequest) { + this.computationState = computationState; + this.workBatch = workBatch; + this.singleKeyRequest = singleKeyRequest; + this.multiKeyRequest = multiKeyRequest; } public final String computationId() { return computationState().getComputationId(); } - public abstract WorkItemCommitRequest request(); + public final String systemName() { + return computationState().getSystemName(); + } + + public @Nullable WorkItemCommitRequest singleKeyRequest() { + return singleKeyRequest; + }; + + public ComputationState computationState() { + return computationState; + } - public abstract ComputationState computationState(); + public @Nullable MultiKeyWorkItemCommitRequest multiKeyRequest() { + return multiKeyRequest; + } - public abstract Work work(); + public ImmutableList workBatch() { + return workBatch; + } + + public final int getSerializedByteSize() { + if (multiKeyRequest() != null) { + return checkStateNotNull(multiKeyRequest()).getSerializedSize(); + } + return checkStateNotNull(singleKeyRequest()).getSerializedSize(); + } - public final int getSize() { - return request().getSerializedSize(); + @Override + public String toString() { + Work work = workBatch.get(0); + return "[systemName=" + + systemName() + + ", shardingKey=" + + work.getShardedKey() + + ", workId=" + + work.id() + + ", workBatchSize=" + + workBatch.size() + + "]"; } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/Commits.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/Commits.java index 498e90f78e29..0607baebeb1d 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/Commits.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/Commits.java @@ -31,6 +31,6 @@ public final class Commits { private Commits() {} public static WeightedSemaphore maxCommitByteSemaphore() { - return WeightedSemaphore.create(MAX_QUEUED_COMMITS_BYTES, Commit::getSize); + return WeightedSemaphore.create(MAX_QUEUED_COMMITS_BYTES, Commit::getSerializedByteSize); } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/CompleteCommit.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/CompleteCommit.java index e33e853d3d76..7e2be8308954 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/CompleteCommit.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/CompleteCommit.java @@ -37,24 +37,14 @@ @AutoValue public abstract class CompleteCommit { - public static CompleteCommit create(Commit commit, CommitStatus commitStatus) { - return new AutoValue_CompleteCommit( - commit.computationId(), - ShardedKey.create(commit.request().getKey(), commit.request().getShardingKey()), - WorkId.builder() - .setWorkToken(commit.request().getWorkToken()) - .setCacheToken(commit.request().getCacheToken()) - .build(), - commitStatus); - } - public static CompleteCommit create( - String computationId, ShardedKey shardedKey, WorkId workId, CommitStatus status) { - return new AutoValue_CompleteCommit(computationId, shardedKey, workId, status); - } - - public static CompleteCommit forFailedWork(Commit commit) { - return create(commit, CommitStatus.ABORTED); + String computationId, + ShardedKey shardedKey, + WorkId workId, + CommitStatus status, + boolean retryableFailure) { + return new AutoValue_CompleteCommit( + computationId, shardedKey, workId, status, retryableFailure); } public abstract String computationId(); @@ -64,4 +54,10 @@ public static CompleteCommit forFailedWork(Commit commit) { public abstract WorkId workId(); public abstract CommitStatus status(); + + /** + * If retryableFailure true, the workitem will be retried locally. Used to retry partial work + * failures in multi key bundles. + */ + public abstract boolean retryableFailure(); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingApplianceWorkCommitter.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingApplianceWorkCommitter.java index 20b95b0661d0..ffb9b64595c5 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingApplianceWorkCommitter.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingApplianceWorkCommitter.java @@ -17,6 +17,9 @@ */ package org.apache.beam.runners.dataflow.worker.windmill.client.commits; +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.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; @@ -112,7 +115,8 @@ private void commitLoop() { } while (commit != null) { ComputationState computationState = commit.computationState(); - commit.work().setState(Work.State.COMMITTING); + checkState(commit.workBatch().size() == 1); + commit.workBatch().get(0).setState(Work.State.COMMITTING); Windmill.ComputationCommitWorkRequest.Builder computationRequestBuilder = computationRequestMap.get(computationState); if (computationRequestBuilder == null) { @@ -120,10 +124,10 @@ private void commitLoop() { computationRequestBuilder.setComputationId(computationState.getComputationId()); computationRequestMap.put(computationState, computationRequestBuilder); } - computationRequestBuilder.addRequests(commit.request()); + computationRequestBuilder.addRequests(checkStateNotNull(commit.singleKeyRequest())); // Send the request if we've exceeded the bytes or there is no more // pending work. commitBytes is a long, so this cannot overflow. - commitBytes += commit.getSize(); + commitBytes += commit.getSerializedByteSize(); if (commitBytes >= TARGET_COMMIT_BUNDLE_BYTES) { break; } @@ -155,7 +159,8 @@ private void completeWork( .setCacheToken(workRequest.getCacheToken()) .setWorkToken(workRequest.getWorkToken()) .build(), - Windmill.CommitStatus.OK)); + Windmill.CommitStatus.OK, + /* retryableFailure= */ false)); } } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingEngineWorkCommitter.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingEngineWorkCommitter.java index b68f53121b86..8ac9b1593c54 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingEngineWorkCommitter.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingEngineWorkCommitter.java @@ -17,6 +17,8 @@ */ package org.apache.beam.runners.dataflow.worker.windmill.client.commits; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + import com.google.auto.value.AutoBuilder; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -30,6 +32,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.WeightedBoundedQueue; import org.apache.beam.runners.dataflow.worker.streaming.WeightedSemaphore; import org.apache.beam.runners.dataflow.worker.streaming.Work; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.CommitStatus; import org.apache.beam.runners.dataflow.worker.windmill.client.CloseableStream; import org.apache.beam.runners.dataflow.worker.windmill.client.WindmillStream.CommitWorkStream; import org.apache.beam.sdk.annotations.Internal; @@ -100,8 +103,8 @@ public void start() { @Override public void commit(Commit commit) { - if (commit.work().isFailed()) { - failCommit(commit); + if (shouldFailCommit(commit)) { + failQueuedCommit(commit); } else { commitQueue.put(commit); } @@ -109,12 +112,7 @@ public void commit(Commit commit) { // Do this check after adding to commitQueue, else commitQueue.put() can race with // drainCommitQueue() in stop() and leave commits orphaned in the queue. if (!this.isRunning.get()) { - LOG.debug( - "Trying to queue commit on shutdown, failing commit=[computationId={}, shardingKey={}," - + " workId={} ].", - commit.computationId(), - commit.work().getShardedKey(), - commit.work().id()); + LOG.debug("Trying to queue commit on shutdown, failing commit={}", commit); drainCommitQueue(); } } @@ -141,14 +139,31 @@ public void stop() { private void drainCommitQueue() { Commit queuedCommit = commitQueue.poll(); while (queuedCommit != null) { - failCommit(queuedCommit); + failQueuedCommit(queuedCommit); queuedCommit = commitQueue.poll(); } } - private void failCommit(Commit commit) { - commit.work().setFailed(); - onCommitComplete.accept(CompleteCommit.forFailedWork(commit)); + private void failQueuedCommit(Commit commit) { + boolean isRunningCopy = isRunning.get(); + + for (Work w : commit.workBatch()) { + // Shutting down, fail everything unconditionally to prevent infinite loops + if (!isRunningCopy) { + w.setFailed(); + } + // fail failed work, and request re-execution for valid ones + boolean retryableFailure = !w.isFailed(); + LOG.debug( + "onCommitComplete for work {} from failed commit, retry: {}", w.id(), retryableFailure); + onCommitComplete.accept( + CompleteCommit.create( + commit.computationId(), + w.getShardedKey(), + w.id(), + CommitStatus.ABORTED, + retryableFailure)); + } } @Override @@ -173,8 +188,8 @@ private void streamingCommitLoop() { // take() blocks until a value is available in the commitQueue. Preconditions.checkNotNull(initialCommit); - if (initialCommit.work().isFailed()) { - onCommitComplete.accept(CompleteCommit.forFailedWork(initialCommit)); + if (shouldFailCommit(initialCommit)) { + failQueuedCommit(initialCommit); initialCommit = null; continue; } @@ -194,29 +209,69 @@ private void streamingCommitLoop() { } } finally { if (initialCommit != null) { - failCommit(initialCommit); + failQueuedCommit(initialCommit); + } + } + } + + boolean shouldFailCommit(Commit commit) { + for (Work w : commit.workBatch()) { + if (w.isFailed()) { + return true; } } + return false; } /** Adds the commit to the batch if it fits, returning true if it is consumed. */ private boolean tryAddToCommitBatch(Commit commit, CommitWorkStream.RequestBatcher batcher) { Preconditions.checkNotNull(commit); - commit.work().setState(Work.State.COMMITTING); - activeCommitBytes.addAndGet(commit.getSize()); - boolean isCommitAccepted = - batcher.commitWorkItem( - commit.computationId(), - commit.request(), - commitStatus -> { - onCommitComplete.accept(CompleteCommit.create(commit, commitStatus)); - activeCommitBytes.addAndGet(-commit.getSize()); - }); + for (Work w : commit.workBatch()) { + w.setState(Work.State.COMMITTING); + } + activeCommitBytes.addAndGet(commit.getSerializedByteSize()); + boolean isCommitAccepted; + if (commit.multiKeyRequest() != null) { + isCommitAccepted = + batcher.commitMultiKeyWorkItem( + commit.computationId(), + checkStateNotNull(commit.multiKeyRequest()), + commitStatus -> { + for (Work w : commit.workBatch()) { + onCommitComplete.accept( + CompleteCommit.create( + commit.computationId(), + w.getShardedKey(), + w.id(), + commitStatus, + /* retryableFailure= */ false)); + } + activeCommitBytes.addAndGet(-commit.getSerializedByteSize()); + }); + } else { + isCommitAccepted = + batcher.commitWorkItem( + commit.computationId(), + checkStateNotNull(commit.singleKeyRequest()), + commitStatus -> { + Work w = commit.workBatch().get(0); + onCommitComplete.accept( + CompleteCommit.create( + commit.computationId(), + w.getShardedKey(), + w.id(), + commitStatus, + /* retryableFailure= */ false)); + activeCommitBytes.addAndGet(-commit.getSerializedByteSize()); + }); + } // Since the commit was not accepted, revert the changes made above. if (!isCommitAccepted) { - commit.work().setState(Work.State.COMMIT_QUEUED); - activeCommitBytes.addAndGet(-commit.getSize()); + for (Work w : commit.workBatch()) { + w.setState(Work.State.COMMIT_QUEUED); + } + activeCommitBytes.addAndGet(-commit.getSerializedByteSize()); } return isCommitAccepted; @@ -246,8 +301,8 @@ private boolean tryAddToCommitBatch(Commit commit, CommitWorkStream.RequestBatch } // Drop commits for failed work. Such commits will be dropped by Windmill anyway. - if (commit.work().isFailed()) { - onCommitComplete.accept(CompleteCommit.forFailedWork(commit)); + if (shouldFailCommit(commit)) { + failQueuedCommit(commit); continue; } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/getdata/StreamGetDataClient.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/getdata/StreamGetDataClient.java index ab12946ad18b..a9134c677520 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/getdata/StreamGetDataClient.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/getdata/StreamGetDataClient.java @@ -19,6 +19,7 @@ import java.io.PrintWriter; import java.util.function.Function; +import org.apache.beam.runners.dataflow.worker.WorkCancellingException; import org.apache.beam.runners.dataflow.worker.WorkItemCancelledException; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.client.WindmillStream.GetDataStream; @@ -62,7 +63,7 @@ public Windmill.KeyedGetDataResponse getStateData( try (AutoCloseable ignored = getDataMetricTracker.trackStateDataFetchWithThrottling()) { return getDataStream.requestKeyedData(computationId, request); } catch (WindmillStreamShutdownException e) { - throw new WorkItemCancelledException(request.getShardingKey()); + throw new WorkCancellingException(request.getShardingKey()); } catch (Exception e) { throw new GetDataException( "Error occurred fetching state for computation=" @@ -87,7 +88,7 @@ public Windmill.GlobalData getSideInputData(Windmill.GlobalDataRequest request) try (AutoCloseable ignored = getDataMetricTracker.trackSideInputFetchWithThrottling()) { return sideInputGetDataStream.requestGlobalData(request); } catch (WindmillStreamShutdownException e) { - throw new WorkItemCancelledException(e); + throw new WorkCancellingException(e); } catch (Exception e) { throw new GetDataException( "Error occurred fetching side input for tag=" + request.getDataId(), e); 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..e2a54b43cf07 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; @@ -35,6 +34,7 @@ import java.util.function.Function; import javax.annotation.Nullable; import org.apache.beam.repackaged.core.org.apache.commons.lang3.tuple.Pair; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.CommitStatus; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.JobHeader; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.StreamingCommitRequestChunk; @@ -77,6 +77,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 +91,7 @@ private GrpcCommitWorkStream( AtomicLong idGenerator, int streamingRpcBatchLimit, Duration halfClosePhysicalStreamAfter, + Duration maxRetryDuration, ScheduledExecutorService executor) { super( LOG, @@ -104,6 +106,7 @@ private GrpcCommitWorkStream( this.idGenerator = idGenerator; this.jobHeader = jobHeader; this.streamingRpcBatchLimit = streamingRpcBatchLimit; + this.maxRetryDuration = maxRetryDuration; } static GrpcCommitWorkStream create( @@ -118,6 +121,7 @@ static GrpcCommitWorkStream create( AtomicLong idGenerator, int streamingRpcBatchLimit, Duration halfClosePhysicalStreamAfter, + Duration maxRetryDuration, ScheduledExecutorService executor) { return new GrpcCommitWorkStream( backendWorkerToken, @@ -130,6 +134,7 @@ static GrpcCommitWorkStream create( idGenerator, streamingRpcBatchLimit, halfClosePhysicalStreamAfter, + maxRetryDuration, executor); } @@ -224,14 +229,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 +309,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().serializedCommit().size() > AbstractWindmillStream.RPC_STREAM_CHUNK_SIZE) { issueMultiChunkRequest(elem.getKey(), elem.getValue()); } else { @@ -289,6 +328,7 @@ private void issueSingleRequest(long id, PendingRequest pendingRequest) .setComputationId(pendingRequest.computationId()) .setRequestId(id) .setShardingKey(pendingRequest.shardingKey()) + .setCommitType(pendingRequest.commitType()) .setSerializedWorkItemCommit(pendingRequest.serializedCommit()); StreamingCommitWorkRequest chunk = requestBuilder.build(); synchronized (this) { @@ -318,7 +358,8 @@ private void issueBatchedRequest(Map requests) chunkBuilder .setRequestId(entry.getKey()) .setShardingKey(request.shardingKey()) - .setSerializedWorkItemCommit(request.serializedCommit()); + .setSerializedWorkItemCommit(request.serializedCommit()) + .setCommitType(request.commitType()); } StreamingCommitWorkRequest request = requestBuilder.build(); synchronized (this) { @@ -360,7 +401,8 @@ private void issueMultiChunkRequest(long id, PendingRequest pendingRequest) .setRequestId(id) .setSerializedWorkItemCommit(chunk) .setComputationId(pendingRequest.computationId()) - .setShardingKey(pendingRequest.shardingKey()); + .setShardingKey(pendingRequest.shardingKey()) + .setCommitType(pendingRequest.commitType()); int remaining = serializedCommit.size() - end; if (remaining > 0) { chunkBuilder.setRemainingBytesForWorkItem(remaining); @@ -376,34 +418,58 @@ private void issueMultiChunkRequest(long id, PendingRequest pendingRequest) } } - @AutoValue - abstract static class PendingRequest { + private static class PendingRequest { + private final String computationId; + private final long shardingKey; + private final ByteString serializedCommit; + private final StreamingCommitRequestChunk.CommitType commitType; + private final Consumer onDone; + private final long startTimeNanos; // System.nanoTime() of when request began. + + private PendingRequest( + String computationId, + long shardingKey, + ByteString serializedCommit, + StreamingCommitRequestChunk.CommitType commitType, + Consumer onDone) { + this.computationId = computationId; + this.shardingKey = shardingKey; + this.serializedCommit = serializedCommit; + this.commitType = commitType; + this.onDone = onDone; + this.startTimeNanos = System.nanoTime(); + } - private static PendingRequest create( - String computationId, WorkItemCommitRequest request, Consumer onDone) { - return new AutoValue_GrpcCommitWorkStream_PendingRequest(computationId, request, onDone); + String computationId() { + return computationId; } - abstract String computationId(); + long shardingKey() { + return shardingKey; + } - abstract WorkItemCommitRequest request(); + ByteString serializedCommit() { + return serializedCommit; + } - abstract Consumer onDone(); + StreamingCommitRequestChunk.CommitType commitType() { + return commitType; + } - private long getBytes() { - return (long) request().getSerializedSize() + computationId().length(); + Consumer onDone() { + return onDone; } - private ByteString serializedCommit() { - return request().toByteString(); + long getStartTimeNanos() { + return startTimeNanos; } - private void completeWithStatus(CommitStatus commitStatus) { - onDone().accept(commitStatus); + private long getBytes() { + return (long) serializedCommit.size() + computationId.length(); } - private long shardingKey() { - return request().getShardingKey(); + private void completeWithStatus(CommitStatus commitStatus) { + onDone.accept(commitStatus); } private void abort() { @@ -462,7 +528,34 @@ public boolean commitWorkItem( return false; } - PendingRequest request = PendingRequest.create(computation, commitRequest, onDone); + PendingRequest request = + new PendingRequest( + computation, + commitRequest.getShardingKey(), + commitRequest.toByteString(), + StreamingCommitRequestChunk.CommitType.COMMIT_TYPE_SINGLE_KEY, + onDone); + add(idGenerator.incrementAndGet(), request); + return true; + } + + @Override + public boolean commitMultiKeyWorkItem( + String computation, + Windmill.MultiKeyWorkItemCommitRequest commitRequest, + Consumer onDone) { + Preconditions.checkArgument(commitRequest.getRequestsCount() > 0); + if (!canAccept(commitRequest.getSerializedSize() + computation.length())) { + return false; + } + PendingRequest request = + new PendingRequest( + computation, + // Any key in the batch for routing + commitRequest.getRequests(0).getShardingKey(), + commitRequest.toByteString(), + StreamingCommitRequestChunk.CommitType.COMMIT_TYPE_MULTI_KEY, + 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 244d2ad3fa14..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,19 +112,20 @@ private GrpcWindmillStreamFactory( Consumer> processHeartbeatResponses, Supplier maxBackOffSupplier, java.time.Duration directStreamingRpcPhysicalStreamHalfCloseAfter, + java.time.Duration commitWorkStreamRetryTimeout, Supplier executorServiceSupplier) { this.jobHeader = jobHeader; this.logEveryNStreamFailures = logEveryNStreamFailures; 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; @@ -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/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternals.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternals.java index db036bee43c3..f10ba70212ef 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternals.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternals.java @@ -55,6 +55,19 @@ public class WindmillStateInternals implements StateInternals { private final CachingStateTable workItemDerivedState; private final Supplier scopedReadStateSupplier; + private boolean poisoned = false; + + public void poison() { + this.poisoned = true; + } + + private void checkNotPoisoned() { + if (poisoned) { + throw new IllegalStateException( + "WindmillStateInternals is poisoned and cannot be used after flushState()."); + } + } + public WindmillStateInternals( @Nullable K key, String stateFamily, @@ -78,6 +91,7 @@ public WindmillStateInternals( @Override public @Nullable K getKey() { + checkNotPoisoned(); return key; } @@ -104,6 +118,7 @@ private void persist( } public void persist(final Windmill.WorkItemCommitRequest.Builder commitBuilder) { + checkNotPoisoned(); List> commitsToMerge = new ArrayList<>(); // Call persist on each first, which may schedule some futures for reading. @@ -126,12 +141,14 @@ public void persist(final Windmill.WorkItemCommitRequest.Builder commitBuilder) @Override public T state(StateNamespace namespace, StateTag address) { + checkNotPoisoned(); return workItemState.get(namespace, address, StateContexts.nullContext()); } @Override public T state( StateNamespace namespace, StateTag address, StateContext c) { + checkNotPoisoned(); return workItemState.get(namespace, address, c); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateReader.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateReader.java index c609bed4eae0..4ac5d006e35b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateReader.java @@ -36,8 +36,8 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nullable; -import org.apache.beam.runners.dataflow.worker.KeyTokenInvalidException; import org.apache.beam.runners.dataflow.worker.WindmillTimeUtils; +import org.apache.beam.runners.dataflow.worker.WorkCancellingException; import org.apache.beam.runners.dataflow.worker.WorkItemCancelledException; import org.apache.beam.runners.dataflow.worker.streaming.Work; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; @@ -153,7 +153,7 @@ static WindmillStateReader forTesting( fetchStateFromWindmillFn, key, shardingKey, workToken, () -> null, () -> Boolean.FALSE); } - public static WindmillStateReader forWork(Work work) { + public static WindmillStateReader forWork(Work work, Supplier workItemIsFailed) { return new WindmillStateReader( work::fetchKeyedState, work.getWorkItem().getKey(), @@ -163,7 +163,7 @@ public static WindmillStateReader forWork(Work work) { work.setState(Work.State.READING); return () -> work.setState(Work.State.PROCESSING); }, - work::isFailed); + workItemIsFailed); } private Future stateFuture(StateTag stateTag, @Nullable Coder coder) { @@ -588,7 +588,8 @@ private KeyedGetDataRequest createRequest(Iterable> toFetch) { private void consumeResponse(KeyedGetDataResponse response, Set> toFetch) { bytesRead += response.getSerializedSize(); if (response.getFailed()) { - throw new KeyTokenInvalidException(key.toStringUtf8()); + // upper layers will fail the work on seeing this exception. + throw new WorkCancellingException(shardingKey); } if (!key.equals(response.getKey())) { 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/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/ComputationWorkExecutorFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/ComputationWorkExecutorFactory.java index 269799903300..b51512252e37 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/ComputationWorkExecutorFactory.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/ComputationWorkExecutorFactory.java @@ -29,7 +29,9 @@ import org.apache.beam.runners.dataflow.worker.DataflowExecutionStateSampler; import org.apache.beam.runners.dataflow.worker.DataflowMapTaskExecutor; import org.apache.beam.runners.dataflow.worker.DataflowMapTaskExecutorFactory; +import org.apache.beam.runners.dataflow.worker.HotKeyLogger; import org.apache.beam.runners.dataflow.worker.IntrinsicMapTaskExecutorFactory; +import org.apache.beam.runners.dataflow.worker.MultiKeyBundleOptions; import org.apache.beam.runners.dataflow.worker.ReaderCache; import org.apache.beam.runners.dataflow.worker.ReaderRegistry; import org.apache.beam.runners.dataflow.worker.SinkRegistry; @@ -48,10 +50,13 @@ import org.apache.beam.runners.dataflow.worker.streaming.ComputationWorkExecutor; import org.apache.beam.runners.dataflow.worker.streaming.StageInfo; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfigHandle; +import org.apache.beam.runners.dataflow.worker.streaming.harness.StreamingCounters; +import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory; import org.apache.beam.runners.dataflow.worker.util.common.worker.MapTaskExecutor; import org.apache.beam.runners.dataflow.worker.util.common.worker.OutputObjectAndByteCounter; import org.apache.beam.runners.dataflow.worker.util.common.worker.ReadOperation; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache; +import org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures.FailureTracker; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.fn.IdGenerator; @@ -82,6 +87,9 @@ final class ComputationWorkExecutorFactory { private final SinkRegistry sinkRegistry; private final DataflowExecutionStateSampler sampler; private final CounterSet pendingDeltaCounters; + private final SideInputStateFetcherFactory sideInputStateFetcherFactory; + private final StreamingCounters streamingCounters; + private final FailureTracker failureTracker; /** * Function which converts map tasks to their network representation for execution. @@ -97,6 +105,8 @@ final class ComputationWorkExecutorFactory { private final IdGenerator idGenerator; private final StreamingGlobalConfigHandle globalConfigHandle; private final boolean throwExceptionOnLargeOutput; + private final HotKeyLogger hotKeyLogger; + private final MultiKeyBundleOptions multiKeyBundleOptions; ComputationWorkExecutorFactory( DataflowWorkerHarnessOptions options, @@ -104,9 +114,13 @@ final class ComputationWorkExecutorFactory { ReaderCache readerCache, Function stateCacheFactory, DataflowExecutionStateSampler sampler, - CounterSet pendingDeltaCounters, + StreamingCounters streamingCounters, + FailureTracker failureTracker, IdGenerator idGenerator, - StreamingGlobalConfigHandle globalConfigHandle) { + StreamingGlobalConfigHandle globalConfigHandle, + HotKeyLogger hotKeyLogger, + SideInputStateFetcherFactory sideInputStateFetcherFactory, + MultiKeyBundleOptions multiKeyBundleOptions) { this.options = options; this.mapTaskExecutorFactory = mapTaskExecutorFactory; this.readerCache = readerCache; @@ -116,7 +130,9 @@ final class ComputationWorkExecutorFactory { this.readerRegistry = ReaderRegistry.defaultRegistry(); this.sinkRegistry = SinkRegistry.defaultRegistry(); this.sampler = sampler; - this.pendingDeltaCounters = pendingDeltaCounters; + this.streamingCounters = streamingCounters; + this.failureTracker = failureTracker; + this.pendingDeltaCounters = streamingCounters.pendingDeltaCounters(); this.mapTaskToNetwork = new MapTaskToNetworkFunction(idGenerator); this.maxSinkBytes = hasExperiment(options, DISABLE_SINK_BYTE_LIMIT_EXPERIMENT) @@ -124,6 +140,9 @@ final class ComputationWorkExecutorFactory { : StreamingDataflowWorker.MAX_SINK_BYTES; this.throwExceptionOnLargeOutput = hasExperiment(options, THROW_EXCEPTIONS_ON_LARGE_OUTPUT_EXPERIMENT); + this.hotKeyLogger = hotKeyLogger; + this.sideInputStateFetcherFactory = sideInputStateFetcherFactory; + this.multiKeyBundleOptions = multiKeyBundleOptions; } private static Nodes.ParallelInstructionNode extractReadNode( @@ -191,8 +210,12 @@ ComputationWorkExecutor createComputationWorkExecutor( DataflowExecutionContext.DataflowExecutionStateTracker executionStateTracker = createExecutionStateTracker(stageInfo, mapTask, workLatencyTrackingId); + boolean hotKeyLoggingEnabled = + options.isHotKeyLoggingEnabled() || hasExperiment(options, "enable_hot_key_logging"); + String stepName = getShuffleTaskStepName(mapTask); StreamingModeExecutionContext context = - createExecutionContext(computationState, stageInfo, executionStateTracker); + createExecutionContext( + computationState, stageInfo, executionStateTracker, hotKeyLoggingEnabled, stepName); DataflowMapTaskExecutor mapTaskExecutor = createMapTaskExecutor(context, mapTask, mapTaskNetwork); ReadOperation readOperation = getValidatedReadOperation(mapTaskExecutor); @@ -255,7 +278,9 @@ ComputationWorkExecutor createComputationWorkExecutor( private StreamingModeExecutionContext createExecutionContext( ComputationState computationState, StageInfo stageInfo, - DataflowExecutionContext.DataflowExecutionStateTracker executionStateTracker) { + DataflowExecutionContext.DataflowExecutionStateTracker executionStateTracker, + boolean hotKeyLoggingEnabled, + String stepName) { String computationId = computationState.getComputationId(); return new StreamingModeExecutionContext( pendingDeltaCounters, @@ -268,7 +293,16 @@ private StreamingModeExecutionContext createExecutionContext( stageInfo.executionStateRegistry(), globalConfigHandle, maxSinkBytes, - throwExceptionOnLargeOutput); + throwExceptionOnLargeOutput, + hotKeyLogger, + hotKeyLoggingEnabled, + stepName, + stageInfo.systemName(), + streamingCounters, + failureTracker, + computationState.sourceBytesProcessCounterName(), + multiKeyBundleOptions, + sideInputStateFetcherFactory); } private DataflowMapTaskExecutor createMapTaskExecutor( @@ -286,6 +320,12 @@ private DataflowMapTaskExecutor createMapTaskExecutor( idGenerator); } + private static String getShuffleTaskStepName(MapTask mapTask) { + // The MapTask instruction is ordered by dependencies, such that the first element is + // always going to be the shuffle task. + return mapTask.getInstructions().get(0).getName(); + } + private DataflowExecutionContext.DataflowExecutionStateTracker createExecutionStateTracker( StageInfo stageInfo, MapTask mapTask, String workLatencyTrackingId) { return new DataflowExecutionContext.DataflowExecutionStateTracker( 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..63cfad5a9a6f 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 @@ -17,50 +17,55 @@ */ package org.apache.beam.runners.dataflow.worker.windmill.work.processing; -import static org.apache.beam.sdk.options.ExperimentalOptions.hasExperiment; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import com.google.api.services.dataflow.model.MapTask; import com.google.auto.value.AutoValue; -import java.util.Optional; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.concurrent.ThreadSafe; +import org.apache.beam.repackaged.core.org.apache.commons.lang3.tuple.Pair; import org.apache.beam.runners.dataflow.options.DataflowWorkerHarnessOptions; import org.apache.beam.runners.dataflow.worker.DataflowExecutionStateSampler; import org.apache.beam.runners.dataflow.worker.DataflowMapTaskExecutorFactory; import org.apache.beam.runners.dataflow.worker.HotKeyLogger; +import org.apache.beam.runners.dataflow.worker.MultiKeyBundleOptions; import org.apache.beam.runners.dataflow.worker.ReaderCache; +import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext; +import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.KeyTransitionListener; 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; -import org.apache.beam.runners.dataflow.worker.streaming.KeyCommitTooLargeException; +import org.apache.beam.runners.dataflow.worker.streaming.FailedWorkHandler; import org.apache.beam.runners.dataflow.worker.streaming.StageInfo; 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.streaming.Work.KeyGroup; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfigHandle; import org.apache.beam.runners.dataflow.worker.streaming.harness.StreamingCounters; -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; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache; -import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateReader; import org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures.FailureTracker; import org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures.WorkFailureProcessor; import org.apache.beam.sdk.annotations.Internal; -import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.fn.IdGenerator; 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.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Duration; import org.joda.time.Instant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,48 +81,40 @@ public class StreamingWorkScheduler { private static final Logger LOG = LoggerFactory.getLogger(StreamingWorkScheduler.class); - private final DataflowWorkerHarnessOptions options; private final Supplier clock; private final ComputationWorkExecutorFactory computationWorkExecutorFactory; - private final SideInputStateFetcherFactory sideInputStateFetcherFactory; - private final FailureTracker failureTracker; private final WorkFailureProcessor workFailureProcessor; private final StreamingCommitFinalizer commitFinalizer; private final StreamingCounters streamingCounters; - private final HotKeyLogger hotKeyLogger; private final ConcurrentMap stageInfoMap; private final DataflowExecutionStateSampler sampler; - private final StreamingGlobalConfigHandle globalConfigHandle; + private final BoundedQueueExecutor workExecutor; + private final MultiKeyBundleOptions multiKeyBundleOptions; public StreamingWorkScheduler( - DataflowWorkerHarnessOptions options, Supplier clock, + BoundedQueueExecutor workExecutor, ComputationWorkExecutorFactory computationWorkExecutorFactory, - SideInputStateFetcherFactory sideInputStateFetcherFactory, - FailureTracker failureTracker, WorkFailureProcessor workFailureProcessor, StreamingCommitFinalizer commitFinalizer, StreamingCounters streamingCounters, - HotKeyLogger hotKeyLogger, ConcurrentMap stageInfoMap, DataflowExecutionStateSampler sampler, - StreamingGlobalConfigHandle globalConfigHandle) { - this.options = options; + MultiKeyBundleOptions multiKeyBundleOptions) { this.clock = clock; + this.workExecutor = workExecutor; this.computationWorkExecutorFactory = computationWorkExecutorFactory; - this.sideInputStateFetcherFactory = sideInputStateFetcherFactory; - this.failureTracker = failureTracker; this.workFailureProcessor = workFailureProcessor; this.commitFinalizer = commitFinalizer; this.streamingCounters = streamingCounters; - this.hotKeyLogger = hotKeyLogger; this.stageInfoMap = stageInfoMap; this.sampler = sampler; - this.globalConfigHandle = globalConfigHandle; + this.multiKeyBundleOptions = multiKeyBundleOptions; } public static StreamingWorkScheduler create( DataflowWorkerHarnessOptions options, + MultiKeyBundleOptions multiKeyBundleOptions, Supplier clock, ReaderCache readerCache, DataflowMapTaskExecutorFactory mapTaskExecutorFactory, @@ -132,6 +129,9 @@ public static StreamingWorkScheduler create( IdGenerator idGenerator, StreamingGlobalConfigHandle globalConfigHandle, ConcurrentMap stageInfoMap) { + SideInputStateFetcherFactory sideInputStateFetcherFactory = + SideInputStateFetcherFactory.fromOptions(options); + ComputationWorkExecutorFactory computationWorkExecutorFactory = new ComputationWorkExecutorFactory( options, @@ -139,23 +139,24 @@ public static StreamingWorkScheduler create( readerCache, stateCacheFactory, sampler, - streamingCounters.pendingDeltaCounters(), + streamingCounters, + failureTracker, idGenerator, - globalConfigHandle); + globalConfigHandle, + hotKeyLogger, + sideInputStateFetcherFactory, + multiKeyBundleOptions); return new StreamingWorkScheduler( - options, clock, + workExecutor, computationWorkExecutorFactory, - SideInputStateFetcherFactory.fromOptions(options), - failureTracker, workFailureProcessor, StreamingCommitFinalizer.create(workExecutor, commitFinalizerCleanupExecutor), streamingCounters, - hotKeyLogger, stageInfoMap, sampler, - globalConfigHandle); + multiKeyBundleOptions); } private static long computeShuffleBytesRead(Windmill.WorkItem workItem) { @@ -175,31 +176,12 @@ private static Windmill.WorkItemCommitRequest.Builder initializeOutputBuilder( .setCacheToken(workItem.getCacheToken()); } - private static Windmill.WorkItemCommitRequest buildWorkItemTruncationRequest( - ByteString key, Windmill.WorkItem workItem, int estimatedCommitSize) { - Windmill.WorkItemCommitRequest.Builder outputBuilder = initializeOutputBuilder(key, workItem); - outputBuilder.setExceedsMaxWorkItemCommitBytes(true); - outputBuilder.setEstimatedWorkItemCommitBytes(estimatedCommitSize); - return outputBuilder.build(); + private static void setLoggingContextComputation(@Nullable String systemStageName) { + DataflowWorkerLoggingMDC.setSystemStageName(systemStageName); } - /** Sets the stage name and workId of the Thread executing the {@link Work} for logging. */ - private static void setUpWorkLoggingContext(String workLatencyTrackingId, String computationId) { + private static void setLoggingContextWorkId(@Nullable String workLatencyTrackingId) { DataflowWorkerLoggingMDC.setWorkId(workLatencyTrackingId); - DataflowWorkerLoggingMDC.setStageName(computationId); - } - - private static String getShuffleTaskStepName(MapTask mapTask) { - // The MapTask instruction is ordered by dependencies, such that the first element is - // always going to be the shuffle task. - return mapTask.getInstructions().get(0).getName(); - } - - /** Resets logging context of the Thread executing the {@link Work} for logging. */ - private void resetWorkLoggingContext(String workLatencyTrackingId) { - sampler.resetForWorkId(workLatencyTrackingId); - DataflowWorkerLoggingMDC.setWorkId(null); - DataflowWorkerLoggingMDC.setStageName(null); } /** @@ -214,11 +196,19 @@ public void scheduleWork( Work.ProcessingContext processingContext, boolean drainMode, ImmutableList getWorkStreamLatencies) { + // Before any processing starts, call any pending OnCommit callbacks + commitFinalizer.finalizeCommits(workItem.getSourceState().getFinalizeIdsList()); computationState.activateWork( ExecutableWork.create( Work.create( - workItem, serializedWorkItemSize, watermarks, processingContext, drainMode, clock), - work -> processWork(computationState, work, getWorkStreamLatencies))); + workItem, + serializedWorkItemSize, + watermarks, + processingContext, + drainMode, + clock, + getWorkStreamLatencies), + (work, handle) -> processWork(computationState, work, handle))); } /** Adds any applied finalize ids to the commit finalizer to have their callbacks executed. */ @@ -232,143 +222,95 @@ 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 */ private void processWork( - ComputationState computationState, - Work work, - ImmutableList getWorkStreamLatencies) { - work.recordGetWorkStreamLatencies(getWorkStreamLatencies); - processWork(computationState, work); - } - - private void processWork(ComputationState computationState, Work work) { + ComputationState computationState, Work work, BoundedQueueExecutorWorkHandle handle) { Windmill.WorkItem workItem = work.getWorkItem(); String computationId = computationState.getComputationId(); - ByteString key = workItem.getKey(); - work.setProcessingThreadName(Thread.currentThread().getName()); - work.setState(Work.State.PROCESSING); - setUpWorkLoggingContext(work.getLatencyTrackingId(), computationId); - LOG.debug("Starting processing for {}:\n{}", computationId, work); - - // Before any processing starts, call any pending OnCommit callbacks. Nothing that requires - // cleanup should be done before this, since we might exit early here. - commitFinalizer.finalizeCommits(workItem.getSourceState().getFinalizeIdsList()); + String systemName = computationState.getSystemName(); + LOG.debug("Starting processing for {}:\n{}", systemName, work); + setLoggingContextComputation(systemName); + KeyTransitionListener keyTransitionListener = createKeyTransitionListener(); + keyTransitionListener.onKeyTransition(null, work); + if (workItem.getSourceState().getOnlyFinalize()) { - Windmill.WorkItemCommitRequest.Builder outputBuilder = initializeOutputBuilder(key, workItem); - outputBuilder.setSourceStateUpdates(Windmill.SourceState.newBuilder().setOnlyFinalize(true)); - work.setState(Work.State.COMMIT_QUEUED); - work.queueCommit(outputBuilder.build(), computationState); + handleOnlyFinalize(computationState, work, workItem); return; } long processingStartTimeNanos = System.nanoTime(); - MapTask mapTask = computationState.getMapTask(); - StageInfo stageInfo = - stageInfoMap.computeIfAbsent( - mapTask.getStageName(), s -> StageInfo.create(s, mapTask.getSystemName())); + StageInfo stageInfo = getStageInfo(computationState); + @Nullable List workBatch = null; try { if (work.isFailed()) { throw new WorkItemCancelledException(workItem.getShardingKey()); } - // Execute the user code for the Work. - ExecuteWorkResult executeWorkResult = executeWork(work, stageInfo, computationState); - Windmill.WorkItemCommitRequest.Builder commitRequest = executeWorkResult.commitWorkRequest(); + // Execute the user code for the Work batch. + ExecuteWorkResult executeWorkResult = + executeWork(work, stageInfo, computationState, handle, keyTransitionListener); + workBatch = executeWorkResult.workBatch(); + List workItemCommits = executeWorkResult.workItemCommits(); - // Validate the commit request, possibly requesting truncation if the commitSize is too large. - Windmill.WorkItemCommitRequest validatedCommitRequest = - validateCommitRequestSize(commitRequest.build(), computationId, workItem); + commitFinalizer.cacheCommitFinalizers(executeWorkResult.finalizationCallbacks()); - // Queue the commit. - work.queueCommit(validatedCommitRequest, computationState); - recordProcessingStats(commitRequest, workItem, executeWorkResult); - LOG.debug("Processing done for work token: {}", workItem.getWorkToken()); + commitWorkBatch(computationState, workBatch, workItemCommits); + + recordProcessingStats(workBatch, workItemCommits, executeWorkResult.stateBytesRead()); + LOG.debug("Processing done for work batch size: {}", workBatch.size()); } catch (Throwable t) { - // OutOfMemoryError that are caught will be rethrown and trigger jvm termination. - try { - workFailureProcessor.logAndProcessFailure( - computationId, - ExecutableWork.create(work, retry -> processWork(computationState, retry)), - t, - invalidWork -> - computationState.completeWorkAndScheduleNextWorkForKey( - invalidWork.getShardedKey(), invalidWork.id())); - } catch (OutOfMemoryError oom) { - throw oom; - } catch (Throwable t2) { - throw new RuntimeException(t2); - } + handleProcessWorkFailure( + computationState, handle.getWorkBatch(), computationId, systemName, work, t); } finally { + List processedWorkBatch = workBatch != null ? workBatch : ImmutableList.of(work); // Update total processing time counters. Updating in finally clause ensures that // work items causing exceptions are also accounted in time spent. - long processingTimeMsecs = - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - processingStartTimeNanos); - stageInfo.totalProcessingMsecs().addValue(processingTimeMsecs); + recordProcessingTime(stageInfo, processedWorkBatch, processingStartTimeNanos); - // Attribute all the processing to timers if the work item contains any timers. - // Tests show that work items rarely contain both timers and message bundles. It should - // be a fairly close approximation. - // Another option: Derive time split between messages and timers based on recent totals. - // either here or in DFE. - if (work.getWorkItem().hasTimers()) { - stageInfo.timerProcessingMsecs().addValue(processingTimeMsecs); + setLoggingContextWorkId(null); + setLoggingContextComputation(null); + sampler.resetForWorkId(work.getLatencyTrackingId()); + for (Work w : processedWorkBatch) { + w.setProcessingThreadName(""); } - - resetWorkLoggingContext(work.getLatencyTrackingId()); - work.setProcessingThreadName(""); - } - } - - private Windmill.WorkItemCommitRequest validateCommitRequestSize( - Windmill.WorkItemCommitRequest commitRequest, - String computationId, - Windmill.WorkItem workItem) { - long byteLimit = globalConfigHandle.getConfig().operationalLimits().getMaxWorkItemCommitBytes(); - int commitSize = commitRequest.getSerializedSize(); - int estimatedCommitSize = commitSize < 0 ? Integer.MAX_VALUE : commitSize; - - // Detect overflow of integer serialized size or if the byte limit was exceeded. - // Commit is too large if overflow has occurred or the commitSize has exceeded the allowed - // commit byte limit. - streamingCounters.windmillMaxObservedWorkItemCommitBytes().addValue(estimatedCommitSize); - if (commitSize >= 0 && commitSize < byteLimit) { - return commitRequest; } - - KeyCommitTooLargeException e = - KeyCommitTooLargeException.causedBy(computationId, byteLimit, commitRequest); - failureTracker.trackFailure(computationId, workItem, e); - LOG.error("{}", e.toString()); - - // Drop the current request in favor of a new, minimal one requesting truncation. - // Messages, timers, counters, and other commit content will not be used by the service - // so, we're purposefully dropping them here - return buildWorkItemTruncationRequest(workItem.getKey(), workItem, estimatedCommitSize); } private void recordProcessingStats( - Windmill.WorkItemCommitRequest.Builder outputBuilder, - Windmill.WorkItem workItem, - ExecuteWorkResult executeWorkResult) { - // Compute shuffle and state byte statistics these will be flushed asynchronously. - long stateBytesWritten = - outputBuilder - .clearOutputMessages() - .clearPerWorkItemLatencyAttributions() - .build() - .getSerializedSize(); - - streamingCounters.windmillShuffleBytesRead().addValue(computeShuffleBytesRead(workItem)); - streamingCounters.windmillStateBytesRead().addValue(executeWorkResult.stateBytesRead()); - streamingCounters.windmillStateBytesWritten().addValue(stateBytesWritten); + List workBatch, + List workItemCommits, + long totalStateBytesRead) { + long totalStateBytesWritten = 0; + long totalShuffleBytesRead = 0; + checkState(workBatch.size() == workItemCommits.size()); + for (int i = 0; i < workBatch.size(); i++) { + Windmill.WorkItem workItem = workBatch.get(i).getWorkItem(); + Windmill.WorkItemCommitRequest commit = workItemCommits.get(i); + // Compute shuffle and state byte statistics these will be flushed asynchronously. + long stateBytesWritten = + commit + .toBuilder() + .clearOutputMessages() + .clearPerWorkItemLatencyAttributions() + .build() + .getSerializedSize(); + totalStateBytesWritten += stateBytesWritten; + totalShuffleBytesRead += computeShuffleBytesRead(workItem); + } + streamingCounters.windmillShuffleBytesRead().addValue(totalShuffleBytesRead); + streamingCounters.windmillStateBytesRead().addValue(totalStateBytesRead); + streamingCounters.windmillStateBytesWritten().addValue(totalStateBytesWritten); } private ExecuteWorkResult executeWork( - Work work, StageInfo stageInfo, ComputationState computationState) throws Exception { - Windmill.WorkItem workItem = work.getWorkItem(); - ByteString key = workItem.getKey(); - Windmill.WorkItemCommitRequest.Builder outputBuilder = initializeOutputBuilder(key, workItem); + Work work, + StageInfo stageInfo, + ComputationState computationState, + BoundedQueueExecutorWorkHandle handle, + KeyTransitionListener keyTransitionListener) + throws Exception { ComputationWorkExecutor computationWorkExecutor = computationState .acquireComputationWorkExecutor() @@ -378,90 +320,223 @@ private ExecuteWorkResult executeWork( stageInfo, computationState, work.getLatencyTrackingId())); try { - WindmillStateReader stateReader = work.createWindmillStateReader(); - SideInputStateFetcher localSideInputStateFetcher = - sideInputStateFetcherFactory.createSideInputStateFetcher(work::fetchSideInput); - - // If the read output KVs, then we can decode Windmill's byte key into userland - // key object and provide it to the execution context for use with per-key state. - // Otherwise, we pass null. - // - // The coder type that will be present is: - // WindowedValueCoder(TimerOrElementCoder(KvCoder)) - Optional> keyCoder = computationWorkExecutor.keyCoder(); - @SuppressWarnings("deprecation") - @Nullable - final Object executionKey = - !keyCoder.isPresent() ? null : keyCoder.get().decode(key.newInput(), Coder.Context.OUTER); - - if (workItem.hasHotKeyInfo()) { - Windmill.HotKeyInfo hotKeyInfo = workItem.getHotKeyInfo(); - Duration hotKeyAge = Duration.millis(hotKeyInfo.getHotKeyAgeUsec() / 1000); - - String stepName = getShuffleTaskStepName(computationState.getMapTask()); - if (executionKey != null - && (options.isHotKeyLoggingEnabled() - || hasExperiment(options, "enable_hot_key_logging")) - && keyCoder.isPresent()) { - hotKeyLogger.logHotKeyDetection(stepName, hotKeyAge, executionKey); - } else { - hotKeyLogger.logHotKeyDetection(stepName, hotKeyAge); - } - } + StreamingModeExecutionContext context = computationWorkExecutor.context(); + + FailedWorkHandler onFailedWorkHandler = getFailedWorkHandler(computationState); // Blocks while executing work. computationWorkExecutor.executeWork( - executionKey, work, stateReader, localSideInputStateFetcher, outputBuilder); + work, workExecutor, handle, keyTransitionListener, onFailedWorkHandler); + + List workBatch; + List workItemCommits; + Map> finalizationCallbacks; + long stateBytesRead; + { + if (context.workIsFailed()) { + throw new WorkItemCancelledException(work.getWorkItem().getShardingKey()); + } + context.flushState(); - if (work.isFailed()) { - throw new WorkItemCancelledException(workItem.getShardingKey()); - } + // Retrieve executed works, work item commits, and accumulated callbacks from execution + // context + workBatch = context.getExecutedWorks(); + workItemCommits = context.getWorkItemCommits(); + finalizationCallbacks = context.getFinalizationCallbacks(); + stateBytesRead = context.getStateBytesRead(); - // Reports source bytes processed to WorkItemCommitRequest if available. - try { - long sourceBytesProcessed = - computationWorkExecutor.computeSourceBytesProcessed( - computationState.sourceBytesProcessCounterName()); - outputBuilder.setSourceBytesProcessed(sourceBytesProcessed); - } catch (Exception e) { - LOG.error("{}", e.toString()); + context.reset(); // Don't use context after this. } - - commitFinalizer.cacheCommitFinalizers(computationWorkExecutor.context().flushState()); - // Release the execution state for another thread to use. computationState.releaseComputationWorkExecutor(computationWorkExecutor); computationWorkExecutor = null; - work.setState(Work.State.COMMIT_QUEUED); - outputBuilder.addAllPerWorkItemLatencyAttributions(work.getLatencyAttributions(sampler)); - return ExecuteWorkResult.create( - outputBuilder, stateReader.getBytesRead() + localSideInputStateFetcher.getBytesRead()); + workBatch, workItemCommits, finalizationCallbacks, stateBytesRead); } catch (Throwable t) { if (computationWorkExecutor != null) { // If processing failed due to a thrown exception, close the executionState. Do not // return/release the executionState back to computationState as that will lead to this // executionState instance being reused. - LOG.debug("Invalidating executor after work item {} failed", workItem.getWorkToken(), t); + LOG.debug( + "Invalidating executor after work item {} failed", + work.getWorkItem().getWorkToken(), + t); computationWorkExecutor.invalidate(); } - // Re-throw the exception, it will be caught and handled by workFailureProcessor downstream. throw t; } } + private void handleOnlyFinalize( + ComputationState computationState, Work work, Windmill.WorkItem workItem) { + Windmill.WorkItemCommitRequest.Builder outputBuilder = + initializeOutputBuilder(workItem.getKey(), workItem); + outputBuilder.setSourceStateUpdates(Windmill.SourceState.newBuilder().setOnlyFinalize(true)); + work.setState(Work.State.COMMIT_QUEUED); + work.queueCommit(outputBuilder.build(), computationState); + } + + private StageInfo getStageInfo(ComputationState computationState) { + MapTask mapTask = computationState.getMapTask(); + return stageInfoMap.computeIfAbsent( + mapTask.getStageName(), s -> StageInfo.create(s, mapTask.getSystemName())); + } + + private void commitWorkBatch( + ComputationState computationState, + List workBatch, + List workItemCommits) { + if (workBatch.isEmpty()) { + return; + } + if (workBatch.size() > 1 || multiKeyBundleOptions.multiKeyBundleEnabled()) { + commitMultiKeyWorkBatch(computationState, workBatch, workItemCommits); + } else { + commitSingleKeyWork(computationState, workBatch.get(0), workItemCommits.get(0)); + } + } + + private void commitMultiKeyWorkBatch( + ComputationState computationState, + List workBatch, + List workItemCommits) { + checkState(!workBatch.isEmpty()); + checkState(workBatch.size() == workItemCommits.size()); + Windmill.MultiKeyWorkItemCommitRequest.Builder multiKeyBuilder = + Windmill.MultiKeyWorkItemCommitRequest.newBuilder(); + + Work primaryWork = workBatch.get(0); + Work.KeyGroup keyGroup = primaryWork.getKeyGroup(); + if (!keyGroup.equals(KeyGroup.DEFAULT)) { + // Don't set if key group is 0. Avoids allocation. + multiKeyBuilder.setKeyGroup( + Windmill.Uint128Proto.newBuilder() + .setHigh(keyGroup.high()) + .setLow(keyGroup.low()) + .build()); + } + for (int i = 0; i < workBatch.size(); i++) { + Windmill.WorkItemCommitRequest commit = workItemCommits.get(i); + // TODO: Retry on commit truncations + checkState( + !commit.getExceedsMaxWorkItemCommitBytes(), + "Commit truncation with multikey bundles not implemented"); + Work w = workBatch.get(i); + multiKeyBuilder.addRequests( + commit + .toBuilder() + .addAllPerWorkItemLatencyAttributions(w.getLatencyAttributions(sampler)) + .build()); + } + + // Transition states of all completed works in the batch to COMMIT_QUEUED and submit + for (Work w : workBatch) { + w.setState(Work.State.COMMIT_QUEUED); + } + + // Package and submit the commit batch transactionally + primaryWork + .workCommitter() + .accept( + Commit.createMultiKey( + multiKeyBuilder.build(), computationState, ImmutableList.copyOf(workBatch))); + } + + private void commitSingleKeyWork( + ComputationState computationState, Work work, Windmill.WorkItemCommitRequest commitRequest) { + work.setState(Work.State.COMMIT_QUEUED); + Windmill.WorkItemCommitRequest commitRequestWithAttributions = + commitRequest + .toBuilder() + .addAllPerWorkItemLatencyAttributions(work.getLatencyAttributions(sampler)) + .build(); + work.queueCommit(commitRequestWithAttributions, computationState); + } + + private void handleProcessWorkFailure( + ComputationState computationState, + List failedBatch, + String computationId, + String systemName, + Work primaryWork, + Throwable t) { + try { + List executableWorks = new ArrayList<>(); + for (Work w : failedBatch) { + executableWorks.add( + ExecutableWork.create(w, (retry, h) -> processWork(computationState, retry, h))); + } + + FailedWorkHandler onFailedWorkHandler = getFailedWorkHandler(computationState); + + workFailureProcessor.logAndProcessFailureBatch( + computationId, systemName, executableWorks, t, onFailedWorkHandler); + } catch (OutOfMemoryError oom) { + throw oom; + } catch (Throwable t2) { + LOG.warn("Failed to process work failure safely for work {}", primaryWork.id(), t2); + throw ExceptionUtils.safeWrapThrowableAsException(t2); + } + } + + private static FailedWorkHandler getFailedWorkHandler(ComputationState computationState) { + return failedWork -> + computationState.completeWorkAndScheduleNextWorkForKey( + failedWork.getShardedKey(), failedWork.id()); + } + + private void recordProcessingTime( + StageInfo stageInfo, List workBatch, long processingStartTimeNanos) { + long processingTimeMsecs = + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - processingStartTimeNanos); + stageInfo.totalProcessingMsecs().addValue(processingTimeMsecs); + if (anyWorkHasTimers(workBatch)) { + // Attribute all the processing to timers if the work item contains any timers. + // Tests show that work items rarely contain both timers and message bundles. It should + // be a fairly close approximation. + // Another option: Derive time split between messages and timers based on recent totals. + // either here or in DFE. + stageInfo.timerProcessingMsecs().addValue(processingTimeMsecs); + } + } + + private static boolean anyWorkHasTimers(List works) { + return works.stream().anyMatch(w -> w.getWorkItem().hasTimers()); + } + + private KeyTransitionListener createKeyTransitionListener() { + return (oldWork, newWork) -> { + newWork.recordGetWorkStreamLatencies(); + newWork.setState(Work.State.PROCESSING); + setLoggingContextWorkId(newWork.getLatencyTrackingId()); + if (oldWork != null) { + newWork.setProcessingThreadName(oldWork.getProcessingThreadName()); + oldWork.setProcessingThreadName(""); + } else { + newWork.setProcessingThreadName(Thread.currentThread().getName()); + } + }; + } + @AutoValue abstract static class ExecuteWorkResult { - - private static ExecuteWorkResult create( - Windmill.WorkItemCommitRequest.Builder commitWorkRequest, long stateBytesRead) { + static ExecuteWorkResult create( + List workBatch, + List workItemCommits, + Map> finalizationCallbacks, + long stateBytesRead) { return new AutoValue_StreamingWorkScheduler_ExecuteWorkResult( - commitWorkRequest, stateBytesRead); + workBatch, workItemCommits, finalizationCallbacks, stateBytesRead); } - abstract Windmill.WorkItemCommitRequest.Builder commitWorkRequest(); + abstract List workBatch(); + + abstract List workItemCommits(); + + // Map> + abstract Map> finalizationCallbacks(); abstract long stateBytesRead(); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessor.java index 18c8e9b8d83c..d23c870178e0 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessor.java @@ -17,15 +17,14 @@ */ package org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures; +import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; import java.util.function.Supplier; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; -import org.apache.beam.runners.dataflow.worker.KeyTokenInvalidException; -import org.apache.beam.runners.dataflow.worker.WorkItemCancelledException; import org.apache.beam.runners.dataflow.worker.status.LastExceptionDataProvider; import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; +import org.apache.beam.runners.dataflow.worker.streaming.FailedWorkHandler; import org.apache.beam.runners.dataflow.worker.streaming.Work; import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor; import org.apache.beam.sdk.annotations.Internal; @@ -99,28 +98,41 @@ private static boolean isOutOfMemoryError(@Nullable Throwable t) { return false; } - /** - * Processes failures caused by thrown exceptions that occur during execution of {@link Work}. May - * attempt to retry execution of the {@link Work} or drop it if it is invalid. - */ - public void logAndProcessFailure( + public void logAndProcessFailureBatch( String computationId, - ExecutableWork executableWork, + String systemName, + List executableWorks, Throwable t, - Consumer onInvalidWork) + FailedWorkHandler onFailedWorkHandler) throws Throwable { - switch (evaluateRetry(computationId, executableWork.work(), t)) { - case DO_NOT_RETRY: - // Consider the item invalid. It will eventually be retried by Windmill if it still needs to - // be processed. - onInvalidWork.accept(executableWork.work()); - break; - case RETRY_LOCALLY: - // Try again after some delay and at the end of the queue to avoid a tight loop. - executeWithDelay(retryLocallyDelayMs, executableWork); - break; - case RETHROW_THROWABLE: - throw t; + List worksToRetryLocally = new java.util.ArrayList<>(); + + for (ExecutableWork executableWork : executableWorks) { + switch (evaluateRetry(computationId, systemName, executableWork.work(), t)) { + case DO_NOT_RETRY: + // Consider the item invalid. It will eventually be retried by Windmill if it still needs + // to be processed. + onFailedWorkHandler.onFailedWork(executableWork.work()); + break; + case RETRY_LOCALLY: + // Try again after some delay and at the end of the queue to avoid a tight loop. + worksToRetryLocally.add(executableWork); + break; + case RETHROW_THROWABLE: + throw t; + } + } + + executeWithDelay(worksToRetryLocally); + } + + private void executeWithDelay(List worksToRetryLocally) { + if (!worksToRetryLocally.isEmpty()) { + // Sleep ONCE for the entire batch delay to avoid sequential thread blocks + Uninterruptibles.sleepUninterruptibly(retryLocallyDelayMs, TimeUnit.MILLISECONDS); + for (ExecutableWork ew : worksToRetryLocally) { + workUnitExecutor.forceExecute(ew, ew.work().getSerializedWorkItemSize()); + } } } @@ -131,37 +143,24 @@ private String tryToDumpHeap() { .orElseGet(() -> "not written"); } - private void executeWithDelay(long delayMs, ExecutableWork executableWork) { - Uninterruptibles.sleepUninterruptibly(delayMs, TimeUnit.MILLISECONDS); - workUnitExecutor.forceExecute( - executableWork, executableWork.work().getSerializedWorkItemSize()); - } - private enum RetryEvaluation { DO_NOT_RETRY, RETRY_LOCALLY, RETHROW_THROWABLE, } - private RetryEvaluation evaluateRetry(String computationId, Work work, Throwable t) { - @Nullable final Throwable cause = t.getCause(); - Throwable parsedException = (t instanceof UserCodeException && cause != null) ? cause : t; - if (KeyTokenInvalidException.isKeyTokenInvalidException(parsedException)) { + private RetryEvaluation evaluateRetry( + String computationId, String systemName, Work work, Throwable t) { + if (work.isFailed()) { LOG.debug( - "Execution of work for computation '{}' on sharding key '{}' failed due to token expiration. " - + "Work will not be retried locally.", - computationId, - work.getWorkItem().getShardingKey()); - return RetryEvaluation.DO_NOT_RETRY; - } - if (WorkItemCancelledException.isWorkItemCancelledException(parsedException)) { - LOG.debug( - "Execution of work for computation '{}' on sharding key '{}' failed. " - + "Work will not be retried locally.", - computationId, + "Execution of work for fused stage '{}' on sharding key '{}' failed. " + + "Work is already marked as failed, not retrying locally.", + systemName, work.getWorkItem().getShardingKey()); return RetryEvaluation.DO_NOT_RETRY; } + @Nullable final Throwable cause = t.getCause(); + Throwable parsedException = (t instanceof UserCodeException && cause != null) ? cause : t; LastExceptionDataProvider.reportException(parsedException); LOG.debug("Failed work: {}", work); @@ -169,9 +168,9 @@ private RetryEvaluation evaluateRetry(String computationId, Work work, Throwable if (isOutOfMemoryError(parsedException)) { String heapDump = tryToDumpHeap(); LOG.error( - "Execution of work for computation '{}' for sharding key '{}' failed with out-of-memory. " + "Execution of work for fused stage '{}' for sharding key '{}' failed with out-of-memory. " + "Work will not be retried locally. Heap dump {}.", - computationId, + systemName, work.getWorkItem().getShardingKey(), heapDump, parsedException); @@ -180,8 +179,9 @@ private RetryEvaluation evaluateRetry(String computationId, Work work, Throwable if (!failureTracker.trackFailure(computationId, work.getWorkItem(), parsedException)) { LOG.error( - "Execution of work for computation '{}' on sharding key '{}' failed with uncaught exception, " + "Execution of work for fused stage '{}' for computation '{}' on sharding key '{}' failed with uncaught exception, " + "and Windmill indicated not to retry locally.", + systemName, computationId, work.getWorkItem().getShardingKey(), parsedException); @@ -189,10 +189,10 @@ private RetryEvaluation evaluateRetry(String computationId, Work work, Throwable } if (elapsedTimeSinceStart.isLongerThan(MAX_LOCAL_PROCESSING_RETRY_DURATION)) { LOG.error( - "Execution of work for computation '{}' for sharding key '{}' failed with uncaught exception, " + "Execution of work for fused stage '{}' for sharding key '{}' failed with uncaught exception, " + "and it will not be retried locally because the elapsed time since start {} " + "exceeds {}.", - computationId, + systemName, work.getWorkItem().getShardingKey(), elapsedTimeSinceStart, MAX_LOCAL_PROCESSING_RETRY_DURATION, @@ -200,9 +200,9 @@ private RetryEvaluation evaluateRetry(String computationId, Work work, Throwable return RetryEvaluation.DO_NOT_RETRY; } LOG.error( - "Execution of work for computation '{}' on sharding key '{}' failed with uncaught exception. " + "Execution of work for fused stage '{}' on sharding key '{}' failed with uncaught exception. " + "Work will be retried locally.", - computationId, + systemName, work.getWorkItem().getShardingKey(), parsedException); return RetryEvaluation.RETRY_LOCALLY; diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresher.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresher.java index 793a1aea95ef..d030df7f252d 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresher.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresher.java @@ -44,8 +44,7 @@ import org.slf4j.LoggerFactory; /** - * Asynchronously GetData requests to Streaming Engine for all sufficiently old active work and - * invalidates stuck commits. + * Asynchronously GetData requests to Streaming Engine for all sufficiently old active work. * *

This informs Windmill that processing is ongoing and the work should not be retried. The age * threshold is determined by {@link #activeWorkRefreshPeriodMillis} @@ -61,7 +60,6 @@ public final class ActiveWorkRefresher { private final int activeWorkRefreshPeriodMillis; private final Supplier> computations; private final DataflowExecutionStateSampler sampler; - private final int stuckCommitDurationMillis; private final HeartbeatTracker heartbeatTracker; private final ScheduledExecutorService activeWorkRefreshExecutor; private final ExecutorService fanOutActiveWorkRefreshExecutor; @@ -69,14 +67,12 @@ public final class ActiveWorkRefresher { public ActiveWorkRefresher( Supplier clock, int activeWorkRefreshPeriodMillis, - int stuckCommitDurationMillis, Supplier> computations, DataflowExecutionStateSampler sampler, ScheduledExecutorService activeWorkRefreshExecutor, HeartbeatTracker heartbeatTracker) { this.clock = clock; this.activeWorkRefreshPeriodMillis = activeWorkRefreshPeriodMillis; - this.stuckCommitDurationMillis = stuckCommitDurationMillis; this.computations = computations; this.sampler = sampler; this.activeWorkRefreshExecutor = activeWorkRefreshExecutor; @@ -101,16 +97,10 @@ public void start() { activeWorkRefreshPeriodMillis, TimeUnit.MILLISECONDS); } - - if (stuckCommitDurationMillis > 0) { - int periodMillis = Math.max(stuckCommitDurationMillis / 10, 100); - activeWorkRefreshExecutor.scheduleWithFixedDelay( - this::invalidateStuckCommits, periodMillis, periodMillis, TimeUnit.MILLISECONDS); - } } public void stop() { - if (activeWorkRefreshPeriodMillis > 0 || stuckCommitDurationMillis > 0) { + if (activeWorkRefreshPeriodMillis > 0) { activeWorkRefreshExecutor.shutdown(); try { activeWorkRefreshExecutor.awaitTermination(300, TimeUnit.SECONDS); @@ -120,13 +110,6 @@ public void stop() { } } - private void invalidateStuckCommits() { - Instant stuckCommitDeadline = clock.get().minus(Duration.millis(stuckCommitDurationMillis)); - for (ComputationState computationState : computations.get()) { - computationState.invalidateStuckCommits(stuckCommitDeadline); - } - } - private void refreshActiveWork() { Instant refreshDeadline = clock.get().minus(Duration.millis(activeWorkRefreshPeriodMillis)); Map heartbeatsBySender = diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOperationContextTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOperationContextTest.java index 34c3b3d5373c..6692f06d75d4 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOperationContextTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOperationContextTest.java @@ -305,11 +305,13 @@ private void verifyLullLog(boolean hasFullThreadDump) throws IOException { String infoLines = Joiner.on("\n").join(Iterables.filter(lines, line -> line.contains("\"INFO\""))); + // Match on the thread name rather than the full Thread.toString() prefix: JDK 21+ inserts + // the thread id (Thread[#51,backgroundThread,...] vs Thread[backgroundThread,...]). if (hasFullThreadDump) { assertThat( infoLines, Matchers.allOf( - Matchers.containsString("Thread[backgroundThread,"), + Matchers.containsString("backgroundThread,"), Matchers.containsString( "org.apache.beam.runners.dataflow.worker.DataflowOperationContext"), Matchers.not(Matchers.containsString(SimpleDoFnRunner.class.getName())))); @@ -318,7 +320,7 @@ private void verifyLullLog(boolean hasFullThreadDump) throws IOException { infoLines, Matchers.not( Matchers.anyOf( - Matchers.containsString("Thread[backgroundThread,"), + Matchers.containsString("backgroundThread,"), Matchers.containsString( "org.apache.beam.runners.dataflow.worker.DataflowOperationContext")))); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounterTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounterTest.java new file mode 100644 index 000000000000..b5c49ee639b5 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounterTest.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.runners.dataflow.worker; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import org.apache.beam.runners.core.KeyedWorkItem; +import org.apache.beam.runners.dataflow.worker.counters.CounterName; +import org.apache.beam.runners.dataflow.worker.counters.CounterSet; +import org.apache.beam.runners.dataflow.worker.counters.NameContext; +import org.apache.beam.runners.dataflow.worker.util.ValueInEmptyWindows; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DataflowOutputCounter}. */ +@RunWith(JUnit4.class) +public class DataflowOutputCounterTest { + private static final String OUTPUT_NAME = "test_output"; + private CounterSet counterSet; + private NameContext nameContext; + + @Before + public void setUp() { + counterSet = new CounterSet(); + nameContext = NameContext.create("stage", "original", "system", OUTPUT_NAME); + } + + @Test + public void testBatchOutputCounterWithEmptyWindows() throws Exception { + DataflowOutputCounter batchCounter = + DataflowOutputCounter.create(OUTPUT_NAME, counterSet, nameContext, false); + + ValueInEmptyWindows> shuffleValue = + new ValueInEmptyWindows<>(KV.of("key", "value")); + batchCounter.update(shuffleValue); + + long elementCount = + (Long) + counterSet + .getExistingCounter( + CounterName.named(DataflowOutputCounter.getElementCounterName(OUTPUT_NAME))) + .getAggregate(); + assertEquals(1L, elementCount); + } + + @Test + public void testStreamingOutputCounterWithKeyedWorkItem() throws Exception { + DataflowOutputCounter streamingCounter = + DataflowOutputCounter.create(OUTPUT_NAME, counterSet, nameContext, true); + + KeyedWorkItem kwi = mock(KeyedWorkItem.class); + WindowedValue element1 = WindowedValues.valueInGlobalWindow("v1"); + WindowedValue element2 = WindowedValues.valueInGlobalWindow("v2"); + doReturn(Arrays.asList(element1, element2)).when(kwi).elementWindowsIterable(); + + ValueInEmptyWindows> streamingValue = + new ValueInEmptyWindows<>(kwi); + streamingCounter.update(streamingValue); + + long elementCount = + (Long) + counterSet + .getExistingCounter( + CounterName.named(DataflowOutputCounter.getElementCounterName(OUTPUT_NAME))) + .getAggregate(); + assertEquals(2L, elementCount); + } + + @Test + public void testStandardWindowedValueCounting() throws Exception { + DataflowOutputCounter counter = + DataflowOutputCounter.create(OUTPUT_NAME, counterSet, nameContext, false); + + WindowedValue standardValue = WindowedValues.valueInGlobalWindow("v1"); + counter.update(standardValue); + + long elementCount = + (Long) + counterSet + .getExistingCounter( + CounterName.named(DataflowOutputCounter.getElementCounterName(OUTPUT_NAME))) + .getAggregate(); + assertEquals(1L, elementCount); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowWorkUnitClientTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowWorkUnitClientTest.java index 85d79e6be3c1..b4b70a3a0aba 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowWorkUnitClientTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowWorkUnitClientTest.java @@ -121,7 +121,7 @@ public void testCloudServiceCallMapTaskStagePropagation() throws Exception { // Publish and acquire a map task work item, and verify we're now processing that stage. final String stageName = "test_stage_name"; MapTask mapTask = new MapTask(); - mapTask.setStageName(stageName); + mapTask.setSystemName(stageName); WorkItem workItem = createWorkItem(PROJECT_ID, JOB_ID); workItem.setMapTask(mapTask); @@ -133,7 +133,7 @@ public void testCloudServiceCallMapTaskStagePropagation() throws Exception { WorkUnitClient client = new DataflowWorkUnitClient(pipelineOptions, LOG); assertEquals(Optional.of(workItem), client.getWorkItem()); - assertEquals(stageName, DataflowWorkerLoggingMDC.getStageName()); + assertEquals(stageName, DataflowWorkerLoggingMDC.getSystemStageName()); } @Test @@ -141,7 +141,7 @@ public void testCloudServiceCallSeqMapTaskStagePropagation() throws Exception { // Publish and acquire a seq map task work item, and verify we're now processing that stage. final String stageName = "test_stage_name"; SeqMapTask seqMapTask = new SeqMapTask(); - seqMapTask.setStageName(stageName); + seqMapTask.setSystemName(stageName); WorkItem workItem = createWorkItem(PROJECT_ID, JOB_ID); workItem.setSeqMapTask(seqMapTask); @@ -153,7 +153,7 @@ public void testCloudServiceCallSeqMapTaskStagePropagation() throws Exception { WorkUnitClient client = new DataflowWorkUnitClient(pipelineOptions, LOG); assertEquals(Optional.of(workItem), client.getWorkItem()); - assertEquals(stageName, DataflowWorkerLoggingMDC.getStageName()); + assertEquals(stageName, DataflowWorkerLoggingMDC.getSystemStageName()); } @Test diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/FakeWindmillServer.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/FakeWindmillServer.java index 5be8ec0a6c72..e5d68376a7d5 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/FakeWindmillServer.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/FakeWindmillServer.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -37,10 +36,12 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.concurrent.GuardedBy; @@ -49,6 +50,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.WorkId; import org.apache.beam.runners.dataflow.worker.windmill.CloudWindmillMetadataServiceV1Alpha1Grpc; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.CommitStatus; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.CommitWorkResponse; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.ComputationCommitWorkRequest; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.ComputationGetDataRequest; @@ -87,8 +89,11 @@ public final class FakeWindmillServer extends WindmillServerStub { private final ResponseQueue dataToOffer; private final ResponseQueue commitsToOffer; private final Map streamingCommitsToOffer; + private final AtomicReference multiKeyCommitStatusToOffer; // Keys are work tokens. private final Map commitsReceived; + private final List multiKeyCommitsReceived = + new CopyOnWriteArrayList<>(); private final ArrayList statsReceived; private final LinkedBlockingQueue exceptions; private final AtomicInteger expectedExceptionCount; @@ -118,7 +123,9 @@ public FakeWindmillServer( commitsToOffer = new ResponseQueue() .returnByDefault(CommitWorkResponse.getDefaultInstance()); - streamingCommitsToOffer = new HashMap<>(); + streamingCommitsToOffer = new ConcurrentHashMap<>(); + // Respond multikey commits with ok, unless overridden. + multiKeyCommitStatusToOffer = new AtomicReference<>(CommitStatus.OK); commitsReceived = new ConcurrentHashMap<>(); exceptions = new LinkedBlockingQueue<>(); expectedExceptionCount = new AtomicInteger(); @@ -153,6 +160,11 @@ public Map whenCommitWorkStreamCalled() { return streamingCommitsToOffer; } + /** @param commitStatus status to return to multiKeyCommits */ + public void setMultiKeyCommitStatus(CommitStatus commitStatus) { + this.multiKeyCommitStatusToOffer.set(commitStatus); + } + @Override public Windmill.GetWorkResponse getWork(Windmill.GetWorkRequest request) { LOG.debug("getWorkRequest: {}", request.toString()); @@ -400,6 +412,7 @@ public void shutdown() {} public RequestBatcher batcher() { return new RequestBatcher() { final List requests = new ArrayList<>(); + final List multiKeyRequests = new ArrayList<>(); @Override public boolean commitWorkItem( @@ -423,6 +436,17 @@ public boolean commitWorkItem( return true; } + @Override + public boolean commitMultiKeyWorkItem( + String computation, + Windmill.MultiKeyWorkItemCommitRequest request, + Consumer onDone) { + LOG.debug("commitWorkStream::commitMultiKeyWorkItem: {}", request); + multiKeyRequests.add(new MultiKeyRequestAndDone(request, onDone)); + flush(); + return true; + } + @Override public void flush() { for (RequestAndDone elem : requests) { @@ -445,6 +469,24 @@ public void flush() { .orElse(Windmill.CommitStatus.OK)); } requests.clear(); + + for (MultiKeyRequestAndDone elem : multiKeyRequests) { + if (dropStreamingCommits) { + for (WorkItemCommitRequest workRequest : elem.request.getRequestsList()) { + droppedStreamingCommits.put(workRequest.getWorkToken(), elem.onDone); + } + continue; + } + + multiKeyCommitsReceived.add(elem.request); + for (WorkItemCommitRequest workRequest : elem.request.getRequestsList()) { + commitsReceived.put(workRequest.getWorkToken(), workRequest); + } + + Windmill.CommitStatus status = multiKeyCommitStatusToOffer.get(); + elem.onDone.accept(status); + } + multiKeyRequests.clear(); } class RequestAndDone { @@ -456,6 +498,18 @@ class RequestAndDone { this.onDone = onDone; } } + + class MultiKeyRequestAndDone { + final Consumer onDone; + final Windmill.MultiKeyWorkItemCommitRequest request; + + MultiKeyRequestAndDone( + Windmill.MultiKeyWorkItemCommitRequest request, + Consumer onDone) { + this.request = request; + this.onDone = onDone; + } + } }; } @@ -518,6 +572,15 @@ public Map waitForAndGetCommits(int numCommits) { public void clearCommitsReceived() { commitsRequested = 0; commitsReceived.clear(); + multiKeyCommitsReceived.clear(); + } + + public List getMultiKeyCommitsReceived() { + return multiKeyCommitsReceived; + } + + public void clearMultiKeyCommitsReceived() { + multiKeyCommitsReceived.clear(); } public ConcurrentHashMap> waitForDroppedCommits( diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactoryTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactoryTest.java index 3443ae0022bc..d3a424758f66 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactoryTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactoryTest.java @@ -330,7 +330,8 @@ public void testCreateReadOperation() throws Exception { when(network.successors(instructionNode)) .thenReturn( ImmutableSet.of( - IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, counterSet) + IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform( + STAGE, counterSet, false) .apply( InstructionOutputNode.create( instructionNode.getParallelInstruction().getOutputs().get(0), @@ -535,7 +536,7 @@ public void testCreateParDoOperation() throws Exception { ExecutionLocation.UNKNOWN); Node outputReceiverNode = - IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, counterSet) + IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, counterSet, false) .apply( InstructionOutputNode.create( instructionNode.getParallelInstruction().getOutputs().get(0), PCOLLECTION_ID)); @@ -614,7 +615,8 @@ public void testCreatePartialGroupByKeyOperation() throws Exception { when(network.successors(instructionNode)) .thenReturn( ImmutableSet.of( - IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, counterSet) + IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform( + STAGE, counterSet, false) .apply( InstructionOutputNode.create( instructionNode.getParallelInstruction().getOutputs().get(0), @@ -669,7 +671,8 @@ public void testCreatePartialGroupByKeyOperationWithCombine() throws Exception { when(network.successors(instructionNode)) .thenReturn( ImmutableSet.of( - IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, counterSet) + IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform( + STAGE, counterSet, false) .apply( InstructionOutputNode.create( instructionNode.getParallelInstruction().getOutputs().get(0), @@ -750,7 +753,8 @@ public void testCreateFlattenOperation() throws Exception { when(network.successors(instructionNode)) .thenReturn( ImmutableSet.of( - IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, counterSet) + IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform( + STAGE, counterSet, false) .apply( InstructionOutputNode.create( instructionNode.getParallelInstruction().getOutputs().get(0), 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..73d69bd0f617 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(Object key) throws Exception {} } // A mock ReadOperation fed to a MapTaskExecutor in test. @@ -217,6 +220,9 @@ public void finishBundle() {} @Override public void abort() {} + + @Override + public void finishKey(Object key) throws Exception {} } /** Verify counts for the per-element-output-time counter are correct. */ @@ -312,6 +318,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(1L); } } + + @Override + public void finishKey(Object key) throws Exception {} }, new Operation(new OutputReceiver[] {}, context2) { @Override @@ -321,6 +330,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(2L); } } + + @Override + public void finishKey(Object key) throws Exception {} }, new Operation(new OutputReceiver[] {}, context3) { @Override @@ -330,6 +342,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(3L); } } + + @Override + public void finishKey(Object key) throws Exception {} }); try (IntrinsicMapTaskExecutor executor = diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/KeyTokenInvalidExceptionTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/KeyTokenInvalidExceptionTest.java deleted file mode 100644 index 1eb2871e8cd3..000000000000 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/KeyTokenInvalidExceptionTest.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.dataflow.worker; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Tests for {@link KeyTokenInvalidException}. */ -@RunWith(JUnit4.class) -public final class KeyTokenInvalidExceptionTest { - @Test - public void testIsKeyTokenInvalidException() throws Exception { - KeyTokenInvalidException exception = new KeyTokenInvalidException("test"); - RuntimeException keyTokenCauseException = new RuntimeException("key token cause", exception); - assertTrue(KeyTokenInvalidException.isKeyTokenInvalidException(exception)); - assertTrue(KeyTokenInvalidException.isKeyTokenInvalidException(keyTokenCauseException)); - assertFalse( - KeyTokenInvalidException.isKeyTokenInvalidException(new RuntimeException("non key token"))); - } -} 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/StreamingDataflowWorkerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java index d8a1d1b90d47..c48b30ecf640 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 @@ -72,8 +72,8 @@ import java.util.Map; import java.util.Optional; import java.util.PriorityQueue; +import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -117,7 +117,6 @@ import org.apache.beam.runners.dataflow.worker.util.WorkerPropertyNames; import org.apache.beam.runners.dataflow.worker.windmill.CloudWindmillServiceV1Alpha1Grpc; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; -import org.apache.beam.runners.dataflow.worker.windmill.Windmill.CommitStatus; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.ComputationGetDataRequest; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.ComputationGetDataResponse; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.ComputationHeartbeatRequest; @@ -261,22 +260,33 @@ public class StreamingDataflowWorkerTest { private static final long MAXIMUM_BYTES_OUTSTANDING = 10000000; private static GetDataResponse emptyDataResponder(GetDataRequest request) { - GetDataResponse.Builder builder = GetDataResponse.newBuilder(); - for (ComputationGetDataRequest compRequest : request.getRequestsList()) { - ComputationGetDataResponse.Builder compBuilder = - builder.addDataBuilder().setComputationId(compRequest.getComputationId()); - for (KeyedGetDataRequest keyRequest : compRequest.getRequestsList()) { - KeyedGetDataResponse.Builder keyBuilder = - compBuilder - .addDataBuilder() - .setKey(keyRequest.getKey()) - .setShardingKey(keyRequest.getShardingKey()); - keyBuilder.addAllValues(keyRequest.getValuesToFetchList()); - keyBuilder.addAllBags(keyRequest.getBagsToFetchList()); - keyBuilder.addAllWatermarkHolds(keyRequest.getWatermarkHoldsToFetchList()); + return emptyDataResponderWithFailedWorkTokens(Set.of()).apply(request); + } + + private static Function emptyDataResponderWithFailedWorkTokens( + Set failedWorkTokens) { + return (GetDataRequest request) -> { + GetDataResponse.Builder builder = GetDataResponse.newBuilder(); + for (ComputationGetDataRequest compRequest : request.getRequestsList()) { + ComputationGetDataResponse.Builder compBuilder = + builder.addDataBuilder().setComputationId(compRequest.getComputationId()); + for (KeyedGetDataRequest keyRequest : compRequest.getRequestsList()) { + KeyedGetDataResponse.Builder keyBuilder = + compBuilder + .addDataBuilder() + .setKey(keyRequest.getKey()) + .setShardingKey(keyRequest.getShardingKey()); + if (failedWorkTokens.contains(keyRequest.getWorkToken())) { + keyBuilder.setFailed(true); + continue; + } + keyBuilder.addAllValues(keyRequest.getValuesToFetchList()); + keyBuilder.addAllBags(keyRequest.getBagsToFetchList()); + keyBuilder.addAllWatermarkHolds(keyRequest.getWatermarkHoldsToFetchList()); + } } - } - return builder.build(); + return builder.build(); + }; } private final boolean streamingEngine; @@ -372,8 +382,11 @@ private static ExecutableWork createMockWork( Work.createProcessingContext( computationId, new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.class)), false, - Instant::now), - processWorkFn); + Instant::now, + ImmutableList.of()), + (work, handle) -> { + processWorkFn.accept(work); + }); } private byte[] intervalWindowBytes(IntervalWindow window) throws Exception { @@ -418,6 +431,7 @@ private ParallelInstruction makeWindowingSourceInstruction(Coder coder) { CloudObjects.asCloudObject(IntervalWindowCoder.of(), /* sdkComponents= */ null))); return new ParallelInstruction() + .setName(DEFAULT_SOURCE_SYSTEM_NAME) .setSystemName(DEFAULT_SOURCE_SYSTEM_NAME) .setOriginalName(DEFAULT_SOURCE_ORIGINAL_NAME) .setRead( @@ -437,6 +451,7 @@ private ParallelInstruction makeWindowingSourceInstruction(Coder coder) { private ParallelInstruction makeSourceInstruction(Coder coder) { return new ParallelInstruction() + .setName(DEFAULT_SOURCE_SYSTEM_NAME) .setSystemName(DEFAULT_SOURCE_SYSTEM_NAME) .setOriginalName(DEFAULT_SOURCE_ORIGINAL_NAME) .setRead( @@ -525,6 +540,7 @@ private ParallelInstruction makeSinkInstruction( CloudObject spec = CloudObject.forClass(WindmillSink.class); addString(spec, "stream_id", streamId); return new ParallelInstruction() + .setName(streamId) .setSystemName(DEFAULT_SINK_SYSTEM_NAME) .setOriginalName(DEFAULT_SINK_ORIGINAL_NAME) .setWrite( @@ -569,11 +585,16 @@ private Windmill.GetWorkResponse buildInput(String input, byte[] metadata) throw Windmill.GetWorkResponse.Builder builder = Windmill.GetWorkResponse.newBuilder(); TextFormat.merge(input, builder); if (metadata != null) { - Windmill.InputMessageBundle.Builder messageBundleBuilder = - builder.getWorkBuilder(0).getWorkBuilder(0).getMessageBundlesBuilder(0); - for (Windmill.Message.Builder messageBuilder : - messageBundleBuilder.getMessagesBuilderList()) { - messageBuilder.setMetadata(addPaneTag(PaneInfo.NO_FIRING, metadata)); + for (Windmill.ComputationWorkItems.Builder compBuilder : builder.getWorkBuilderList()) { + for (Windmill.WorkItem.Builder workBuilder : compBuilder.getWorkBuilderList()) { + for (Windmill.InputMessageBundle.Builder messageBundleBuilder : + workBuilder.getMessageBundlesBuilderList()) { + for (Windmill.Message.Builder messageBuilder : + messageBundleBuilder.getMessagesBuilderList()) { + messageBuilder.setMetadata(addPaneTag(PaneInfo.NO_FIRING, metadata)); + } + } + } } } @@ -891,7 +912,7 @@ private ByteString addPaneTag(PaneInfo paneInfo, byte[] windowBytes) throws IOEx } private DataflowWorkerHarnessOptions createTestingPipelineOptions(String... args) { - List argsList = Lists.newArrayList(args); + List argsList = new ArrayList<>(Arrays.asList(args)); if (streamingEngine) { argsList.add("--experiments=enable_streaming_engine"); } @@ -1242,46 +1263,421 @@ public void testNumberOfWorkerHarnessThreadsIsHonored() throws Exception { } @Test - public void testKeyTokenInvalidException() throws Exception { - if (streamingEngine) { - // TODO: This test needs to be adapted to work with streamingEngine=true. + public void testMultiKeyCommit_success() throws Exception { + if (!streamingEngine) { return; } - KvCoder kvCoder = KvCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of()); + StreamingDataflowWorker worker = makeMultiKeyEnabledWorker(); + worker.start(); - List instructions = - Arrays.asList( - makeSourceInstruction(kvCoder), - makeDoFnInstruction(new KeyTokenInvalidFn(), 0, kvCoder), - makeSinkInstruction(kvCoder, 1)); + String batchInputText = + "work {" + + " computation_id: \"" + + DEFAULT_COMPUTATION_ID + + "\"" + + " input_data_watermark: 0" + + " work {" + + " key: \"key1\"" + + " sharding_key: 1" + + " work_token: 1" + + " cache_token: 2" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data1\"" + + " }" + + " }" + + " }" + + " work {" + + " key: \"key2\"" + + " sharding_key: 2" + + " work_token: 2" + + " cache_token: 3" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data2\"" + + " }" + + " }" + + " }" + + " work {" + + " key: \"key3\"" + + " sharding_key: 3" + + " work_token: 3" + + " cache_token: 4" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data3\"" + + " }" + + " }" + + " }" + + "}"; + Windmill.GetWorkResponse batchInput = + buildInput( + batchInputText, + CoderUtils.encodeToByteArray( + CollectionCoder.of(IntervalWindow.getCoder()), + Collections.singletonList(DEFAULT_WINDOW))); + + server.whenGetDataCalled().answerByDefault(StreamingDataflowWorkerTest::emptyDataResponder); + + server.whenGetWorkCalled().thenReturn(batchInput); + + Map result = server.waitForAndGetCommits(3); + + assertEquals(3, result.size()); + + List multiKeyCommits = + server.getMultiKeyCommitsReceived(); + assertEquals(1, multiKeyCommits.size()); + Windmill.MultiKeyWorkItemCommitRequest multiKeyCommit = multiKeyCommits.get(0); + assertEquals(3, multiKeyCommit.getRequestsCount()); + assertEquals(1, multiKeyCommit.getRequests(0).getWorkToken()); + assertEquals(2, multiKeyCommit.getRequests(1).getWorkToken()); + assertEquals(3, multiKeyCommit.getRequests(2).getWorkToken()); + + worker.stop(); + } + + @Test + public void testMultiKeyCommit_elementFailure() throws Exception { + if (!streamingEngine) { + return; + } + StreamingDataflowWorker worker = makeMultiKeyEnabledWorker(); + worker.start(); + + String batchInputText = + "work {" + + " computation_id: \"" + + DEFAULT_COMPUTATION_ID + + "\"" + + " input_data_watermark: 0" + + " work {" + + " key: \"key1\"" + + " sharding_key: 1" + + " work_token: 1" + + " cache_token: 2" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data1\"" + + " }" + + " }" + + " }" + + " work {" + + " key: \"key2\"" + + " sharding_key: 2" + + " work_token: 2" + + " cache_token: 3" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data2\"" + + " }" + + " }" + + " }" + + " work {" + + " key: \"key3\"" + + " sharding_key: 3" + + " work_token: 3" + + " cache_token: 4" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data3\"" + + " }" + + " }" + + " }" + + "}"; + Windmill.GetWorkResponse batchInput = + buildInput( + batchInputText, + CoderUtils.encodeToByteArray( + CollectionCoder.of(IntervalWindow.getCoder()), + Collections.singletonList(DEFAULT_WINDOW))); server - .whenGetWorkCalled() - .thenReturn(makeInput(0, 0, DEFAULT_KEY_STRING, DEFAULT_SHARDING_KEY)); + .whenGetDataCalled() + .answerByDefault( + StreamingDataflowWorkerTest.emptyDataResponderWithFailedWorkTokens(Set.of(2L))); - StreamingDataflowWorker worker = - makeWorker(defaultWorkerParams().setInstructions(instructions).publishCounters().build()); + server.whenGetWorkCalled().thenReturn(batchInput); + + Map result = server.waitForAndGetCommits(2); + + assertTrue(result.containsKey(1L)); + assertTrue(result.containsKey(3L)); + assertFalse(result.containsKey(2L)); + + List multiKeyCommits = + server.getMultiKeyCommitsReceived(); + assertEquals(1, multiKeyCommits.size()); + Windmill.MultiKeyWorkItemCommitRequest multiKeyCommit = multiKeyCommits.get(0); + assertEquals(2, multiKeyCommit.getRequestsCount()); + assertEquals(3, multiKeyCommit.getRequests(0).getWorkToken()); + assertEquals(1, multiKeyCommit.getRequests(1).getWorkToken()); + + worker.stop(); + } + + @Test + public void testCompleteCommit_retryableFailureTriggersReExecution() throws Exception { + if (!streamingEngine) { + return; + } + StreamingDataflowWorker worker = makeMultiKeyEnabledWorker(); worker.start(); - server.waitForEmptyWorkQueue(); + String batchInputText = + "work {" + + " computation_id: \"" + + DEFAULT_COMPUTATION_ID + + "\"" + + " input_data_watermark: 0" + + " work {" + + " key: \"key1\"" + + " sharding_key: 1" + + " work_token: 1" + + " cache_token: 2" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data1\"" + + " }" + + " }" + + " }" + + " work {" + + " key: \"key2\"" + + " sharding_key: 2" + + " work_token: 2" + + " cache_token: 3" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data2\"" + + " }" + + " }" + + " }" + + "}"; + Windmill.GetWorkResponse batchInput = + buildInput( + batchInputText, + CoderUtils.encodeToByteArray( + CollectionCoder.of(IntervalWindow.getCoder()), + Collections.singletonList(DEFAULT_WINDOW))); server - .whenGetWorkCalled() - .thenReturn(makeInput(1, 0, DEFAULT_KEY_STRING, DEFAULT_SHARDING_KEY)); + .whenGetDataCalled() + .answerByDefault( + StreamingDataflowWorkerTest.emptyDataResponderWithFailedWorkTokens(Set.of(2L))); + + server.whenGetWorkCalled().thenReturn(batchInput); Map result = server.waitForAndGetCommits(1); - assertEquals( - makeExpectedOutput(1, 0, DEFAULT_KEY_STRING, DEFAULT_SHARDING_KEY, DEFAULT_KEY_STRING) - .build(), - removeDynamicFields(result.get(1L))); - assertEquals(1, result.size()); + assertTrue(result.containsKey(1L)); + assertFalse(result.containsKey(2L)); + + List multiKeyCommits = + server.getMultiKeyCommitsReceived(); + assertEquals(1, multiKeyCommits.size()); + Windmill.MultiKeyWorkItemCommitRequest multiKeyCommit = multiKeyCommits.get(0); + assertEquals(1, multiKeyCommit.getRequestsCount()); + assertEquals(1, multiKeyCommit.getRequests(0).getWorkToken()); worker.stop(); } @Test - public void testKeyCommitTooLargeException() throws Exception { + public void testMultiKeyCommit_queuedWorkItemFailsAndSubsequentWorkItemPickedUp() + throws Exception { + if (!streamingEngine) { + return; + } + BlockingKvDoFn.reset(); + StreamingDataflowWorker worker = makeMultiKeyEnabledWorker(new BlockingKvDoFn()); + worker.start(); + + String batchInputText1 = + "work {" + + " computation_id: \"" + + DEFAULT_COMPUTATION_ID + + "\"" + + " input_data_watermark: 0" + + " work {" + + " key: \"key1\"" + + " sharding_key: 1" + + " work_token: 1" + + " cache_token: 2" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data1\"" + + " }" + + " }" + + " }" + + " work {" + + " key: \"key2\"" + + " sharding_key: 2" + + " work_token: 2" + + " cache_token: 3" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data2\"" + + " }" + + " }" + + " }" + + "}"; + + String batchInputText2 = + "work {" + + " computation_id: \"" + + DEFAULT_COMPUTATION_ID + + "\"" + + " input_data_watermark: 0" + + " work {" + + " key: \"key2\"" + + " sharding_key: 2" + + " work_token: 3" + + " cache_token: 4" + + " key_group { high: 0 low: 1 }" + + " message_bundles {" + + " source_computation_id: \"" + + DEFAULT_SOURCE_COMPUTATION_ID + + "\"" + + " messages {" + + " timestamp: 0" + + " data: \"data3\"" + + " }" + + " }" + + " }" + + "}"; + Windmill.GetWorkResponse batchInput1 = + buildInput( + batchInputText1, + CoderUtils.encodeToByteArray( + CollectionCoder.of(IntervalWindow.getCoder()), + Collections.singletonList(DEFAULT_WINDOW))); + Windmill.GetWorkResponse batchInput2 = + buildInput( + batchInputText2, + CoderUtils.encodeToByteArray( + CollectionCoder.of(IntervalWindow.getCoder()), + Collections.singletonList(DEFAULT_WINDOW))); + + server.whenGetDataCalled().answerByDefault(StreamingDataflowWorkerTest::emptyDataResponder); + + server.whenGetWorkCalled().thenReturn(batchInput1).thenReturn(batchInput2); + server.waitForEmptyWorkQueue(); + + // Wait for key1 to start processing and block on BlockingKvDoFn. + BlockingKvDoFn.counter.get().acquire(1); + + // Fail key2 (work token 2) via failed heartbeat while key1 is still processing. + ComputationHeartbeatResponse.Builder failedHeartbeat = + ComputationHeartbeatResponse.newBuilder(); + failedHeartbeat + .setComputationId(DEFAULT_COMPUTATION_ID) + .addHeartbeatResponsesBuilder() + .setCacheToken(3) + .setWorkToken(2) + .setShardingKey(2) + .setFailed(true); + + // Fake server processes heartbeat responses are processed synchronously in sendFailedHeartbeats + server.sendFailedHeartbeats(Collections.singletonList(failedHeartbeat.build())); + + // Unblock key1 to allow bundle to poll key2 (token 2 -> failed, skipped) and key2 (token 3). + BlockingKvDoFn.blocker.get().countDown(); + + Map result = server.waitForAndGetCommits(2); + + assertTrue(result.containsKey(1L)); + assertTrue(result.containsKey(3L)); + assertFalse(result.containsKey(2L)); + + List multiKeyCommits = + server.getMultiKeyCommitsReceived(); + assertEquals(1, multiKeyCommits.size()); + Windmill.MultiKeyWorkItemCommitRequest multiKeyCommit = multiKeyCommits.get(0); + assertEquals(2, multiKeyCommit.getRequestsCount()); + assertEquals(1, multiKeyCommit.getRequests(0).getWorkToken()); + assertEquals(3, multiKeyCommit.getRequests(1).getWorkToken()); + + worker.stop(); + } + + private StreamingDataflowWorker makeMultiKeyEnabledWorker() { + return makeMultiKeyEnabledWorker(new WorkDoFn()); + } + + private StreamingDataflowWorker makeMultiKeyEnabledWorker( + DoFn, KV> doFn) { + KvCoder kvCoder = KvCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of()); + + List instructions = + Arrays.asList( + makeSourceInstruction(kvCoder), + makeDoFnInstruction(doFn, 0, kvCoder), + makeSinkInstruction(kvCoder, 1)); + + StreamingDataflowWorker worker = + makeWorker( + defaultWorkerParams( + "--experiments=unstable_enable_multi_key_bundle,windmill_max_key_group_batch_time_ms=50000", + "--numberOfWorkerHarnessThreads=1") + .setLocalRetryTimeoutMs(100) + .setInstructions(instructions) + .build()); + return worker; + } + + private void runKeyCommitTooLargeExceptionTest( + StreamingDataflowWorkerTestParams.Builder workerParams, boolean expectKeyInErrorMessage) + throws Exception { KvCoder kvCoder = KvCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of()); List instructions = @@ -1294,7 +1690,7 @@ public void testKeyCommitTooLargeException() throws Exception { StreamingDataflowWorker worker = makeWorker( - defaultWorkerParams() + workerParams .setInstructions(instructions) .setStreamingGlobalConfig( StreamingGlobalConfig.builder() @@ -1325,8 +1721,7 @@ public void testKeyCommitTooLargeException() throws Exception { makeExpectedTruncationRequestOutput( 1, "large_key", DEFAULT_SHARDING_KEY, largeCommit.getEstimatedWorkItemCommitBytes()) .build(), - largeCommit); - + removeDynamicFields(largeCommit)); // Check this explicitly since the estimated commit bytes weren't actually // checked against an expected value in the previous step assertTrue(largeCommit.getEstimatedWorkItemCommitBytes() > 1000); @@ -1350,12 +1745,35 @@ public void testKeyCommitTooLargeException() throws Exception { foundErrors = true; String errorMessage = status.getErrors().get(0).getMessage(); assertThat(errorMessage, Matchers.containsString("KeyCommitTooLargeException")); + assertThat(errorMessage, Matchers.containsString("Commit request for stage computation")); + if (expectKeyInErrorMessage) { + assertThat(errorMessage, Matchers.containsString("and key large_key")); + } else { + assertThat(errorMessage, Matchers.not(Matchers.containsString("and key large_key"))); + } } } assertTrue(foundErrors); worker.stop(); } + @Test + public void testKeyCommitTooLargeException() throws Exception { + runKeyCommitTooLargeExceptionTest(defaultWorkerParams(), /* expectKeyInErrorMessage= */ false); + } + + @Test + public void testKeyCommitTooLargeException_withHotKeyLoggingEnabled() throws Exception { + runKeyCommitTooLargeExceptionTest( + defaultWorkerParams("--hotKeyLoggingEnabled=true"), /* expectKeyInErrorMessage= */ true); + } + + @Test + public void testKeyCommitTooLargeException_withHotKeyLoggingDisabled() throws Exception { + runKeyCommitTooLargeExceptionTest( + defaultWorkerParams("--hotKeyLoggingEnabled=false"), /* expectKeyInErrorMessage= */ false); + } + @Test public void testOutputKeyTooLargeException() throws Exception { KvCoder kvCoder = KvCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of()); @@ -2493,6 +2911,7 @@ private List makeUnboundedSourcePipeline( return Arrays.asList( new ParallelInstruction() + .setName("Read") .setSystemName("Read") .setOriginalName("OriginalReadName") .setRead( @@ -3034,7 +3453,8 @@ public void testMaxThreadMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3095,7 +3515,8 @@ public void testActiveThreadMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3165,7 +3586,8 @@ public void testOutstandingBytesMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3239,7 +3661,8 @@ public void testOutstandingBundlesMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3657,8 +4080,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(); } @@ -3679,7 +4102,8 @@ public void testLatencyAttributionProtobufsPopulated() { ignored -> {}, mock(HeartbeatSender.class)), false, - clock); + clock, + ImmutableList.of()); clock.sleep(Duration.millis(10)); work.setState(Work.State.PROCESSING); @@ -3948,11 +4372,16 @@ public void testDoFnLatencyBreakdownsReportedOnCommit() throws Exception { LatencyAttribution.newBuilder().setState(State.ACTIVE).setTotalDurationMillis(100); for (LatencyAttribution la : commit.getPerWorkItemLatencyAttributionsList()) { if (la.getState() == State.ACTIVE) { - assertThat(la.getActiveLatencyBreakdownCount(), equalTo(1)); - assertThat( - la.getActiveLatencyBreakdown(0).getUserStepName(), equalTo(DEFAULT_PARDO_USER_NAME)); - Assert.assertTrue(la.getActiveLatencyBreakdown(0).hasProcessingTimesDistribution()); - Assert.assertFalse(la.getActiveLatencyBreakdown(0).hasActiveMessageMetadata()); + LatencyAttribution.ActiveLatencyBreakdown pardoBreakdown = null; + for (LatencyAttribution.ActiveLatencyBreakdown lb : la.getActiveLatencyBreakdownList()) { + if (DEFAULT_PARDO_USER_NAME.equals(lb.getUserStepName())) { + pardoBreakdown = lb; + break; + } + } + Assert.assertNotNull("Expected breakdown for " + DEFAULT_PARDO_USER_NAME, pardoBreakdown); + Assert.assertTrue(pardoBreakdown.hasProcessingTimesDistribution()); + Assert.assertFalse(pardoBreakdown.hasActiveMessageMetadata()); } } @@ -4152,10 +4581,9 @@ public void testLimitOnOutputBundleSizeWithMultipleSinks() throws Exception { } @Test - public void testStuckCommit() throws Exception { + public void testStuckCommitMarksWorkerUnhealthy() throws Exception { if (!streamingEngine) { - // Stuck commits have only been observed with streaming engine and thus recovery from them is - // not implemented for non-streaming engine. + // Stuck commits have only been observed with streaming engine. return; } @@ -4164,40 +4592,31 @@ public void testStuckCommit() throws Exception { makeSourceInstruction(StringUtf8Coder.of()), makeSinkInstruction(StringUtf8Coder.of(), 0)); + FakeClock clock = new FakeClock(); StreamingDataflowWorker worker = makeWorker( defaultWorkerParams("--stuckCommitDurationMillis=2000") .setInstructions(instructions) + .setClock(clock) .publishCounters() .build()); worker.start(); + assertTrue(worker.isHealthy()); + // Prevent commit callbacks from being called to simulate a stuck commit. server.setDropStreamingCommits(true); - // Add some work for key 1. + // Add work to trigger a commit that will get stuck. server .whenGetWorkCalled() - .thenReturn(makeInput(10, TimeUnit.MILLISECONDS.toMicros(2), DEFAULT_KEY_STRING, 1)) - .thenReturn(makeInput(15, TimeUnit.MILLISECONDS.toMicros(3), DEFAULT_KEY_STRING, 5)); - ConcurrentHashMap> droppedCommits = - server.waitForDroppedCommits(2); + .thenReturn(makeInput(10, TimeUnit.MILLISECONDS.toMicros(2), DEFAULT_KEY_STRING, 1)); + server.waitForDroppedCommits(1); + + clock.sleep(Duration.millis(3000)); + + assertFalse(worker.isHealthy()); server.setDropStreamingCommits(false); - // Enqueue another work item for key 1. - server - .whenGetWorkCalled() - .thenReturn(makeInput(1, TimeUnit.MILLISECONDS.toMicros(1), DEFAULT_KEY_STRING, 1)); - // Ensure that this work item processes. - Map result = server.waitForAndGetCommits(1); - // Now ensure that nothing happens if a dropped commit actually completes. - droppedCommits.values().iterator().next().accept(CommitStatus.OK); worker.stop(); - - assertTrue(result.containsKey(1L)); - assertEquals( - makeExpectedOutput( - 1, TimeUnit.MILLISECONDS.toMicros(1), DEFAULT_KEY_STRING, 1, DEFAULT_KEY_STRING) - .build(), - removeDynamicFields(result.get(1L))); } @Test @@ -4523,18 +4942,38 @@ public void evaluate() throws Throwable { } } - static class KeyTokenInvalidFn extends DoFn, KV> { - - static boolean thrown = false; + static class WorkDoFn extends DoFn, KV> { + @StateId("state") + private final StateSpec> stateSpec = StateSpecs.value(StringUtf8Coder.of()); @ProcessElement - public void processElement(ProcessContext c) { - if (!thrown) { - thrown = true; - throw new KeyTokenInvalidException("key"); - } else { - c.output(c.element()); + public void processElement(ProcessContext c, @StateId("state") ValueState state) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } + state.read(); + c.output(c.element()); + } + } + + static class BlockingKvDoFn extends DoFn, KV> { + public static final AtomicReference blocker = + new AtomicReference<>(new CountDownLatch(1)); + public static final AtomicReference counter = + new AtomicReference<>(new Semaphore(0)); + + @ProcessElement + public void processElement(ProcessContext c) throws InterruptedException { + counter.get().release(); + blocker.get().await(); + c.output(c.element()); + } + + public static void reset() { + blocker.set(new CountDownLatch(1)); + counter.set(new Semaphore(0)); } } 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..fe5ba60acec3 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 = @@ -747,8 +753,7 @@ DoFnRunner, KV> makeRunnerForGabw NullSideInputReader.empty(), outputManager, stepContext); - return DoFnRunners.lateDataDroppingRunner( - doFnRunner, stepContext.timerInternals(), windowingStrategy); + return DoFnRunners.lateDataDroppingRunner(doFnRunner, stepContext, windowingStrategy); } private IntervalWindow window(long start, long end) { 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/StreamingModeExecutionContextTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java index 4bfa6efc8880..5aceb0ca9564 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 @@ -24,7 +24,9 @@ import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; import static org.mockito.Mockito.mock; import com.google.api.services.dataflow.model.CounterMetadata; @@ -42,6 +44,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.beam.runners.core.SideInputReader; +import org.apache.beam.runners.core.StateInternals; import org.apache.beam.runners.core.StateNamespaceForTest; import org.apache.beam.runners.core.TimerInternals; import org.apache.beam.runners.core.TimerInternals.TimerData; @@ -51,26 +54,37 @@ import org.apache.beam.runners.dataflow.options.DataflowWorkerHarnessOptions; import org.apache.beam.runners.dataflow.worker.DataflowExecutionContext.DataflowExecutionStateTracker; import org.apache.beam.runners.dataflow.worker.MetricsToCounterUpdateConverter.Kind; +import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.KeyTransitionListener; import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.StreamingModeExecutionState; import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.StreamingModeExecutionStateRegistry; import org.apache.beam.runners.dataflow.worker.counters.CounterSet; import org.apache.beam.runners.dataflow.worker.counters.NameContext; import org.apache.beam.runners.dataflow.worker.profiler.ScopedProfiler.NoopProfileScope; import org.apache.beam.runners.dataflow.worker.profiler.ScopedProfiler.ProfileScope; +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.FailedWorkHandler; 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.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.streaming.config.StreamingGlobalConfigHandle; +import org.apache.beam.runners.dataflow.worker.streaming.harness.StreamingCounters; +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.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; -import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateReader; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTagEncodingV1; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillTagEncodingV2; +import org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures.FailureTracker; import org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender; import org.apache.beam.sdk.Pipeline; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.CoderException; import org.apache.beam.sdk.metrics.MetricsContainer; +import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.state.TimeDomain; import org.apache.beam.sdk.transforms.Create; @@ -79,10 +93,12 @@ import org.apache.beam.sdk.values.CausedByDrain; 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.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.hamcrest.Matchers; import org.joda.time.Duration; import org.joda.time.Instant; +import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -96,9 +112,18 @@ @RunWith(JUnit4.class) public class StreamingModeExecutionContextTest { + private static final FailedWorkHandler FAILING_FAILED_WORK_HANDLER = + ignored -> { + Assert.fail(); + }; + + private static final KeyTransitionListener FAILING_KEY_TRANSISITON = + (oldWork, newWork) -> { + Assert.fail(); + }; @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"; @@ -108,36 +133,52 @@ public class StreamingModeExecutionContextTest { DataflowWorkerHarnessOptions options; private FakeGlobalConfigHandle globalConfigHandle; + private StreamingModeExecutionContext createExecutionContext( + DataflowWorkerHarnessOptions options, 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, + new HotKeyLogger(), + /*hotKeyLoggingEnabled=*/ false, + /*stepName=*/ "stepName", + /*systemName=*/ "systemName", + StreamingCounters.create(), + mock(FailureTracker.class), + "sourceBytesProcessCounterName", + MultiKeyBundleOptions.fromOptions(options), + SideInputStateFetcherFactory.fromOptions(options)); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); options = PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class); - CounterSet counterSet = new CounterSet(); - ConcurrentHashMap stateNameMap = new ConcurrentHashMap<>(); + options + .as(ExperimentalOptions.class) + .setExperiments(List.of("unstable_enable_multi_key_bundle")); 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(options, globalConfigHandle); } private static Work createMockWork(Windmill.WorkItem workItem, Watermarks watermarks) { @@ -148,27 +189,49 @@ private static Work createMockWork(Windmill.WorkItem workItem, Watermarks waterm Work.createProcessingContext( COMPUTATION_ID, new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.class)), false, - Instant::now); + Instant::now, + ImmutableList.of()); + } + + private void start(Work work) { + start(executionContext, work, null); + } + + private void start(Work work, Coder keyCoder) { + start(executionContext, work, keyCoder); + } + + private void start(StreamingModeExecutionContext context, Work work) { + start(context, work, null); + } + + private void start(StreamingModeExecutionContext context, Work work, Coder keyCoder) { + try { + context.start( + work, + workExecutor, + /* workQueueExecutor= */ mock(BoundedQueueExecutor.class), + /* budgetHandle= */ mock(BoundedQueueExecutorWorkHandle.class), + keyCoder, + FAILING_KEY_TRANSISITON, + /* onFailedWorkHandler= */ FAILING_FAILED_WORK_HANDLER); + } catch (CoderException e) { + throw new RuntimeException(e); + } } @Test - public void testTimerInternalsSetTimer() { - Windmill.WorkItemCommitRequest.Builder outputBuilder = - Windmill.WorkItemCommitRequest.newBuilder(); + public void testTimerInternalsSetTimer() throws Exception { NameContext nameContext = NameContextsForTests.nameContextForTest(); DataflowOperationContext operationContext = executionContext.createOperationContext(nameContext); StreamingModeExecutionContext.StepContext stepContext = executionContext.getStepContext(operationContext); - executionContext.start( - "key", + start( createMockWork( Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(), - Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), - stateReader, - sideInputStateFetcher, - outputBuilder); + Watermarks.builder().setInputDataWatermark(new Instant(1000)).build())); TimerInternals timerInternals = stepContext.timerInternals(); @@ -179,8 +242,10 @@ public void testTimerInternalsSetTimer() { new Instant(5000), TimeDomain.EVENT_TIME, CausedByDrain.NORMAL)); + executionContext.finishKey(); executionContext.flushState(); + Windmill.WorkItemCommitRequest.Builder outputBuilder = executionContext.getOutputBuilder(); Windmill.Timer timer = outputBuilder.buildPartial().getOutputTimers(0); assertThat(timer.getTag().toStringUtf8(), equalTo("/skey+0:5000")); assertThat(timer.getTimestamp(), equalTo(TimeUnit.MILLISECONDS.toMicros(5000))); @@ -189,9 +254,6 @@ public void testTimerInternalsSetTimer() { @Test public void testTimerInternalsProcessingTimeSkew() { - Windmill.WorkItemCommitRequest.Builder outputBuilder = - Windmill.WorkItemCommitRequest.newBuilder(); - NameContext nameContext = NameContextsForTests.nameContextForTest(); DataflowOperationContext operationContext = executionContext.createOperationContext(nameContext); @@ -211,14 +273,10 @@ public void testTimerInternalsProcessingTimeSkew() { .setTimestamp(timerTimestamp.getMillis() * 1000) .setType(Windmill.Timer.Type.REALTIME); - executionContext.start( - "key", + start( createMockWork( workItemBuilder.build(), - Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), - stateReader, - sideInputStateFetcher, - outputBuilder); + Watermarks.builder().setInputDataWatermark(new Instant(1000)).build())); TimerInternals timerInternals = stepContext.timerInternals(); assertTrue(timerTimestamp.isBefore(timerInternals.currentProcessingTime())); } @@ -416,19 +474,430 @@ 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); - assertEquals(expectedEncoding, executionContext.getWindmillTagEncoding().getClass()); + FakeGlobalConfigHandle configHandle = + new FakeGlobalConfigHandle( + StreamingGlobalConfig.builder().setEnableStateTagEncodingV2(isV2Encoding).build()); + StreamingModeExecutionContext context = createExecutionContext(options, configHandle); + assertEquals(expectedEncoding, context.getWindmillTagEncoding().getClass()); + } + } + + @Test + public void testSetBacklogBytes() { + NameContext nameContext = NameContextsForTests.nameContextForTest(); + DataflowOperationContext operationContext = + executionContext.createOperationContext(nameContext); + StreamingModeExecutionContext.StepContext stepContext = + executionContext.getStepContext(operationContext); + + start( + createMockWork( + Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(), + Watermarks.builder().setInputDataWatermark(new Instant(1000)).build())); + + stepContext.setBacklogBytes(1234.0); + executionContext.finishKey(); + executionContext.flushState(); + + assertEquals(1234, executionContext.getOutputBuilder().getSourceBacklogBytes()); + } + + @Test + public void testFinishKeyReentrantSafety() { + start( + createMockWork( + Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(), + Watermarks.builder().setInputDataWatermark(new Instant(1000)).build())); + + // First call + executionContext.finishKey(); + // Second call - should not throw any Exception + executionContext.finishKey(); + } + + @Test + public void testStart_internalKeyDecoding() throws Exception { + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("decodedKey")) + .setWorkToken(17L) + .build(); + Work work = + createMockWork( + workItem, Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()); + + start(work, org.apache.beam.sdk.coders.StringUtf8Coder.of()); + + assertEquals("decodedKey", executionContext.getKey()); + } + + @Test + public void testAdvance_success() throws Exception { + BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + BoundedQueueExecutorWorkHandle mockHandle = mock(BoundedQueueExecutorWorkHandle.class); + + Windmill.Uint128Proto keyGroup = + Windmill.Uint128Proto.newBuilder().setHigh(1).setLow(2).build(); + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(1L) + .setKeyGroup(keyGroup) + .build(); + Work work1 = + createMockWork( + workItem1, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + + Windmill.WorkItem workItem2 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key2")) + .setWorkToken(2L) + .setKeyGroup(keyGroup) + .build(); + Work work2 = + createMockWork( + workItem2, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + ExecutableWork executableWork2 = ExecutableWork.create(work2, (w, h) -> {}); + + when(mockExecutor.pollWork(eq(COMPUTATION_ID), eq(work1.getKeyGroup()), eq(mockHandle), any())) + .thenReturn(executableWork2) + .thenReturn(null); + + StreamingModeExecutionContext.KeyTransitionListener mockListener = + mock(StreamingModeExecutionContext.KeyTransitionListener.class); + executionContext.start( + work1, + workExecutor, + mockExecutor, + mockHandle, + null, + mockListener, + FAILING_FAILED_WORK_HANDLER); + + assertTrue(executionContext.advance()); + assertEquals("key2", executionContext.getSerializedKey().toStringUtf8()); + verify(mockListener, times(1)).onKeyTransition(work1, work2); + assertFalse(executionContext.advance()); + } + + @Test + public void testAdvance_noMoreWork() throws Exception { + BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + BoundedQueueExecutorWorkHandle mockHandle = mock(BoundedQueueExecutorWorkHandle.class); + + Windmill.Uint128Proto keyGroup = + Windmill.Uint128Proto.newBuilder().setHigh(1).setLow(2).build(); + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(1L) + .setKeyGroup(keyGroup) + .build(); + Work work1 = + createMockWork( + workItem1, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + + when(mockExecutor.pollWork(eq(COMPUTATION_ID), eq(work1.getKeyGroup()), eq(mockHandle), any())) + .thenReturn(null); + + executionContext.start( + work1, + workExecutor, + mockExecutor, + mockHandle, + null, + FAILING_KEY_TRANSISITON, + FAILING_FAILED_WORK_HANDLER); + + assertFalse(executionContext.advance()); + } + + @Test + public void testAdvance_respectsMaxBatchSize() throws Exception { + DataflowWorkerHarnessOptions optionsWithBatchSize = + PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class); + optionsWithBatchSize + .as(ExperimentalOptions.class) + .setExperiments(List.of("windmill_max_key_group_batch_size=1")); + StreamingModeExecutionContext context = + createExecutionContext(optionsWithBatchSize, globalConfigHandle); + + BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + BoundedQueueExecutorWorkHandle mockHandle = mock(BoundedQueueExecutorWorkHandle.class); + + Windmill.Uint128Proto keyGroup = + Windmill.Uint128Proto.newBuilder().setHigh(1).setLow(2).build(); + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(1L) + .setKeyGroup(keyGroup) + .build(); + Work work1 = + createMockWork( + workItem1, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + + context.start( + work1, + workExecutor, + mockExecutor, + mockHandle, + null, + FAILING_KEY_TRANSISITON, + FAILING_FAILED_WORK_HANDLER); + + assertFalse(context.advance()); + verifyNoInteractions(mockExecutor); + } + + @Test + public void testAdvance_respectsMaxBatchTime() throws Exception { + DataflowWorkerHarnessOptions optionsWithBatchTime = + PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class); + optionsWithBatchTime + .as(ExperimentalOptions.class) + .setExperiments(List.of("windmill_max_key_group_batch_time_ms=0")); + StreamingModeExecutionContext context = + createExecutionContext(optionsWithBatchTime, globalConfigHandle); + + BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + BoundedQueueExecutorWorkHandle mockHandle = mock(BoundedQueueExecutorWorkHandle.class); + + Windmill.Uint128Proto keyGroup = + Windmill.Uint128Proto.newBuilder().setHigh(1).setLow(2).build(); + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(1L) + .setKeyGroup(keyGroup) + .build(); + Work work1 = + createMockWork( + workItem1, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + + context.start( + work1, + workExecutor, + mockExecutor, + mockHandle, + null, + FAILING_KEY_TRANSISITON, + FAILING_FAILED_WORK_HANDLER); + + assertFalse(context.advance()); + verifyNoInteractions(mockExecutor); + } + + @Test + public void testAdvance_workFailed() throws Exception { + BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + BoundedQueueExecutorWorkHandle mockHandle = mock(BoundedQueueExecutorWorkHandle.class); + + Windmill.Uint128Proto keyGroup = + Windmill.Uint128Proto.newBuilder().setHigh(1).setLow(2).build(); + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(1L) + .setKeyGroup(keyGroup) + .build(); + Work work1 = + createMockWork( + workItem1, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + + executionContext.start( + work1, + workExecutor, + mockExecutor, + mockHandle, + null, + FAILING_KEY_TRANSISITON, + FAILING_FAILED_WORK_HANDLER); + + work1.setFailed(); + + assertThrows(WorkItemCancelledException.class, () -> executionContext.advance()); + verifyNoMoreInteractions(mockExecutor); + } + + @Test + public void testAdvance_defaultKeyGroup() throws Exception { + BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + BoundedQueueExecutorWorkHandle mockHandle = mock(BoundedQueueExecutorWorkHandle.class); + + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(1L) + .build(); + Work work1 = + createMockWork( + workItem1, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + + executionContext.start( + work1, + workExecutor, + mockExecutor, + mockHandle, + null, + FAILING_KEY_TRANSISITON, + FAILING_FAILED_WORK_HANDLER); + + assertFalse(executionContext.advance()); + verifyNoInteractions(mockExecutor); + } + + @Test + public void testAdvance_experimentDisabled() throws Exception { + DataflowWorkerHarnessOptions optionsDisabled = + PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class); + StreamingModeExecutionContext context = + createExecutionContext(optionsDisabled, globalConfigHandle); + + BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + BoundedQueueExecutorWorkHandle mockHandle = mock(BoundedQueueExecutorWorkHandle.class); + + Windmill.Uint128Proto keyGroup = + Windmill.Uint128Proto.newBuilder().setHigh(1).setLow(2).build(); + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(1L) + .setKeyGroup(keyGroup) + .build(); + Work work1 = + createMockWork( + workItem1, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + + context.start( + work1, + workExecutor, + mockExecutor, + mockHandle, + null, + FAILING_KEY_TRANSISITON, + FAILING_FAILED_WORK_HANDLER); + + assertFalse(context.advance()); + verifyNoInteractions(mockExecutor); + } + + @Test + public void testAdvance_respectsMaxBatchSinkBytes() throws Exception { + DataflowWorkerHarnessOptions optionsWithSinkBytes = + PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class); + optionsWithSinkBytes + .as(ExperimentalOptions.class) + .setExperiments( + List.of( + "unstable_enable_multi_key_bundle", "windmill_max_key_group_batch_sink_bytes=100")); + StreamingModeExecutionContext context = + createExecutionContext(optionsWithSinkBytes, globalConfigHandle); + + BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + BoundedQueueExecutorWorkHandle mockHandle = mock(BoundedQueueExecutorWorkHandle.class); + + Windmill.Uint128Proto keyGroup = + Windmill.Uint128Proto.newBuilder().setHigh(1).setLow(2).build(); + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(1L) + .setKeyGroup(keyGroup) + .build(); + Work work1 = + createMockWork( + workItem1, Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build()); + + context.start( + work1, + workExecutor, + mockExecutor, + mockHandle, + null, + FAILING_KEY_TRANSISITON, + FAILING_FAILED_WORK_HANDLER); + + context.reportBytesSinked(50); + assertFalse(context.advance()); + verify(mockExecutor) + .pollWork(eq(COMPUTATION_ID), eq(work1.getKeyGroup()), eq(mockHandle), any()); + + reset(mockExecutor); + + context.reportBytesSinked(60); + assertFalse(context.advance()); + verifyNoInteractions(mockExecutor); + } + + @Test + public void testExperimentParsingWithInvalidValues() { + DataflowWorkerHarnessOptions optionsInvalid = + PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class); + optionsInvalid + .as(ExperimentalOptions.class) + .setExperiments( + List.of( + "windmill_max_key_group_batch_size=invalid_size", + "windmill_max_key_group_batch_time_ms=invalid_time", + "windmill_max_key_group_batch_sink_bytes=invalid_bytes")); + + // This should not throw NumberFormatException + StreamingModeExecutionContext context = + createExecutionContext(optionsInvalid, globalConfigHandle); + + org.junit.Assert.assertNotNull(context); + } + + @Test + public void testInternalsPoisonedAfterFlushState() throws Exception { + NameContext nameContext = NameContextsForTests.nameContextForTest(); + DataflowOperationContext operationContext = + executionContext.createOperationContext(nameContext); + StreamingModeExecutionContext.StepContext stepContext = + executionContext.getStepContext(operationContext); + + start( + createMockWork( + Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(), + Watermarks.builder().setInputDataWatermark(new Instant(1000)).build())); + + TimerInternals timerInternals = stepContext.timerInternals(); + StateInternals stateInternals = stepContext.stateInternals(); + + executionContext.finishKey(); + executionContext.flushState(); + + // Verify timerInternals is poisoned + try { + timerInternals.currentProcessingTime(); + org.junit.Assert.fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertThat(e.getMessage(), Matchers.containsString("poisoned")); + } + + // Verify stateInternals is poisoned + try { + stateInternals.getKey(); + org.junit.Assert.fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertThat(e.getMessage(), Matchers.containsString("poisoned")); + } + + // Verify stepContext.stateInternals() returns poisoned instance + try { + stepContext.stateInternals().getKey(); + org.junit.Assert.fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertThat(e.getMessage(), Matchers.containsString("poisoned")); + } + + // Verify stepContext.timerInternals() returns poisoned instance + try { + stepContext.timerInternals().currentProcessingTime(); + org.junit.Assert.fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertThat(e.getMessage(), Matchers.containsString("poisoned")); } } } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputDoFnRunnerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputDoFnRunnerTest.java index d18bc512723e..c110cc0d2bf7 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputDoFnRunnerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingSideInputDoFnRunnerTest.java @@ -150,6 +150,7 @@ public void testSideInputNotReady() throws Exception { runner.startBundle(); runner.processElement(createDatum("e", 0)); + runner.finishKey("key"); runner.finishBundle(); assertTrue(outputManager.getOutput(mainOutputTag).isEmpty()); @@ -214,6 +215,7 @@ public void testMultipleWindowsNotReady() throws Exception { runner.startBundle(); runner.processElement(elem); + runner.finishKey("key"); runner.finishBundle(); assertTrue(outputManager.getOutput(mainOutputTag).isEmpty()); @@ -317,6 +319,7 @@ public void testSideInputNotification() throws Exception { when(mockSideInputReader.get(eq(view), any(BoundedWindow.class))).thenReturn("data"); runner.startBundle(); + runner.finishKey("key"); runner.finishBundle(); assertThat(outputManager.getOutput(mainOutputTag), contains(createDatum("e:data", 0))); @@ -373,6 +376,7 @@ public void testMultipleSideInputs() throws Exception { runner.startBundle(); runner.processElement(createDatum("e2", 2)); + runner.finishKey("key"); runner.finishBundle(); assertThat( 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/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..be77da540889 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,18 +19,25 @@ 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; import java.util.Arrays; import java.util.List; import java.util.concurrent.ThreadLocalRandom; +import org.apache.beam.runners.dataflow.worker.streaming.Work; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.sdk.coders.CoderException; import org.apache.beam.sdk.options.ValueProvider; 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; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -40,8 +47,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 +88,122 @@ 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); + when(mockContext.advance()).thenReturn(false); + + try (TestWindmillReaderIterator iter = + new TestWindmillReaderIterator(mockContext, ValueProvider.StaticValueProvider.of(false))) { + assertTrue(iter.start()); + assertFalse(iter.advance()); // This should trigger finishKey + verify(mockContext).finishKey(); + } + } + + @Test + public void testAdvanceKeyChaining() throws Exception { + StreamingModeExecutionContext mockContext = mock(StreamingModeExecutionContext.class); + when(mockContext.workIsFailed()).thenReturn(false); + + // Work item A (1 message) + Windmill.WorkItem workItemA = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("keyA")) + .setWorkToken(100L) + .addMessageBundles( + Windmill.InputMessageBundle.newBuilder() + .setSourceComputationId("foo") + .addMessages( + Windmill.Message.newBuilder() + .setTimestamp(1000) + .setData(ByteString.EMPTY) + .build()) + .build()) + .build(); + when(mockContext.getWorkItem()).thenReturn(workItemA); + + // Work item B (1 message) + Windmill.WorkItem workItemB = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("keyB")) + .setWorkToken(200L) + .addMessageBundles( + Windmill.InputMessageBundle.newBuilder() + .setSourceComputationId("foo") + .addMessages( + Windmill.Message.newBuilder() + .setTimestamp(2000) + .setData(ByteString.EMPTY) + .build()) + .build()) + .build(); + + // Set up context.advance() to mock transition + when(mockContext.advance()) + .thenAnswer( + new org.mockito.stubbing.Answer() { + private int count = 0; + + @Override + public Boolean answer(org.mockito.invocation.InvocationOnMock invocation) { + if (count == 0) { + count++; + when(mockContext.getWorkItem()).thenReturn(workItemB); + return true; + } + return false; + } + }); + + try (TestWindmillReaderIterator iter = + new TestWindmillReaderIterator(mockContext, ValueProvider.StaticValueProvider.of(false))) { + assertTrue(iter.start()); + assertEquals(1000L, iter.getCurrent().getValue().longValue()); + + // Advance should trigger context.advance(), transition to workItemB, and decode message from + // workItemB (timestamp 2000) + assertTrue(iter.advance()); + assertEquals(2000L, iter.getCurrent().getValue().longValue()); + + // Next advance should exhaust it and return false + assertFalse(iter.advance()); + } + } + private void testForMessageBundleCounts(int... messageBundleCounts) throws IOException { testForMessageBundleCounts(false, messageBundleCounts); } @@ -111,9 +234,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)); @@ -125,4 +252,25 @@ private void testForMessageBundleCounts(boolean skipErrors, int... messageBundle assertEquals(Arrays.toString(messageBundleCounts) + skipErrors, expected, actual); } } + + private static Work createMockWork(Windmill.WorkItem workItem) { + return Work.create( + workItem, + workItem.getSerializedSize(), + org.apache.beam.runners.dataflow.worker.streaming.Watermarks.builder() + .setInputDataWatermark(new org.joda.time.Instant(1000)) + .build(), + Work.createProcessingContext( + "computationId", + mock( + org.apache.beam.runners.dataflow.worker.windmill.client.getdata.GetDataClient + .class), + ignored -> {}, + mock( + org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender + .class)), + false, + org.joda.time.Instant::now, + ImmutableList.of()); + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReaderTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReaderTest.java new file mode 100644 index 000000000000..3c778650eb3e --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReaderTest.java @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import org.apache.beam.runners.core.KeyedWorkItem; +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.common.worker.NativeReader; +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.WindmillTagEncodingV1; +import org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.ListCoder; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.coders.VarLongCoder; +import org.apache.beam.sdk.options.ValueProvider; +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.util.ByteStringOutputStream; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; +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.Iterables; +import org.joda.time.Instant; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class WindowingWindmillReaderTest { + private StreamingModeExecutionContext mockContext; + private WindowingWindmillReader reader; + + @SuppressWarnings("unchecked") + @Before + public void setUp() { + mockContext = mock(StreamingModeExecutionContext.class); + when(mockContext.workIsFailed()).thenReturn(false); + when(mockContext.getWindmillTagEncoding()).thenReturn(WindmillTagEncodingV1.instance()); + when(mockContext.getDrainMode()).thenReturn(false); + + Coder keyCoder = StringUtf8Coder.of(); + Coder valueCoder = VarLongCoder.of(); + KvCoder kvCoder = KvCoder.of(keyCoder, valueCoder); + WindmillKeyedWorkItem.FakeKeyedWorkItemCoder keyedWorkItemCoder = + (WindmillKeyedWorkItem.FakeKeyedWorkItemCoder) + WindmillKeyedWorkItem.FakeKeyedWorkItemCoder.of(kvCoder); + FullWindowedValueCoder> coder = + FullWindowedValueCoder.of(keyedWorkItemCoder, IntervalWindowCoder.of()); + + reader = + WindowingWindmillReader.create( + coder, mockContext, ValueProvider.StaticValueProvider.of(false)); + } + + private static Work createMockWork(Windmill.WorkItem workItem) { + return Work.create( + workItem, + workItem.getSerializedSize(), + Watermarks.builder().setInputDataWatermark(new Instant(1000)).build(), + Work.createProcessingContext( + "computationId", new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.class)), + false, + Instant::now, + ImmutableList.of()); + } + + private static ByteString encodeMetadata(List windows) throws IOException { + ByteStringOutputStream stream = new ByteStringOutputStream(); + PaneInfoCoder.INSTANCE.encode(PaneInfo.NO_FIRING, stream); + ListCoder.of(IntervalWindowCoder.of()).encode(windows, stream); + return stream.toByteString(); + } + + private static ByteString encodeValue(long value) throws IOException { + ByteStringOutputStream stream = new ByteStringOutputStream(); + VarLongCoder.of().encode(value, stream); + return stream.toByteString(); + } + + @Test + public void testSingleNonEmptyKey() throws IOException { + IntervalWindow window = new IntervalWindow(new Instant(0), new Instant(1000)); + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(100L) + .addMessageBundles( + Windmill.InputMessageBundle.newBuilder() + .setSourceComputationId("foo") + .addMessages( + Windmill.Message.newBuilder() + .setTimestamp(1000) + .setData(encodeValue(42L)) + .setMetadata(encodeMetadata(ImmutableList.of(window))) + .build()) + .build()) + .build(); + Work work = createMockWork(workItem); + + when(mockContext.getKey()).thenReturn("key1"); + when(mockContext.getWorkItem()).thenReturn(workItem); + when(mockContext.getWork()).thenReturn(work); + when(mockContext.advance()).thenReturn(false); + + try (NativeReader.NativeReaderIterator>> iter = + reader.iterator()) { + assertTrue(iter.start()); + WindowedValue> current = iter.getCurrent(); + assertEquals("key1", current.getValue().key()); + assertFalse(Iterables.isEmpty(current.getValue().elementsIterable())); + WindowedValue elem = Iterables.getOnlyElement(current.getValue().elementsIterable()); + assertEquals(42L, elem.getValue().longValue()); + + assertFalse(iter.advance()); + verify(mockContext).finishKey(); + } + } + + @Test + public void testSingleEmptyKey() throws IOException { + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(100L) + .build(); // No message bundles or timers + Work work = createMockWork(workItem); + + when(mockContext.getKey()).thenReturn("key1"); + when(mockContext.getWorkItem()).thenReturn(workItem); + when(mockContext.getWork()).thenReturn(work); + when(mockContext.advance()).thenReturn(false); + + try (NativeReader.NativeReaderIterator>> iter = + reader.iterator()) { + assertFalse( + iter.start()); // Should skip the empty key and return false because advance returns false + verify(mockContext).finishKey(); + } + } + + @Test + public void testMultipleKeys_withEmptyAndNonEmpty() throws IOException { + IntervalWindow window = new IntervalWindow(new Instant(0), new Instant(1000)); + // Key 1: Empty + Windmill.WorkItem workItem1 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setWorkToken(100L) + .build(); + Work work1 = createMockWork(workItem1); + + // Key 2: Non-empty + Windmill.WorkItem workItem2 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key2")) + .setWorkToken(200L) + .addMessageBundles( + Windmill.InputMessageBundle.newBuilder() + .setSourceComputationId("foo") + .addMessages( + Windmill.Message.newBuilder() + .setTimestamp(2000) + .setData(encodeValue(84L)) + .setMetadata(encodeMetadata(ImmutableList.of(window))) + .build()) + .build()) + .build(); + Work work2 = createMockWork(workItem2); + + // Key 3: Empty + Windmill.WorkItem workItem3 = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key3")) + .setWorkToken(300L) + .build(); + Work work3 = createMockWork(workItem3); + + // Initial state + when(mockContext.getKey()).thenReturn("key1"); + when(mockContext.getWorkItem()).thenReturn(workItem1); + when(mockContext.getWork()).thenReturn(work1); + + // Mock transition behaviour of context.advance() + when(mockContext.advance()) + .thenAnswer( + new org.mockito.stubbing.Answer() { + private int count = 0; + + @Override + public Boolean answer(org.mockito.invocation.InvocationOnMock invocation) { + if (count == 0) { + count++; + when(mockContext.getKey()).thenReturn("key2"); + when(mockContext.getWorkItem()).thenReturn(workItem2); + when(mockContext.getWork()).thenReturn(work2); + return true; + } else if (count == 1) { + count++; + when(mockContext.getKey()).thenReturn("key3"); + when(mockContext.getWorkItem()).thenReturn(workItem3); + when(mockContext.getWork()).thenReturn(work3); + return true; + } + return false; + } + }); + + try (NativeReader.NativeReaderIterator>> iter = + reader.iterator()) { + // Key 1 is empty, so start() calls advance() which calls finishKey(1) and advance() to Key 2. + // Key 2 is non-empty, so start() returns true yielding Key 2. + assertTrue(iter.start()); + assertEquals("key2", iter.getCurrent().getValue().key()); + WindowedValue elem = + Iterables.getOnlyElement(iter.getCurrent().getValue().elementsIterable()); + assertEquals(84L, elem.getValue().longValue()); + + // Next advance() calls finishKey(2), calls advance() to Key 3. + // Key 3 is empty, so it loops, calls finishKey(3), calls advance() which returns false. + // So iter.advance() should return false. + assertFalse(iter.advance()); + + verify(mockContext, times(3)) + .finishKey(); // finishKey should have been called on key1, key2, key3 + } + } + + @Test + public void testWorkItemCancelled() throws IOException { + when(mockContext.workIsFailed()).thenReturn(true); + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(0L).build(); + when(mockContext.getWorkItem()).thenReturn(workItem); + + try (NativeReader.NativeReaderIterator>> iter = + reader.iterator()) { + iter.start(); + fail("Expected WorkItemCancelledException"); + } catch (WorkItemCancelledException e) { + // Expected + } + } +} 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..2a741da7fd88 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 @@ -83,28 +83,34 @@ import org.apache.beam.runners.dataflow.util.CloudObject; import org.apache.beam.runners.dataflow.util.PropertyNames; import org.apache.beam.runners.dataflow.worker.DataflowExecutionContext.DataflowExecutionStateTracker; +import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.KeyTransitionListener; import org.apache.beam.runners.dataflow.worker.StreamingModeExecutionContext.StreamingModeExecutionStateRegistry; import org.apache.beam.runners.dataflow.worker.WorkerCustomSources.SplittableOnlyBoundedSource; import org.apache.beam.runners.dataflow.worker.counters.CounterSet; import org.apache.beam.runners.dataflow.worker.counters.NameContext; import org.apache.beam.runners.dataflow.worker.profiler.ScopedProfiler.NoopProfileScope; +import org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle; 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.streaming.config.FixedGlobalConfigHandle; 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.streaming.harness.StreamingCounters; +import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory; import org.apache.beam.runners.dataflow.worker.testing.TestCountingSource; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor; 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; -import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateReader; +import org.apache.beam.runners.dataflow.worker.windmill.work.processing.failures.FailureTracker; import org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender; import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.coders.BigEndianIntegerCoder; import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.CoderException; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.VarIntCoder; import org.apache.beam.sdk.extensions.gcp.auth.TestCredential; @@ -205,7 +211,23 @@ private static Work createMockWork(Windmill.WorkItem workItem, Watermarks waterm Work.createProcessingContext( COMPUTATION_ID, new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.class)), false, - Instant::now); + Instant::now, + ImmutableList.of()); + } + + private void startContext(StreamingModeExecutionContext context, Work work) { + try { + context.start( + work, + mock(WorkExecutor.class), + /* workQueueExecutor= */ mock(BoundedQueueExecutor.class), + /* budgetHandle= */ mock(BoundedQueueExecutorWorkHandle.class), + /* keyCoder= */ null, + /* keyTransitionListener= */ mock(KeyTransitionListener.class), + /* onFailedWorkHandler= */ ignored -> {}); + } catch (CoderException e) { + throw new RuntimeException(e); + } } private static class SourceProducingSubSourcesInSplit extends MockSource { @@ -619,7 +641,16 @@ public void testReadUnboundedReader() throws Exception { executionStateRegistry, globalConfigHandle, Long.MAX_VALUE, - /*throwExceptionOnLargeOutput=*/ false); + /*throwExceptionOnLargeOutput=*/ false, + new HotKeyLogger(), + /*hotKeyLoggingEnabled=*/ false, + /*stepName=*/ "stepName", + /*systemName=*/ "systemName", + StreamingCounters.create(), + mock(FailureTracker.class), + "sourceBytesProcessCounterName", + MultiKeyBundleOptions.fromOptions(options), + SideInputStateFetcherFactory.fromOptions(options)); options.setNumWorkers(5); int maxElements = 10; @@ -630,8 +661,8 @@ public void testReadUnboundedReader() throws Exception { for (int i = 0; i < 10 * maxElements; /* Incremented in inner loop */ ) { // Initialize streaming context with state from previous iteration. - context.start( - "key", + startContext( + context, createMockWork( Windmill.WorkItem.newBuilder() .setKey(ByteString.copyFromUtf8("0000000000000001")) // key is zero-padded index. @@ -640,10 +671,7 @@ public void testReadUnboundedReader() throws Exception { .setSourceState( Windmill.SourceState.newBuilder().setState(state).build()) // Source state. .build(), - Watermarks.builder().setInputDataWatermark(new Instant(0)).build()), - mock(WindmillStateReader.class), - mock(SideInputStateFetcher.class), - Windmill.WorkItemCommitRequest.newBuilder()); + Watermarks.builder().setInputDataWatermark(new Instant(0)).build())); @SuppressWarnings({"unchecked", "rawtypes"}) NativeReader>>> reader = @@ -990,7 +1018,16 @@ public void testFailedWorkItemsAbort() throws Exception { executionStateRegistry, globalConfigHandle, Long.MAX_VALUE, - /*throwExceptionOnLargeOutput=*/ false); + /*throwExceptionOnLargeOutput=*/ false, + new HotKeyLogger(), + /*hotKeyLoggingEnabled=*/ false, + /*stepName=*/ "stepName", + /*systemName=*/ "systemName", + StreamingCounters.create(), + mock(FailureTracker.class), + "sourceBytesProcessCounterName", + MultiKeyBundleOptions.fromOptions(options), + SideInputStateFetcherFactory.fromOptions(options)); options.setNumWorkers(5); int maxElements = 100; @@ -1017,13 +1054,9 @@ public void testFailedWorkItemsAbort() throws Exception { ignored -> {}, mock(HeartbeatSender.class)), false, - Instant::now); - context.start( - "key", - dummyWork, - mock(WindmillStateReader.class), - mock(SideInputStateFetcher.class), - Windmill.WorkItemCommitRequest.newBuilder()); + Instant::now, + ImmutableList.of()); + startContext(context, dummyWork); @SuppressWarnings({"unchecked", "rawtypes"}) NativeReader>>> reader = @@ -1038,14 +1071,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/logging/DataflowWorkerLoggingHandlerTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingHandlerTest.java index c6a8581cf507..56c042972409 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingHandlerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingHandlerTest.java @@ -29,6 +29,16 @@ import com.google.cloud.logging.LogEntry; import com.google.cloud.logging.Payload; import com.google.cloud.logging.Severity; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.samplers.Sampler; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; @@ -106,11 +116,14 @@ public OutputStream get() { /** Encodes a LogRecord into a Json string. */ private static String createJson(LogRecord record) throws IOException { - return createJson(record, null, null); + return createJson(record, null, null, null); } private static String createJson( - LogRecord record, @Nullable Formatter formatter, @Nullable Boolean enableMdc) + LogRecord record, + @Nullable Formatter formatter, + @Nullable Boolean enableMdc, + @Nullable Boolean openTelemetryTraceAndSpanId) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); FixedOutputStreamFactory factory = new FixedOutputStreamFactory(output); @@ -121,6 +134,9 @@ private static String createJson( if (enableMdc != null) { handler.setLogMdc(enableMdc); } + if (openTelemetryTraceAndSpanId != null) { + handler.setLogOpenTelemetryTraceAndSpanId(openTelemetryTraceAndSpanId); + } // Format the record as JSON. handler.publish(record); // Decode the binary output as UTF-8 and return the generated string. @@ -128,7 +144,7 @@ private static String createJson( } private static LogEntry createLogEntry(LogRecord record) throws IOException { - return createLogEntry(record, null, null); + return createLogEntry(record, null, null, null); } private static PipelineOptions pipelineOptionsForTest() { @@ -144,7 +160,10 @@ private static PipelineOptions pipelineOptionsForTest() { } private static LogEntry createLogEntry( - LogRecord record, @Nullable Formatter formatter, @Nullable Boolean enableMdc) + LogRecord record, + @Nullable Formatter formatter, + @Nullable Boolean enableMdc, + @Nullable Boolean openTelemetryTraceAndSpanId) throws IOException { ByteArrayOutputStream fileOutput = new ByteArrayOutputStream(); FixedOutputStreamFactory factory = new FixedOutputStreamFactory(fileOutput); @@ -155,6 +174,9 @@ private static LogEntry createLogEntry( if (enableMdc != null) { handler.setLogMdc(enableMdc); } + if (openTelemetryTraceAndSpanId != null) { + handler.setLogOpenTelemetryTraceAndSpanId(openTelemetryTraceAndSpanId); + } handler.enableDirectLogging(pipelineOptionsForTest(), Level.SEVERE, (e) -> {}); return handler.constructDirectLogEntry( record, @@ -227,7 +249,7 @@ public void testWithAllValuesInMDC() throws IOException { String testWorkId = "testWorkId"; DataflowWorkerLoggingMDC.setJobId(testJobId); - DataflowWorkerLoggingMDC.setStageName(testStage); + DataflowWorkerLoggingMDC.setSystemStageName(testStage); DataflowWorkerLoggingMDC.setWorkerId(testWorkerId); DataflowWorkerLoggingMDC.setWorkId(testWorkId); @@ -279,7 +301,8 @@ public synchronized String formatMessage(LogRecord record) { + "\"message\":\"testMdcValue:test.message\",\"thread\":\"2\",\"job\":\"testJobId\"," + "\"worker\":\"testWorkerId\",\"work\":\"testWorkId\",\"logger\":\"LoggerName\"}" + System.lineSeparator(), - createJson(createLogRecord("test.message", null /* throwable */), customFormatter, null)); + createJson( + createLogRecord("test.message", null /* throwable */), customFormatter, null, null)); } } @@ -345,7 +368,7 @@ public void testWithCustomDataEnabledNoMdc() throws IOException { "{\"timestamp\":{\"seconds\":0,\"nanos\":1000000},\"severity\":\"INFO\"," + "\"message\":\"test.message\",\"thread\":\"2\",\"logger\":\"LoggerName\"}" + System.lineSeparator(), - createJson(createLogRecord(), null, true)); + createJson(createLogRecord(), null, true, null)); } @Test @@ -369,7 +392,43 @@ public void testWithCustomDataEnabledWithMdc() throws IOException { + "\"message\":\"test.message\",\"thread\":\"2\",\"logger\":\"LoggerName\"," + "\"custom_data\":{\"key1\":\"cool value\",\"key2\":\"another\"}}" + System.lineSeparator(), - createJson(createLogRecord(), null, true)); + createJson(createLogRecord(), null, true, null)); + } + } + + @Test + public void testWithOpenTelemetryTrace() throws IOException { + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOn()) + .addSpanProcessor(BatchSpanProcessor.builder(InMemorySpanExporter.create()).build()) + .build(); + + // 2. Build the OpenTelemetry instance + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .build(); // Automatically calls GlobalOpenTelemetry.set() + Tracer tracer = openTelemetry.getTracer("foo"); + Span span = tracer.spanBuilder("test").startSpan(); + try (Scope scope = span.makeCurrent()) { + SpanContext spanContext = Span.current().getSpanContext(); + assertEquals( + "{\"timestamp\":{\"seconds\":0,\"nanos\":1000000},\"severity\":\"INFO\"," + + "\"message\":\"test.message\",\"thread\":\"2\",\"logger\":\"LoggerName\"," + + "\"trace\":\"" + + spanContext.getTraceId() + + "\"," + + "\"spanId\":\"" + + spanContext.getSpanId() + + "\"," + + "\"trace_sampled\":" + + spanContext.isSampled() + + "}" + + System.lineSeparator(), + createJson(createLogRecord(), null, false, true)); + } finally { + span.end(); } } @@ -514,7 +573,7 @@ public void testConstructLogEntryWithAllValuesInMDC() throws IOException { String testWorkId = "testWorkId"; String testJobId = "testJobId"; - DataflowWorkerLoggingMDC.setStageName(testStage); + DataflowWorkerLoggingMDC.setSystemStageName(testStage); DataflowWorkerLoggingMDC.setWorkerId(testWorkerId); DataflowWorkerLoggingMDC.setWorkId(testWorkId); DataflowWorkerLoggingMDC.setJobId(testJobId); @@ -560,7 +619,7 @@ public synchronized String formatMessage(LogRecord record) { try (MDC.MDCCloseable ignored = MDC.putCloseable("testMdcKey", "testMdcValue")) { LogEntry entry = createLogEntry( - createLogRecord("test.message", null /* throwable */), customFormatter, null); + createLogRecord("test.message", null /* throwable */), customFormatter, null, null); assertEquals( Payload.JsonPayload.of( ImmutableMap.of( @@ -630,7 +689,7 @@ public void testDirectLoggingWithException() throws IOException { @Test public void testDirectLoggingWithCustomDataEnabledNoMdc() throws IOException { - LogEntry entry = createLogEntry(createLogRecord(), null, true); + LogEntry entry = createLogEntry(createLogRecord(), null, true, null); assertEquals( Payload.JsonPayload.of( ImmutableMap.of("message", "test.message", "thread", "2", "logger", "LoggerName")), @@ -649,12 +708,36 @@ public void testDirectLoggingWithCustomDataDisabledWithMdc() throws IOException } } + @Test + public void testDirectMethodWithOpenTelemetryTrace() throws IOException { + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOn()) + .addSpanProcessor(BatchSpanProcessor.builder(InMemorySpanExporter.create()).build()) + .build(); + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .build(); // Automatically calls GlobalOpenTelemetry.set() + Tracer tracer = openTelemetry.getTracer("foo"); + Span span = tracer.spanBuilder("test").startSpan(); + try (Scope ignored = span.makeCurrent()) { + SpanContext spanContext = Span.current().getSpanContext(); + LogEntry entry = createLogEntry(createLogRecord(), null, null, true); + assertEquals(spanContext.getSpanId(), entry.getSpanId()); + assertEquals(spanContext.getTraceId(), entry.getTrace()); + assertEquals(spanContext.isSampled(), entry.getTraceSampled()); + } finally { + span.end(); + } + } + @Test public void testDirectLoggingWithCustomDataEnabledWithMdc() throws IOException { MDC.clear(); try (MDC.MDCCloseable ignored = MDC.putCloseable("key1", "cool value"); MDC.MDCCloseable ignored2 = MDC.putCloseable("key2", "another")) { - LogEntry entry = createLogEntry(createLogRecord(), null, true); + LogEntry entry = createLogEntry(createLogRecord(), null, true, null); assertEquals( Payload.JsonPayload.of( ImmutableMap.of( 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 diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/status/ThreadzServletTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/status/ThreadzServletTest.java index 7737f7d405b5..1c2352954bd0 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/status/ThreadzServletTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/status/ThreadzServletTest.java @@ -36,13 +36,18 @@ public class ThreadzServletTest { @Test public void testDeduping() throws Exception { + // Use Thread.toString() rather than hard-coded strings: JDK 21+ includes the thread id + // (e.g. Thread[#42,Thread1,5,main] vs Thread[Thread1,5,main]). + Thread thread1 = new Thread("Thread1"); + Thread thread2 = new Thread("Thread2"); + Thread thread3 = new Thread("Thread3"); Map stacks = ImmutableMap.of( - new Thread("Thread1"), + thread1, new StackTraceElement[] {new StackTraceElement("Class", "Method1", "File", 11)}, - new Thread("Thread2"), + thread2, new StackTraceElement[] {new StackTraceElement("Class", "Method1", "File", 11)}, - new Thread("Thread3"), + thread3, new StackTraceElement[] {new StackTraceElement("Class", "Method2", "File", 17)}); Map> deduped = ThreadzServlet.deduplicateThreadStacks(stacks); @@ -54,13 +59,13 @@ public void testDeduping() throws Exception { new Stack( new StackTraceElement[] {new StackTraceElement("Class", "Method1", "File", 11)}, Thread.State.NEW), - Arrays.asList("Thread[Thread1,5,main]", "Thread[Thread2,5,main]"))); + Arrays.asList(thread1.toString(), thread2.toString()))); assertThat( deduped, Matchers.hasEntry( new Stack( new StackTraceElement[] {new StackTraceElement("Class", "Method2", "File", 17)}, Thread.State.NEW), - Arrays.asList("Thread[Thread3,5,main]"))); + Arrays.asList(thread3.toString()))); } } 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..1826b7817fda 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 @@ -20,10 +20,11 @@ import static com.google.common.truth.Truth.assertThat; 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.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import java.util.Arrays; import java.util.Collections; @@ -34,6 +35,7 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Supplier; import org.apache.beam.runners.dataflow.worker.streaming.ActiveWorkState.ActivateWorkResult; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; @@ -42,6 +44,7 @@ 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.collect.ImmutableList; +import org.joda.time.Duration; import org.joda.time.Instant; import org.junit.Before; import org.junit.Rule; @@ -64,7 +67,7 @@ private static ShardedKey shardedKey(String str, long shardKey) { return ShardedKey.create(ByteString.copyFromUtf8(str), shardKey); } - private static ExecutableWork createWork(Windmill.WorkItem workItem) { + private static ExecutableWork createWork(Windmill.WorkItem workItem, Supplier clock) { return ExecutableWork.create( Work.create( workItem, @@ -72,20 +75,13 @@ private static ExecutableWork createWork(Windmill.WorkItem workItem) { Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build(), createWorkProcessingContext(), false, - Instant::now), - ignored -> {}); + clock, + ImmutableList.of()), + (work, handle) -> {}); } - private static ExecutableWork expiredWork(Windmill.WorkItem workItem) { - return ExecutableWork.create( - Work.create( - workItem, - workItem.getSerializedSize(), - Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build(), - createWorkProcessingContext(), - false, - () -> Instant.EPOCH), - ignored -> {}); + private static ExecutableWork createWork(Windmill.WorkItem workItem) { + return createWork(workItem, Instant::now); } private static Work.ProcessingContext createWorkProcessingContext() { @@ -315,25 +311,23 @@ public void testCurrentActiveWorkBudget_correctlyAggregatesActiveWorkBudget_mult } @Test - public void testInvalidateStuckCommits() { - Map invalidatedCommits = new HashMap<>(); + public void testHasStuckCommits() { ShardedKey shardedKey1 = shardedKey("someKey", 1L); ShardedKey shardedKey2 = shardedKey("anotherKey", 2L); - ExecutableWork stuckWork1 = expiredWork(createWorkItem(1L, 1L, shardedKey1)); - stuckWork1.work().setState(Work.State.COMMITTING); - ExecutableWork stuckWork2 = expiredWork(createWorkItem(2L, 1L, shardedKey2)); - stuckWork2.work().setState(Work.State.COMMITTING); + Instant now = Instant.now(); + ExecutableWork stuckWork1 = createWork(createWorkItem(1L, 1L, shardedKey1), () -> now); + ExecutableWork unstuckWork2 = createWork(createWorkItem(2L, 1L, shardedKey2), () -> now); activeWorkState.activateWorkForKey(stuckWork1); - activeWorkState.activateWorkForKey(stuckWork2); + activeWorkState.activateWorkForKey(unstuckWork2); - activeWorkState.invalidateStuckCommits(Instant.now(), invalidatedCommits::put); + stuckWork1.work().setState(Work.State.COMMITTING); + unstuckWork2.work().setState(Work.State.PROCESSING); - assertThat(invalidatedCommits).containsEntry(shardedKey1, stuckWork1.id()); - assertThat(invalidatedCommits).containsEntry(shardedKey2, stuckWork2.id()); - verify(computationStateCache).invalidate(shardedKey1.key(), shardedKey1.shardingKey()); - verify(computationStateCache).invalidate(shardedKey2.key(), shardedKey2.shardingKey()); + assertThat(activeWorkState.hasStuckCommits(now.minus(Duration.millis(1)))).isFalse(); + assertThat(activeWorkState.hasStuckCommits(now)).isFalse(); + assertThat(activeWorkState.hasStuckCommits(now.plus(Duration.millis(1)))).isTrue(); } @Test @@ -565,6 +559,31 @@ public void testFailWork_batchFail() { } } + @Test + public void testGetActiveWork() { + ShardedKey shardedKey = shardedKey("someKey", 1L); + ExecutableWork work = createWork(createWorkItem(1L, 1L, shardedKey)); + + // Initially empty + assertNull(activeWorkState.getActiveWork(shardedKey, work.id())); + + // Activate work + activeWorkState.activateWorkForKey(work); + + // Should find it now + ExecutableWork activeWork = activeWorkState.getActiveWork(shardedKey, work.id()); + assertNotNull(activeWork); + assertSame(work, activeWork); + + // Should not find it with different workId + assertNull(activeWorkState.getActiveWork(shardedKey, workId(2L, 1L))); + assertNull(activeWorkState.getActiveWork(shardedKey, workId(1L, 2L))); + + // Should not find it with different shardedKey + ShardedKey otherShardedKey = shardedKey("otherKey", 2L); + assertNull(activeWorkState.getActiveWork(otherShardedKey, work.id())); + } + private static ExecutableWork firstValue(Map map) { Iterator> iterator = map.entrySet().iterator(); if (iterator.hasNext()) { 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..f57e20d4b5fb 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 @@ -41,6 +41,7 @@ import org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender; import org.apache.beam.sdk.fn.IdGenerators; 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.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; import org.joda.time.Instant; @@ -76,8 +77,9 @@ private static ExecutableWork createWork(ShardedKey shardedKey, long workToken, ignored -> {}, mock(HeartbeatSender.class)), false, - Instant::now), - ignored -> {}); + Instant::now, + ImmutableList.of()), + (work, handle) -> {}); } @Before diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateTest.java new file mode 100644 index 000000000000..22ddc8e4de5b --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateTest.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.dataflow.worker.streaming; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import com.google.api.services.dataflow.model.MapTask; +import java.util.Collections; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor; +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; +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.collect.ImmutableList; +import org.joda.time.Instant; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ComputationStateTest { + + private final BoundedQueueExecutor mockExecutor = mock(BoundedQueueExecutor.class); + private final WindmillStateCache.ForComputation mockStateCache = + mock(WindmillStateCache.ForComputation.class); + private final HeartbeatSender mockHeartbeatSender = mock(HeartbeatSender.class); + + private ComputationState computationState; + + private static ShardedKey shardedKey(String str, long shardKey) { + return ShardedKey.create(ByteString.copyFromUtf8(str), shardKey); + } + + private ExecutableWork createWork(Windmill.WorkItem workItem) { + return ExecutableWork.create( + Work.create( + workItem, + workItem.getSerializedSize(), + Watermarks.builder().setInputDataWatermark(Instant.EPOCH).build(), + Work.createProcessingContext( + "computationId", new FakeGetDataClient(), ignored -> {}, mockHeartbeatSender), + false, + Instant::now, + ImmutableList.of()), + (work, handle) -> {}); + } + + private static Windmill.WorkItem createWorkItem( + long workToken, long cacheToken, ShardedKey shardedKey) { + return Windmill.WorkItem.newBuilder() + .setShardingKey(shardedKey.shardingKey()) + .setKey(shardedKey.key()) + .setWorkToken(workToken) + .setCacheToken(cacheToken) + .build(); + } + + @Before + public void setUp() { + MapTask mapTask = new MapTask(); + mapTask.setStageName("stage"); + mapTask.setSystemName("system"); + computationState = + new ComputationState( + "computationId", mapTask, mockExecutor, Collections.emptyMap(), mockStateCache); + } + + @Test + public void testReexecuteActiveWork_workNotActive() { + ShardedKey shardedKey = shardedKey("key", 1L); + WorkId workId = WorkId.builder().setWorkToken(1L).setCacheToken(1L).build(); + + computationState.reexecuteActiveWork(shardedKey, workId); + + verifyNoInteractions(mockExecutor); + } + + @Test + public void testReexecuteActiveWork_workActive() { + ShardedKey shardedKey = shardedKey("key", 1L); + Windmill.WorkItem workItem = createWorkItem(1L, 1L, shardedKey); + ExecutableWork work = createWork(workItem); + + // Activate work first. This will execute it once. + computationState.activateWork(work); + verify(mockExecutor).execute(work, work.work().getSerializedWorkItemSize()); + + // Now re-execute + computationState.reexecuteActiveWork(shardedKey, work.id()); + verify(mockExecutor).forceExecute(work, work.work().getSerializedWorkItemSize()); + + verifyNoMoreInteractions(mockExecutor); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/WorkTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/WorkTest.java new file mode 100644 index 000000000000..61e52ddd61bd --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/WorkTest.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.CountDownLatch; +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.AtomicBoolean; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill; +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.collect.ImmutableList; +import org.joda.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class WorkTest { + + private static Work createTestWork() { + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.copyFromUtf8("key")) + .setWorkToken(1L) + .setShardingKey(2L) + .build(); + return Work.create( + workItem, + workItem.getSerializedSize(), + Watermarks.builder().setInputDataWatermark(Instant.now()).build(), + Work.createProcessingContext( + "comp", + mock( + org.apache.beam.runners.dataflow.worker.windmill.client.getdata.GetDataClient + .class), + commit -> {}, + mock(HeartbeatSender.class)), + false, + Instant::now, + ImmutableList.of()); + } + + @Test + public void testSetFailedBeforeListener() { + Work work = createTestWork(); + assertFalse(work.isFailed()); + + work.setFailed(); + assertTrue(work.isFailed()); + + AtomicBoolean listener = new AtomicBoolean(false); + work.setOnFailureListener(listener); + assertTrue(listener.get()); + } + + @Test + public void testSetFailedAfterListener() { + Work work = createTestWork(); + AtomicBoolean listener = new AtomicBoolean(false); + work.setOnFailureListener(listener); + assertFalse(listener.get()); + assertFalse(work.isFailed()); + + work.setFailed(); + assertTrue(work.isFailed()); + assertTrue(listener.get()); + } + + @Test + public void testConcurrentSetFailedAndSetOnFailureListener() throws Exception { + int numTrials = 5000; + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + for (int i = 0; i < numTrials; i++) { + Work work = createTestWork(); + AtomicBoolean listener = new AtomicBoolean(false); + CountDownLatch latch = new CountDownLatch(1); + + Future f1 = + executor.submit( + () -> { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + work.setFailed(); + }); + + Future f2 = + executor.submit( + () -> { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + work.setOnFailureListener(listener); + }); + + latch.countDown(); + f1.get(5, TimeUnit.SECONDS); + f2.get(5, TimeUnit.SECONDS); + + assertTrue("Trial " + i + " failed: work should be failed", work.isFailed()); + assertTrue("Trial " + i + " failed: listener should be set to true", listener.get()); + } + } finally { + executor.shutdownNow(); + } + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/testing/RestoreDataflowLoggingMDC.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/testing/RestoreDataflowLoggingMDC.java index 0bd5ceea1de0..1b1226cb7dd0 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/testing/RestoreDataflowLoggingMDC.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/testing/RestoreDataflowLoggingMDC.java @@ -23,7 +23,7 @@ /** Saves, clears and restores the current thread-local logging parameters for tests. */ public class RestoreDataflowLoggingMDC extends ExternalResource { private String previousJobId; - private String previousStageName; + private String previousSystemStageName; private String previousWorkerId; private String previousWorkId; @@ -32,11 +32,11 @@ public RestoreDataflowLoggingMDC() {} @Override protected void before() throws Throwable { previousJobId = DataflowWorkerLoggingMDC.getJobId(); - previousStageName = DataflowWorkerLoggingMDC.getStageName(); + previousSystemStageName = DataflowWorkerLoggingMDC.getSystemStageName(); previousWorkerId = DataflowWorkerLoggingMDC.getWorkerId(); previousWorkId = DataflowWorkerLoggingMDC.getWorkId(); DataflowWorkerLoggingMDC.setJobId(null); - DataflowWorkerLoggingMDC.setStageName(null); + DataflowWorkerLoggingMDC.setSystemStageName(null); DataflowWorkerLoggingMDC.setWorkerId(null); DataflowWorkerLoggingMDC.setWorkId(null); } @@ -44,7 +44,7 @@ protected void before() throws Throwable { @Override protected void after() { DataflowWorkerLoggingMDC.setJobId(previousJobId); - DataflowWorkerLoggingMDC.setStageName(previousStageName); + DataflowWorkerLoggingMDC.setSystemStageName(previousSystemStageName); DataflowWorkerLoggingMDC.setWorkerId(previousWorkerId); DataflowWorkerLoggingMDC.setWorkId(previousWorkId); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/testing/RestoreDataflowLoggingMDCTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/testing/RestoreDataflowLoggingMDCTest.java index 3b78e93cce39..15dfd4ede9d7 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/testing/RestoreDataflowLoggingMDCTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/testing/RestoreDataflowLoggingMDCTest.java @@ -42,7 +42,7 @@ public void testOldValuesAreRestored() throws Throwable { final boolean[] evaluateRan = new boolean[1]; DataflowWorkerLoggingMDC.setJobId("oldJobId"); - DataflowWorkerLoggingMDC.setStageName("oldStageName"); + DataflowWorkerLoggingMDC.setSystemStageName("oldStageName"); DataflowWorkerLoggingMDC.setWorkerId("oldWorkerId"); DataflowWorkerLoggingMDC.setWorkId("oldWorkId"); @@ -54,19 +54,19 @@ public void evaluate() { evaluateRan[0] = true; // Ensure parameters are cleared before the test runs assertNull("null JobId", DataflowWorkerLoggingMDC.getJobId()); - assertNull("null StageName", DataflowWorkerLoggingMDC.getStageName()); + assertNull("null StageName", DataflowWorkerLoggingMDC.getSystemStageName()); assertNull("null WorkerId", DataflowWorkerLoggingMDC.getWorkerId()); assertNull("null WorkId", DataflowWorkerLoggingMDC.getWorkId()); // Simulate updating parameters for the test DataflowWorkerLoggingMDC.setJobId("newJobId"); - DataflowWorkerLoggingMDC.setStageName("newStageName"); + DataflowWorkerLoggingMDC.setSystemStageName("newStageName"); DataflowWorkerLoggingMDC.setWorkerId("newWorkerId"); DataflowWorkerLoggingMDC.setWorkId("newWorkId"); // Ensure that the values changed assertEquals("newJobId", DataflowWorkerLoggingMDC.getJobId()); - assertEquals("newStageName", DataflowWorkerLoggingMDC.getStageName()); + assertEquals("newStageName", DataflowWorkerLoggingMDC.getSystemStageName()); assertEquals("newWorkerId", DataflowWorkerLoggingMDC.getWorkerId()); assertEquals("newWorkId", DataflowWorkerLoggingMDC.getWorkId()); } @@ -77,7 +77,7 @@ public void evaluate() { // Validate that the statement ran and that the values were reverted assertTrue(evaluateRan[0]); assertEquals("oldJobId", DataflowWorkerLoggingMDC.getJobId()); - assertEquals("oldStageName", DataflowWorkerLoggingMDC.getStageName()); + assertEquals("oldStageName", DataflowWorkerLoggingMDC.getSystemStageName()); assertEquals("oldWorkerId", DataflowWorkerLoggingMDC.getWorkerId()); assertEquals("oldWorkId", DataflowWorkerLoggingMDC.getWorkId()); } 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..75230594fc12 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,24 +20,35 @@ 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; +import static org.mockito.Mockito.verify; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import java.util.function.Consumer; +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.FailedWorkHandler; 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; 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.util.concurrent.ThreadFactoryBuilder; import org.joda.time.Instant; +import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -59,19 +70,75 @@ public static Collection useFairMonitor() { return Arrays.asList(new Object[][] {{false}, {true}}); } + private static final FailedWorkHandler FAILING_FAILED_WORK_HANDLER = + ignored -> { + Assert.fail(); + }; private static final long MAXIMUM_BYTES_OUTSTANDING = 10000000; private static final int DEFAULT_MAX_THREADS = 2; private static final int DEFAULT_THREAD_EXPIRATION_SEC = 60; @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) { + return createWorkWithHandle( + computationId, keyGroup, (work, handle) -> executeWorkFn.accept(work)); + } + + private static ExecutableWork createWorkWithCompIdAndKeyGroupAndWorkToken( + String computationId, Work.KeyGroup keyGroup, long workToken, Consumer executeWorkFn) { + WorkItem workItem = + WorkItem.newBuilder() + .setKey(ByteString.EMPTY) + .setShardingKey(1) + .setWorkToken(workToken) + .setCacheToken(1) + .setKeyGroup( + Windmill.Uint128Proto.newBuilder() + .setHigh(keyGroup.high()) + .setLow(keyGroup.low()) + .build()) + .build(); + return 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, + ImmutableList.of()), + (work, handle) -> executeWorkFn.accept(work)); + } + + private static ExecutableWork createWorkWithHandle( + String computationId, + Work.KeyGroup keyGroup, + BiConsumer 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( @@ -79,24 +146,23 @@ 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), + Instant::now, + ImmutableList.of()), executeWorkFn); } - 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 @@ -112,7 +178,8 @@ public void setUp() { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - useFairMonitor); + useFairMonitor, + /*useKeyGroupWorkQueue=*/ false); } @Test @@ -123,9 +190,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 +219,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 +254,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 +292,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 +331,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 +375,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 +418,62 @@ 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 { + Work work1 = createWork(ignored -> {}).work(); + Work work2 = createWork(ignored -> {}).work(); + Work work3 = createWork(ignored -> {}).work(); + BoundedQueueExecutorWorkHandleImpl handle1 = executor.createBudgetHandle(work1, 100L); + BoundedQueueExecutorWorkHandleImpl handle2 = executor.createBudgetHandle(work2, 200L); + handle2.merge(executor.createBudgetHandle(work3, 0L)); + + handle1.merge(handle2); + + // Verify that handle2 has 0 budget and is closed. + assertEquals(0, handle2.getWorkBatch().size()); + assertEquals(0, handle2.bytes()); + assertTrue(handle2.isClosed()); + + // Verify that handle1 has the combined budget and is not closed. + assertEquals(3, handle1.getWorkBatch().size()); + assertTrue(handle1.getWorkBatch().contains(work1)); + assertTrue(handle1.getWorkBatch().contains(work2)); + assertTrue(handle1.getWorkBatch().contains(work3)); + assertEquals(300L, handle1.bytes()); + assertFalse(handle1.isClosed()); + } + @Test public void testRenderSummaryHtml() { String expectedSummaryHtml = @@ -360,4 +483,219 @@ 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); + AtomicReference blockerHandleRef = new AtomicReference<>(); + ExecutableWork blockerWork = + createWorkWithHandle( + "blockerComp", + DEFAULT_KEY_GROUP, + (work, handle) -> { + blockerHandleRef.set(handle); + blockerStart.countDown(); + try { + blockerStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + testExecutor.execute(blockerWork, 0); + blockerStart.await(); + BoundedQueueExecutorWorkHandleImpl stealHandle = + (BoundedQueueExecutorWorkHandleImpl) blockerHandleRef.get(); + assertNotNull(stealHandle); + + // 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 + ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup2, stealHandle, ignored -> {}); + assertNotNull(stolen); + assertEquals(work2, stolen); + + // Run the stolen task + stolen.run(stealHandle); + targetStart.await(); + + // Steal work1 using pollWork with compA and keyGroup1 + ExecutableWork stolen1 = + testExecutor.pollWork("compA", keyGroup1, stealHandle, FAILING_FAILED_WORK_HANDLER); + assertNotNull(stolen1); + assertEquals(work1, stolen1); + + // 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); + AtomicReference blockerHandleRef = new AtomicReference<>(); + ExecutableWork blockerWork = + createWorkWithHandle( + "blockerComp", + DEFAULT_KEY_GROUP, + (work, handle) -> { + blockerHandleRef.set(handle); + blockerStart.countDown(); + try { + blockerStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + testExecutor.execute(blockerWork, 0); + blockerStart.await(); + BoundedQueueExecutorWorkHandleImpl stealHandle = + (BoundedQueueExecutorWorkHandleImpl) blockerHandleRef.get(); + assertNotNull(stealHandle); + + Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1); + ExecutableWork work = createWorkWithCompIdAndKeyGroup("compA", keyGroup, ignored -> {}); + testExecutor.execute(work, 100); + + ExecutableWork stolen = + testExecutor.pollWork("compA", keyGroup, stealHandle, FAILING_FAILED_WORK_HANDLER); + assertNull(stolen); + + blockerStop.countDown(); + testExecutor.shutdown(); + } + + @Test + public void testPollWork_skipsFailedWorkAndCallsOnFailedWorkHandler() throws Exception { + BoundedQueueExecutor testExecutor = + new BoundedQueueExecutor( + 1, + 60, + TimeUnit.SECONDS, + 100, + 10000000, + new ThreadFactoryBuilder().setNameFormat("testPollWork-%d").setDaemon(true).build(), + useFairMonitor, + /* useKeyGroupWorkQueue= */ true); + + CountDownLatch blockerStart = new CountDownLatch(1); + CountDownLatch blockerStop = new CountDownLatch(1); + AtomicReference blockerHandleRef = new AtomicReference<>(); + ExecutableWork blockerWork = + createWorkWithHandle( + "compA", + DEFAULT_KEY_GROUP, + (work, handle) -> { + blockerHandleRef.set(handle); + blockerStart.countDown(); + try { + blockerStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + testExecutor.execute(blockerWork, 10); + blockerStart.await(); + BoundedQueueExecutorWorkHandleImpl stealHandle = + (BoundedQueueExecutorWorkHandleImpl) blockerHandleRef.get(); + assertNotNull(stealHandle); + + Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1); + FailedWorkHandler onFailedWorkHandler = mock(FailedWorkHandler.class); + + ExecutableWork work1 = + createWorkWithCompIdAndKeyGroupAndWorkToken("compA", keyGroup, 101, ignored -> {}); + ExecutableWork work2 = + createWorkWithCompIdAndKeyGroupAndWorkToken("compA", keyGroup, 102, ignored -> {}); + ExecutableWork work3 = + createWorkWithCompIdAndKeyGroupAndWorkToken("compA", keyGroup, 103, ignored -> {}); + + // Enqueue both tasks (they will wait in the queue because the thread is blocked). + testExecutor.execute(work1, 100); + testExecutor.execute(work2, 150); + testExecutor.execute(work3, 200); + + assertEquals(4, testExecutor.elementsOutstanding()); + assertEquals(460, testExecutor.bytesOutstanding()); + + // Mark work1, work3 as failed while waiting in the queue. + work1.work().setFailed(); + work3.work().setFailed(); + + // pollWork should skip work1, close work1's handle, invoke + // onFailedWorkHandler callback, and return work2. + ExecutableWork stolen = + testExecutor.pollWork("compA", keyGroup, stealHandle, onFailedWorkHandler); + assertNotNull(stolen); + assertEquals(work2, stolen); + + verify(onFailedWorkHandler).onFailedWork(work1.work()); + + // Verify stealHandle merged (blockerWork: 10 bytes, work2: 150 bytes). + assertEquals(160, stealHandle.bytes()); + + stolen = testExecutor.pollWork("compA", keyGroup, stealHandle, onFailedWorkHandler); + assertNull(stolen); + verify(onFailedWorkHandler).onFailedWork(work3.work()); + + // Still 160, nothing should be merged in. + assertEquals(160, stealHandle.bytes()); + + // Polling again should return null since no more tasks exist for keyGroup. + assertNull(testExecutor.pollWork("compA", keyGroup, stealHandle, onFailedWorkHandler)); + + blockerStop.countDown(); + stealHandle.close(); + assertEquals(0, testExecutor.elementsOutstanding()); + assertEquals(0, testExecutor.bytesOutstanding()); + 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..77fcb0597586 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java @@ -0,0 +1,502 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.collect.ImmutableList; +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, + ImmutableList.of()), + (w, h) -> {}); + return new QueuedWork(work, executor.createBudgetHandle(work.work(), 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/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..ac7c787b1d26 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(Object key) 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..40f4c7b0e715 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(Object key) throws Exception {} } // A mock ReadOperation fed to a MapTaskExecutor in test. @@ -213,6 +216,9 @@ public void finishBundle() {} @Override public void abort() {} + + @Override + public void finishKey(Object key) throws Exception {} } /** Verify counts for the per-element-output-time counter are correct. */ @@ -309,6 +315,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(1L); } } + + @Override + public void finishKey(Object key) throws Exception {} }, new Operation(new OutputReceiver[] {}, context2) { @Override @@ -318,6 +327,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(2L); } } + + @Override + public void finishKey(Object key) throws Exception {} }, new Operation(new OutputReceiver[] {}, context3) { @Override @@ -327,6 +339,9 @@ public void start() throws Exception { Metrics.counter("TestMetric", "MetricCounter").inc(3L); } } + + @Override + public void finishKey(Object key) 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..0c9667acf38b 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 @@ -85,6 +85,9 @@ public void finishBundle() throws Exception { public void abort() throws Exception { outputReceiver.process("a-aborted"); } + + @Override + public void finishKey(Object key) throws Exception {} } @Test @@ -104,6 +107,7 @@ public void testRunParDoOperation() throws Exception { parDoOperation.process(""); parDoOperation.process("bob"); + parDoOperation.finishKey("key"); parDoOperation.finish(); parDoOperation.abort(); @@ -147,6 +151,7 @@ public void testParDoOperationContext() throws Exception { operation.start(); operation.process("hello"); + operation.finishKey("key"); operation.finish(); InOrder inOrder = @@ -161,6 +166,7 @@ public void testParDoOperationContext() throws Exception { inOrder.verify(processCloseable).close(); inOrder.verify(context).enterProcessTimers(); inOrder.verify(fn).processTimers(); + inOrder.verify(fn).finishKey("key"); inOrder.verify(processTimersCloseable).close(); inOrder.verify(context).enterFinish(); inOrder.verify(fn).finishBundle(); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingApplianceWorkCommitterTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingApplianceWorkCommitterTest.java index 5c3132ae471d..b0ca89ac4c2b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingApplianceWorkCommitterTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingApplianceWorkCommitterTest.java @@ -39,6 +39,7 @@ 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.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.joda.time.Instant; import org.junit.After; @@ -75,7 +76,8 @@ private static Work createMockWork(long workToken) { }, mock(HeartbeatSender.class)), false, - Instant::now); + Instant::now, + ImmutableList.of()); } private static ComputationState createComputationState(String computationId) { @@ -128,10 +130,11 @@ public void testCommit() { fakeWindmillServer.waitForAndGetCommits(commits.size()); for (Commit commit : commits) { + assertThat(commit.workBatch()).hasSize(1); Windmill.WorkItemCommitRequest request = - committed.get(commit.work().getWorkItem().getWorkToken()); + committed.get(commit.workBatch().get(0).getWorkItem().getWorkToken()); assertNotNull(request); - assertThat(request).isEqualTo(commit.request()); + assertThat(request).isEqualTo(commit.singleKeyRequest()); } assertThat(completeCommits).hasSize(commits.size()); @@ -141,12 +144,14 @@ public void testCommit() { (CompleteCommit completeCommit, Commit commit) -> completeCommit.computationId().equals(commit.computationId()) && completeCommit.status() == Windmill.CommitStatus.OK - && completeCommit.workId().equals(commit.work().id()) + && commit.workBatch().size() == 1 + && completeCommit.workId().equals(commit.workBatch().get(0).id()) && completeCommit .shardedKey() .equals( ShardedKey.create( - commit.request().getKey(), commit.request().getShardingKey())), + commit.singleKeyRequest().getKey(), + commit.singleKeyRequest().getShardingKey())), "expected to equal")) .containsExactlyElementsIn(commits); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingEngineWorkCommitterTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingEngineWorkCommitterTest.java index 01197622c24d..4c2b8a9f44fb 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingEngineWorkCommitterTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/commits/StreamingEngineWorkCommitterTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.apache.beam.runners.dataflow.worker.windmill.Windmill.CommitStatus.OK; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -53,6 +54,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.WorkId; import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.CommitStatus; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItemCommitRequest; import org.apache.beam.runners.dataflow.worker.windmill.client.CloseableStream; @@ -62,6 +64,7 @@ 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.grpc.v1p69p0.io.grpc.testing.GrpcCleanupRule; +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; @@ -122,7 +125,8 @@ private static Work createMockWork(long workToken) { }, mock(HeartbeatSender.class)), false, - Instant::now); + Instant::now, + ImmutableList.of()); } private static ComputationState createComputationState(String computationId) { @@ -134,12 +138,11 @@ private static ComputationState createComputationState(String computationId) { null); } - private static CompleteCommit asCompleteCommit(Commit commit, Windmill.CommitStatus status) { - if (commit.work().isFailed()) { - return CompleteCommit.forFailedWork(commit); - } - - return CompleteCommit.create(commit, status); + private static CompleteCommit asCompleteCommit( + String computationId, Work work, Windmill.CommitStatus status) { + Windmill.CommitStatus finalStatus = work.isFailed() ? Windmill.CommitStatus.ABORTED : status; + return CompleteCommit.create( + computationId, work.getShardedKey(), work.id(), finalStatus, /* retryableFailure= */ false); } @Before @@ -186,10 +189,15 @@ public void testCommit_sendsCommitsToStreamingEngine() { waitForExpectedSetSize(completeCommits, 5); for (Commit commit : commits) { - WorkItemCommitRequest request = committed.get(commit.work().getWorkItem().getWorkToken()); + assertThat(commit.workBatch()).hasSize(1); + WorkItemCommitRequest request = + committed.get(commit.workBatch().get(0).getWorkItem().getWorkToken()); assertNotNull(request); - assertThat(request).isEqualTo(commit.request()); - assertThat(completeCommits).contains(asCompleteCommit(commit, Windmill.CommitStatus.OK)); + assertThat(request).isEqualTo(commit.singleKeyRequest()); + assertThat(completeCommits) + .contains( + asCompleteCommit( + commit.computationId(), commit.workBatch().get(0), Windmill.CommitStatus.OK)); } workCommitter.stop(); @@ -224,14 +232,24 @@ public void testCommit_handlesFailedCommits() { waitForExpectedSetSize(completeCommits, 10); for (Commit commit : commits) { - if (commit.work().isFailed()) { + assertThat(commit.workBatch()).hasSize(1); + if (commit.workBatch().get(0).isFailed()) { assertThat(completeCommits) - .contains(asCompleteCommit(commit, Windmill.CommitStatus.ABORTED)); - assertThat(committed).doesNotContainKey(commit.work().getWorkItem().getWorkToken()); + .contains( + asCompleteCommit( + commit.computationId(), + commit.workBatch().get(0), + Windmill.CommitStatus.ABORTED)); + assertThat(committed) + .doesNotContainKey(commit.workBatch().get(0).getWorkItem().getWorkToken()); } else { - assertThat(completeCommits).contains(asCompleteCommit(commit, Windmill.CommitStatus.OK)); + assertThat(completeCommits) + .contains( + asCompleteCommit( + commit.computationId(), commit.workBatch().get(0), Windmill.CommitStatus.OK)); assertThat(committed) - .containsEntry(commit.work().getWorkItem().getWorkToken(), commit.request()); + .containsEntry( + commit.workBatch().get(0).getWorkItem().getWorkToken(), commit.singleKeyRequest()); } } @@ -282,11 +300,17 @@ public void testCommit_handlesCompleteCommits_commitStatusNotOK() { waitForExpectedSetSize(completeCommits, commits.size()); for (Commit commit : commits) { - WorkItemCommitRequest request = committed.get(commit.work().getWorkItem().getWorkToken()); + assertThat(commit.workBatch()).hasSize(1); + WorkItemCommitRequest request = + committed.get(commit.workBatch().get(0).getWorkItem().getWorkToken()); assertNotNull(request); - assertThat(request).isEqualTo(commit.request()); + assertThat(request).isEqualTo(commit.singleKeyRequest()); assertThat(completeCommits) - .contains(asCompleteCommit(commit, expectedCommitStatus.get(commit.work().id()))); + .contains( + asCompleteCommit( + commit.computationId(), + commit.workBatch().get(0), + expectedCommitStatus.get(commit.workBatch().get(0).id()))); } workCommitter.stop(); @@ -313,6 +337,14 @@ public boolean commitWorkItem( return false; } + @Override + public boolean commitMultiKeyWorkItem( + String computation, + Windmill.MultiKeyWorkItemCommitRequest request, + Consumer onDone) { + return false; + } + @Override public void flush() {} }; @@ -367,10 +399,12 @@ public void shutdown() {} assertThat(commits.size()).isEqualTo(completeCommits.size()); for (CompleteCommit completeCommit : completeCommits) { assertThat(completeCommit.status()).isEqualTo(Windmill.CommitStatus.ABORTED); + assertThat(completeCommit.retryableFailure()).isFalse(); } for (Commit commit : commits) { - assertTrue(commit.work().isFailed()); + assertThat(commit.workBatch()).hasSize(1); + assertTrue(commit.workBatch().get(0).isFailed()); } } @@ -409,10 +443,15 @@ public void testMultipleCommitSendersSingleStream() { waitForExpectedSetSize(completeCommits, commits.size()); for (Commit commit : commits) { - WorkItemCommitRequest request = committed.get(commit.work().getWorkItem().getWorkToken()); + assertThat(commit.workBatch()).hasSize(1); + WorkItemCommitRequest request = + committed.get(commit.workBatch().get(0).getWorkItem().getWorkToken()); assertNotNull(request); - assertThat(request).isEqualTo(commit.request()); - assertThat(completeCommits).contains(asCompleteCommit(commit, Windmill.CommitStatus.OK)); + assertThat(request).isEqualTo(commit.singleKeyRequest()); + assertThat(completeCommits) + .contains( + asCompleteCommit( + commit.computationId(), commit.workBatch().get(0), Windmill.CommitStatus.OK)); } workCommitter.stop(); @@ -474,4 +513,244 @@ public void testStop_drainsCommitQueue_concurrentCommit() waitForExpectedSetSize(completeCommits, sentCommits.intValue()); } + + @Test + public void testCommit_multiKeyCommitSuccess() { + Set completeCommits = Collections.newSetFromMap(new ConcurrentHashMap<>()); + workCommitter = createWorkCommitter(completeCommits::add); + + Work workA = createMockWork(101L); + Work workB = createMockWork(102L); + Work workC = createMockWork(103L); + + Windmill.MultiKeyWorkItemCommitRequest multiKeyRequest = + Windmill.MultiKeyWorkItemCommitRequest.newBuilder() + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workA.getWorkItem().getKey()) + .setShardingKey(workA.getWorkItem().getShardingKey()) + .setWorkToken(workA.getWorkItem().getWorkToken()) + .setCacheToken(workA.getWorkItem().getCacheToken()) + .build()) + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workB.getWorkItem().getKey()) + .setShardingKey(workB.getWorkItem().getShardingKey()) + .setWorkToken(workB.getWorkItem().getWorkToken()) + .setCacheToken(workB.getWorkItem().getCacheToken()) + .build()) + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workC.getWorkItem().getKey()) + .setShardingKey(workC.getWorkItem().getShardingKey()) + .setWorkToken(workC.getWorkItem().getWorkToken()) + .setCacheToken(workC.getWorkItem().getCacheToken()) + .build()) + .build(); + + Commit commit = + Commit.createMultiKey( + multiKeyRequest, + createComputationState("computationId"), + ImmutableList.of(workA, workB, workC)); + + workCommitter.start(); + workCommitter.commit(commit); + + // Wait for the server to receive and process the commits + fakeWindmillServer.waitForAndGetCommits(3); + waitForExpectedSetSize(completeCommits, 3); + + // Verify that FakeWindmillServer received all 3 work requests in multiKeyCommitsReceived + List multiKeyCommits = + fakeWindmillServer.getMultiKeyCommitsReceived(); + assertThat(multiKeyCommits).hasSize(1); + assertThat(multiKeyCommits.get(0)).isEqualTo(multiKeyRequest); + + // Verify all three works are completed successfully + assertThat(completeCommits) + .containsExactly( + CompleteCommit.create( + "computationId", + workA.getShardedKey(), + workA.id(), + CommitStatus.OK, + /* retryableFailure= */ false), + CompleteCommit.create( + "computationId", + workB.getShardedKey(), + workB.id(), + CommitStatus.OK, + /* retryableFailure= */ false), + CompleteCommit.create( + "computationId", + workC.getShardedKey(), + workC.id(), + CommitStatus.OK, + /* retryableFailure= */ false)); + + // There should be no more commits in the queue + assertEquals(0, workCommitter.currentActiveCommitBytes()); + workCommitter.stop(); + } + + @Test + public void testCommit_multiKeyCommitFailedWork() { + Set completeCommits = Collections.newSetFromMap(new ConcurrentHashMap<>()); + workCommitter = createWorkCommitter(completeCommits::add); + + Work workA = createMockWork(101L); + Work workB = createMockWork(102L); + Work workC = createMockWork(103L); + + // Mark non-primary key B as failed + workB.setFailed(); + + Windmill.MultiKeyWorkItemCommitRequest multiKeyRequest = + Windmill.MultiKeyWorkItemCommitRequest.newBuilder() + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workA.getWorkItem().getKey()) + .setShardingKey(workA.getWorkItem().getShardingKey()) + .setWorkToken(workA.getWorkItem().getWorkToken()) + .setCacheToken(workA.getWorkItem().getCacheToken()) + .build()) + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workB.getWorkItem().getKey()) + .setShardingKey(workB.getWorkItem().getShardingKey()) + .setWorkToken(workB.getWorkItem().getWorkToken()) + .setCacheToken(workB.getWorkItem().getCacheToken()) + .build()) + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workC.getWorkItem().getKey()) + .setShardingKey(workC.getWorkItem().getShardingKey()) + .setWorkToken(workC.getWorkItem().getWorkToken()) + .setCacheToken(workC.getWorkItem().getCacheToken()) + .build()) + .build(); + + Commit commit = + Commit.createMultiKey( + multiKeyRequest, + createComputationState("computationId"), + ImmutableList.of(workA, workB, workC)); + + workCommitter.start(); + workCommitter.commit(commit); + + // The entire batch must be aborted immediately without making network calls + waitForExpectedSetSize(completeCommits, 3); + + // Verify all three works are aborted individually and that key A and key C can be retried + // locally. + assertThat(completeCommits) + .containsExactly( + CompleteCommit.create( + "computationId", + workA.getShardedKey(), + workA.id(), + CommitStatus.ABORTED, + /* retryableFailure= */ true), + CompleteCommit.create( + "computationId", + workB.getShardedKey(), + workB.id(), + CommitStatus.ABORTED, + /* retryableFailure= */ false), + CompleteCommit.create( + "computationId", + workC.getShardedKey(), + workC.id(), + CommitStatus.ABORTED, + /* retryableFailure= */ true)); + + // There should be no more commits in the queue + assertEquals(0, workCommitter.currentActiveCommitBytes()); + workCommitter.stop(); + } + + @Test + public void testCommit_multiKeyCommitStatusNotOK() { + Set completeCommits = Collections.newSetFromMap(new ConcurrentHashMap<>()); + workCommitter = createWorkCommitter(completeCommits::add); + + Work workA = createMockWork(101L); + Work workB = createMockWork(102L); + Work workC = createMockWork(103L); + + Windmill.MultiKeyWorkItemCommitRequest multiKeyRequest = + Windmill.MultiKeyWorkItemCommitRequest.newBuilder() + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workA.getWorkItem().getKey()) + .setShardingKey(workA.getWorkItem().getShardingKey()) + .setWorkToken(workA.getWorkItem().getWorkToken()) + .setCacheToken(workA.getWorkItem().getCacheToken()) + .build()) + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workB.getWorkItem().getKey()) + .setShardingKey(workB.getWorkItem().getShardingKey()) + .setWorkToken(workB.getWorkItem().getWorkToken()) + .setCacheToken(workB.getWorkItem().getCacheToken()) + .build()) + .addRequests( + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(workC.getWorkItem().getKey()) + .setShardingKey(workC.getWorkItem().getShardingKey()) + .setWorkToken(workC.getWorkItem().getWorkToken()) + .setCacheToken(workC.getWorkItem().getCacheToken()) + .build()) + .build(); + + Commit commit = + Commit.createMultiKey( + multiKeyRequest, + createComputationState("computationId"), + ImmutableList.of(workA, workB, workC)); + + // Respond to multi key commit with NOT_FOUND status. + fakeWindmillServer.setMultiKeyCommitStatus(CommitStatus.NOT_FOUND); + + workCommitter.start(); + workCommitter.commit(commit); + + // Wait for the server to receive and process the commits + fakeWindmillServer.waitForAndGetCommits(3); + waitForExpectedSetSize(completeCommits, 3); + + // Verify that FakeWindmillServer received the multi-key commit + List multiKeyCommits = + fakeWindmillServer.getMultiKeyCommitsReceived(); + assertThat(multiKeyCommits).hasSize(1); + assertThat(multiKeyCommits.get(0)).isEqualTo(multiKeyRequest); + + // Verify all three works in the multi-key commit are completed with NOT_FOUND status + assertThat(completeCommits) + .containsExactly( + CompleteCommit.create( + "computationId", + workA.getShardedKey(), + workA.id(), + CommitStatus.NOT_FOUND, + /* retryableFailure= */ false), + CompleteCommit.create( + "computationId", + workB.getShardedKey(), + workB.id(), + CommitStatus.NOT_FOUND, + /* retryableFailure= */ false), + CompleteCommit.create( + "computationId", + workC.getShardedKey(), + workC.id(), + CommitStatus.NOT_FOUND, + /* retryableFailure= */ false)); + + // There should be no more commits in the queue + assertEquals(0, workCommitter.currentActiveCommitBytes()); + workCommitter.stop(); + } } 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..147f053fd8ae 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,9 +38,12 @@ 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; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.CommitStatus; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.StreamingCommitResponse; import org.apache.beam.runners.dataflow.worker.windmill.WindmillConnection; import org.apache.beam.runners.dataflow.worker.windmill.client.TriggeredScheduledExecutorService; import org.apache.beam.runners.dataflow.worker.windmill.client.WindmillStream; @@ -1133,6 +1136,358 @@ public void testCommitWorkItem_multiplePhysicalStreams_multipleHandovers_halfClo assertTrue(commitWorkStream.awaitTermination(10, TimeUnit.SECONDS)); } + @Test + public void testCommit_multiKeyCommit() throws Exception { + testMultiKeyCommit(CommitStatus.OK); + } + + @Test + public void testCommit_multiKeyCommit_Failure() throws Exception { + testMultiKeyCommit(CommitStatus.NOT_FOUND); + } + + private void testMultiKeyCommit(CommitStatus commitStatus) throws Exception { + GrpcCommitWorkStream commitWorkStream = createCommitWorkStream(); + FakeWindmillGrpcService.CommitStreamInfo streamInfo = waitForConnectionAndConsumeHeader(); + + CompletableFuture commitStatusFuture = new CompletableFuture<>(); + + // 1. Construct two individual WorkItemCommitRequests + long shardingKey1 = 101L; + long workToken1 = 201L; + long cacheToken1 = 301L; + long shardingKey2 = 102L; + long workToken2 = 202L; + long cacheToken2 = 302L; + Windmill.WorkItemCommitRequest request1 = + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setShardingKey(shardingKey1) + .setWorkToken(workToken1) + .setCacheToken(cacheToken1) + .build(); + Windmill.WorkItemCommitRequest request2 = + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(ByteString.copyFromUtf8("key2")) + .setShardingKey(shardingKey2) + .setWorkToken(workToken2) + .setCacheToken(cacheToken2) + .build(); + + // 2. Wrap them into a MultiKeyWorkItemCommitRequest + Windmill.MultiKeyWorkItemCommitRequest multiKeyRequest = + Windmill.MultiKeyWorkItemCommitRequest.newBuilder() + .addRequests(request1) + .addRequests(request2) + .build(); + + // 3. Commit the multi-key work item using the request batcher + try (WindmillStream.CommitWorkStream.RequestBatcher batcher = commitWorkStream.batcher()) { + assertTrue( + batcher.commitMultiKeyWorkItem( + COMPUTATION_ID, multiKeyRequest, commitStatusFuture::complete)); + } + + // 4. Receive and assert request properties on FakeWindmillGrpcService + Windmill.StreamingCommitWorkRequest request = streamInfo.requests.take(); + assertThat(request.getCommitChunkCount()).isEqualTo(1); + + Windmill.StreamingCommitRequestChunk chunk = request.getCommitChunk(0); + + // Assert that the commit type is correctly identified as COMMIT_TYPE_MULTI_KEY + assertThat(chunk.getCommitType()) + .isEqualTo(Windmill.StreamingCommitRequestChunk.CommitType.COMMIT_TYPE_MULTI_KEY); + + // Assert that the routing sharding key is mapped to the first request's sharding key + assertThat(chunk.getShardingKey()).isEqualTo(request1.getShardingKey()); + + // Assert that the serialized payload matches the input multiKeyRequest + Windmill.MultiKeyWorkItemCommitRequest parsedRequest = + Windmill.MultiKeyWorkItemCommitRequest.parseFrom(chunk.getSerializedWorkItemCommit()); + assertThat(parsedRequest).isEqualTo(multiKeyRequest); + + // 5. Respond with the generated requestId to complete the commit + long requestId = chunk.getRequestId(); + StreamingCommitResponse.Builder builder = + StreamingCommitResponse.newBuilder().addRequestId(requestId); + if (commitStatus != CommitStatus.OK) { + builder.addStatus(commitStatus); + } + streamInfo.responseObserver.onNext(builder.build()); + + // 6. Verify callback completed with expected sCommitStatus + assertThat(commitStatusFuture.get()).isEqualTo(commitStatus); + } + + @Test + public void testCommit_multiKeyCommit_multichunk() throws Exception { + GrpcCommitWorkStream commitWorkStream = createCommitWorkStream(); + FakeWindmillGrpcService.CommitStreamInfo streamInfo = waitForConnectionAndConsumeHeader(); + + CompletableFuture commitStatusFuture = new CompletableFuture<>(); + + long shardingKey1 = 101L; + long workToken1 = 201L; + long cacheToken1 = 301L; + long shardingKey2 = 102L; + long workToken2 = 202L; + long cacheToken2 = 302L; + + Windmill.WorkItemCommitRequest request1 = + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setShardingKey(shardingKey1) + .setWorkToken(workToken1) + .setCacheToken(cacheToken1) + .addBagUpdates(Windmill.TagBag.newBuilder().setTag(LARGE_BYTE_STRING).build()) + .build(); + + Windmill.WorkItemCommitRequest request2 = + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(ByteString.copyFromUtf8("key2")) + .setShardingKey(shardingKey2) + .setWorkToken(workToken2) + .setCacheToken(cacheToken2) + .build(); + + Windmill.MultiKeyWorkItemCommitRequest multiKeyRequest = + Windmill.MultiKeyWorkItemCommitRequest.newBuilder() + .addRequests(request1) + .addRequests(request2) + .build(); + + try (WindmillStream.CommitWorkStream.RequestBatcher batcher = commitWorkStream.batcher()) { + assertTrue( + batcher.commitMultiKeyWorkItem( + COMPUTATION_ID, multiKeyRequest, commitStatusFuture::complete)); + } + + Windmill.StreamingCommitWorkRequest requestChunk1 = streamInfo.requests.take(); + assertThat(requestChunk1.getCommitChunkCount()).isEqualTo(1); + Windmill.StreamingCommitRequestChunk chunk1 = requestChunk1.getCommitChunk(0); + + assertThat(chunk1.getCommitType()) + .isEqualTo(Windmill.StreamingCommitRequestChunk.CommitType.COMMIT_TYPE_MULTI_KEY); + assertThat(chunk1.getShardingKey()).isEqualTo(request1.getShardingKey()); + assertThat(chunk1.getRemainingBytesForWorkItem()).isGreaterThan(0); + + Windmill.StreamingCommitWorkRequest requestChunk2 = streamInfo.requests.take(); + assertThat(requestChunk2.getCommitChunkCount()).isEqualTo(1); + Windmill.StreamingCommitRequestChunk chunk2 = requestChunk2.getCommitChunk(0); + + assertThat(chunk2.getCommitType()) + .isEqualTo(Windmill.StreamingCommitRequestChunk.CommitType.COMMIT_TYPE_MULTI_KEY); + assertThat(chunk2.getShardingKey()).isEqualTo(request1.getShardingKey()); + assertThat(chunk2.getRemainingBytesForWorkItem()).isEqualTo(0); + + ByteString reconstructedBytes = + chunk1.getSerializedWorkItemCommit().concat(chunk2.getSerializedWorkItemCommit()); + Windmill.MultiKeyWorkItemCommitRequest parsedRequest = + Windmill.MultiKeyWorkItemCommitRequest.parseFrom(reconstructedBytes); + assertThat(parsedRequest).isEqualTo(multiKeyRequest); + + long requestId = chunk1.getRequestId(); + assertThat(chunk2.getRequestId()).isEqualTo(requestId); + + streamInfo.responseObserver.onNext( + Windmill.StreamingCommitResponse.newBuilder().addRequestId(requestId).build()); + + assertThat(commitStatusFuture.get()).isEqualTo(Windmill.CommitStatus.OK); + } + + @Test + public void testCommitMultiKeyWorkItem_retryOnNewStream() throws Exception { + GrpcCommitWorkStream commitWorkStream = createCommitWorkStream(); + FakeWindmillGrpcService.CommitStreamInfo streamInfo = waitForConnectionAndConsumeHeader(); + + CompletableFuture commitStatusFuture = new CompletableFuture<>(); + + long shardingKey1 = 101L; + long workToken1 = 201L; + long cacheToken1 = 301L; + Windmill.WorkItemCommitRequest request1 = + Windmill.WorkItemCommitRequest.newBuilder() + .setKey(ByteString.copyFromUtf8("key1")) + .setShardingKey(shardingKey1) + .setWorkToken(workToken1) + .setCacheToken(cacheToken1) + .build(); + Windmill.MultiKeyWorkItemCommitRequest multiKeyRequest = + Windmill.MultiKeyWorkItemCommitRequest.newBuilder().addRequests(request1).build(); + + try (WindmillStream.CommitWorkStream.RequestBatcher batcher = commitWorkStream.batcher()) { + assertTrue( + batcher.commitMultiKeyWorkItem( + COMPUTATION_ID, multiKeyRequest, commitStatusFuture::complete)); + } + + Windmill.StreamingCommitWorkRequest request = streamInfo.requests.take(); + assertThat(request.getCommitChunkCount()).isEqualTo(1); + Windmill.StreamingCommitRequestChunk chunk = request.getCommitChunk(0); + assertThat(chunk.getCommitType()) + .isEqualTo(Windmill.StreamingCommitRequestChunk.CommitType.COMMIT_TYPE_MULTI_KEY); + long requestId = chunk.getRequestId(); + + streamInfo.responseObserver.onError(new IOException("test error")); + + FakeWindmillGrpcService.CommitStreamInfo reconnectStreamInfo = + waitForConnectionAndConsumeHeader(); + Windmill.StreamingCommitWorkRequest reconnectRequest = reconnectStreamInfo.requests.take(); + assertThat(reconnectRequest.getCommitChunkCount()).isEqualTo(1); + Windmill.StreamingCommitRequestChunk reconnectChunk = reconnectRequest.getCommitChunk(0); + assertThat(reconnectChunk.getCommitType()) + .isEqualTo(Windmill.StreamingCommitRequestChunk.CommitType.COMMIT_TYPE_MULTI_KEY); + assertThat(reconnectChunk.getRequestId()).isEqualTo(requestId); + + Windmill.MultiKeyWorkItemCommitRequest parsedRequest = + Windmill.MultiKeyWorkItemCommitRequest.parseFrom( + reconnectChunk.getSerializedWorkItemCommit()); + assertThat(parsedRequest).isEqualTo(multiKeyRequest); + + reconnectStreamInfo.responseObserver.onNext( + Windmill.StreamingCommitResponse.newBuilder().addRequestId(requestId).build()); + assertThat(commitStatusFuture.get()).isEqualTo(Windmill.CommitStatus.OK); + } + + @Test + public void testCommitWorkItem_retryOnNewStream_multichunk() throws Exception { + GrpcCommitWorkStream commitWorkStream = createCommitWorkStream(); + FakeWindmillGrpcService.CommitStreamInfo streamInfo = waitForConnectionAndConsumeHeader(); + + CompletableFuture commitStatusFuture = new CompletableFuture<>(); + + Windmill.WorkItemCommitRequest largeRequest = + workItemCommitRequest(1) + .toBuilder() + .addBagUpdates(Windmill.TagBag.newBuilder().setTag(LARGE_BYTE_STRING).build()) + .build(); + + try (WindmillStream.CommitWorkStream.RequestBatcher batcher = commitWorkStream.batcher()) { + assertTrue( + batcher.commitWorkItem(COMPUTATION_ID, largeRequest, commitStatusFuture::complete)); + } + + Windmill.StreamingCommitWorkRequest requestChunk1 = streamInfo.requests.take(); + assertThat(requestChunk1.getCommitChunkCount()).isEqualTo(1); + Windmill.StreamingCommitRequestChunk chunk1 = requestChunk1.getCommitChunk(0); + long requestId = chunk1.getRequestId(); + assertThat(chunk1.getRemainingBytesForWorkItem()).isGreaterThan(0); + + Windmill.StreamingCommitWorkRequest requestChunk2 = streamInfo.requests.take(); + assertThat(requestChunk2.getCommitChunkCount()).isEqualTo(1); + Windmill.StreamingCommitRequestChunk chunk2 = requestChunk2.getCommitChunk(0); + assertThat(chunk2.getRequestId()).isEqualTo(requestId); + assertThat(chunk2.getRemainingBytesForWorkItem()).isEqualTo(0); + + streamInfo.responseObserver.onError(new IOException("test error")); + + FakeWindmillGrpcService.CommitStreamInfo reconnectStreamInfo = + waitForConnectionAndConsumeHeader(); + + Windmill.StreamingCommitWorkRequest reconnectChunk1 = reconnectStreamInfo.requests.take(); + assertThat(reconnectChunk1.getCommitChunkCount()).isEqualTo(1); + Windmill.StreamingCommitRequestChunk reconChunk1 = reconnectChunk1.getCommitChunk(0); + assertThat(reconChunk1.getRequestId()).isEqualTo(requestId); + assertThat(reconChunk1.getRemainingBytesForWorkItem()).isGreaterThan(0); + + Windmill.StreamingCommitWorkRequest reconnectChunk2 = reconnectStreamInfo.requests.take(); + assertThat(reconnectChunk2.getCommitChunkCount()).isEqualTo(1); + Windmill.StreamingCommitRequestChunk reconChunk2 = reconnectChunk2.getCommitChunk(0); + assertThat(reconChunk2.getRequestId()).isEqualTo(requestId); + assertThat(reconChunk2.getRemainingBytesForWorkItem()).isEqualTo(0); + + ByteString reconstructedBytes = + reconChunk1.getSerializedWorkItemCommit().concat(reconChunk2.getSerializedWorkItemCommit()); + Windmill.WorkItemCommitRequest parsedRequest = + Windmill.WorkItemCommitRequest.parseFrom(reconstructedBytes); + assertThat(parsedRequest).isEqualTo(largeRequest); + + reconnectStreamInfo.responseObserver.onNext( + Windmill.StreamingCommitResponse.newBuilder().addRequestId(requestId).build()); + assertThat(commitStatusFuture.get()).isEqualTo(Windmill.CommitStatus.OK); + } + + @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(); 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/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateReaderTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateReaderTest.java index 1611fdac25dc..e38388ff566b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateReaderTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateReaderTest.java @@ -35,9 +35,9 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.Future; -import org.apache.beam.runners.dataflow.worker.KeyTokenInvalidException; import org.apache.beam.runners.dataflow.worker.WindmillStateTestUtils; import org.apache.beam.runners.dataflow.worker.WindmillTimeUtils; +import org.apache.beam.runners.dataflow.worker.WorkCancellingException; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.KeyedGetDataRequest; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.SortedListEntry; @@ -1572,16 +1572,16 @@ public void testKeyTokenInvalid() throws Exception { try { watermarkFuture.get(); - fail("Expected KeyTokenInvalidException"); + fail("Expected WorkCancelingException"); } catch (Exception e) { - assertTrue(KeyTokenInvalidException.isKeyTokenInvalidException(e)); + assertTrue(WorkCancellingException.isWorkCancellingException(e)); } try { bagFuture.get(); - fail("Expected KeyTokenInvalidException"); + fail("Expected WorkCancelingException"); } catch (Exception e) { - assertTrue(KeyTokenInvalidException.isKeyTokenInvalidException(e)); + assertTrue(WorkCancellingException.isWorkCancellingException(e)); } } 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 68a11895fa12..741cc35376fc 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 @@ -22,14 +22,13 @@ import static org.mockito.Mockito.mock; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Supplier; -import org.apache.beam.runners.dataflow.worker.KeyTokenInvalidException; -import org.apache.beam.runners.dataflow.worker.WorkItemCancelledException; 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; @@ -39,6 +38,7 @@ 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.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; import org.joda.time.Duration; import org.joda.time.Instant; @@ -50,6 +50,7 @@ public class WorkFailureProcessorTest { private static final String DEFAULT_COMPUTATION_ID = "computationId"; + private static final String DEFAULT_SYSTEM_NAME = "systemName"; private static WorkFailureProcessor createWorkFailureProcessor( FailureTracker failureTracker, Supplier clock) { @@ -64,7 +65,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); } @@ -97,8 +99,11 @@ private static ExecutableWork createWork(Supplier clock, Consumer ignored -> {}, mock(HeartbeatSender.class)), false, - clock), - processWorkFn); + clock, + ImmutableList.of()), + (work, handle) -> { + processWorkFn.accept(work); + }); } private static ExecutableWork createWork(Consumer processWorkFn) { @@ -106,30 +111,18 @@ private static ExecutableWork createWork(Consumer processWorkFn) { } @Test - public void logAndProcessFailure_doesNotRetryKeyTokenInvalidException() throws Throwable { + public void logAndProcessFailureBatch_doesNotRetryFailedWork() throws Throwable { Set executedWork = new HashSet<>(); ExecutableWork work = createWork(executedWork::add); + work.work().setFailed(); WorkFailureProcessor workFailureProcessor = createWorkFailureProcessor(streamingEngineFailureReporter()); Set invalidWork = new HashSet<>(); - workFailureProcessor.logAndProcessFailure( - DEFAULT_COMPUTATION_ID, work, new KeyTokenInvalidException("key"), invalidWork::add); - - assertThat(executedWork).isEmpty(); - assertThat(invalidWork).containsExactly(work.work()); - } - - @Test - public void logAndProcessFailure_doesNotRetryWhenWorkItemCancelled() throws Throwable { - Set executedWork = new HashSet<>(); - ExecutableWork work = createWork(executedWork::add); - WorkFailureProcessor workFailureProcessor = - createWorkFailureProcessor(streamingEngineFailureReporter()); - Set invalidWork = new HashSet<>(); - workFailureProcessor.logAndProcessFailure( + workFailureProcessor.logAndProcessFailureBatch( DEFAULT_COMPUTATION_ID, - work, - new WorkItemCancelledException(work.getWorkItem().getShardingKey()), + DEFAULT_SYSTEM_NAME, + List.of(work), + new RuntimeException(), invalidWork::add); assertThat(executedWork).isEmpty(); @@ -137,7 +130,7 @@ public void logAndProcessFailure_doesNotRetryWhenWorkItemCancelled() throws Thro } @Test - public void logAndProcessFailure_doesNotRetryOOM() { + public void logAndProcessFailureBatch_doesNotRetryOOM() { Set executedWork = new HashSet<>(); ExecutableWork work = createWork(executedWork::add); WorkFailureProcessor workFailureProcessor = @@ -146,69 +139,137 @@ public void logAndProcessFailure_doesNotRetryOOM() { assertThrows( OutOfMemoryError.class, () -> - workFailureProcessor.logAndProcessFailure( - DEFAULT_COMPUTATION_ID, work, new OutOfMemoryError(), invalidWork::add)); + workFailureProcessor.logAndProcessFailureBatch( + DEFAULT_COMPUTATION_ID, + DEFAULT_SYSTEM_NAME, + List.of(work), + new OutOfMemoryError(), + invalidWork::add)); assertThat(executedWork).isEmpty(); + assertThat(invalidWork).isEmpty(); } @Test - public void logAndProcessFailure_doesNotRetryWhenFailureReporterMarksAsNonRetryable() + public void logAndProcessFailureBatch_doesNotRetryWhenFailureReporterMarksAsNonRetryable() throws Throwable { Set executedWork = new HashSet<>(); ExecutableWork work = createWork(executedWork::add); WorkFailureProcessor workFailureProcessor = createWorkFailureProcessor(streamingApplianceFailureReporter(true)); Set invalidWork = new HashSet<>(); - workFailureProcessor.logAndProcessFailure( - DEFAULT_COMPUTATION_ID, work, new RuntimeException(), invalidWork::add); + workFailureProcessor.logAndProcessFailureBatch( + DEFAULT_COMPUTATION_ID, + DEFAULT_SYSTEM_NAME, + List.of(work), + new RuntimeException(), + invalidWork::add); assertThat(executedWork).isEmpty(); assertThat(invalidWork).containsExactly(work.work()); } @Test - public void logAndProcessFailure_doesNotRetryAfterLocalRetryTimeout() throws Throwable { + public void logAndProcessFailureBatch_doesNotRetryAfterLocalRetryTimeout() throws Throwable { Set executedWork = new HashSet<>(); ExecutableWork veryOldWork = createWork(() -> Instant.now().minus(Duration.standardDays(30)), executedWork::add); WorkFailureProcessor workFailureProcessor = createWorkFailureProcessor(streamingEngineFailureReporter()); Set invalidWork = new HashSet<>(); - workFailureProcessor.logAndProcessFailure( - DEFAULT_COMPUTATION_ID, veryOldWork, new RuntimeException(), invalidWork::add); + workFailureProcessor.logAndProcessFailureBatch( + DEFAULT_COMPUTATION_ID, + DEFAULT_SYSTEM_NAME, + List.of(veryOldWork), + new RuntimeException(), + invalidWork::add); assertThat(executedWork).isEmpty(); assertThat(invalidWork).contains(veryOldWork.work()); } @Test - public void logAndProcessFailure_retriesOnUncaughtUnhandledException_streamingEngine() + public void logAndProcessFailureBatch_retriesOnUncaughtUnhandledException_streamingEngine() throws Throwable { CountDownLatch runWork = new CountDownLatch(1); ExecutableWork work = createWork(ignored -> runWork.countDown()); WorkFailureProcessor workFailureProcessor = createWorkFailureProcessor(streamingEngineFailureReporter()); Set invalidWork = new HashSet<>(); - workFailureProcessor.logAndProcessFailure( - DEFAULT_COMPUTATION_ID, work, new RuntimeException(), invalidWork::add); + workFailureProcessor.logAndProcessFailureBatch( + DEFAULT_COMPUTATION_ID, + DEFAULT_SYSTEM_NAME, + List.of(work), + new RuntimeException(), + invalidWork::add); runWork.await(); assertThat(invalidWork).isEmpty(); } @Test - public void logAndProcessFailure_retriesOnUncaughtUnhandledException_streamingAppliance() + public void logAndProcessFailureBatch_retriesOnUncaughtUnhandledException_streamingAppliance() throws Throwable { CountDownLatch runWork = new CountDownLatch(1); ExecutableWork work = createWork(ignored -> runWork.countDown()); WorkFailureProcessor workFailureProcessor = createWorkFailureProcessor(streamingApplianceFailureReporter(false)); Set invalidWork = new HashSet<>(); - workFailureProcessor.logAndProcessFailure( - DEFAULT_COMPUTATION_ID, work, new RuntimeException(), invalidWork::add); + workFailureProcessor.logAndProcessFailureBatch( + DEFAULT_COMPUTATION_ID, + DEFAULT_SYSTEM_NAME, + List.of(work), + new RuntimeException(), + invalidWork::add); runWork.await(); assertThat(invalidWork).isEmpty(); } + + @Test + public void logAndProcessFailureBatch_retryAll() throws Throwable { + CountDownLatch runWork1 = new CountDownLatch(1); + CountDownLatch runWork2 = new CountDownLatch(1); + ExecutableWork work1 = createWork(ignored -> runWork1.countDown()); + ExecutableWork work2 = createWork(ignored -> runWork2.countDown()); + + WorkFailureProcessor workFailureProcessor = + createWorkFailureProcessor(streamingEngineFailureReporter()); + Set invalidWork = new HashSet<>(); + + workFailureProcessor.logAndProcessFailureBatch( + DEFAULT_COMPUTATION_ID, + DEFAULT_SYSTEM_NAME, + List.of(work1, work2), + new RuntimeException(), + invalidWork::add); + + runWork1.await(); + runWork2.await(); + assertThat(invalidWork).isEmpty(); + } + + @Test + public void logAndProcessFailureBatch_mixRetryAndAbort() throws Throwable { + CountDownLatch runWork1 = new CountDownLatch(1); + Set executedWork2 = new HashSet<>(); + ExecutableWork work1 = createWork(ignored -> runWork1.countDown()); + ExecutableWork work2 = createWork(executedWork2::add); + work2.work().setFailed(); + + WorkFailureProcessor workFailureProcessor = + createWorkFailureProcessor(streamingEngineFailureReporter()); + Set invalidWork = new HashSet<>(); + + workFailureProcessor.logAndProcessFailureBatch( + DEFAULT_COMPUTATION_ID, + DEFAULT_SYSTEM_NAME, + List.of(work1, work2), + new RuntimeException(), + invalidWork::add); + + runWork1.await(); + assertThat(executedWork2).isEmpty(); + assertThat(invalidWork).containsExactly(work2.work()); + } } 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..37655152710c 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 @@ -19,12 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.google.api.services.dataflow.model.MapTask; @@ -53,8 +48,7 @@ import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache; import org.apache.beam.runners.direct.Clock; 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.HashBasedTable; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Table; +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.util.concurrent.ThreadFactoryBuilder; import org.joda.time.Duration; import org.joda.time.Instant; @@ -80,7 +74,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) { @@ -101,13 +96,11 @@ private static ComputationState createComputationState( private ActiveWorkRefresher createActiveWorkRefresher( Supplier clock, int activeWorkRefreshPeriodMillis, - int stuckCommitDurationMillis, Supplier> computations, ActiveWorkRefresher.HeartbeatTracker heartbeatTracker) { return new ActiveWorkRefresher( clock, activeWorkRefreshPeriodMillis, - stuckCommitDurationMillis, computations, DataflowExecutionStateSampler.instance(), Executors.newSingleThreadScheduledExecutor(), @@ -136,8 +129,11 @@ private ExecutableWork createOldWork( Work.createProcessingContext( "computationId", new FakeGetDataClient(), ignored -> {}, heartbeatSender), false, - ActiveWorkRefresherTest::aLongTimeAgo), - processWork); + ActiveWorkRefresherTest::aLongTimeAgo, + ImmutableList.of()), + (work, handle) -> { + processWork.accept(work); + }); } @Test @@ -175,7 +171,6 @@ public void testActiveWorkRefresh() throws InterruptedException { createActiveWorkRefresher( fakeClock::now, activeWorkRefreshPeriodMillis, - 0, () -> computations, heartbeats -> heartbeatsSent::countDown); @@ -233,7 +228,6 @@ public void testEmptyActiveWorkRefresh() throws InterruptedException { createActiveWorkRefresher( fakeClock::now, activeWorkRefreshPeriodMillis, - 0, () -> computations, heartbeats -> heartbeatsSent::countDown); @@ -243,64 +237,6 @@ public void testEmptyActiveWorkRefresh() throws InterruptedException { activeWorkRefresher.stop(); } - @Test - public void testInvalidateStuckCommits() throws InterruptedException { - int stuckCommitDurationMillis = 100; - Table computations = - HashBasedTable.create(); - WindmillStateCache stateCache = WindmillStateCache.builder().setSizeMb(100).build(); - ByteString key = ByteString.EMPTY; - for (int i = 0; i < 5; i++) { - WindmillStateCache.ForComputation perComputationStateCache = - spy(stateCache.forComputation(COMPUTATION_ID_PREFIX + i)); - ComputationState computationState = spy(createComputationState(i, perComputationStateCache)); - ExecutableWork fakeWork = createOldWork(ShardedKey.create(key, i), i, ignored -> {}); - fakeWork.work().setState(Work.State.COMMITTING); - computationState.activateWork(fakeWork); - computations.put(computationState, fakeWork, perComputationStateCache); - } - - TestClock fakeClock = new TestClock(Instant.now()); - CountDownLatch invalidateStuckCommitRan = new CountDownLatch(computations.size()); - - // Count down the latch every time to avoid waiting/sleeping arbitrarily. - for (ComputationState computation : computations.rowKeySet()) { - doAnswer( - invocation -> { - invocation.callRealMethod(); - invalidateStuckCommitRan.countDown(); - return null; - }) - .when(computation) - .invalidateStuckCommits(any(Instant.class)); - } - - ActiveWorkRefresher activeWorkRefresher = - createActiveWorkRefresher( - fakeClock::now, - 0, - stuckCommitDurationMillis, - computations.rowMap()::keySet, - ignored -> () -> {}); - - activeWorkRefresher.start(); - fakeClock.advance(Duration.millis(stuckCommitDurationMillis)); - invalidateStuckCommitRan.await(); - activeWorkRefresher.stop(); - - for (Table.Cell cell : - computations.cellSet()) { - ComputationState computation = cell.getRowKey(); - ExecutableWork work = cell.getColumnKey(); - WindmillStateCache.ForComputation perComputationStateCache = cell.getValue(); - verify(perComputationStateCache, times(1)) - .invalidate(eq(key), eq(work.getWorkItem().getShardingKey())); - verify(computation, times(1)) - .completeWorkAndScheduleNextWorkForKey( - eq(ShardedKey.create(key, work.getWorkItem().getShardingKey())), eq(work.id())); - } - } - static class TestClock implements Clock { private Instant time; 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..a7a99e2ca5a1 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; } @@ -671,9 +678,24 @@ message WorkItemCommitRequest { reserved 6, 23; } +message MultiKeyWorkItemCommitRequest { + optional Uint128Proto key_group = 7; + + repeated WorkItemCommitRequest requests = 1; + + repeated OutputMessageBundle output_messages = 2; + + repeated PubSubMessageBundle pubsub_messages = 3; + + repeated int64 finalize_ids = 4 [packed = true]; + + reserved 6; +} + message ComputationCommitWorkRequest { required string computation_id = 1; repeated WorkItemCommitRequest requests = 2; + repeated MultiKeyWorkItemCommitRequest multi_key_requests = 3; } message CommitWorkRequest { @@ -899,6 +921,14 @@ message StreamingCommitRequestChunk { // before handing off to the WindmillHost for processing. optional int64 remaining_bytes_for_work_item = 4; optional bytes serialized_work_item_commit = 5; + + enum CommitType { + COMMIT_TYPE_UNSPECIFIED = 0; + COMMIT_TYPE_SINGLE_KEY = 1; + COMMIT_TYPE_MULTI_KEY = 2; + } + + optional CommitType commit_type = 7; } message StreamingCommitResponse { @@ -979,7 +1009,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/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/BundleCheckpointHandlers.java b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/BundleCheckpointHandlers.java index 34d952b15608..f869b047f339 100644 --- a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/BundleCheckpointHandlers.java +++ b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/BundleCheckpointHandlers.java @@ -23,12 +23,15 @@ import org.apache.beam.runners.core.StateInternalsFactory; import org.apache.beam.runners.core.StateNamespace; 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.core.TimerInternalsFactory; import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.fn.IdGenerator; import org.apache.beam.sdk.fn.IdGenerators; +import org.apache.beam.sdk.state.MapState; import org.apache.beam.sdk.state.TimeDomain; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.util.CoderUtils; @@ -48,7 +51,7 @@ public class BundleCheckpointHandlers { /** * A {@link BundleCheckpointHandler} which uses {@link * org.apache.beam.runners.core.TimerInternals.TimerData} and {@link - * org.apache.beam.sdk.state.ValueState} to reschedule {@link DelayedBundleApplication}. + * org.apache.beam.sdk.state.MapState} to reschedule {@link DelayedBundleApplication}. */ public static class StateAndTimerBundleCheckpointHandler implements BundleCheckpointHandler { @@ -82,6 +85,20 @@ private static String constructSdfCheckpointId(String id, int index) { return SDF_PREFIX + ":" + id + ":" + index; } + /** The state id under which all SDF self-checkpoint residuals for a key/window are stored. */ + public static final String SDF_RESIDUAL_STATE = "sdf_checkpoint_residuals"; + + /** + * The single, stable state tag holding every SDF self-checkpoint residual for a key/window, + * keyed by the per-residual checkpoint id. Storing all residuals under one stable descriptor, + * instead of a fresh {@link StateTags#value} per residual, keeps a polling SDF's keyed state + * bounded. See https://github.com/apache/beam/issues/27648. + */ + public static StateTag>> residualStateTag( + Coder> residualCoder) { + return StateTags.map(SDF_RESIDUAL_STATE, StringUtf8Coder.of(), residualCoder); + } + @Override public void onCheckpoint(ProcessBundleResponse response) { String id = idGenerator.getId(); @@ -126,8 +143,9 @@ public void onCheckpoint(ProcessBundleResponse response) { Instant.ofEpochMilli(outputTimestamp), TimeDomain.PROCESSING_TIME); stateInternals - .state(stateNamespace, StateTags.value(tag, residualCoder)) - .write( + .state(stateNamespace, residualStateTag(residualCoder)) + .put( + tag, WindowedValues.of( stateValue.getValue(), stateValue.getTimestamp(), 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/ServerFactoryTest.java b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/ServerFactoryTest.java index f89d0e00bd3b..5d5bb6657ebe 100644 --- a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/ServerFactoryTest.java +++ b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/ServerFactoryTest.java @@ -31,6 +31,7 @@ import java.net.Inet4Address; import java.net.InetAddress; +import java.net.ServerSocket; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.CountDownLatch; @@ -99,9 +100,14 @@ public void usesUrlFactory() throws Exception { @Test public void defaultServerWithPortSupplier() throws Exception { + int port; + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } + int staticPort = port; Endpoints.ApiServiceDescriptor apiServiceDescriptor = runTestUsing( - ServerFactory.createWithPortSupplier(() -> 65535), + ServerFactory.createWithPortSupplier(() -> staticPort), ManagedChannelFactory.createDefault()); HostAndPort hostAndPort = HostAndPort.fromString(apiServiceDescriptor.getUrl()); assertThat( @@ -109,21 +115,26 @@ public void defaultServerWithPortSupplier() throws Exception { anyOf( equalTo(InetAddress.getLoopbackAddress().getHostName()), equalTo(InetAddress.getLoopbackAddress().getHostAddress()))); - assertThat(hostAndPort.getPort(), is(65535)); + assertThat(hostAndPort.getPort(), is(staticPort)); } @Test public void urlFactoryWithPortSupplier() throws Exception { + int port; + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } + int staticPort = port; ServerFactory serverFactory = ServerFactory.createWithUrlFactoryAndPortSupplier( - (host, port) -> "foo" + ":" + port, () -> 65535); + (host, p) -> "foo" + ":" + p, () -> staticPort); CallStreamObserver observer = TestStreams.withOnNext((Elements unused) -> {}).withOnCompleted(() -> {}).build(); TestDataService service = new TestDataService(observer); ApiServiceDescriptor.Builder descriptorBuilder = ApiServiceDescriptor.newBuilder(); grpcCleanupRule.register( serverFactory.allocateAddressAndCreate(ImmutableList.of(service), descriptorBuilder)); - assertThat(descriptorBuilder.getUrl(), is("foo:65535")); + assertThat(descriptorBuilder.getUrl(), is("foo:" + staticPort)); } @Test diff --git a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/BundleCheckpointHandlersTest.java b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/BundleCheckpointHandlersTest.java new file mode 100644 index 000000000000..24714dc498d6 --- /dev/null +++ b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/control/BundleCheckpointHandlersTest.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.fnexecution.control; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; + +import java.util.HashSet; +import java.util.Set; +import org.apache.beam.model.fnexecution.v1.BeamFnApi.BundleApplication; +import org.apache.beam.model.fnexecution.v1.BeamFnApi.DelayedBundleApplication; +import org.apache.beam.model.fnexecution.v1.BeamFnApi.ProcessBundleResponse; +import org.apache.beam.runners.core.InMemoryStateInternals; +import org.apache.beam.runners.core.InMemoryTimerInternals; +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.TimerInternalsFactory; +import org.apache.beam.runners.fnexecution.control.BundleCheckpointHandlers.StateAndTimerBundleCheckpointHandler; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.state.State; +import org.apache.beam.sdk.state.StateContext; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.util.CoderUtils; +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; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link BundleCheckpointHandlers}. */ +@RunWith(JUnit4.class) +public class BundleCheckpointHandlersTest { + + private static final Coder> RESIDUAL_CODER = + WindowedValues.getFullCoder(StringUtf8Coder.of(), GlobalWindow.Coder.INSTANCE); + + /** + * Regression test for https://github.com/apache/beam/issues/27648. + * + *

A polling SDF self-checkpoints on every poll via {@code tracker.defer_remainder()}. Each + * self-checkpoint used to store its residual under a brand new, unique state tag + * ("sdf_checkpoint:<id>:<index>"), so every poll registered another keyed-state + * descriptor and the job leaked heap without bound. All residuals for a key/window must instead + * live under a single, stable state descriptor. + */ + @Test + public void repeatedSelfCheckpointsUseBoundedStateDescriptors() throws Exception { + Set stateDescriptorIds = new HashSet<>(); + StateInternalsFactory stateInternalsFactory = + key -> new RecordingStateInternals(InMemoryStateInternals.forKey(key), stateDescriptorIds); + TimerInternalsFactory timerInternalsFactory = key -> new InMemoryTimerInternals(); + + StateAndTimerBundleCheckpointHandler handler = + new StateAndTimerBundleCheckpointHandler<>( + timerInternalsFactory, + stateInternalsFactory, + RESIDUAL_CODER, + GlobalWindow.Coder.INSTANCE); + + int selfCheckpoints = 100; + for (int i = 0; i < selfCheckpoints; i++) { + handler.onCheckpoint(residualResponse("key")); + } + + // One stable descriptor for all residuals, no matter how many times the SDF checkpointed. + assertThat(stateDescriptorIds, hasSize(1)); + } + + private static ProcessBundleResponse residualResponse(String key) throws Exception { + // The residual element's value is used as the SDF key. + byte[] encodedElement = + CoderUtils.encodeToByteArray(RESIDUAL_CODER, WindowedValues.valueInGlobalWindow(key)); + return ProcessBundleResponse.newBuilder() + .addResidualRoots( + DelayedBundleApplication.newBuilder() + .setApplication( + BundleApplication.newBuilder() + .setElement(ByteString.copyFrom(encodedElement)) + .build()) + .build()) + .build(); + } + + /** A {@link StateInternals} that records every state descriptor id it is asked for. */ + private static class RecordingStateInternals implements StateInternals { + private final StateInternals delegate; + private final Set descriptorIds; + + RecordingStateInternals(StateInternals delegate, Set descriptorIds) { + this.delegate = delegate; + this.descriptorIds = descriptorIds; + } + + @Override + public Object getKey() { + return delegate.getKey(); + } + + @Override + public T state( + StateNamespace namespace, StateTag address, StateContext c) { + descriptorIds.add(address.getId()); + return delegate.state(namespace, address, c); + } + } +} 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/runners/java-job-service/src/main/java/org/apache/beam/runners/jobsubmission/JobInvocation.java b/runners/java-job-service/src/main/java/org/apache/beam/runners/jobsubmission/JobInvocation.java index 9da3bf38e575..3bd0a1311b04 100644 --- a/runners/java-job-service/src/main/java/org/apache/beam/runners/jobsubmission/JobInvocation.java +++ b/runners/java-job-service/src/main/java/org/apache/beam/runners/jobsubmission/JobInvocation.java @@ -116,6 +116,12 @@ public void onSuccess(PortablePipelineResult pipelineResult) { case RUNNING: setState(JobState.Enum.RUNNING); break; + case DRAINING: + setState(JobState.Enum.DRAINING); + break; + case DRAINED: + setState(JobState.Enum.DRAINED); + break; case CANCELLED: setState(JobState.Enum.CANCELLED); break; @@ -169,9 +175,12 @@ public synchronized void cancel() { new FutureCallback() { @Override public void onSuccess(PortablePipelineResult pipelineResult) { - // Do not cancel when we are already done. - if (pipelineResult != null - && pipelineResult.getState() != PipelineResult.State.DONE) { + // Do not cancel when the runner has already successfully finished. + if (pipelineResult != null) { + PipelineResult.State state = pipelineResult.getState(); + if (state == PipelineResult.State.DONE || state == PipelineResult.State.DRAINED) { + return; + } try { pipelineResult.cancel(); setState(JobState.Enum.CANCELLED); diff --git a/runners/java-job-service/src/test/java/org/apache/beam/runners/jobsubmission/JobInvocationTest.java b/runners/java-job-service/src/test/java/org/apache/beam/runners/jobsubmission/JobInvocationTest.java index 31380ace43c3..7100e57717be 100644 --- a/runners/java-job-service/src/test/java/org/apache/beam/runners/jobsubmission/JobInvocationTest.java +++ b/runners/java-job-service/src/test/java/org/apache/beam/runners/jobsubmission/JobInvocationTest.java @@ -80,6 +80,28 @@ public void testStateAfterCompletion() throws Exception { awaitJobState(jobInvocation, JobApi.JobState.Enum.DONE); } + @Test(timeout = 10_000) + public void testStateAfterDrainCompleted() throws Exception { + jobInvocation.start(); + assertThat(jobInvocation.getState(), is(JobApi.JobState.Enum.RUNNING)); + + TestPipelineResult pipelineResult = new TestPipelineResult(PipelineResult.State.DRAINED); + runner.setResult(pipelineResult); + + awaitJobState(jobInvocation, JobApi.JobState.Enum.DRAINED); + } + + @Test(timeout = 10_000) + public void testStateAfterDrainStarted() throws Exception { + jobInvocation.start(); + assertThat(jobInvocation.getState(), is(JobApi.JobState.Enum.RUNNING)); + + TestPipelineResult pipelineResult = new TestPipelineResult(PipelineResult.State.DRAINING); + runner.setResult(pipelineResult); + + awaitJobState(jobInvocation, JobApi.JobState.Enum.DRAINING); + } + @Test(timeout = 10_000) public void testStateAfterCompletionWithoutResult() throws Exception { jobInvocation.start(); @@ -128,6 +150,20 @@ public void testNoCancellationWhenDone() throws Exception { assertThat(pipelineResult.cancelLatch.getCount(), is(1L)); } + @Test(timeout = 10_000) + public void testNoCancellationWhenDrained() throws Exception { + jobInvocation.start(); + assertThat(jobInvocation.getState(), is(JobApi.JobState.Enum.RUNNING)); + + TestPipelineResult pipelineResult = new TestPipelineResult(PipelineResult.State.DRAINED); + runner.setResult(pipelineResult); + awaitJobState(jobInvocation, JobApi.JobState.Enum.DRAINED); + + jobInvocation.cancel(); + assertThat(jobInvocation.getState(), is(JobApi.JobState.Enum.DRAINED)); + assertThat(pipelineResult.cancelLatch.getCount(), is(1L)); + } + @Test(timeout = 10_000) public void testReturnsMetricsFromJobInvocationAfterSuccess() throws Exception { JobApi.MetricResults expectedMonitoringInfos = JobApi.MetricResults.newBuilder().build(); diff --git a/runners/portability/java/src/main/java/org/apache/beam/runners/portability/JobServicePipelineResult.java b/runners/portability/java/src/main/java/org/apache/beam/runners/portability/JobServicePipelineResult.java index 0f3d3b6d1910..7c72a1039c0e 100644 --- a/runners/portability/java/src/main/java/org/apache/beam/runners/portability/JobServicePipelineResult.java +++ b/runners/portability/java/src/main/java/org/apache/beam/runners/portability/JobServicePipelineResult.java @@ -160,7 +160,7 @@ private void waitForTerminalState() { } private void propagateErrors() { - if (terminalState != State.DONE) { + if (terminalState != State.DONE && terminalState != State.DRAINED) { JobMessagesRequest messageStreamRequest = JobMessagesRequest.newBuilder().setJobIdBytes(jobId).build(); Iterator messageStreamIterator = @@ -196,10 +196,9 @@ private static State getJavaState(JobApi.JobState.Enum protoState) { case UPDATED: return State.UPDATED; case DRAINING: - // TODO: Determine the correct mappings for the states below. - return State.UNKNOWN; + return State.DRAINING; case DRAINED: - return State.UNKNOWN; + return State.DRAINED; case STARTING: return State.RUNNING; case CANCELLING: diff --git a/runners/portability/java/src/test/java/org/apache/beam/runners/portability/PortableRunnerTest.java b/runners/portability/java/src/test/java/org/apache/beam/runners/portability/PortableRunnerTest.java index 68f3f6eae396..068d70b0d250 100644 --- a/runners/portability/java/src/test/java/org/apache/beam/runners/portability/PortableRunnerTest.java +++ b/runners/portability/java/src/test/java/org/apache/beam/runners/portability/PortableRunnerTest.java @@ -109,6 +109,27 @@ public void stagesAndRunsJob() throws Exception { assertThat(state, is(State.DONE)); } + @Test + public void mapsDrainingJobState() throws Exception { + createJobServer(JobState.Enum.DRAINING, JobApi.MetricResults.getDefaultInstance()); + PortableRunner runner = PortableRunner.create(options, ManagedChannelFactory.createInProcess()); + PipelineResult result = runner.run(p); + + try { + assertThat(result.getState(), is(State.DRAINING)); + } finally { + ((AutoCloseable) result).close(); + } + } + + @Test + public void mapsDrainedJobState() throws Exception { + createJobServer(JobState.Enum.DRAINED, JobApi.MetricResults.getDefaultInstance()); + PortableRunner runner = PortableRunner.create(options, ManagedChannelFactory.createInProcess()); + State state = runner.run(p).waitUntilFinish(); + assertThat(state, is(State.DRAINED)); + } + @Test public void extractsMetrics() throws Exception { JobApi.MetricResults metricResults = generateMetricResults(); diff --git a/runners/portability/test_pipeline_jar.sh b/runners/portability/test_pipeline_jar.sh index f01d67b6580c..871a604e319f 100755 --- a/runners/portability/test_pipeline_jar.sh +++ b/runners/portability/test_pipeline_jar.sh @@ -123,7 +123,16 @@ OUTPUT_JAR="test-pipeline-${RUNNER}-$(date +%Y%m%d-%H%M%S).jar" if [[ "$TEST_EXIT_CODE" -eq 0 ]]; then # Execute the jar - java -jar $OUTPUT_JAR || TEST_EXIT_CODE=$? + JAVA_ARGS=() + if [[ "$RUNNER" = "SparkRunner" ]]; then + JAVA_ARGS+=( + "--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" + ) + fi + java "${JAVA_ARGS[@]}" -jar $OUTPUT_JAR || TEST_EXIT_CODE=$? fi rm -rf $ENV_DIR diff --git a/runners/prism/java/build.gradle b/runners/prism/java/build.gradle index c89974cb6ea5..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', @@ -185,6 +188,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/runners/spark/3/build.gradle b/runners/spark/3/build.gradle index c2c492a6b2e9..274fa45d2f75 100644 --- a/runners/spark/3/build.gradle +++ b/runners/spark/3/build.gradle @@ -88,3 +88,9 @@ tasks.register("sparkVersionsTest") { group = "Verification" dependsOn sparkVersions.collect{k,v -> "sparkVersion${k}Test"} } + +// Spark 3 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/spark/3/job-server/build.gradle b/runners/spark/3/job-server/build.gradle index 68bb8d9a10e1..59165e80be58 100644 --- a/runners/spark/3/job-server/build.gradle +++ b/runners/spark/3/job-server/build.gradle @@ -28,4 +28,10 @@ project.ext { } // Load the main build script which contains all build logic. -apply from: "$basePath/spark_job_server.gradle" \ No newline at end of file +apply from: "$basePath/spark_job_server.gradle" + +// Spark 3 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/spark/4/build.gradle b/runners/spark/4/build.gradle index ec1af8df38a4..f2746b06158e 100644 --- a/runners/spark/4/build.gradle +++ b/runners/spark/4/build.gradle @@ -61,3 +61,8 @@ tasks.named("copyTestSourceOverrides") { exclude "**/translation/streaming/**" } +// Spark 4 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/spark/4/job-server/build.gradle b/runners/spark/4/job-server/build.gradle index 598cf3b4913a..3154d92ea325 100644 --- a/runners/spark/4/job-server/build.gradle +++ b/runners/spark/4/job-server/build.gradle @@ -29,3 +29,9 @@ project.ext { // Load the main build script which contains all build logic. apply from: "$basePath/spark_job_server.gradle" + +// Spark 4 uses org.lz4:lz4-java instead of at.yawk.lz4:lz4-java +// Explicitly prefer at.yawk.lz4 candidates to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} diff --git a/runners/spark/job-server/spark_job_server.gradle b/runners/spark/job-server/spark_job_server.gradle index 500a8ba865b1..2811f875f84f 100644 --- a/runners/spark/job-server/spark_job_server.gradle +++ b/runners/spark/job-server/spark_job_server.gradle @@ -199,10 +199,11 @@ def portableValidatesRunnerTask(String name, boolean streaming, boolean docker, excludeCategories 'org.apache.beam.sdk.testing.UsesKeyInParDo' excludeCategories 'org.apache.beam.sdk.testing.UsesOnWindowExpiration' excludeCategories 'org.apache.beam.sdk.testing.UsesTestStream' - // TODO (https://github.com/apache/beam/issues/19468) SplittableDoFnTests - excludeCategories 'org.apache.beam.sdk.testing.UsesBoundedSplittableParDo' + // TODO (https://github.com/apache/beam/issues/19468) unbounded SDF needs residuals to + // survive across micro-batches, which the streaming path cannot do yet. excludeCategories 'org.apache.beam.sdk.testing.UsesUnboundedSplittableParDo' excludeCategories 'org.apache.beam.sdk.testing.UsesStrictTimerOrdering' + // TODO (https://github.com/apache/beam/issues/19517) bundle finalization excludeCategories 'org.apache.beam.sdk.testing.UsesBundleFinalizer' } testFilter = { @@ -210,6 +211,8 @@ def portableValidatesRunnerTask(String name, boolean streaming, boolean docker, excludeTestsMatching 'org.apache.beam.sdk.transforms.FlattenTest.testFlattenWithDifferentInputAndOutputCoders2' // TODO(https://github.com/apache/beam/issues/31231) excludeTestsMatching 'org.apache.beam.sdk.transforms.RedistributeTest.testRedistributePreservesMetadata' + // Spark does not support side inputs in timers (added in #38363) + excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$StateTests.testSideInputNotReadyTimer' for (String test : sickbayTests) { excludeTestsMatching test } @@ -265,13 +268,35 @@ tasks.register("validatesRunnerSickbay", Test) { def jobPort = BeamModulePlugin.getRandomPort() def artifactPort = BeamModulePlugin.getRandomPort() +def sparkJobServerJvmArgs() { + def testJavaVer = project.findProperty('testJavaVersion') ? (project.property('testJavaVersion') as int) : JavaVersion.current().majorVersion.toInteger() + if (testJavaVer >= 17) { + return [ + "--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" + ] + } + return [] +} + +// TestPortableRunner starts SparkJobServerDriver in-process in the test JVM. +['validatesPortableRunnerDocker', 'validatesPortableRunnerBatch', 'validatesPortableRunnerStreaming'].each { taskName -> + tasks.named(taskName) { + jvmArgs += sparkJobServerJvmArgs() + } +} + def setupTask = project.tasks.register("sparkJobServerSetup", Exec) { dependsOn shadowJar def pythonDir = project.project(":sdks:python").projectDir def sparkJobServerJar = shadowJar.archivePath + def jvmArgs = sparkJobServerJvmArgs().join(' ') + def jvmArgsOpt = jvmArgs ? "--jvm_args \"${jvmArgs}\"" : "" 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 ${sparkJobServerJar}" + 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 ${sparkJobServerJar} ${jvmArgsOpt}" } def cleanupTask = project.tasks.register("sparkJobServerCleanup", Exec) { diff --git a/runners/spark/spark_runner.gradle b/runners/spark/spark_runner.gradle index cd108372fea5..77da3d36db92 100644 --- a/runners/spark/spark_runner.gradle +++ b/runners/spark/spark_runner.gradle @@ -77,9 +77,8 @@ def sparkTestProperties(overrides = [:]) { def sparkTestJvmArgs() { - // run tests with Java 17 using -PtestJavaVersion=17 -Pjava17Home=??? - if (project.hasProperty('testJavaVersion') && - project.getProperty('testJavaVersion') in ['17', '21']) { + def testJavaVer = project.findProperty('testJavaVersion') ? (project.property('testJavaVersion') as int) : JavaVersion.current().majorVersion.toInteger() + if (testJavaVer >= 17) { return [ "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", // add-opens below required for Kryo FieldSerializer / SparkRunnerKryoRegistratorTest @@ -288,10 +287,9 @@ dependencies { testImplementation project(path: ":sdks:java:extensions:avro", configuration: "testRuntimeMigration") testImplementation project(":sdks:java:harness") testImplementation library.java.avro - // 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 (spark_scala_version == '2.13' ? library.java.kafka_scala_2_13 : library.java.kafka_scala_2_12) testImplementation library.java.kafka_clients + testImplementation library.java.kafka_server testImplementation library.java.junit testImplementation library.java.mockito_core testImplementation "org.assertj:assertj-core:3.11.1" @@ -406,6 +404,8 @@ def validatesRunnerBatch = tasks.register("validatesRunnerBatch", Test) { excludeTestsMatching 'org.apache.beam.sdk.transforms.RedistributeTest.testRedistributePreservesMetadata' // TODO(https://github.com/apache/beam/issues/32021) excludeTestsMatching 'org.apache.beam.sdk.metrics.MetricsTest$AttemptedMetricTests.testBoundedSourceMetricsInSplit' + // Spark does not support side inputs in timers (added in #38363) + excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$StateTests.testSideInputNotReadyTimer' } } 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 99ce3dc69889..5e8703a05b06 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 @@ -49,6 +49,7 @@ 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; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** @@ -284,6 +285,9 @@ public void finishBundle() { } } + @Override + public void finishKey(KeyT key) {} + @Override public DoFn getFn() { throw new UnsupportedOperationException(); diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/DoFnRunnerWithMetrics.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/DoFnRunnerWithMetrics.java index 28dbf44cb8fe..7202de0f0aa8 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/DoFnRunnerWithMetrics.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/DoFnRunnerWithMetrics.java @@ -30,6 +30,7 @@ import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.WindowedValue; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** DoFnRunner decorator which registers {@link MetricsContainer}. */ @@ -104,6 +105,9 @@ public void finishBundle() { } } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { delegate.onWindowExpiration(window, timestamp, key); diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/DoFnRunnerWithMetrics.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/DoFnRunnerWithMetrics.java index c8cd7eb5f262..bc434b2117de 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/DoFnRunnerWithMetrics.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/DoFnRunnerWithMetrics.java @@ -29,6 +29,7 @@ import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.WindowedValue; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; /** DoFnRunner decorator which registers {@link MetricsContainerImpl}. */ @@ -103,6 +104,9 @@ public void finishBundle() { } } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { delegate.onWindowExpiration(window, timestamp, key); diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkBatchPortablePipelineTranslator.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkBatchPortablePipelineTranslator.java index ba3aa0e4d24a..521f95835978 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkBatchPortablePipelineTranslator.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkBatchPortablePipelineTranslator.java @@ -262,7 +262,9 @@ private static void translateExecutableStage( SparkExecutableStageContextFactory.getInstance(), broadcastVariables, MetricsAccumulator.getInstance(), - windowCoder); + windowCoder, + getWindowedValueCoder(inputPCollectionId, components), + true); staged = groupedByKey.flatMap(function.forPair()); } else { JavaRDD> inputRdd2 = ((BoundedDataset) inputDataset).getRDD(); @@ -275,7 +277,9 @@ private static void translateExecutableStage( SparkExecutableStageContextFactory.getInstance(), broadcastVariables, MetricsAccumulator.getInstance(), - windowCoder); + windowCoder, + getWindowedValueCoder(inputPCollectionId, components), + true); staged = inputRdd2.mapPartitions(function2); } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunction.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunction.java index 757740e2df5a..a04899bf9c9f 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunction.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunction.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.Iterator; @@ -32,10 +33,15 @@ import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateKey; import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateKey.TypeCase; import org.apache.beam.model.pipeline.v1.RunnerApi; +import org.apache.beam.runners.core.InMemoryStateInternals; import org.apache.beam.runners.core.InMemoryTimerInternals; +import org.apache.beam.runners.core.StateInternals; import org.apache.beam.runners.core.TimerInternals; import org.apache.beam.runners.core.construction.SerializablePipelineOptions; import org.apache.beam.runners.core.metrics.MetricsContainerImpl; +import org.apache.beam.runners.fnexecution.control.BundleCheckpointHandler; +import org.apache.beam.runners.fnexecution.control.BundleCheckpointHandlers; +import org.apache.beam.runners.fnexecution.control.BundleFinalizationHandler; import org.apache.beam.runners.fnexecution.control.BundleProgressHandler; import org.apache.beam.runners.fnexecution.control.ExecutableStageContext; import org.apache.beam.runners.fnexecution.control.JobBundleFactory; @@ -57,8 +63,10 @@ import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.fn.data.FnDataReceiver; import org.apache.beam.sdk.io.FileSystems; +import org.apache.beam.sdk.state.MapState; import org.apache.beam.sdk.transforms.join.RawUnionValue; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.util.construction.PTransformTranslation; import org.apache.beam.sdk.util.construction.Timer; import org.apache.beam.sdk.util.construction.graph.ExecutableStage; import org.apache.beam.sdk.values.WindowedValue; @@ -95,10 +103,16 @@ class SparkExecutableStageFunction sideInputs; private final MetricsContainerStepMapAccumulator metricsAccumulator; private final Coder windowCoder; + // Coder for this stage's input, used to hold and replay splittable DoFn residuals. + private final Coder> inputCoder; + // Batch replays residuals in place. Streaming has nowhere to hold them across micro-batches yet. + private final boolean batch; private final JobInfo jobInfo; private transient InMemoryBagUserStateFactory bagUserStateHandlerFactory; private transient Object currentTimerKey; + private transient InMemoryTimerInternals sdfTimerInternals; + private transient StateInternals sdfStateInternals; SparkExecutableStageFunction( SerializablePipelineOptions pipelineOptions, @@ -108,7 +122,9 @@ class SparkExecutableStageFunction SparkExecutableStageContextFactory contextFactory, Map>, WindowedValueCoder>> sideInputs, MetricsContainerStepMapAccumulator metricsAccumulator, - Coder windowCoder) { + Coder windowCoder, + Coder> inputCoder, + boolean batch) { this.pipelineOptions = pipelineOptions; this.stagePayload = stagePayload; this.jobInfo = jobInfo; @@ -117,6 +133,8 @@ class SparkExecutableStageFunction this.sideInputs = sideInputs; this.metricsAccumulator = metricsAccumulator; this.windowCoder = windowCoder; + this.inputCoder = inputCoder; + this.batch = batch; } /** Call the executable stage function on the values of a PairRDD, ignoring the key. */ @@ -144,9 +162,18 @@ public Iterator call(Iterator> inputs) thro StateRequestHandler stateRequestHandler = getStateRequestHandler( executableStage, stageBundleFactory.getProcessBundleDescriptor()); + BundleCheckpointHandler checkpointHandler = getBundleCheckpointHandler(executableStage); if (executableStage.getTimers().size() == 0) { ReceiverFactory receiverFactory = new ReceiverFactory(collector, outputMap); - processElements(stateRequestHandler, receiverFactory, null, stageBundleFactory, inputs); + processElements( + stateRequestHandler, + receiverFactory, + null, + stageBundleFactory, + inputs, + checkpointHandler); + replaySdfResiduals( + stateRequestHandler, receiverFactory, null, stageBundleFactory, checkpointHandler); return collector.iterator(); } // Used with Batch, we know that all the data is available for this key. We can't use the @@ -173,7 +200,12 @@ public Iterator call(Iterator> inputs) thro // Process inputs. processElements( - stateRequestHandler, receiverFactory, timerReceiverFactory, stageBundleFactory, inputs); + stateRequestHandler, + receiverFactory, + timerReceiverFactory, + stageBundleFactory, + inputs, + checkpointHandler); // Finish any pending windows by advancing the input watermark to infinity. timerInternals.advanceInputWatermark(BoundedWindow.TIMESTAMP_MAX_VALUE); @@ -182,19 +214,30 @@ public Iterator call(Iterator> inputs) thro timerInternals.advanceSynchronizedProcessingTime(BoundedWindow.TIMESTAMP_MAX_VALUE); // Now we fire the timers and process elements generated by timers (which may be timers - // itself) - while (timerInternals.hasPendingTimers()) { - try (RemoteBundle bundle = - stageBundleFactory.getBundle( - receiverFactory, - timerReceiverFactory, - stateRequestHandler, - getBundleProgressHandler())) { + // itself). A replayed splittable DoFn residual can set a timer, and a fired timer can + // produce a residual, so alternate until neither has anything left. + do { + while (timerInternals.hasPendingTimers()) { + try (RemoteBundle bundle = + stageBundleFactory.getBundle( + receiverFactory, + timerReceiverFactory, + stateRequestHandler, + getBundleProgressHandler(), + getBundleFinalizationHandler(), + checkpointHandler)) { - PipelineTranslatorUtils.fireEligibleTimers( - timerInternals, bundle.getTimerReceivers(), currentTimerKey); + PipelineTranslatorUtils.fireEligibleTimers( + timerInternals, bundle.getTimerReceivers(), currentTimerKey); + } } - } + replaySdfResiduals( + stateRequestHandler, + receiverFactory, + timerReceiverFactory, + stageBundleFactory, + checkpointHandler); + } while (timerInternals.hasPendingTimers()); return collector.iterator(); } } @@ -207,14 +250,17 @@ private void processElements( ReceiverFactory receiverFactory, TimerReceiverFactory timerReceiverFactory, StageBundleFactory stageBundleFactory, - Iterator> inputs) + Iterator> inputs, + BundleCheckpointHandler checkpointHandler) throws Exception { try (RemoteBundle bundle = stageBundleFactory.getBundle( receiverFactory, timerReceiverFactory, stateRequestHandler, - getBundleProgressHandler())) { + getBundleProgressHandler(), + getBundleFinalizationHandler(), + checkpointHandler)) { FnDataReceiver> mainReceiver = Iterables.getOnlyElement(bundle.getInputReceivers().values()); while (inputs.hasNext()) { @@ -224,6 +270,100 @@ private void processElements( } } + private static boolean hasSdf(ExecutableStage executableStage) { + return executableStage.getTransforms().stream() + .anyMatch( + transform -> + transform + .getTransform() + .getSpec() + .getUrn() + .equals( + PTransformTranslation + .SPLITTABLE_PROCESS_SIZED_ELEMENTS_AND_RESTRICTIONS_URN)); + } + + // Holds a splittable DoFn's self-checkpoint residual in memory under a processing time timer, so + // it can be replayed once this stage has drained its inputs. + private BundleCheckpointHandler getBundleCheckpointHandler(ExecutableStage executableStage) { + sdfTimerInternals = null; + sdfStateInternals = null; + if (!batch) { + return response -> { + throw new UnsupportedOperationException( + "Splittable DoFn self-checkpointing is not supported on the portable Spark runner in " + + "streaming mode. For more details, please refer to " + + "https://github.com/apache/beam/issues/19468."); + }; + } + if (!hasSdf(executableStage)) { + return response -> { + throw new UnsupportedOperationException( + "Self-checkpoint is only supported on splittable DoFn."); + }; + } + sdfTimerInternals = new InMemoryTimerInternals(); + sdfStateInternals = InMemoryStateInternals.forKey("sdf_state"); + return new BundleCheckpointHandlers.StateAndTimerBundleCheckpointHandler<>( + key -> sdfTimerInternals, key -> sdfStateInternals, inputCoder, windowCoder); + } + + // Bundle finalization needs the runner to have durably committed the bundle's output first, which + // this runner cannot report, so it is rejected rather than silently finalized early. + private BundleFinalizationHandler getBundleFinalizationHandler() { + return bundleId -> { + throw new UnsupportedOperationException( + "The portable Spark runner does not support bundle finalization. For more details, please " + + "refer to https://github.com/apache/beam/issues/19517."); + }; + } + + // Replays held residuals until the splittable DoFn stops asking to resume. Processing time is at + // infinity, so every residual is due immediately and a bounded restriction always runs out. + private void replaySdfResiduals( + StateRequestHandler stateRequestHandler, + ReceiverFactory receiverFactory, + TimerReceiverFactory timerReceiverFactory, + StageBundleFactory stageBundleFactory, + BundleCheckpointHandler checkpointHandler) + throws Exception { + if (sdfTimerInternals == null) { + return; + } + sdfTimerInternals.advanceProcessingTime(BoundedWindow.TIMESTAMP_MAX_VALUE); + sdfTimerInternals.advanceSynchronizedProcessingTime(BoundedWindow.TIMESTAMP_MAX_VALUE); + while (sdfTimerInternals.hasPendingTimers()) { + try (RemoteBundle bundle = + stageBundleFactory.getBundle( + receiverFactory, + timerReceiverFactory, + stateRequestHandler, + getBundleProgressHandler(), + getBundleFinalizationHandler(), + checkpointHandler)) { + List> residuals = new ArrayList<>(); + TimerInternals.TimerData timer; + while ((timer = sdfTimerInternals.removeNextProcessingTimer()) != null) { + MapState> residualState = + sdfStateInternals.state( + timer.getNamespace(), + BundleCheckpointHandlers.StateAndTimerBundleCheckpointHandler.residualStateTag( + inputCoder)); + WindowedValue residual = residualState.get(timer.getTimerId()).read(); + residualState.remove(timer.getTimerId()); + if (residual != null) { + residuals.add(residual); + } + } + FnDataReceiver> mainReceiver = + Iterables.getOnlyElement(bundle.getInputReceivers().values()); + for (WindowedValue residual : residuals) { + mainReceiver.accept(residual); + } + } + } + } + private BundleProgressHandler getBundleProgressHandler() { String stageName = stagePayload.getInput(); MetricsContainerImpl container = metricsAccumulator.value().getContainer(stageName); 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 9975c81b56a4..db3551454dcd 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 @@ -251,7 +251,9 @@ private static void translateExecutableStage( SparkExecutableStageContextFactory.getInstance(), broadcastVariables, MetricsAccumulator.getInstance(), - windowCoder); + windowCoder, + getWindowedValueCoder(inputPCollectionId, components), + false); JavaDStream staged = inputDStream.mapPartitions(function); String intermediateId = getExecutableStageIntermediateId(transformNode); diff --git a/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunctionTest.java b/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunctionTest.java index 98601389f5c9..31ba649cfff9 100644 --- a/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunctionTest.java +++ b/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkExecutableStageFunctionTest.java @@ -18,6 +18,7 @@ package org.apache.beam.runners.spark.translation; import static org.apache.beam.sdk.util.construction.PTransformTranslation.PAR_DO_TRANSFORM_URN; +import static org.apache.beam.sdk.util.construction.PTransformTranslation.SPLITTABLE_PROCESS_SIZED_ELEMENTS_AND_RESTRICTIONS_URN; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.mockito.ArgumentMatchers.any; @@ -33,6 +34,9 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import org.apache.beam.model.fnexecution.v1.BeamFnApi.BundleApplication; +import org.apache.beam.model.fnexecution.v1.BeamFnApi.DelayedBundleApplication; +import org.apache.beam.model.fnexecution.v1.BeamFnApi.ProcessBundleResponse; import org.apache.beam.model.pipeline.v1.RunnerApi; import org.apache.beam.model.pipeline.v1.RunnerApi.Components; import org.apache.beam.model.pipeline.v1.RunnerApi.ExecutableStagePayload; @@ -52,13 +56,18 @@ import org.apache.beam.runners.fnexecution.control.TimerReceiverFactory; import org.apache.beam.runners.fnexecution.state.StateRequestHandler; import org.apache.beam.runners.spark.metrics.MetricsContainerStepMapAccumulator; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.VarIntCoder; import org.apache.beam.sdk.fn.data.FnDataReceiver; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.join.RawUnionValue; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.util.CoderUtils; import org.apache.beam.sdk.util.construction.Timer; 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.grpc.v1p69p0.com.google.protobuf.ByteString; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.junit.Before; import org.junit.Test; @@ -97,12 +106,31 @@ public class SparkExecutableStageFunctionTest { .build()) .build(); + private final ExecutableStagePayload sdfStagePayload = + ExecutableStagePayload.newBuilder() + .setInput(inputId) + .addTransforms("sdf-transform-id") + .setComponents( + Components.newBuilder() + .putTransforms( + "sdf-transform-id", + RunnerApi.PTransform.newBuilder() + .putInputs("input-name", inputId) + .setSpec( + RunnerApi.FunctionSpec.newBuilder() + .setUrn(SPLITTABLE_PROCESS_SIZED_ELEMENTS_AND_RESTRICTIONS_URN)) + .build()) + .putPcollections(inputId, PCollection.getDefaultInstance()) + .build()) + .build(); + @Before public void setUpMocks() throws Exception { MockitoAnnotations.initMocks(this); when(contextFactory.get(any())).thenReturn(stageContext); when(stageContext.getStageBundleFactory(any())).thenReturn(stageBundleFactory); - when(stageBundleFactory.getBundle(any(), any(), any(), any(BundleProgressHandler.class))) + when(stageBundleFactory.getBundle( + any(), any(), any(), any(BundleProgressHandler.class), any(), any())) .thenReturn(remoteBundle); @SuppressWarnings("unchecked") ImmutableMap inputReceiver = @@ -126,7 +154,8 @@ public void expectedInputsAreSent() throws Exception { SparkExecutableStageFunction function = getFunction(Collections.emptyMap()); RemoteBundle bundle = Mockito.mock(RemoteBundle.class); - when(stageBundleFactory.getBundle(any(), any(), any(), any(BundleProgressHandler.class))) + when(stageBundleFactory.getBundle( + any(), any(), any(), any(BundleProgressHandler.class), any(), any())) .thenReturn(bundle); @SuppressWarnings("unchecked") @@ -247,7 +276,8 @@ public void testStageBundleClosed() throws Exception { List> inputs = new ArrayList<>(); inputs.add(WindowedValues.valueInGlobalWindow(0)); function.call(inputs.iterator()); - verify(stageBundleFactory).getBundle(any(), any(), any(), any(BundleProgressHandler.class)); + verify(stageBundleFactory) + .getBundle(any(), any(), any(), any(BundleProgressHandler.class), any(), any()); verify(stageBundleFactory).getProcessBundleDescriptor(); verify(stageBundleFactory).close(); verifyNoMoreInteractions(stageBundleFactory); @@ -260,6 +290,104 @@ public void testNoCallOnEmptyInputIterator() throws Exception { verifyNoInteractions(stageBundleFactory); } + @Test + public void sdfResidualsAreReplayedUntilDrained() throws Exception { + // A stage whose bundle self-checkpoints once: the first bundle returns a residual, the replay + // bundle returns none. + List> received = new ArrayList<>(); + WindowedValue residualValue = WindowedValues.valueInGlobalWindow(7); + Coder> residualCoder = + WindowedValues.getFullCoder(VarIntCoder.of(), GlobalWindow.Coder.INSTANCE); + ProcessBundleResponse withResidual = + ProcessBundleResponse.newBuilder() + .addResidualRoots( + DelayedBundleApplication.newBuilder() + .setApplication( + BundleApplication.newBuilder() + .setElement( + ByteString.copyFrom( + CoderUtils.encodeToByteArray(residualCoder, residualValue))))) + .build(); + + StageBundleFactory bundleFactory = + new StageBundleFactory() { + private int bundles; + + @Override + public RemoteBundle getBundle( + OutputReceiverFactory receiverFactory, + TimerReceiverFactory timerReceiverFactory, + StateRequestHandler stateRequestHandler, + BundleProgressHandler progressHandler, + BundleFinalizationHandler finalizationHandler, + BundleCheckpointHandler checkpointHandler) { + boolean checkpointThisBundle = bundles++ == 0; + return new RemoteBundle() { + @Override + public String getId() { + return "bundle-id"; + } + + @Override + public Map getInputReceivers() { + FnDataReceiver> receiver = received::add; + return ImmutableMap.of("input", receiver); + } + + @Override + public Map, FnDataReceiver> getTimerReceivers() { + return Collections.emptyMap(); + } + + @Override + public void requestProgress() {} + + @Override + public void split(double fractionOfRemainder) {} + + @Override + public void close() { + if (checkpointThisBundle) { + checkpointHandler.onCheckpoint(withResidual); + } + } + }; + } + + @Override + public ProcessBundleDescriptors.ExecutableProcessBundleDescriptor + getProcessBundleDescriptor() { + return null; + } + + @Override + public InstructionRequestHandler getInstructionRequestHandler() { + return null; + } + + @Override + public void close() {} + }; + when(stageContext.getStageBundleFactory(any())).thenReturn(bundleFactory); + + SparkExecutableStageFunction function = + new SparkExecutableStageFunction<>( + pipelineOptions, + sdfStagePayload, + null, + Collections.emptyMap(), + contextFactory, + Collections.emptyMap(), + metricsAccumulator, + GlobalWindow.Coder.INSTANCE, + residualCoder, + true); + + function.call(Collections.singletonList(WindowedValues.valueInGlobalWindow(1)).iterator()); + + assertThat(received, contains(WindowedValues.valueInGlobalWindow(1), residualValue)); + } + private SparkExecutableStageFunction getFunction( Map outputMap) { return new SparkExecutableStageFunction<>( @@ -270,6 +398,8 @@ private SparkExecutableStageFunction ge contextFactory, Collections.emptyMap(), metricsAccumulator, - null); + null, + null, + true); } } diff --git a/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkInputDataProcessorTest.java b/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkInputDataProcessorTest.java index 2dc428d5a6b2..35fe7b745ea3 100644 --- a/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkInputDataProcessorTest.java +++ b/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/SparkInputDataProcessorTest.java @@ -41,6 +41,7 @@ import org.apache.beam.sdk.values.WindowedValues; 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; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; import org.junit.Rule; import org.junit.Test; @@ -259,6 +260,9 @@ public void onTimer( @Override public void finishBundle() {} + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) {} diff --git a/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/streaming/utils/EmbeddedKafkaCluster.java b/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/streaming/utils/EmbeddedKafkaCluster.java index df5646fed590..3acbf664e2cf 100644 --- a/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/streaming/utils/EmbeddedKafkaCluster.java +++ b/runners/spark/src/test/java/org/apache/beam/runners/spark/translation/streaming/utils/EmbeddedKafkaCluster.java @@ -29,7 +29,7 @@ import java.util.Properties; import java.util.Random; import kafka.server.KafkaConfig; -import kafka.server.KafkaServerStartable; +import kafka.server.KafkaServer; import org.apache.zookeeper.server.NIOServerCnxnFactory; import org.apache.zookeeper.server.ServerCnxnFactory; import org.apache.zookeeper.server.ZooKeeperServer; @@ -47,7 +47,7 @@ public class EmbeddedKafkaCluster { private final String brokerList; - private final List brokers; + private final List brokers; private final List logDirs; private EmbeddedKafkaCluster(String zkConnection) { @@ -114,15 +114,20 @@ public void startup() { properties.setProperty("offsets.topic.replication.factor", "1"); properties.setProperty("log.flush.interval.messages", String.valueOf(1)); - KafkaServerStartable broker = startBroker(properties); + KafkaServer broker = startBroker(properties); brokers.add(broker); logDirs.add(logDir); } } - private static KafkaServerStartable startBroker(Properties props) { - KafkaServerStartable server = new KafkaServerStartable(new KafkaConfig(props)); + private static KafkaServer startBroker(Properties props) { + KafkaServer server = + new KafkaServer( + new KafkaConfig(props), + KafkaServer.$lessinit$greater$default$2(), + KafkaServer.$lessinit$greater$default$3(), + KafkaServer.$lessinit$greater$default$4()); server.startup(); return server; } @@ -148,7 +153,7 @@ public String getZkConnection() { @SuppressWarnings("Slf4jDoNotLogMessageOfExceptionExplicitly") public void shutdown() { - for (KafkaServerStartable broker : brokers) { + for (KafkaServer broker : brokers) { try { broker.shutdown(); } catch (Exception e) { diff --git a/scripts/beam-sql.sh b/scripts/beam-sql.sh index c9ec365b99bc..e0a28b1cc0ea 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.77.0" MAIN_CLASS="org.apache.beam.sdk.extensions.sql.jdbc.BeamSqlLine" # Directory to store cached executable JAR files CACHE_DIR="${HOME}/.beam/cache" @@ -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 + + 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" diff --git a/scripts/ci/pr-bot/processNewPrs.ts b/scripts/ci/pr-bot/processNewPrs.ts index 776ae8aa83a9..dd20d4a98c2b 100644 --- a/scripts/ci/pr-bot/processNewPrs.ts +++ b/scripts/ci/pr-bot/processNewPrs.ts @@ -182,6 +182,26 @@ async function processPull( reviewerConfig: typeof ReviewerConfig, stateClient: typeof PersistentState ) { + if (pull.user.login === "dependabot[bot]") { + const files = await github.getPrFiles(pull.number); + const touchesContainer = files.some((file: string) => + file.startsWith("sdks/python/container/") + ); + if (touchesContainer) { + console.log( + `Closing PR ${pull.number} because it is a dependabot PR touching container/` + ); + await github.addPrComment( + pull.number, + "Closing this PR because dependabot updates for container/** are not allowed due to generated files " + + "and excluded_paths is disabled due to dependabot/dependabot-core#14408. " + + "Once issue is resolved, please remove this step." + ); + await github.closePr(pull.number); + return; + } + } + let prState = await stateClient.getPrState(pull.number); if (!needsProcessed(pull, prState)) { return; diff --git a/scripts/ci/pr-bot/shared/githubUtils.ts b/scripts/ci/pr-bot/shared/githubUtils.ts index 667255b16924..0d287bd09379 100644 --- a/scripts/ci/pr-bot/shared/githubUtils.ts +++ b/scripts/ci/pr-bot/shared/githubUtils.ts @@ -70,6 +70,27 @@ export async function nextActionAuthor( }); } +export async function closePr(pullNumber: number) { + await getGitHubClient().rest.pulls.update({ + owner: REPO_OWNER, + repo: REPO, + pull_number: pullNumber, + state: "closed", + }); +} + +export async function getPrFiles(pullNumber: number): Promise { + const files = await getGitHubClient().paginate( + getGitHubClient().rest.pulls.listFiles, + { + owner: REPO_OWNER, + repo: REPO, + pull_number: pullNumber, + } + ); + return files.map((file: any) => file.filename); +} + export async function checkIfCommitter(username: string): Promise { const permissionLevel = ( await getGitHubClient().rest.repos.getCollaboratorPermissionLevel({ diff --git a/scripts/tools/bomupgrader.py b/scripts/tools/bomupgrader.py index bf6e7dfdd31f..715c19a899d1 100644 --- a/scripts/tools/bomupgrader.py +++ b/scripts/tools/bomupgrader.py @@ -20,6 +20,7 @@ import re import subprocess import sys +import urllib.request """ This Python script is used for upgrading the GCP-BOM in BeamModulePlugin. Specifically, it @@ -27,7 +28,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 @@ -42,6 +44,59 @@ # To format: yapf --style sdks/python/setup.cfg --in-place scripts/tools/bomupgrader.py +def get_latest_bom(): + url = "https://repo1.maven.org/maven2/com/google/cloud/libraries-bom/maven-metadata.xml" + with urllib.request.urlopen(url, timeout=15) as response: + xml = response.read().decode('utf-8') + match = re.search(r'([^<]+)', xml) + if match: + return match.group(1) + raise RuntimeError("Could not find latest release in Maven metadata") + + +def get_current_bom(): + path = "buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy" + with open(path) as f: + content = f.read() + match = re.search( + r'google_cloud_platform_libraries_bom\s*:\s*[\"\']com\.google\.cloud:libraries-bom:([0-9.]+)[\"\']', + content) + if match: + return match.group(1) + raise RuntimeError( + "Could not find current libraries-bom in BeamModulePlugin.groovy") + + +def to_tuple(version_str): + parts = [] + for part in version_str.split('.'): + match = re.match(r'^(\d+)', part) + parts.append(int(match.group(1)) if match else 0) + return tuple(parts) + + +def check_bom(): + latest = get_latest_bom() + current = get_current_bom() + print(f"Latest libraries-bom version: {latest}") + print(f"Current libraries-bom version: {current}") + + should_upgrade = to_tuple(latest) > to_tuple(current) + + github_output = os.getenv('GITHUB_OUTPUT') + if github_output: + with open(github_output, 'a') as f: + f.write(f"should_upgrade={str(should_upgrade).lower()}\n") + f.write(f"latest_version={latest}\n") + f.write(f"current_version={current}\n") + + if should_upgrade: + print("A newer version of libraries-bom is available. Upgrade needed.") + else: + print("libraries-bom is up-to-date.") + return should_upgrade, latest + + class BeamModulePluginProcessor: # Known dependencies managed by GCP BOM and also used by Beam. # We only need to have one dependency for each project to figure out the target version @@ -254,17 +309,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): @@ -278,7 +367,16 @@ def run(self): if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) if len(sys.argv) < 2: - print("Usage: python scripts/tools/bomupgrader.py target_version") + print("Usage: python scripts/tools/bomupgrader.py [--check | latest | target_version]") exit(1) - processor = BeamModulePluginProcessor(sys.argv[1]) - processor.run() + + arg = sys.argv[1] + if arg in ['--check', '--check-only']: + check_bom() + else: + if arg in ['latest', '--latest']: + _, target_ver = check_bom() + else: + target_ver = arg + processor = BeamModulePluginProcessor(target_ver) + processor.run() diff --git a/sdks/go.mod b/sdks/go.mod index c72969b14bbd..0b1f8eb9d44d 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -25,44 +25,44 @@ go 1.26.0 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/bigquery v1.80.0 + cloud.google.com/go/bigtable v1.52.0 + cloud.google.com/go/datastore v1.26.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/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/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 + cloud.google.com/go/pubsub v1.51.0 + cloud.google.com/go/spanner v1.94.0 + cloud.google.com/go/storage v1.64.0 + github.com/aws/aws-sdk-go-v2 v1.43.5 + github.com/aws/aws-sdk-go-v2/config v1.32.36 + github.com/aws/aws-sdk-go-v2/credentials v1.19.35 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.41 + github.com/aws/aws-sdk-go-v2/service/s3 v1.107.0 + github.com/aws/smithy-go v1.27.7 + 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 github.com/google/uuid v1.6.0 - github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 + github.com/johannesboyne/gofakes3 v1.2.0 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.4 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 - github.com/tetratelabs/wazero v1.11.0 + github.com/testcontainers/testcontainers-go v0.44.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 - golang.org/x/net v0.54.0 + golang.org/x/net v0.57.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.37.0 - google.golang.org/api v0.279.0 - google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 - google.golang.org/grpc v1.81.1 + golang.org/x/sync v0.22.0 + golang.org/x/sys v0.47.0 + golang.org/x/text v0.40.0 + google.golang.org/api v0.293.0 + google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 + google.golang.org/grpc v1.83.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -70,144 +70,138 @@ 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 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 + github.com/fsouza/fake-gcs-server v1.55.1 + github.com/golang-cz/devslog v0.0.17 + github.com/moby/moby/api v1.55.0 + github.com/moby/moby/client v0.5.1 + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a ) require ( - cel.dev/expr v0.25.1 // indirect - cloud.google.com/go/auth v0.20.0 // indirect + cel.dev/expr v0.25.2 // indirect + cloud.google.com/go/auth v0.23.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.1 // 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/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.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.2-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.5.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 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.10.0 // indirect + github.com/ebitengine/purego v0.10.1 // 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 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 - 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 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.26.3 // indirect - github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.6 // indirect + github.com/spiffe/go-spiffe/v2 v2.7.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/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.opentelemetry.io/contrib/detectors/gcp v1.44.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.69.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-20260409153401-be6f6cb8b1fa // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // 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/longrunning v0.9.0 // indirect + cloud.google.com/go/iam v1.11.0 // indirect + cloud.google.com/go/longrunning v1.2.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 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/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/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/sts v1.42.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.37 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.36 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.45.5 // 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 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/felixge/httpsnoop v1.1.0 // 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/gax-go/v2 v2.22.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect + github.com/googleapis/gax-go/v2 v2.23.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.5 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/compress v1.19.0 // 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/sys/sequential v0.7.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/pkg/xattr v0.4.10 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/pkg/xattr v0.4.12 // 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.54.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/tools v0.47.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/api v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index 4290813b294a..a3676bc030c3 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= @@ -34,8 +34,8 @@ cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= 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/auth v0.23.0 h1:6Gg1CMgpgubRG7DGz5Vf1pcoNo8RfiRiRAPS4crTp54= +cloud.google.com/go/auth v0.23.0/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0= 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/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -44,40 +44,40 @@ 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.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/bigquery v1.80.0 h1:BbDo+XURgr6uZaOEGnuDq2O2wwAMiHfzCbw3q0ApgzU= +cloud.google.com/go/bigquery v1.80.0/go.mod h1:cc0XscySNQNuHBxuZSg5yyxFsg/ZHAfViAG49gJbWew= +cloud.google.com/go/bigtable v1.52.0 h1:8DS8rq5Mn3XmPqXD/vk+1lyqcWJ4hs+0Hh8Ruqroj+U= +cloud.google.com/go/bigtable v1.52.0/go.mod h1:51/CYIIpOoGrZ6koxZ9F5MFMwhtluPPeh0BvCJjl5RQ= 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/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.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.26.0 h1:9lgjj+DRv5Ay/tQ+vk9Ryz/G84ncnfwRC0RuHUGZm0U= +cloud.google.com/go/datastore v1.26.0/go.mod h1:jvJVNe+S2nHVIndV1H/B4s9K3MLsTMqOKlxSrzHTxB4= 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= 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/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= -cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= +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.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= +cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= 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= @@ -85,13 +85,13 @@ 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.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 v1.51.0 h1:XOaCejsqX7EEtUdQz+WPag66wWsUUGliyCOfGPKfo90= +cloud.google.com/go/pubsub v1.51.0/go.mod h1:NERXf11sd82UV3VnflcUj8POIyQUXT/QwrKlxD8di/I= +cloud.google.com/go/pubsub/v2 v2.6.1 h1:jX6gnC4n8BgYx6MOYICgbbaXZpr1vKeNOE3Bn17P5zg= +cloud.google.com/go/pubsub/v2 v2.6.1/go.mod h1:1y2lZnKfUFPZz0PU4YmXyk4lA11+xmYA42zbC32RkxQ= 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.94.0 h1:tve2XojeMa32SsrDNkp3J08TW4TZUVrv8TvTqGUif4A= +cloud.google.com/go/spanner v1.94.0/go.mod h1:Z2+83J5oVDmd1n5ntVMmjEuiNoXOpAyNeG7y1tuEHk0= 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= @@ -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.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg= +cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ= 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,22 +163,22 @@ 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.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= +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= 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.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/antithesishq/antithesis-sdk-go v0.7.2-default-no-op h1:p2zFsAzvhIpFya8AIOHIbWf7NGvO34QpLGclyf7nXj8= +github.com/antithesishq/antithesis-sdk-go v0.7.2-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= @@ -194,95 +194,90 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo github.com/aws/aws-sdk-go v1.30.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -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.43.5 h1:yKT5GYnFWhuDo+DqKvE5ZPwVn3RjC4MAeBtZGlh6AVM= +github.com/aws/aws-sdk-go-v2 v1.43.5/go.mod h1:wZjAJppCntyOGgVSmgVTfDyRJK5PHOasO6Wsy8U7Axk= 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.16 h1:aiuaKlDweRC5qExJondpWjOgyzMHpofpwspGXUtwn4c= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.16/go.mod h1:nG/LOlmox9BDe9HvQnXWzgcK8uKbgBMZ/Hp5pVt/21I= 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.36 h1:mX6ietU7UlB4w/2IUaexJdsyUDvhTd+jYPjVePiyi6s= +github.com/aws/aws-sdk-go-v2/config v1.32.36/go.mod h1:rMpV4xk7ZK59edraSaHP0jsWrztWTT5tbCwWY495hug= 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.35 h1:Cxua2RVdRwL0sfjHM/SnQoOnQ7xKng9m5EQBO8BnZlg= +github.com/aws/aws-sdk-go-v2/credentials v1.19.35/go.mod h1:9XQ+RSIGPkycr+oCJYnB1uTv5kMVVR+rd2vYK0Hxj2w= 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.36 h1:gucL1KH/PAYbpTpBg09CiVpBdTu4qkCl8C7xOTBixUg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.36/go.mod h1:usTB+PHhNMhrx2dxUeHcM7OrT5pySvmjYI++IsefPN0= 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.41 h1:RgRlA38QGJPnsG6TNEnMTQq8COa6Ye7AGwdnFlzd3Ys= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.41/go.mod h1:14QhagNwgiQqYUcAAJjQwgBXCcpxUau+8S/0YRLt5Uo= 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.36 h1:5CrzwxDqf4w3x1Vs3/NiZ0nsC34Hbm3pIDMWbsLebOE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36/go.mod h1:A3gHdKZIvG/QXERzZwcxNS3RNDFcRCuhhTFBYp+V/nw= 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.36 h1:A4N2f4YPcST0v+dWtX+xrpPPCL9VTBhoIFFUWYqbacE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36/go.mod h1:B/Qr859uxWUEfZeGotK5KAEoof4Q9YWgNtPSwV6jcyk= 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.37 h1:oyd3ke4V9AhKcRR7rRgxk1VyI+DjK2CBQtbxh3OkdaA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.37/go.mod h1:aA9D7SqfG9IC1b7FLD7Iyc8Q4JN0a8gHhNjN4zPlIaI= 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.16 h1:iE4NGbvqUZnHDqddQAauZzCILYtFjOHwRM5MOOKLB5A= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.16/go.mod h1:VsjEgrP+ibcou8TlWA4tYaB+0OojuhirsmCe+U60hTA= 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.28 h1:Q1TF1J9jVD+vFo0LzNnmNdQ9EAt52TS+MQlq9Ir+Yxo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.28/go.mod h1:4KqXXC/p1hrotmouDFbrRoWaLy962b9PMUReCG6+uWo= 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.36 h1:fx2ujmozWn+C/GtfXfz5k6Ckzza40ElOpIW7d92fLWQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.36/go.mod h1:QT2ufGVJ+xTRxtXPHTQ1kHkAdWIKPCmD+BqYAXWv8/4= 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.36 h1:EUIwBoN+q7UmhAejxgD27APiRjh1vwCFo53gSqdT0BM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.36/go.mod h1:6u00gmlTGR6W0b2k9NBrld7MnOEmf1Spqx0VVt6AqyE= 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.107.0 h1:OkYV+1171za+ab9otU1tGxMXhx6uZvwVEtVddjLuYTg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.107.0/go.mod h1:5FTZoQxhmLEiCAtYVk6V+t0iS/B5yGZVLZ3Wq5FDJZI= 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.5.5 h1:0VTFBfOgPJrUSpGMgzoi8qLcXF5dbmiBuxpo14eBWUw= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.5/go.mod h1:sNZYlBxoohYMBYl47BO/bFtAM6I8HSsPa1qwwPPRGoQ= 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.33.5 h1:jDQARFp1mJ2PEnllQf01nfFXGfWMJ59e0/HCHUTTZCk= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.5/go.mod h1:OcT2AhgTuxGAwZk5hgxaNLGpS33W8s8dUQadGVDVY9I= 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.38.5 h1:8xo1q9ttkYqMJ6vOXX67FPSpVEI7BWKVTKh77g82w+8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.5/go.mod h1:hbBeEUrZg6VddXYZpbKPyF0tl4XEnM+Dbx92RW3vmZI= 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.45.5 h1:eQ5BtXDrPg2wK0AjtVPzeBhUpYPeqHE/ptiH7xJRGek= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.5/go.mod h1:f9ImhnOISY7BuTZLM8qHepCYnglHBVLk5wVzatmP++w= 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.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= +github.com/aws/smithy-go v1.27.7/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= 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,17 +331,15 @@ 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= 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.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= -github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= +github.com/ebitengine/purego v0.10.1/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= @@ -364,15 +357,15 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 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/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsouza/fake-gcs-server v1.52.3 h1:hXddOPMGDKq5ENmttw6xkodVJy0uVhf7HhWvQgAOH6g= -github.com/fsouza/fake-gcs-server v1.52.3/go.mod h1:A0XtSRX+zz5pLRAt88j9+Of0omQQW+RMqipFbvdNclQ= +github.com/fsouza/fake-gcs-server v1.55.1 h1:8IsUkVFYQnS/QLyci2ORdpn7pkmqs+6hnJ8w51bN7Gg= +github.com/fsouza/fake-gcs-server v1.55.1/go.mod h1:5XZDg/ZKtY/ciEwJcFRaVz/YQ0FqffGAVTXyyaZZAuA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= @@ -387,8 +380,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-gorp/gorp v2.2.0+incompatible h1:xAUh4QgEeqPPhK3vxZN+bzrim1z5Av6q837gtjUlshc= github.com/go-gorp/gorp v2.2.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= 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= @@ -415,12 +406,12 @@ 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= -github.com/golang-cz/devslog v0.0.15/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= @@ -471,16 +462,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 +486,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 +518,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,24 +533,21 @@ 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.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk= +github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= 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.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= -github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= +github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= +github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= 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= 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= @@ -620,11 +607,10 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 h1:CMbkEl1h9JvRURFFprSbyy2f4Gf71SFz9h74iSAETGo= -github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999/go.mod h1:t6osVdP++3g4v2awHz4+HFccij23BbdT1rX3W7IijqQ= +github.com/johannesboyne/gofakes3 v1.2.0 h1:I9VEzPWvvAUAGzDlhYFoZjF0AXMlkcEyZlmBwiI6Oms= +github.com/johannesboyne/gofakes3 v1.2.0/go.mod h1:UHhRZRod9rENGFrUWTYnQHZqlNgSmjOq8DaD/ATQYRM= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -638,13 +624,15 @@ 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.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/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= -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/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= 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 +656,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= @@ -678,15 +666,15 @@ 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/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= -github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= 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= -github.com/minio/minio-go/v7 v7.0.92 h1:jpBFWyRS3p8P/9tsRc+NuvqoFi7qAmTCFPoRFmobbVw= -github.com/minio/minio-go/v7 v7.0.92/go.mod h1:vTIc8DNcnAZIhyFsk8EB90AbPjj3j68aWIEQCiPj7d0= +github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw= +github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -695,16 +683,14 @@ 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.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.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw= +github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM= 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/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= +github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= @@ -718,18 +704,16 @@ 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/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/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.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.4 h1:efgjZ8cdExAKRuqSg8UPJFprb+l7NlBtSDPhDlw3rO4= +github.com/nats-io/nats-server/v2 v2.14.4/go.mod h1:BltdpOYestjbtQSnVO2zGHdg5SGBZjt+GYTgB9LZq/I= 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= @@ -738,21 +722,20 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/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/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= +github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= +github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -778,8 +761,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.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= +github.com/shirou/gopsutil/v4 v4.26.6/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= @@ -788,14 +771,16 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs 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= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= 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/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/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.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4= +github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -816,22 +801,22 @@ 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.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.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/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI= +github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE= +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.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +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,11 +838,12 @@ 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= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= 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= @@ -873,31 +859,27 @@ 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/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= -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/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw= +go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s= +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.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +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.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q= +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.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= @@ -919,6 +901,7 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= gocloud.dev v0.26.0/go.mod h1:mkUgejbnbLotorqDyvedJO20XcZNTynmSeVSQS9btVg= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -941,8 +924,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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= 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 +940,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= @@ -994,9 +977,8 @@ 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.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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= 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= @@ -1046,14 +1028,12 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 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= -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.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= 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= @@ -1089,8 +1069,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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.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= @@ -1173,23 +1153,20 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.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= 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-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= 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= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= 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= @@ -1200,12 +1177,11 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= 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= @@ -1231,7 +1207,6 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1279,9 +1254,8 @@ 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.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.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= 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= @@ -1342,8 +1316,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.293.0 h1:p9XIWOf63U4OgYx120ZwVU8+vl4XTPmWfgVPnmOAS9w= +google.golang.org/api v0.293.0/go.mod h1:6n5tjEB1gzwniZTepZ0g5u+wM7Bof5GeULCx/zh8ZE0= 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= @@ -1437,12 +1411,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-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 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-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea h1:kVhQEPTpKQahD5+JSBTfBB19wcgQTTjAIn45MBqnyHk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea/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= @@ -1472,8 +1446,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.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= 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= @@ -1498,11 +1472,14 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= +gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= gopkg.in/jcmturner/gokrb5.v7 v7.3.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/sdks/go/README.md b/sdks/go/README.md index 4d6a50472578..bb7f22934f6b 100644 --- a/sdks/go/README.md +++ b/sdks/go/README.md @@ -93,7 +93,7 @@ $ go run wordcount.go --runner=dataflow --project= --region=/output* | head +$ gcloud storage cat /output* | head Blanket: 1 blot: 1 Kneeling: 3 diff --git a/sdks/go/container/boot.go b/sdks/go/container/boot.go index 469285821f7e..aeeb87cc8115 100644 --- a/sdks/go/container/boot.go +++ b/sdks/go/container/boot.go @@ -17,7 +17,6 @@ package main import ( "context" - "encoding/json" "errors" "flag" "fmt" @@ -31,7 +30,6 @@ import ( "github.com/apache/beam/sdks/v2/go/container/pool" "github.com/apache/beam/sdks/v2/go/container/tools" "github.com/apache/beam/sdks/v2/go/pkg/beam/artifact" - "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime" // Import gcs filesystem so that it can be used to upload heap dumps _ "github.com/apache/beam/sdks/v2/go/pkg/beam/io/filesystem/gcs" @@ -61,23 +59,16 @@ const ( workerPoolIdEnv = "BEAM_GO_WORKER_POOL_ID" ) -func configureGoogleCloudProfilerEnvVars(ctx context.Context, logger *tools.Logger, metadata map[string]string, options string) error { +func configureGoogleCloudProfilerEnvVars(ctx context.Context, logger *tools.Logger, metadata map[string]string, po *tools.PipelineOptions) error { const profilerKey = "enable_google_cloud_profiler=" - var parsed map[string]interface{} - if err := json.Unmarshal([]byte(options), &parsed); err != nil { - panic(err) - } - var profilerServiceName string - // Try from "beam:option:go_options:v1" -> "options" -> "dataflow_service_options" - if goOpts, ok := parsed["beam:option:go_options:v1"].(map[string]interface{}); ok { - if options, ok := goOpts["options"].(map[string]interface{}); ok { - if profilerServiceNameRaw, ok := options["dataflow_service_options"].(string); ok { - if strings.HasPrefix(profilerServiceNameRaw, profilerKey) { - profilerServiceName = strings.TrimPrefix(profilerServiceNameRaw, profilerKey) - } + if serviceOpts, err := po.GetStringSlice("dataflow_service_options"); err == nil { + for _, opt := range serviceOpts { + if strings.HasPrefix(opt, profilerKey) { + profilerServiceName = strings.TrimPrefix(opt, profilerKey) + break } } } @@ -159,8 +150,11 @@ func main() { logger.Fatalf(ctx, "Failed to convert pipeline options: %v", err) } + // Go SDK wraps pipeline options inside the URN namespace: "beam:option:go_options:v1". + po := tools.ParseOptionsFromProto(info.GetPipelineOptions(), "go_options") + // Inject artifact validation enabled state into context - ctx = artifact.WithArtifactValidation(ctx, !artifact.HasExperiment(info.GetPipelineOptions(), "disable_staged_file_integrity_checks")) + ctx = artifact.WithArtifactValidation(ctx, !po.HasExperiment("disable_staged_file_integrity_checks")) // (2) Retrieve the staged files. // @@ -210,9 +204,11 @@ func main() { os.Setenv("RUNNER_CAPABILITIES", strings.Join(info.GetRunnerCapabilities(), " ")) } - enableGoogleCloudProfiler := strings.Contains(options, enableGoogleCloudProfilerOption) + // Go SDK models multi-value list flags (like dataflow_service_options) as comma-separated strings. + serviceOpts, _ := po.GetString("dataflow_service_options") + enableGoogleCloudProfiler := strings.Contains(serviceOpts, "enable_google_cloud_profiler") if enableGoogleCloudProfiler { - err := configureGoogleCloudProfilerEnvVars(ctx, logger, info.Metadata, options) + err := configureGoogleCloudProfilerEnvVars(ctx, logger, info.Metadata, po) if err != nil { logger.Printf(ctx, "could not configure Google Cloud Profiler variables, got %v", err) } @@ -221,12 +217,8 @@ func main() { err = execx.Execute(prog, args...) if err != nil { - var opt runtime.RawOptionsWrapper - err := json.Unmarshal([]byte(options), &opt) - if err == nil { - if tempLocation, ok := opt.Options.Options["temp_location"]; ok { - diagnostics.UploadHeapProfile(ctx, fmt.Sprintf("%v/heapProfiles/profile-%v-%d", strings.TrimSuffix(tempLocation, "/"), *id, time.Now().Unix())) - } + if tempLocation, err := po.GetString("temp_location"); err == nil && tempLocation != "" { + diagnostics.UploadHeapProfile(ctx, fmt.Sprintf("%v/heapProfiles/profile-%v-%d", strings.TrimSuffix(tempLocation, "/"), *id, time.Now().Unix())) } } diff --git a/sdks/go/container/boot_test.go b/sdks/go/container/boot_test.go index bb94aca36be3..57aa083245fb 100644 --- a/sdks/go/container/boot_test.go +++ b/sdks/go/container/boot_test.go @@ -21,11 +21,14 @@ import ( "path/filepath" "testing" + "encoding/json" + "github.com/apache/beam/sdks/v2/go/container/tools" "github.com/apache/beam/sdks/v2/go/pkg/beam/artifact" fnpb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/fnexecution_v1" pipepb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/pipeline_v1" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" ) func TestEnsureEndpointsSet_AllSet(t *testing.T) { @@ -224,7 +227,7 @@ func TestConfigureGoogleCloudProfilerEnvVars(t *testing.T) { options: `{ "beam:option:go_options:v1": { "options": { - "dataflow_service_options": "enable_google_cloud_profiler=custom_profiler" + "dataflow_service_options": "enable_google_cloud_profiler=custom_profiler,another_option" } } }`, @@ -287,7 +290,17 @@ func TestConfigureGoogleCloudProfilerEnvVars(t *testing.T) { clearEnvVars() ctx := context.Background() - err := configureGoogleCloudProfilerEnvVars(ctx, &tools.Logger{}, tt.metadata, tt.options) + var raw map[string]interface{} + if err := json.Unmarshal([]byte(tt.options), &raw); err != nil { + t.Fatalf("failed to unmarshal JSON for test: %v", err) + } + st, err := structpb.NewStruct(raw) + if err != nil { + t.Fatalf("failed to create structpb for test: %v", err) + } + po := tools.ParseOptionsFromProto(st, "go_options") + + err = configureGoogleCloudProfilerEnvVars(ctx, &tools.Logger{}, tt.metadata, po) if tt.expectingError { if err == nil { diff --git a/sdks/go/container/tools/buffered_logging.go b/sdks/go/container/tools/buffered_logging.go index a0937b8eb14a..333b136e16f7 100644 --- a/sdks/go/container/tools/buffered_logging.go +++ b/sdks/go/container/tools/buffered_logging.go @@ -16,6 +16,7 @@ package tools import ( + "bytes" "context" "log" "os" @@ -52,23 +53,59 @@ func NewBufferedLoggerWithFlushInterval(ctx context.Context, logger *Logger, int return &BufferedLogger{logger: logger, lastFlush: time.Now(), flushInterval: interval, periodicFlushContext: ctx, now: time.Now} } -// Write implements the io.Writer interface, converting input to a string -// and storing it in the BufferedLogger's buffer. If a logger is not provided, -// the output is sent directly to os.Stderr. +// Write implements the io.Writer interface. It buffers byte streams line-by-line +// into memory and flushes periodically or upon calling Flush(), FlushAtError(), or +// FlushAtDebug(). It is used primarily to redirect stdout/stderr of subprocesses or +// standard Go log output. If a logger is not provided, the output is sent directly to os.Stderr. func (b *BufferedLogger) Write(p []byte) (int, error) { if b.logger == nil { return os.Stderr.Write(p) } - n, err := b.builder.Write(p) + if b.logs == nil { b.logs = make([]string, 0, initialLogSize) } - b.logs = append(b.logs, b.builder.String()) - b.builder.Reset() + + start := 0 + for { + // Look for the next newline in the incoming byte slice directly + nl := bytes.IndexByte(p[start:], '\n') + if nl == -1 { + break + } + + // Write the segment up to the newline into the builder + b.builder.Write(p[start : start+nl]) + + // The builder now contains any previous partial line + the current complete segment + b.logs = append(b.logs, strings.TrimSuffix(b.builder.String(), "\r")) + b.builder.Reset() + + start += nl + 1 + } + + // Buffer any remaining bytes that didn't end in a newline + if start < len(p) { + b.builder.Write(p[start:]) + } + if b.now().Sub(b.lastFlush) > b.flushInterval { b.FlushAtDebug(b.periodicFlushContext) } - return n, err + + return len(p), nil +} + +// Flush flushes the contents of the buffer to the logging service. +// If err is non-nil, it flushes at Error severity; otherwise it flushes at Debug severity. +// It returns the provided error. +func (b *BufferedLogger) Flush(ctx context.Context, err error) error { + if err != nil { + b.FlushAtError(ctx) + } else { + b.FlushAtDebug(ctx) + } + return err } // FlushAtError flushes the contents of the buffer to the logging @@ -77,6 +114,10 @@ func (b *BufferedLogger) FlushAtError(ctx context.Context) { if b.logger == nil { return } + if b.builder.Len() > 0 { + b.logs = append(b.logs, strings.TrimSuffix(b.builder.String(), "\r")) + b.builder.Reset() + } for _, message := range b.logs { b.logger.Errorf(ctx, "%s", message) } @@ -90,6 +131,10 @@ func (b *BufferedLogger) FlushAtDebug(ctx context.Context) { if b.logger == nil { return } + if b.builder.Len() > 0 { + b.logs = append(b.logs, strings.TrimSuffix(b.builder.String(), "\r")) + b.builder.Reset() + } for _, message := range b.logs { b.logger.Printf(ctx, "%s", message) } @@ -97,8 +142,9 @@ func (b *BufferedLogger) FlushAtDebug(ctx context.Context) { b.lastFlush = time.Now() } -// Prints directly to the logging service. If the logger is nil, prints directly to the -// console. Used for the container pre-build workflow. +// Printf directly writes formatted messages to the underlying logger/service, +// bypassing line buffering. If the logger is nil, it prints directly to the +// console. Used for direct informational logs and the container pre-build workflow. func (b *BufferedLogger) Printf(ctx context.Context, format string, args ...any) { if b.logger == nil { log.Printf(format, args...) diff --git a/sdks/go/container/tools/buffered_logging_test.go b/sdks/go/container/tools/buffered_logging_test.go index 9f542d2d5ab6..314d496475a6 100644 --- a/sdks/go/container/tools/buffered_logging_test.go +++ b/sdks/go/container/tools/buffered_logging_test.go @@ -17,21 +17,48 @@ package tools import ( "context" + "errors" "testing" "time" fnpb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/fnexecution_v1" ) +func getAllLogEntries(catcher *logCatcher) []*fnpb.LogEntry { + var entries []*fnpb.LogEntry + for _, list := range catcher.msgs { + entries = append(entries, list.GetLogEntries()...) + } + return entries +} + func TestBufferedLogger(t *testing.T) { ctx := context.Background() + t.Run("printf", func(t *testing.T) { + catcher := &logCatcher{} + l := &Logger{client: catcher} + bl := NewBufferedLogger(l) + + bl.Printf(ctx, "test message") + + received := catcher.msgs[0].GetLogEntries()[0] + + if got, want := received.Message, "test message"; got != want { + t.Errorf("got message %q, want %q", got, want) + } + + if got, want := received.Severity, fnpb.LogEntry_Severity_DEBUG; got != want { + t.Errorf("got severity %v, want %v", got, want) + } + }) + t.Run("write", func(t *testing.T) { catcher := &logCatcher{} l := &Logger{client: catcher} bl := NewBufferedLogger(l) - message := []byte("test message") + message := []byte("test message\n") n, err := bl.Write(message) if err != nil { t.Errorf("got error %v", err) @@ -77,7 +104,8 @@ func TestBufferedLogger(t *testing.T) { l := &Logger{client: catcher} bl := NewBufferedLogger(l) - messages := []string{"foo", "bar", "baz"} + messages := []string{"foo\n", "bar\n", "baz\n"} + expected := []string{"foo", "bar", "baz"} for _, message := range messages { messBytes := []byte(message) @@ -93,10 +121,14 @@ func TestBufferedLogger(t *testing.T) { bl.FlushAtDebug(ctx) - received := catcher.msgs[0].GetLogEntries() + received := getAllLogEntries(catcher) + + if got, want := len(received), len(expected); got != want { + t.Fatalf("expected %d log entries received, got %d", want, got) + } for i, message := range received { - if got, want := message.Message, messages[i]; got != want { + if got, want := message.Message, expected[i]; got != want { t.Errorf("got message %q, want %q", got, want) } @@ -139,7 +171,8 @@ func TestBufferedLogger(t *testing.T) { l := &Logger{client: catcher} bl := NewBufferedLogger(l) - messages := []string{"foo", "bar", "baz"} + messages := []string{"foo\n", "bar\n", "baz\n"} + expected := []string{"foo", "bar", "baz"} for _, message := range messages { messBytes := []byte(message) @@ -155,10 +188,14 @@ func TestBufferedLogger(t *testing.T) { bl.FlushAtError(ctx) - received := catcher.msgs[0].GetLogEntries() + received := getAllLogEntries(catcher) + + if got, want := len(received), len(expected); got != want { + t.Fatalf("expected %d log entries received, got %d", want, got) + } for i, message := range received { - if got, want := message.Message, messages[i]; got != want { + if got, want := message.Message, expected[i]; got != want { t.Errorf("got message %q, want %q", got, want) } @@ -168,6 +205,55 @@ func TestBufferedLogger(t *testing.T) { } }) + t.Run("flush with nil error", func(t *testing.T) { + catcher := &logCatcher{} + l := &Logger{client: catcher} + bl := NewBufferedLogger(l) + + message := []byte("success message\n") + _, err := bl.Write(message) + if err != nil { + t.Fatalf("unexpected write error: %v", err) + } + + if gotErr := bl.Flush(ctx, nil); gotErr != nil { + t.Errorf("Flush(ctx, nil) returned error %v, want nil", gotErr) + } + + received := catcher.msgs[0].GetLogEntries()[0] + if got, want := received.Message, "success message"; got != want { + t.Errorf("got message %q, want %q", got, want) + } + if got, want := received.Severity, fnpb.LogEntry_Severity_DEBUG; got != want { + t.Errorf("got severity %v, want %v", got, want) + } + }) + + t.Run("flush with non-nil error", func(t *testing.T) { + catcher := &logCatcher{} + l := &Logger{client: catcher} + bl := NewBufferedLogger(l) + + message := []byte("error message\n") + _, err := bl.Write(message) + if err != nil { + t.Fatalf("unexpected write error: %v", err) + } + + originalErr := errors.New("command failed") + if gotErr := bl.Flush(ctx, originalErr); gotErr != originalErr { + t.Errorf("Flush(ctx, err) returned %v, want %v", gotErr, originalErr) + } + + received := catcher.msgs[0].GetLogEntries()[0] + if got, want := received.Message, "error message"; got != want { + t.Errorf("got message %q, want %q", got, want) + } + if got, want := received.Severity, fnpb.LogEntry_Severity_ERROR; got != want { + t.Errorf("got severity %v, want %v", got, want) + } + }) + t.Run("direct print", func(t *testing.T) { catcher := &logCatcher{} l := &Logger{client: catcher} @@ -195,7 +281,8 @@ func TestBufferedLogger(t *testing.T) { startTime := time.Now() bl.now = func() time.Time { return startTime } - messages := []string{"foo", "bar"} + messages := []string{"foo\n", "bar\n"} + expected := []string{"foo", "bar"} for i, message := range messages { if i > 1 { @@ -212,7 +299,8 @@ func TestBufferedLogger(t *testing.T) { } } - lastMessage := "baz" + lastMessage := "baz\n" + expected = append(expected, "baz") bl.now = func() time.Time { return startTime.Add(6 * time.Second) } messBytes := []byte(lastMessage) n, err := bl.Write(messBytes) @@ -225,11 +313,14 @@ func TestBufferedLogger(t *testing.T) { } // Type should have auto-flushed at debug after the third message - received := catcher.msgs[0].GetLogEntries() - messages = append(messages, lastMessage) + received := getAllLogEntries(catcher) + + if got, want := len(received), len(expected); got != want { + t.Fatalf("expected %d log entries received, got %d", want, got) + } for i, message := range received { - if got, want := message.Message, messages[i]; got != want { + if got, want := message.Message, expected[i]; got != want { t.Errorf("got message %q, want %q", got, want) } @@ -238,4 +329,57 @@ func TestBufferedLogger(t *testing.T) { } } }) + + t.Run("partial write splitting", func(t *testing.T) { + catcher := &logCatcher{} + l := &Logger{client: catcher} + bl := NewBufferedLogger(l) + + // Write a partial line + n, err := bl.Write([]byte("hello ")) + if err != nil { + t.Errorf("got error %v", err) + } + if n != 6 { + t.Errorf("got %d, want 6", n) + } + if len(bl.logs) != 0 { + t.Errorf("expected no logs buffered yet, got %d", len(bl.logs)) + } + + // Write remainder and a second line + n, err = bl.Write([]byte("world\nline2\npartial")) + if err != nil { + t.Errorf("got error %v", err) + } + if n != 19 { + t.Errorf("got %d, want 19", n) + } + + if got, want := len(bl.logs), 2; got != want { + t.Errorf("expected 2 logs buffered, got %d", got) + } + if got, want := bl.logs[0], "hello world"; got != want { + t.Errorf("got %q, want %q", got, want) + } + if got, want := bl.logs[1], "line2"; got != want { + t.Errorf("got %q, want %q", got, want) + } + + // Flush should flush the final partial message + bl.FlushAtDebug(ctx) + received := getAllLogEntries(catcher) + if got, want := len(received), 3; got != want { + t.Fatalf("expected 3 log entries received, got %d", got) + } + if got, want := received[0].Message, "hello world"; got != want { + t.Errorf("got message %q, want %q", got, want) + } + if got, want := received[1].Message, "line2"; got != want { + t.Errorf("got message %q, want %q", got, want) + } + if got, want := received[2].Message, "partial"; got != want { + t.Errorf("got message %q, want %q", got, want) + } + }) } diff --git a/sdks/go/container/tools/pipeline_options.go b/sdks/go/container/tools/pipeline_options.go index 026fb31b0991..dff515403118 100644 --- a/sdks/go/container/tools/pipeline_options.go +++ b/sdks/go/container/tools/pipeline_options.go @@ -19,6 +19,10 @@ import ( "encoding/json" "fmt" "os" + "strconv" + "strings" + + structpb "google.golang.org/protobuf/types/known/structpb" ) // MakePipelineOptionsFileAndEnvVar writes the pipeline options to a file. @@ -42,3 +46,217 @@ func MakePipelineOptionsFileAndEnvVar(options string) error { os.Setenv("PIPELINE_OPTIONS_FILE", f.Name()) return nil } + +// PipelineOptions represents parsed pipeline options as a normalized map. +type PipelineOptions struct { + options map[string]any + experiments map[string]string +} + +// ParseOptionsFromProto creates normalized PipelineOptions directly from a protobuf Struct. +func ParseOptionsFromProto(opt *structpb.Struct, sdkNamespace string) *PipelineOptions { + if opt == nil { + return &PipelineOptions{options: make(map[string]any), experiments: make(map[string]string)} + } + raw := opt.AsMap() + flat := make(map[string]any) + + // 1. Extract nested options if present (Dataflow runner uses this structure) + if optsVal, ok := raw["options"]; ok { + if optsMap, ok := optsVal.(map[string]any); ok { + for k, v := range optsMap { + flat[k] = v + } + } + } + + // 2. Extract standard URN keys (Portable runners use this structure) + for k, v := range raw { + if k == "options" || k == "display_data" { + continue + } + if strings.HasPrefix(k, "beam:option:") && strings.HasSuffix(k, ":v1") { + name := strings.TrimPrefix(k, "beam:option:") + name = strings.TrimSuffix(name, ":v1") + flat[name] = v + } + } + + // 3. Promote specified SDK namespace options (Highest precedence, may overwrite earlier entries). + // Beam Go SDK uses this structure. + if sdkNamespace != "" { + sdkURN := fmt.Sprintf("beam:option:%s:v1", sdkNamespace) + if sdkVal, ok := raw[sdkURN]; ok { + if urnMap, ok := sdkVal.(map[string]any); ok { + if nestedOpts, ok := urnMap["options"].(map[string]any); ok { + for nk, nv := range nestedOpts { + flat[nk] = nv + } + } + } + } + } + + po := &PipelineOptions{ + options: flat, + experiments: make(map[string]string), + } + if exps, err := po.GetStringSlice("experiments"); err == nil { + po.experiments = parseExperiments(exps) + } + return po +} + +func parseExperiments(slice []string) map[string]string { + res := make(map[string]string) + for _, item := range slice { + if strings.Contains(item, "=") { + parts := strings.SplitN(item, "=", 2) + res[parts[0]] = parts[1] + } else { + res[item] = "" + } + } + return res +} + +// HasOption returns true if the option is defined and not nil. +func (po *PipelineOptions) HasOption(name string) bool { + val, ok := po.options[name] + return ok && val != nil +} + +// GetString returns the value of an option as a string. +// As a convenience and to maintain compatibility with Go SDK's flags serialization style, +// if the option is stored as a string slice/array, GetString will conjoin the elements +// into a single comma-separated string (e.g. ["opt1", "opt2"] -> "opt1,opt2"). +func (po *PipelineOptions) GetString(name string) (string, error) { + val, ok := po.options[name] + if !ok || val == nil { + return "", fmt.Errorf("option %q not defined", name) + } + if str, ok := val.(string); ok { + return str, nil + } + if slice, ok := val.([]any); ok { + var parts []string + for _, item := range slice { + if str, ok := item.(string); ok { + parts = append(parts, str) + } else { + return "", fmt.Errorf("option %q: expected string slice element, got type %T", name, item) + } + } + return strings.Join(parts, ","), nil + } + return "", fmt.Errorf("option %q: expected string, got type %T", name, val) +} + +// GetStringSlice returns the value of an option as a string slice. +// As a convenience and to maintain compatibility with Go SDK's flags serialization style, +// if the option is stored as a single comma-separated string (such as experiments +// or dataflow_service_options), GetStringSlice will parse it by splitting the string +// by comma (e.g. "opt1,opt2" -> ["opt1", "opt2"]). +func (po *PipelineOptions) GetStringSlice(name string) ([]string, error) { + val, ok := po.options[name] + if !ok || val == nil { + return nil, fmt.Errorf("option %q not defined", name) + } + if slice, ok := val.([]any); ok { + var res []string + for _, item := range slice { + if str, ok := item.(string); ok { + res = append(res, str) + } else { + return nil, fmt.Errorf("option %q: expected string slice element, got type %T", name, item) + } + } + return res, nil + } + if str, ok := val.(string); ok { + // Go SDK models multi-value list flags (like experiments or dataflow_service_options) + // as comma-separated string values. + if str == "" { + return nil, nil + } + return strings.Split(str, ","), nil + } + return nil, fmt.Errorf("option %q: expected string slice, got type %T", name, val) +} + +// GetInt returns the value of an option as an integer. +func (po *PipelineOptions) GetInt(name string) (int, error) { + val, ok := po.options[name] + if !ok || val == nil { + return 0, fmt.Errorf("option %q not defined", name) + } + switch v := val.(type) { + case float64: + return int(v), nil + case string: + res, err := strconv.Atoi(v) + if err == nil { + return res, nil + } + return 0, fmt.Errorf("option %q: failed to parse %q as int: %w", name, v, err) + default: + return 0, fmt.Errorf("option %q: expected int (represented as number or string), got type %T", name, val) + } +} + +// GetBool returns the value of an option as a boolean. +func (po *PipelineOptions) GetBool(name string) (bool, error) { + val, ok := po.options[name] + if !ok || val == nil { + return false, fmt.Errorf("option %q not defined", name) + } + switch v := val.(type) { + case bool: + return v, nil + case string: + res, err := strconv.ParseBool(v) + if err != nil { + return false, fmt.Errorf("option %q: failed to parse %q as bool: %w", name, v, err) + } + return res, nil + case float64: + return v != 0, nil + default: + return false, fmt.Errorf("option %q: expected bool, got type %T", name, val) + } +} + +// GetFloat64 returns the value of an option as a float64. +func (po *PipelineOptions) GetFloat64(name string) (float64, error) { + val, ok := po.options[name] + if !ok || val == nil { + return 0, fmt.Errorf("option %q not defined", name) + } + switch v := val.(type) { + case float64: + return v, nil + case string: + res, err := strconv.ParseFloat(v, 64) + if err == nil { + return res, nil + } + return 0, fmt.Errorf("option %q: failed to parse %q as float64: %w", name, v, err) + default: + return 0, fmt.Errorf("option %q: expected float64 (represented as number or string), got type %T", name, val) + } +} + +// LookupExperiment returns the value of an experiment option if present. +// - If the experiment is present but has no value (e.g., --experiments=foo), it returns "", true. +// - If the experiment is present as a key-value pair (e.g., --experiments=foo=bar), it returns "bar", true. +// - If the experiment is not present, it returns "", false. +func (po *PipelineOptions) LookupExperiment(key string) (string, bool) { + val, ok := po.experiments[key] + return val, ok +} + +// HasExperiment returns true if the specified experiment is present in the options (either as a flag or key-value pair). +func (po *PipelineOptions) HasExperiment(name string) bool { + _, ok := po.LookupExperiment(name) + return ok +} diff --git a/sdks/go/container/tools/pipeline_options_test.go b/sdks/go/container/tools/pipeline_options_test.go index 7a0d7ebd5f09..c220a501756a 100644 --- a/sdks/go/container/tools/pipeline_options_test.go +++ b/sdks/go/container/tools/pipeline_options_test.go @@ -16,10 +16,28 @@ package tools import ( + "encoding/json" "os" "testing" + + structpb "google.golang.org/protobuf/types/known/structpb" ) +func parseProtoForTest(t *testing.T, options string) *structpb.Struct { + if options == "" { + options = "{}" + } + var raw map[string]interface{} + if err := json.Unmarshal([]byte(options), &raw); err != nil { + t.Fatalf("failed to unmarshal JSON for test: %v", err) + } + st, err := structpb.NewStruct(raw) + if err != nil { + t.Fatalf("failed to create structpb for test: %v", err) + } + return st +} + func TestMakePipelineOptionsFileAndEnvVar(t *testing.T) { tests := []struct { name string @@ -56,3 +74,228 @@ func TestMakePipelineOptionsFileAndEnvVar(t *testing.T) { } os.Remove("pipeline_options.json") } + +func TestParseOptionsFromProto_NestedOptionsNoNamespace(t *testing.T) { + p := parseProtoForTest(t, `{ + "options": { + "profiler_agent": "memray", + "profile_upload_interval_sec": 10, + "profiler_stop_after_crash": true, + "profile_sample_rate": 0.5, + "experiments": ["beam_fn_api", "pip_use_build_isolation"] + } + }`) + po := ParseOptionsFromProto(p, "") + + if got, err := po.GetString("profiler_agent"); err != nil || got != "memray" { + t.Errorf("GetString(profiler_agent) = (%q, %v), want (\"memray\", nil)", got, err) + } + if got, err := po.GetInt("profile_upload_interval_sec"); err != nil || got != 10 { + t.Errorf("GetInt(profile_upload_interval_sec) = (%d, %v), want (10, nil)", got, err) + } + if got, err := po.GetBool("profiler_stop_after_crash"); err != nil || got != true { + t.Errorf("GetBool(profiler_stop_after_crash) = (%t, %v), want (true, nil)", got, err) + } + // Sample float option in Beam. Unused for memray in practice. + if got, err := po.GetFloat64("profile_sample_rate"); err != nil || got != 0.5 { + t.Errorf("GetFloat64(profile_sample_rate) = (%f, %v), want (0.5, nil)", got, err) + } + experiments, err := po.GetStringSlice("experiments") + if err != nil || len(experiments) != 2 || experiments[0] != "beam_fn_api" || experiments[1] != "pip_use_build_isolation" { + t.Errorf("GetStringSlice(experiments) = (%v, %v), want ([beam_fn_api, pip_use_build_isolation], nil)", experiments, err) + } + if !po.HasExperiment("beam_fn_api") || !po.HasExperiment("pip_use_build_isolation") { + t.Errorf("expected experiments beam_fn_api and pip_use_build_isolation to be present, experiments map: %+v", po.experiments) + } +} + +func TestParseOptionsFromProto_FlatOptionsWithURN(t *testing.T) { + p := parseProtoForTest(t, `{ + "beam:option:profiler_agent:v1": "memray", + "beam:option:profile_upload_interval_sec:v1": "10", + "beam:option:profiler_stop_after_crash:v1": "true", + "beam:option:profile_sample_rate:v1": "0.5", + "beam:option:experiments:v1": ["beam_fn_api", "another_exp"] + }`) + po := ParseOptionsFromProto(p, "") + + if got, err := po.GetString("profiler_agent"); err != nil || got != "memray" { + t.Errorf("GetString(profiler_agent) = (%q, %v), want (\"memray\", nil)", got, err) + } + if got, err := po.GetInt("profile_upload_interval_sec"); err != nil || got != 10 { + t.Errorf("GetInt(profile_upload_interval_sec) = (%d, %v), want (10, nil)", got, err) + } + if got, err := po.GetBool("profiler_stop_after_crash"); err != nil || got != true { + t.Errorf("GetBool(profiler_stop_after_crash) = (%t, %v), want (true, nil)", got, err) + } + if got, err := po.GetFloat64("profile_sample_rate"); err != nil || got != 0.5 { + t.Errorf("GetFloat64(profile_sample_rate) = (%f, %v), want (0.5, nil)", got, err) + } + if got, err := po.GetString("experiments"); err != nil || got != "beam_fn_api,another_exp" { + t.Errorf("GetString(experiments) = (%q, %v), want (\"beam_fn_api,another_exp\", nil)", got, err) + } + experiments, err := po.GetStringSlice("experiments") + if err != nil || len(experiments) != 2 || experiments[0] != "beam_fn_api" || experiments[1] != "another_exp" { + t.Errorf("GetStringSlice(experiments) = (%v, %v), want ([beam_fn_api, another_exp], nil)", experiments, err) + } + if !po.HasExperiment("beam_fn_api") || !po.HasExperiment("another_exp") { + t.Errorf("expected experiments beam_fn_api and another_exp to be present, experiments map: %+v", po.experiments) + } +} + +func TestParseOptionsFromProto_CommaSeparatedExperiments(t *testing.T) { + p := parseProtoForTest(t, `{ + "beam:option:go_options:v1": { + "options": { + "experiments": "exp1,exp2,exp3", + "dataflow_service_options": "opt1" + } + } + }`) + po := ParseOptionsFromProto(p, "go_options") + + experiments, err := po.GetStringSlice("experiments") + if err != nil || len(experiments) != 3 || experiments[0] != "exp1" || experiments[1] != "exp2" || experiments[2] != "exp3" { + t.Errorf("GetStringSlice(experiments) = (%v, %v), want ([exp1, exp2, exp3], nil)", experiments, err) + } + if !po.HasExperiment("exp1") || !po.HasExperiment("exp2") || !po.HasExperiment("exp3") { + t.Errorf("expected experiments exp1, exp2, and exp3 to be present, experiments map: %+v", po.experiments) + } + serviceOpts, err := po.GetStringSlice("dataflow_service_options") + if err != nil || len(serviceOpts) != 1 || serviceOpts[0] != "opt1" { + t.Errorf("GetStringSlice(dataflow_service_options) = (%v, %v), want ([opt1], nil)", serviceOpts, err) + } +} + +func TestParseOptionsFromProto_MalformedOptions(t *testing.T) { + t.Run("malformed integer", func(t *testing.T) { + p := parseProtoForTest(t, `{"options": {"profile_upload_interval_sec": "invalid"}}`) + po := ParseOptionsFromProto(p, "") + _, err := po.GetInt("profile_upload_interval_sec") + if err == nil { + t.Errorf("expected error, got nil") + } + }) + + t.Run("malformed bool", func(t *testing.T) { + p := parseProtoForTest(t, `{"options": {"profiler_stop_after_crash": "maybe"}}`) + po := ParseOptionsFromProto(p, "") + _, err := po.GetBool("profiler_stop_after_crash") + if err == nil { + t.Errorf("expected error, got nil") + } + }) + + t.Run("type mismatch int expected got bool", func(t *testing.T) { + p := parseProtoForTest(t, `{"options": {"profile_upload_interval_sec": true}}`) + po := ParseOptionsFromProto(p, "") + _, err := po.GetInt("profile_upload_interval_sec") + if err == nil { + t.Errorf("expected error, got nil") + } + }) + + t.Run("missing key returns error", func(t *testing.T) { + p := parseProtoForTest(t, `{}`) + po := ParseOptionsFromProto(p, "") + _, errInt := po.GetInt("profile_upload_interval_sec") + _, errBool := po.GetBool("profiler_stop_after_crash") + _, errFloat := po.GetFloat64("profile_sample_rate") + if errInt == nil || errBool == nil || errFloat == nil { + t.Errorf("expected error for missing keys, got: intErr=%v, boolErr=%v, floatErr=%v", errInt, errBool, errFloat) + } + }) +} + +func TestPipelineOptions_HasOption(t *testing.T) { + p := parseProtoForTest(t, `{"options": {"profile_upload_interval_sec": 10}}`) + po := ParseOptionsFromProto(p, "") + if !po.HasOption("profile_upload_interval_sec") { + t.Errorf("HasOption(profile_upload_interval_sec) = false, want true") + } + if po.HasOption("profiler_stop_after_crash") { + t.Errorf("HasOption(profiler_stop_after_crash) = true, want false") + } +} + +func TestPipelineOptions_HasExperiment(t *testing.T) { + p := parseProtoForTest(t, `{"options": {"experiments": ["exp1", "exp2=val2"]}}`) + po := ParseOptionsFromProto(p, "") + if !po.HasExperiment("exp1") { + t.Errorf("HasExperiment(exp1) = false, want true") + } + if !po.HasExperiment("exp2") { + t.Errorf("HasExperiment(exp2) = false, want true") + } + if po.HasExperiment("exp3") { + t.Errorf("HasExperiment(exp3) = true, want false") + } +} + +func TestPipelineOptions_LookupExperiment(t *testing.T) { + p := parseProtoForTest(t, `{"options": {"experiments": ["exp1", "exp2=val2", "exp3=val3=val4"]}}`) + po := ParseOptionsFromProto(p, "") + + val, ok := po.LookupExperiment("exp1") + if !ok || val != "" { + t.Errorf("LookupExperiment(exp1) = (%q, %t), want (\"\", true)", val, ok) + } + + val, ok = po.LookupExperiment("exp2") + if !ok || val != "val2" { + t.Errorf("LookupExperiment(exp2) = (%q, %t), want (\"val2\", true)", val, ok) + } + + val, ok = po.LookupExperiment("exp3") + if !ok || val != "val3=val4" { + t.Errorf("LookupExperiment(exp3) = (%q, %t), want (\"val3=val4\", true)", val, ok) + } + + val, ok = po.LookupExperiment("exp4") + if ok || val != "" { + t.Errorf("LookupExperiment(exp4) = (%q, %t), want (\"\", false)", val, ok) + } +} + +func TestParseOptionsFromProto_SDKOptionsPromotion(t *testing.T) { + optionsStruct, err := structpb.NewStruct(map[string]interface{}{ + "options": map[string]interface{}{ + "region": "us-central1", + }, + "beam:option:experiments:v1": []interface{}{"expA", "expB"}, + "beam:option:go_options:v1": map[string]interface{}{ + "options": map[string]interface{}{ + "dataflow_service_options": "enable_google_cloud_profiler,enable_new_custom_feature", + }, + }, + }) + if err != nil { + t.Fatalf("failed to create proto Struct: %v", err) + } + + po := ParseOptionsFromProto(optionsStruct, "go_options") + if got, err := po.GetString("region"); err != nil || got != "us-central1" { + t.Errorf("GetString(region) = (%q, %v), want (\"us-central1\", nil)", got, err) + } + if !po.HasExperiment("expA") || !po.HasExperiment("expB") { + t.Errorf("expected experiments expA and expB to be present, options: %+v", po.options) + } + if got, err := po.GetString("dataflow_service_options"); err != nil || got != "enable_google_cloud_profiler,enable_new_custom_feature" { + t.Errorf("GetString(dataflow_service_options) = (%q, %v), want (\"enable_google_cloud_profiler,enable_new_custom_feature\", nil)", got, err) + } + serviceOpts, err := po.GetStringSlice("dataflow_service_options") + if err != nil || len(serviceOpts) != 2 || serviceOpts[0] != "enable_google_cloud_profiler" || serviceOpts[1] != "enable_new_custom_feature" { + t.Errorf("GetStringSlice(dataflow_service_options) = (%v, %v), want ([enable_google_cloud_profiler, enable_new_custom_feature], nil)", serviceOpts, err) + } + goOpts, ok := po.options["go_options"].(map[string]any) + if !ok { + t.Errorf("expected go_options map to be present, options: %+v", po.options) + } + nestedOpts, ok := goOpts["options"].(map[string]any) + if !ok { + t.Errorf("expected nested options map inside go_options, got: %+v", goOpts) + } + if got := nestedOpts["dataflow_service_options"]; got != "enable_google_cloud_profiler,enable_new_custom_feature" { + t.Errorf("got dataflow_service_options = %v, want enable_google_cloud_profiler,enable_new_custom_feature", got) + } +} diff --git a/sdks/go/examples/wasm/README.md b/sdks/go/examples/wasm/README.md index e4ab54d4a3ed..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.17,1.18,1.19,1.20' +'flink_versions: 1.19,1.20,2.0,2.1,2.2' ``` -#### 2. Set to the latest flink runner version i.e. 1.16 +#### 2. Set to the latest flink runner version i.e. 2.2 ```shell -FLINK_VERSION=1.16 +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/go/pkg/beam/artifact/options.go b/sdks/go/pkg/beam/artifact/options.go deleted file mode 100644 index 47356433161c..000000000000 --- a/sdks/go/pkg/beam/artifact/options.go +++ /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 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 deleted file mode 100644 index a9f0e4bb7e35..000000000000 --- a/sdks/go/pkg/beam/artifact/options_test.go +++ /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 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/go/pkg/beam/coder.go b/sdks/go/pkg/beam/coder.go index b03b739ed7be..c38a8e37ecce 100644 --- a/sdks/go/pkg/beam/coder.go +++ b/sdks/go/pkg/beam/coder.go @@ -89,6 +89,21 @@ func (c Coder) String() string { return c.coder.String() } +// IsDeterministic reports whether this coder produces a byte-deterministic +// encoding: encoding two equal values always yields identical byte +// sequences. +// +// Determinism is required for any coder used as a state key in a stateful +// DoFn or as the key component of a KV consumed by GroupByKey / +// GroupIntoBatches. A non-deterministic key coder would silently corrupt +// state keying, splintering state across apparently-distinct keys. +func (c Coder) IsDeterministic() bool { + if c.coder == nil { + return false + } + return c.coder.IsDeterministic() +} + // NewElementEncoder returns a new encoding function for the given type. func NewElementEncoder(t reflect.Type) ElementEncoder { c, err := inferCoder(typex.New(t)) @@ -249,6 +264,8 @@ func inferCoder(t FullType) (*coder.Coder, error) { // are non-windowed? We either need to know the windowing strategy or // we should remove this case. return &coder.Coder{Kind: coder.WindowedValue, T: t, Components: c, Window: coder.NewGlobalWindow()}, nil + case typex.ShardedKeyType: + return &coder.Coder{Kind: coder.ShardedKey, T: t, Components: c}, nil default: panic(fmt.Sprintf("Unexpected composite type: %v", t)) diff --git a/sdks/go/pkg/beam/core/core.go b/sdks/go/pkg/beam/core/core.go index e3a8900b1e77..ce75136ccdd7 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.77.0.dev" // DefaultDockerImage represents the associated image for this release. DefaultDockerImage = "apache/beam_go_sdk:" + SdkVersion diff --git a/sdks/go/pkg/beam/core/graph/coder/coder.go b/sdks/go/pkg/beam/core/graph/coder/coder.go index 28e235860bd9..f5f7aa2d7575 100644 --- a/sdks/go/pkg/beam/core/graph/coder/coder.go +++ b/sdks/go/pkg/beam/core/graph/coder/coder.go @@ -84,6 +84,18 @@ func (c *CustomCoder) String() string { return fmt.Sprintf("%v[%v;%v]", c.Type, c.Name, c.ID) } +// IsDeterministic reports whether this CustomCoder produces a deterministic +// encoding. A CustomCoder is deterministic iff the user opted in by +// registering the coder via RegisterDeterministicCoder. Default is false +// (conservative): a non-deterministic key coder would silently corrupt state +// keying in stateful DoFns. +func (c *CustomCoder) IsDeterministic() bool { + if c == nil { + return false + } + return isCustomCoderDeterministic(c.Type) +} + // Type signatures of encode/decode for verification. var ( encodeSig = &funcx.Signature{ @@ -156,6 +168,20 @@ func NewCustomCoder(id string, t reflect.Type, encode, decode any) (*CustomCoder return c, nil } +// NewCustomCoderWithFuncs creates a CustomCoder from pre-wrapped +// reflectx.Func values. This allows the caller to control the Name() +// returned by each function — critical for closures inside Go generic +// functions where the compiler assigns identical names to different +// type instantiations. +func NewCustomCoderWithFuncs(id string, t reflect.Type, enc, dec *funcx.Fn) *CustomCoder { + return &CustomCoder{ + Name: id, + Type: t, + Enc: enc, + Dec: dec, + } +} + // Kind represents the type of coder used. type Kind string @@ -195,6 +221,17 @@ const ( // // TODO(https://github.com/apache/beam/issues/18032): once this JIRA is done, this coder should become the new thing. CoGBK Kind = "CoGBK" + + // ShardedKey encodes a user key wrapped with an opaque shard identifier, + // used by GroupIntoBatchesWithShardedKey to distribute a single logical + // key's processing across workers. Wire format + // (beam:coder:sharded_key:v1): + // + // ByteArrayCoder.encode(shardId) ++ keyCoder.encode(key) + // + // matching sdks/java/core ShardedKey and the Python sharded_key + // encoding for cross-SDK interoperability. + ShardedKey Kind = "SK" ) // Coder is a description of how to encode and decode values of a given type. @@ -273,6 +310,62 @@ func (c *Coder) String() string { return ret } +// IsDeterministic reports whether this Coder produces a deterministic +// byte encoding — i.e. encoding two equal values always yields identical +// byte sequences. +// +// Determinism is a prerequisite for any Coder used as a state key in a +// stateful DoFn, as the key component of a KV consumed by GroupByKey, or as +// a grouping key in a CoGroupByKey. A non-deterministic key coder causes +// state-keyed operations to silently corrupt: two encodings of the same +// logical key map to distinct physical keys, splintering state across +// apparently-distinct keys. +// +// Built-in coders for primitive types (bytes, bool, varint, double, +// string) are deterministic. Composite coders (KV, Iterable, Nullable) +// are deterministic iff every component is. The Map coder is +// non-deterministic because Go map iteration order is unspecified. +// Custom user-registered coders are non-deterministic by default; users +// opt in by registering with RegisterDeterministicCoder. +func (c *Coder) IsDeterministic() bool { + if c == nil { + return false + } + switch c.Kind { + case Bytes, Bool, VarInt, Double, String: + return true + case Custom: + return c.Custom.IsDeterministic() + case KV, CoGBK, Nullable, Iterable, LP, ShardedKey: + for _, comp := range c.Components { + if !comp.IsDeterministic() { + return false + } + } + return true + case WindowedValue, ParamWindowedValue, Window, Timer, PaneInfo, IW: + // These coders are structural: they wrap runner/window bookkeeping that is + // not used as a state key. Recurse into the data component when present so + // that a non-deterministic inner coder is still reported. + for _, comp := range c.Components { + if !comp.IsDeterministic() { + return false + } + } + return true + case Row: + // Schema (row) coding encodes fields in a fixed field-id order and + // produces a stable byte layout; however, row coders may contain fields + // backed by custom coders we cannot introspect here. Conservative + // default: return false and allow users to opt in via schema-level + // determinism guarantees once they're exposed. Structs wanting + // deterministic behavior can register a deterministic custom coder + // instead. + return false + } + return false +} + // NewBytes returns a new []byte coder using the built-in scheme. It // is always nested, for now. func NewBytes() *Coder { @@ -428,6 +521,29 @@ func NewCoGBK(components []*Coder) *Coder { } } +// NewSK returns a coder for ShardedKey-typed values. The component +// keyCoder encodes the user key; the ShardID is encoded as a +// length-prefixed byte string preceding it (beam:coder:sharded_key:v1). +// +// The resulting FullType root is typex.ShardedKeyType with the key's +// FullType as the single component, following the same Composite +// pattern as KV. +func NewSK(keyCoder *Coder) *Coder { + if keyCoder == nil { + panic("NewSK: keyCoder must not be nil") + } + return &Coder{ + Kind: ShardedKey, + T: typex.New(typex.ShardedKeyType, keyCoder.T), + Components: []*Coder{keyCoder}, + } +} + +// IsSK returns true iff the coder is for a ShardedKey. +func IsSK(c *Coder) bool { + return c != nil && c.Kind == ShardedKey +} + // SkipW returns the data coder used by a WindowedValue, or returns the coder. This // allows code to seamlessly traverse WindowedValues without additional conditional // code. diff --git a/sdks/go/pkg/beam/core/graph/coder/coder_test.go b/sdks/go/pkg/beam/core/graph/coder/coder_test.go index 040a0402c85e..b60cbd72848e 100644 --- a/sdks/go/pkg/beam/core/graph/coder/coder_test.go +++ b/sdks/go/pkg/beam/core/graph/coder/coder_test.go @@ -578,6 +578,72 @@ func TestNewNullable(t *testing.T) { } } +func TestCoder_IsDeterministic(t *testing.T) { + ints := NewVarInt() + bytes := NewBytes() + bools := NewBool() + doubles := NewDouble() + strs := NewString() + + enc := func(string) []byte { return nil } + dec := func([]byte) string { return "" } + + nonDetCustom, err := NewCustomCoder("nonDet", reflectx.String, enc, dec) + if err != nil { + t.Fatal(err) + } + nonDetC := &Coder{Kind: Custom, Custom: nonDetCustom, T: typex.New(reflectx.String)} + + // Register a deterministic custom coder for a dedicated type. + type detType struct{} + detT := reflect.TypeOf((*detType)(nil)).Elem() + detEnc := func(detType) []byte { return nil } + detDec := func([]byte) detType { return detType{} } + RegisterDeterministicCoder(detT, detEnc, detDec) + detCustom, err := NewCustomCoder("det", detT, detEnc, detDec) + if err != nil { + t.Fatal(err) + } + detC := &Coder{Kind: Custom, Custom: detCustom, T: typex.New(detT)} + + tests := []struct { + name string + c *Coder + want bool + }{ + {"nil", nil, false}, + {"bytes", bytes, true}, + {"bool", bools, true}, + {"varint", ints, true}, + {"double", doubles, true}, + {"string", strs, true}, + {"nonDetCustom", nonDetC, false}, + {"detCustom", detC, true}, + {"KV_bytes_varint", NewKV([]*Coder{bytes, ints}), true}, + {"KV_bytes_nonDet", NewKV([]*Coder{bytes, nonDetC}), false}, + {"KV_nonDet_bytes", NewKV([]*Coder{nonDetC, bytes}), false}, + {"iterable_varint", NewI(ints), true}, + {"iterable_nonDet", NewI(nonDetC), false}, + {"nullable_string", NewN(strs), true}, + {"nullable_nonDet", NewN(nonDetC), false}, + {"CoGBK_bytes_varint", NewCoGBK([]*Coder{bytes, ints}), true}, + {"CoGBK_nonDet_varint", NewCoGBK([]*Coder{nonDetC, ints}), false}, + {"WindowedValue_varint", NewW(ints, NewGlobalWindow()), true}, + {"WindowedValue_nonDet", NewW(nonDetC, NewGlobalWindow()), false}, + {"Row", NewR(typex.New(reflect.TypeOf((*namedTypeForTest)(nil)))), false}, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + got := test.c.IsDeterministic() + if got != test.want { + t.Errorf("IsDeterministic(%v) = %v, want %v", test.c, got, test.want) + } + }) + } +} + func TestNewCoGBK(t *testing.T) { bytes := NewBytes() ints := NewVarInt() diff --git a/sdks/go/pkg/beam/core/graph/coder/registry.go b/sdks/go/pkg/beam/core/graph/coder/registry.go index f6677071b860..05d211898df1 100644 --- a/sdks/go/pkg/beam/core/graph/coder/registry.go +++ b/sdks/go/pkg/beam/core/graph/coder/registry.go @@ -18,12 +18,14 @@ package coder import ( "reflect" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/funcx" "github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors" ) var ( - coderRegistry = make(map[reflect.Type]func(reflect.Type) *CustomCoder) - interfaceOrdering []reflect.Type + coderRegistry = make(map[reflect.Type]func(reflect.Type) *CustomCoder) + interfaceOrdering []reflect.Type + deterministicRegistry = make(map[reflect.Type]bool) ) // RegisterCoder registers a user defined coder for a given type, and will @@ -76,6 +78,60 @@ func RegisterCoder(t reflect.Type, enc, dec any) { } } +// RegisterDeterministicCoderWithFuncs is like RegisterDeterministicCoder +// but accepts pre-wrapped reflectx.Func values (typically built via +// reflectx.MakeFuncWithName) so the caller controls the function name +// used during cross-worker serialization. This is required for +// closures inside Go generic functions where different type +// instantiations produce closures with the same compiler name. +func RegisterDeterministicCoderWithFuncs(t reflect.Type, encFn, decFn *funcx.Fn) { + name := t.String() + coderRegistry[t] = func(rt reflect.Type) *CustomCoder { + return NewCustomCoderWithFuncs(name, rt, encFn, decFn) + } + deterministicRegistry[t] = true +} + +// RegisterDeterministicCoder is the deterministic-affirming counterpart to +// RegisterCoder: it registers the (enc, dec) pair for t AND records that the +// resulting CustomCoder produces a deterministic encoding. The caller asserts +// by calling this function that enc produces byte-identical output for any +// two equal input values of type t. +// +// Deterministic coders are required for any type used as a state key in a +// stateful DoFn, as the key of a KV consumed by GroupByKey / GroupIntoBatches, +// or as a grouping key for CoGroupByKey. +// +// Prefer this over RegisterCoder whenever the encoded type may be used as a +// key. For types that cannot guarantee determinism (e.g. encodings backed by +// map[K]V iteration order), use the plain RegisterCoder. +func RegisterDeterministicCoder(t reflect.Type, enc, dec any) { + RegisterCoder(t, enc, dec) + deterministicRegistry[t] = true +} + +// isCustomCoderDeterministic returns true iff t has been registered via +// RegisterDeterministicCoder. +func isCustomCoderDeterministic(t reflect.Type) bool { + if t == nil { + return false + } + if ok, present := deterministicRegistry[t]; present { + return ok + } + // Also match against interface registrations: if the type implements a + // registered-deterministic interface, honor that. + for rt, det := range deterministicRegistry { + if !det { + continue + } + if rt.Kind() == reflect.Interface && t.Implements(rt) { + return true + } + } + return false +} + // LookupCustomCoder returns the custom coder for the type if any, // first checking for a specific matching type, and then iterating // through registered interface coders in reverse registration order. diff --git a/sdks/go/pkg/beam/core/graph/coder/sharded_key_test.go b/sdks/go/pkg/beam/core/graph/coder/sharded_key_test.go new file mode 100644 index 000000000000..fc9b93b0070f --- /dev/null +++ b/sdks/go/pkg/beam/core/graph/coder/sharded_key_test.go @@ -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 coder + +import ( + "reflect" + "testing" + + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/typex" +) + +func TestNewSK(t *testing.T) { + t.Run("nilKeyCoder_panics", func(t *testing.T) { + defer func() { + if p := recover(); p == nil { + t.Fatal("expected panic on nil keyCoder, got none") + } + }() + NewSK(nil) + }) + + t.Run("valid_string_key", func(t *testing.T) { + sk := NewSK(NewString()) + if sk.Kind != ShardedKey { + t.Fatalf("Kind = %v, want %v", sk.Kind, ShardedKey) + } + if !IsSK(sk) { + t.Fatalf("IsSK(%v) = false, want true", sk) + } + if len(sk.Components) != 1 { + t.Fatalf("Components = %d, want 1", len(sk.Components)) + } + if sk.Components[0].Kind != String { + t.Fatalf("Components[0].Kind = %v, want %v", sk.Components[0].Kind, String) + } + if sk.T.Type() != typex.ShardedKeyType { + t.Fatalf("T.Type() = %v, want %v", sk.T.Type(), typex.ShardedKeyType) + } + }) + + t.Run("nested_composite_panics", func(t *testing.T) { + defer func() { + if p := recover(); p == nil { + t.Fatal("expected panic on nested composite key, got none") + } + }() + // KV components inside a ShardedKey key are disallowed by fulltype.New. + NewSK(NewKV([]*Coder{NewString(), NewBytes()})) + }) +} + +func TestSK_IsDeterministic(t *testing.T) { + detSK := NewSK(NewString()) + if !detSK.IsDeterministic() { + t.Errorf("ShardedKey.IsDeterministic() = false, want true") + } + + nonDet, err := NewCustomCoder("nonDet", reflect.TypeOf(""), + func(string) []byte { return nil }, func([]byte) string { return "" }) + if err != nil { + t.Fatal(err) + } + nonDetC := &Coder{Kind: Custom, Custom: nonDet, T: typex.New(reflect.TypeOf(""))} + nonDetSK := NewSK(nonDetC) + if nonDetSK.IsDeterministic() { + t.Errorf("ShardedKey.IsDeterministic() = true, want false") + } +} diff --git a/sdks/go/pkg/beam/core/runtime/exec/coder.go b/sdks/go/pkg/beam/core/runtime/exec/coder.go index 2c21ebea56b5..b68943355383 100644 --- a/sdks/go/pkg/beam/core/runtime/exec/coder.go +++ b/sdks/go/pkg/beam/core/runtime/exec/coder.go @@ -166,6 +166,11 @@ func MakeElementEncoder(c *coder.Coder) ElementEncoder { be: boolEncoder{}, } + case coder.ShardedKey: + return &shardedKeyEncoder{ + key: MakeElementEncoder(c.Components[0]), + } + default: panic(fmt.Sprintf("Unexpected coder: %v", c)) } @@ -288,6 +293,11 @@ func MakeElementDecoder(c *coder.Coder) ElementDecoder { bd: boolDecoder{}, } + case coder.ShardedKey: + return &shardedKeyDecoder{ + key: MakeElementDecoder(c.Components[0]), + } + default: panic(fmt.Sprintf("Unexpected coder: %v", c)) } @@ -1356,3 +1366,56 @@ func decodeTimer(dec ElementDecoder, win WindowDecoder, r io.Reader) (TimerRecv, return tm, nil } + +// shardedKeyEncoder encodes ShardedKey-typed values in the standard +// beam:coder:sharded_key:v1 wire format: +// +// ByteArrayCoder.encode(ShardID) ++ keyCoder.encode(Key) +// +// Runtime values are carried by a FullValue whose Elm holds the user key +// and whose Elm2 holds the []byte shard identifier — the same two-part +// convention used by the KV coder. This matches the Java +// util.ShardedKey.Coder and Python sharded_key encodings exactly; any +// divergence of a single byte would silently corrupt cross-SDK pipelines. +type shardedKeyEncoder struct { + key ElementEncoder +} + +func (e *shardedKeyEncoder) Encode(val *FullValue, w io.Writer) error { + shardID, ok := val.Elm2.([]byte) + if !ok { + return errors.Errorf( + "shardedKeyEncoder: Elm2 must be []byte shardID (got %T)", val.Elm2) + } + if err := coder.EncodeBytes(shardID, w); err != nil { + return errors.WithContext(err, "shardedKeyEncoder: shardID") + } + return e.key.Encode(&FullValue{Elm: val.Elm}, w) +} + +// shardedKeyDecoder is the inverse of shardedKeyEncoder. Decoded values +// are placed in FullValue{Elm: key, Elm2: shardID}. +type shardedKeyDecoder struct { + key ElementDecoder +} + +func (d *shardedKeyDecoder) DecodeTo(r io.Reader, fv *FullValue) error { + shardID, err := coder.DecodeBytes(r) + if err != nil { + return errors.WithContext(err, "shardedKeyDecoder: shardID") + } + keyFV, err := d.key.Decode(r) + if err != nil { + return errors.WithContext(err, "shardedKeyDecoder: key") + } + *fv = FullValue{Elm: keyFV.Elm, Elm2: shardID} + return nil +} + +func (d *shardedKeyDecoder) Decode(r io.Reader) (*FullValue, error) { + fv := &FullValue{} + if err := d.DecodeTo(r, fv); err != nil { + return nil, err + } + return fv, nil +} diff --git a/sdks/go/pkg/beam/core/runtime/exec/coder_test.go b/sdks/go/pkg/beam/core/runtime/exec/coder_test.go index 75d18e533cf1..155fd72776e1 100644 --- a/sdks/go/pkg/beam/core/runtime/exec/coder_test.go +++ b/sdks/go/pkg/beam/core/runtime/exec/coder_test.go @@ -158,6 +158,90 @@ func compareFV(t *testing.T, got *FullValue, want *FullValue) { } } +// TestShardedKeyCoder_WireFormat verifies the exact bytes produced by the +// ShardedKey coder against the standard_coders.yaml fixtures (lines +// 501-521, urn "beam:coder:sharded_key:v1" with a string_utf8 key +// component). A single divergent byte would silently corrupt cross-SDK +// pipelines on Dataflow / Flink. +func TestShardedKeyCoder_WireFormat(t *testing.T) { + c := coder.NewSK(coder.NewString()) + enc := MakeElementEncoder(c) + dec := MakeElementDecoder(c) + + type fixture struct { + name string + key string + shardID []byte + wire []byte + } + fixtures := []fixture{ + { + name: "empty_empty", + key: "", + shardID: []byte{}, + wire: []byte{0x00, 0x00}, + }, + { + name: "shardId_emptyKey", + key: "", + shardID: []byte("shard_id"), + wire: append( + append([]byte{0x08}, []byte("shard_id")...), + 0x00, + ), + }, + { + name: "shardId_key", + key: "key", + shardID: []byte("shard_id"), + wire: append( + append([]byte{0x08}, []byte("shard_id")...), + append([]byte{0x03}, []byte("key")...)..., + ), + }, + { + name: "emptyShardId_key", + key: "key", + shardID: []byte{}, + wire: append([]byte{0x00, 0x03}, []byte("key")...), + }, + } + + for _, f := range fixtures { + f := f + t.Run(f.name, func(t *testing.T) { + var buf bytes.Buffer + // ShardedKey values are carried as FullValue{Elm: key, Elm2: shardID}. + if err := enc.Encode(&FullValue{Elm: f.key, Elm2: f.shardID}, &buf); err != nil { + t.Fatalf("Encode: %v", err) + } + if got := buf.Bytes(); !bytes.Equal(got, f.wire) { + t.Fatalf("Encode: got bytes %#v, want %#v", got, f.wire) + } + + fv, err := dec.Decode(bytes.NewReader(f.wire)) + if err != nil { + t.Fatalf("Decode: %v", err) + } + gotKey, ok := fv.Elm.(string) + if !ok { + t.Fatalf("Decode Elm: got %T, want string", fv.Elm) + } + if gotKey != f.key { + t.Errorf("Decode Elm: got %q, want %q", gotKey, f.key) + } + gotShard, ok := fv.Elm2.([]byte) + if !ok { + t.Fatalf("Decode Elm2: got %T, want []byte", fv.Elm2) + } + // Both sides "empty" — accept nil or zero-length slice equivalence. + if len(gotShard) != len(f.shardID) || (len(gotShard) > 0 && !bytes.Equal(gotShard, f.shardID)) { + t.Errorf("Decode Elm2: got %#v, want %#v", gotShard, f.shardID) + } + }) + } +} + func TestIterableCoder(t *testing.T) { cod := coder.NewI(coder.NewVarInt()) wantVals := []int64{8, 24, 72} 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)) diff --git a/sdks/go/pkg/beam/core/runtime/graphx/coder.go b/sdks/go/pkg/beam/core/runtime/graphx/coder.go index 2b769c873ec4..ced4d34679b9 100644 --- a/sdks/go/pkg/beam/core/runtime/graphx/coder.go +++ b/sdks/go/pkg/beam/core/runtime/graphx/coder.go @@ -47,6 +47,7 @@ const ( urnTimerCoder = "beam:coder:timer:v1" urnRowCoder = "beam:coder:row:v1" urnNullableCoder = "beam:coder:nullable:v1" + urnShardedKeyCoder = "beam:coder:sharded_key:v1" urnGlobalWindow = "beam:coder:global_window:v1" urnIntervalWindow = "beam:coder:interval_window:v1" @@ -74,6 +75,7 @@ func knownStandardCoders() []string { urnRowCoder, urnNullableCoder, urnTimerCoder, + urnShardedKeyCoder, } } @@ -378,6 +380,15 @@ func (b *CoderUnmarshaller) makeCoder(id string, c *pipepb.Coder) (*coder.Coder, return nil, err } return coder.NewN(elm), nil + case urnShardedKeyCoder: + if len(components) != 1 { + return nil, errors.Errorf("could not unmarshal sharded_key coder from %v, expected one component (key) but got %d", c, len(components)) + } + keyC, err := b.Coder(components[0]) + if err != nil { + return nil, err + } + return coder.NewSK(keyC), nil case urnIntervalWindow: return coder.NewIntervalWindowCoder(), nil @@ -493,6 +504,16 @@ func (b *CoderMarshaller) Add(c *coder.Coder) (string, error) { stream := b.internBuiltInCoder(urnIterableCoder, value) return b.internBuiltInCoder(urnKVCoder, comp[0], stream), nil + case coder.ShardedKey: + comp, err := b.AddMulti(c.Components) + if err != nil { + return "", errors.Wrapf(err, "failed to marshal ShardedKey coder %v", c) + } + if len(comp) != 1 { + return "", errors.Errorf("ShardedKey coder requires exactly 1 component (key), got %d", len(comp)) + } + return b.internBuiltInCoder(urnShardedKeyCoder, comp...), nil + case coder.WindowedValue: comp := []string{} if ids, err := b.AddMulti(c.Components); err != nil { diff --git a/sdks/go/pkg/beam/core/runtime/symbols.go b/sdks/go/pkg/beam/core/runtime/symbols.go index 84afe9b769af..9640af288b6b 100644 --- a/sdks/go/pkg/beam/core/runtime/symbols.go +++ b/sdks/go/pkg/beam/core/runtime/symbols.go @@ -83,6 +83,26 @@ func RegisterFunction(fn any) { cache[key] = fn } +// RegisterFunctionWithName registers fn under the given name, +// overriding the automatically derived symbol name. This is necessary +// for closures produced by Go generic functions where multiple type +// instantiations generate closures with the same compiler-assigned +// name (e.g. "pkg.Func[...].func1") — without distinct names the +// last registration wins and cross-worker deserialization resolves +// the wrong function. +// +// Callers must ensure that name is stable across process invocations +// (pipeline driver and workers must agree). A typical choice is +// ".[].enc". +// +// Must be called in init() only. +func RegisterFunctionWithName(name string, fn any) { + if initialized { + panic("Init hooks have already run. Register function during init() instead.") + } + cache[name] = fn +} + // ResolveFunction resolves the runtime value of a given function by symbol name // and type. func ResolveFunction(name string, t reflect.Type) (any, error) { diff --git a/sdks/go/pkg/beam/core/typex/class.go b/sdks/go/pkg/beam/core/typex/class.go index 570b7e279218..6c8f3549893e 100644 --- a/sdks/go/pkg/beam/core/typex/class.go +++ b/sdks/go/pkg/beam/core/typex/class.go @@ -231,10 +231,10 @@ func IsUniversal(t reflect.Type) bool { } // IsComposite returns true iff the given type is one of the predefined -// Composite marker types: KV, CoGBK or WindowedValue. +// Composite marker types: KV, CoGBK, WindowedValue, Timers or ShardedKey. func IsComposite(t reflect.Type) bool { switch t { - case KVType, CoGBKType, WindowedValueType, TimersType: + case KVType, CoGBKType, WindowedValueType, TimersType, ShardedKeyType: return true default: return false diff --git a/sdks/go/pkg/beam/core/typex/fulltype.go b/sdks/go/pkg/beam/core/typex/fulltype.go index ff5520c28617..88e26568dffe 100644 --- a/sdks/go/pkg/beam/core/typex/fulltype.go +++ b/sdks/go/pkg/beam/core/typex/fulltype.go @@ -89,6 +89,8 @@ func printShortComposite(t reflect.Type) string { return "KV" case NullableType: return "Nullable" + case ShardedKeyType: + return "SK" default: return fmt.Sprintf("invalid(%v)", t) } @@ -146,6 +148,14 @@ func New(t reflect.Type, components ...FullType) FullType { return &tree{class, t, components} case TimersType: return &tree{class, t, components} + case ShardedKeyType: + if len(components) != 1 { + panic(fmt.Sprintf("Invalid number of components for ShardedKey: %v, %v", t, components)) + } + if components[0].Class() == Composite { + panic(fmt.Sprintf("Invalid to nest composite inside ShardedKey: %v, %v", t, components)) + } + return &tree{class, t, components} default: panic(fmt.Sprintf("Unexpected composite type: %v", t)) } @@ -226,6 +236,19 @@ func NewCoGBK(components ...FullType) FullType { return New(CoGBKType, components...) } +// IsShardedKey returns true iff the type is a ShardedKey. +func IsShardedKey(t FullType) bool { + return t.Type() == ShardedKeyType +} + +// NewShardedKey constructs a new ShardedKey FullType wrapping the given +// key component. The ShardedKey has exactly one component — the user key +// type — because the ShardID byte-string has a fixed representation and +// is not a user-configurable type. +func NewShardedKey(keyType FullType) FullType { + return New(ShardedKeyType, keyType) +} + // IsStructurallyAssignable returns true iff a from value is structurally // assignable to the to value of the given types. Types that are // "structurally assignable" (SA) are assignable if type variables are diff --git a/sdks/go/pkg/beam/core/typex/special.go b/sdks/go/pkg/beam/core/typex/special.go index 9093ddc782c3..6cf1cb99f757 100644 --- a/sdks/go/pkg/beam/core/typex/special.go +++ b/sdks/go/pkg/beam/core/typex/special.go @@ -44,6 +44,7 @@ var ( CoGBKType = reflect.TypeOf((*CoGBK)(nil)).Elem() WindowedValueType = reflect.TypeOf((*WindowedValue)(nil)).Elem() BundleFinalizationType = reflect.TypeOf((*BundleFinalization)(nil)).Elem() + ShardedKeyType = reflect.TypeOf((*ShardedKey)(nil)).Elem() ) // T, U, V, W, X, Y, Z are universal types. They play the role of generic @@ -128,8 +129,10 @@ type Timers struct { Pane PaneInfo } -// KV, Nullable, CoGBK, WindowedValue represent composite generic types. They are not used -// directly in user code signatures, but only in FullTypes. +// KV, Nullable, CoGBK, WindowedValue, ShardedKey represent composite +// generic types. They are not used directly in user code signatures, but +// only in FullTypes — each appears as the root of a FullType tree whose +// component list holds the concrete sub-types. type KV struct{} @@ -138,3 +141,14 @@ type Nullable struct{} type CoGBK struct{} type WindowedValue struct{} + +// ShardedKey is the composite marker for sharded-key encoded pairs +// (user key + opaque shard identifier). It is never constructed by user +// code; it appears only as the root of a FullType tree whose single +// component is the key's FullType. +// +// Runtime values are carried through FullValue.Elm (user key) and +// FullValue.Elm2 ([]byte shardID). The corresponding wire encoding is +// URN beam:coder:sharded_key:v1, byte-identical to the Java and Python +// sharded_key encodings. +type ShardedKey struct{} diff --git a/sdks/go/pkg/beam/core/util/reflectx/call.go b/sdks/go/pkg/beam/core/util/reflectx/call.go index 9b1955427f7a..e14ed016425d 100644 --- a/sdks/go/pkg/beam/core/util/reflectx/call.go +++ b/sdks/go/pkg/beam/core/util/reflectx/call.go @@ -87,6 +87,37 @@ func (c *reflectFunc) Call(args []any) []any { return Interface(c.fn.Call(ValueOf(args))) } +// MakeFuncWithName returns a Func that wraps fn but whose Name() +// returns the provided name instead of the compiler-derived symbol. +// This is essential for closures inside Go generic functions: all +// type instantiations produce closures with the same compiler name +// (e.g. "pkg.Func[...].func1"), so the default name-based +// serialization cannot distinguish them. A stable, type-qualified +// name ensures cross-worker deserialization resolves the correct +// function. +func MakeFuncWithName(name string, fn any) Func { + inner := MakeFunc(fn) + return &namedFunc{inner: inner, name: name} +} + +type namedFunc struct { + inner Func + name string +} + +func (f *namedFunc) Name() string { return f.name } +func (f *namedFunc) Type() reflect.Type { return f.inner.Type() } +func (f *namedFunc) Call(args []any) []any { return f.inner.Call(args) } + +// Interface returns the original unwrapped function, which +// runtime.RegisterFunction needs for pointer extraction. +func (f *namedFunc) Interface() any { + if rf, ok := f.inner.(*reflectFunc); ok { + return rf.fn.Interface() + } + return nil +} + // CallNoPanic calls the given Func and catches any panic. func CallNoPanic(fn Func, args []any) (ret []any, err error) { defer func() { 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() diff --git a/sdks/go/pkg/beam/pcollection.go b/sdks/go/pkg/beam/pcollection.go index e5dc63289f39..2138266a667a 100644 --- a/sdks/go/pkg/beam/pcollection.go +++ b/sdks/go/pkg/beam/pcollection.go @@ -17,6 +17,7 @@ package beam import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/window" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/typex" "github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors" ) @@ -80,6 +81,21 @@ func (p PCollection) SetCoder(c Coder) error { return nil } +// WindowingStrategy returns the windowing strategy of the PCollection. It +// describes how elements are assigned to windows and — for transforms that +// honor it — the allowed lateness after which windows are closed. +// +// Transforms that use state and timers keyed by window, such as +// GroupIntoBatches, consult this strategy to compute end-of-window +// event-time timers and to bound partial-batch flushes by the pipeline's +// allowed lateness. +func (p PCollection) WindowingStrategy() *window.WindowingStrategy { + if !p.IsValid() { + panic("Invalid PCollection") + } + return p.n.WindowingStrategy() +} + func (p PCollection) String() string { if !p.IsValid() { return "(invalid)" 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/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) { diff --git a/sdks/go/pkg/beam/runners/prism/internal/coders.go b/sdks/go/pkg/beam/runners/prism/internal/coders.go index d326a332b8d3..0f770849a984 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/coders.go +++ b/sdks/go/pkg/beam/runners/prism/internal/coders.go @@ -367,6 +367,17 @@ func pullDecoderNoAlloc(c *pipepb.Coder, coders map[string]*pipepb.Coder) func(i ed(r) wd(r) } + case urns.CoderShardedKey: + ccids := c.GetComponentCoderIds() + if len(ccids) != 1 { + panic(fmt.Sprintf("ShardedKey coder must have only 1 component: %s", prototext.Format(c))) + } + kd := pullDecoderNoAlloc(coders[ccids[0]], coders) + return func(r io.Reader) { + l, _ := coder.DecodeVarInt(r) + ioutilx.ReadN(r, int(l)) + kd(r) + } case urns.CoderRow: panic(fmt.Sprintf("Runner forgot to LP this Row Coder. %v", prototext.Format(c))) default: diff --git a/sdks/go/pkg/beam/runners/prism/internal/coders_test.go b/sdks/go/pkg/beam/runners/prism/internal/coders_test.go index 4656a94e03ec..1d1a8b6d4596 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/coders_test.go +++ b/sdks/go/pkg/beam/runners/prism/internal/coders_test.go @@ -370,6 +370,22 @@ func Test_pullDecoder(t *testing.T) { }, }, []byte{3, 0}, + }, { + "sharded_key", + &pipepb.Coder{ + Spec: &pipepb.FunctionSpec{ + Urn: urns.CoderShardedKey, + }, + ComponentCoderIds: []string{"key"}, + }, + map[string]*pipepb.Coder{ + "key": { + Spec: &pipepb.FunctionSpec{ + Urn: urns.CoderVarInt, + }, + }, + }, + []byte{3, 1, 2, 3, 255, 3}, }, } for _, test := range tests { 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..2b502c679db9 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() { @@ -919,6 +922,7 @@ func (em *ElementManager) PersistBundle(rb RunBundle, col2Coders map[string]PCol // ProcessingTime timers need to be scheduled into the processing time based queue. newHolds, ptRefreshes := em.triageTimers(d, inputInfo, stage) + // TODO(https://github.com/apache/beam/issues/39446) // Return unprocessed to this stage's pending // TODO sort out pending element watermark holds for process continuation residuals. unprocessedElements := reElementResiduals(residuals.Data, inputInfo, rb) @@ -2496,7 +2500,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()) } 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()) } }() 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/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") + } +} 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/go/pkg/beam/transforms/batch/batch.go b/sdks/go/pkg/beam/transforms/batch/batch.go new file mode 100644 index 000000000000..6cc378c2f44e --- /dev/null +++ b/sdks/go/pkg/beam/transforms/batch/batch.go @@ -0,0 +1,677 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 batch provides transforms that group elements of a KV-keyed +// PCollection into batches of a target size for downstream per-batch +// processing (rate-limited API calls, bulk sinks, etc.). +// +// GroupIntoBatches mirrors the behavior of the Java and Python +// transforms of the same name. GroupIntoBatchesWithShardedKey adds +// opaque per-element shard identifiers to the keys so the processing +// of a single hot logical key spreads across multiple workers. +// +// # Behavior +// +// Given a PCollection>, GroupIntoBatches buffers values per +// key and emits batches as KV whenever one of the following +// limits is reached: +// +// - len(batch) reaches BatchSize, OR +// - sum of byte sizes reaches BatchSizeBytes, OR +// - MaxBufferingDuration elapses in processing time since the first +// element of the current batch (if set), OR +// - the window advances past MaxTimestamp + AllowedLateness of the +// input PCollection's WindowingStrategy. +// +// Elements of different windows are never combined into the same +// batch. +// +// # Determinism requirement +// +// The key coder MUST be deterministic. State keying depends on +// byte-stable encodings: a non-deterministic key coder would silently +// split the logical key across multiple physical keys, producing +// corrupt batches. The transform panics at pipeline build time if the +// key coder is not known to be deterministic. For user-defined key +// types, register the type's coder via +// coder.RegisterDeterministicCoder. +// +// # Differences from Java/Python +// +// - BatchSize / BatchSizeBytes are int64 (parity with proto and Java +// long, avoiding overflow on 32-bit platforms). +// - BatchSizeBytes is limited to primitive value types ([]byte, +// string, numeric, bool) in this release; opaque V types panic at +// build time if BatchSizeBytes > 0. +package batch + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "reflect" + "sync" + "sync/atomic" + "time" + + "github.com/apache/beam/sdks/v2/go/pkg/beam" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/funcx" + beamcoder "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/coder" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/mtime" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/state" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/timers" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/typex" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/reflectx" + "github.com/apache/beam/sdks/v2/go/pkg/beam/register" + "github.com/google/uuid" +) + +// ShardedKey pairs a user key with an opaque shard identifier. It is +// the key type of the PCollection produced by +// GroupIntoBatchesWithShardedKey. +type ShardedKey[K any] struct { + Key K + ShardID []byte +} + +// RegisterShardedKeyType registers a ShardedKey[K] instantiation so +// its coder survives cross-worker serialization. Common key types +// (string, []byte, int, int64) are registered automatically at init. +// Users of other K types must call this at init time. +func RegisterShardedKeyType[K any]() { + var zero K + keyT := reflect.TypeOf(zero) + skT := reflect.TypeOf(ShardedKey[K]{}) + + register.DoFn3x0[K, typex.V, func(ShardedKey[K], typex.V)](&wrapShardedKeyFn[K]{}) + register.Emitter2[ShardedKey[K], typex.V]() + beam.RegisterType(skT) + + keyEnc := beam.NewElementEncoder(keyT) + keyDec := beam.NewElementDecoder(keyT) + + enc := func(sk ShardedKey[K]) []byte { + var buf bytes.Buffer + writeVarInt(&buf, int64(len(sk.ShardID))) + buf.Write(sk.ShardID) + if err := keyEnc.Encode(sk.Key, &buf); err != nil { + panic(err) + } + return buf.Bytes() + } + dec := func(b []byte) ShardedKey[K] { + r := bytes.NewReader(b) + n := readVarInt(r) + shardID := make([]byte, n) + if n > 0 { + if _, err := r.Read(shardID); err != nil { + panic(err) + } + } + k, err := keyDec.Decode(r) + if err != nil { + panic(err) + } + return ShardedKey[K]{Key: k.(K), ShardID: shardID} + } + + // Closures inside generic functions share the same compiler + // symbol name for every type instantiation. We wrap them with a + // type-qualified name so the cross-worker deserializer resolves + // the correct enc/dec for each ShardedKey[K]. + encName := fmt.Sprintf("batch.encShardedKey[%v]", keyT) + decName := fmt.Sprintf("batch.decShardedKey[%v]", keyT) + + encFn := reflectx.MakeFuncWithName(encName, enc) + decFn := reflectx.MakeFuncWithName(decName, dec) + + // Register in the runtime cache under the qualified name so + // ResolveFunction finds them at deserialization time. + runtime.RegisterFunctionWithName(encName, enc) + runtime.RegisterFunctionWithName(decName, dec) + + encWrapped, err := funcx.New(encFn) + if err != nil { + panic(fmt.Sprintf("RegisterShardedKeyType: bad enc for %v: %v", skT, err)) + } + decWrapped, err := funcx.New(decFn) + if err != nil { + panic(fmt.Sprintf("RegisterShardedKeyType: bad dec for %v: %v", skT, err)) + } + + beamcoder.RegisterDeterministicCoderWithFuncs(skT, encWrapped, decWrapped) +} + +// Params configures GroupIntoBatches and +// GroupIntoBatchesWithShardedKey. +// +// At least one of BatchSize or BatchSizeBytes must be > 0. +type Params struct { + // BatchSize is the target maximum number of elements per batch. A + // batch is emitted as soon as it holds BatchSize elements. Zero + // disables the count-based trigger. + BatchSize int64 + + // BatchSizeBytes is the target maximum cumulative byte size per + // batch. A batch is emitted as soon as adding another element + // would exceed BatchSizeBytes. Zero disables the byte-based + // trigger. + BatchSizeBytes int64 + + // MaxBufferingDuration, when > 0, triggers emission of a partial + // batch after this much processing time has elapsed since the + // first element of the current batch was buffered. + MaxBufferingDuration time.Duration +} + +func (p Params) validate() error { + if p.BatchSize < 0 { + return fmt.Errorf("Params.BatchSize must be >= 0; got %d", p.BatchSize) + } + if p.BatchSizeBytes < 0 { + return fmt.Errorf("Params.BatchSizeBytes must be >= 0; got %d", p.BatchSizeBytes) + } + if p.BatchSize == 0 && p.BatchSizeBytes == 0 { + return fmt.Errorf("Params: at least one of BatchSize or BatchSizeBytes must be > 0") + } + if p.MaxBufferingDuration < 0 { + return fmt.Errorf("Params.MaxBufferingDuration must be >= 0; got %s", p.MaxBufferingDuration) + } + return nil +} + +const ( + sizerNone int32 = 0 + sizerPrimitive int32 = 1 +) + +// codecCache keeps a per-value-type ElementEncoder/Decoder pair. +type codecCache struct { + once sync.Once + enc beam.ElementEncoder + dec beam.ElementDecoder +} + +func (c *codecCache) init(t reflect.Type) { + c.once.Do(func() { + c.enc = beam.NewElementEncoder(t) + c.dec = beam.NewElementDecoder(t) + }) +} + +func (c *codecCache) encode(v any) []byte { + var buf bytes.Buffer + if err := c.enc.Encode(v, &buf); err != nil { + panic(err) + } + return buf.Bytes() +} + +func (c *codecCache) decode(b []byte) any { + v, err := c.dec.Decode(bytes.NewReader(b)) + if err != nil { + panic(err) + } + return v +} + +// groupIntoBatchesFn is the stateful DoFn without a processing-time +// buffering timer. +type groupIntoBatchesFn struct { + Buffer state.Bag[[]byte] + Count state.Value[int64] + ByteSize state.Value[int64] + WindowEnd timers.EventTime + + ValueType beam.EncodedType + + BatchSize int64 + BatchSizeBytes int64 + AllowedLatenessMs int64 + SizerKind int32 + + codec codecCache +} + +func (fn *groupIntoBatchesFn) ProcessElement( + w beam.Window, sp state.Provider, tp timers.Provider, + key typex.T, value typex.V, emit func(typex.T, []typex.V), +) { + fn.codec.init(fn.ValueType.T) + + count, _, err := fn.Count.Read(sp) + if err != nil { + panic(err) + } + + if w.MaxTimestamp() < mtime.MaxTimestamp { + windowEnd := w.MaxTimestamp().ToTime() + if fn.AllowedLatenessMs > 0 { + windowEnd = windowEnd.Add(time.Duration(fn.AllowedLatenessMs) * time.Millisecond) + } + fn.WindowEnd.Set(tp, windowEnd, timers.WithNoOutputTimestamp()) + } + + if err := fn.Buffer.Add(sp, fn.codec.encode(value)); err != nil { + panic(err) + } + count++ + if err := fn.Count.Write(sp, count); err != nil { + panic(err) + } + + newBytes := int64(0) + if fn.BatchSizeBytes > 0 { + cur, _, err := fn.ByteSize.Read(sp) + if err != nil { + panic(err) + } + cur += sizeOf(fn.SizerKind, value) + if err := fn.ByteSize.Write(sp, cur); err != nil { + panic(err) + } + newBytes = cur + } + + if fn.BatchSize > 0 && count >= fn.BatchSize { + fn.flush(sp, key, emit) + return + } + if fn.BatchSizeBytes > 0 && newBytes >= fn.BatchSizeBytes { + fn.flush(sp, key, emit) + return + } +} + +func (fn *groupIntoBatchesFn) OnTimer( + ctx context.Context, ts beam.EventTime, sp state.Provider, tp timers.Provider, + key typex.T, timer timers.Context, emit func(typex.T, []typex.V), +) { + if timer.Family != fn.WindowEnd.Family { + panic(fmt.Sprintf("batch.groupIntoBatchesFn: unexpected timer family %q", timer.Family)) + } + fn.codec.init(fn.ValueType.T) + fn.flush(sp, key, emit) +} + +func (fn *groupIntoBatchesFn) flush( + sp state.Provider, key typex.T, emit func(typex.T, []typex.V), +) { + buf, ok, err := fn.Buffer.Read(sp) + if err != nil { + panic(err) + } + if !ok || len(buf) == 0 { + return + } + + out := make([]typex.V, len(buf)) + for i, b := range buf { + out[i] = fn.codec.decode(b) + } + emit(key, out) + + if err := fn.Buffer.Clear(sp); err != nil { + panic(err) + } + if err := fn.Count.Clear(sp); err != nil { + panic(err) + } + if fn.BatchSizeBytes > 0 { + if err := fn.ByteSize.Clear(sp); err != nil { + panic(err) + } + } +} + +// groupIntoBatchesBufferedFn adds a processing-time buffering timer. +type groupIntoBatchesBufferedFn struct { + Buffer state.Bag[[]byte] + Count state.Value[int64] + ByteSize state.Value[int64] + TimerSet state.Value[bool] + Buffering timers.ProcessingTime + WindowEnd timers.EventTime + + ValueType beam.EncodedType + + BatchSize int64 + BatchSizeBytes int64 + MaxBufferingMs int64 + AllowedLatenessMs int64 + SizerKind int32 + + codec codecCache +} + +func (fn *groupIntoBatchesBufferedFn) ProcessElement( + w beam.Window, sp state.Provider, tp timers.Provider, + key typex.T, value typex.V, emit func(typex.T, []typex.V), +) { + fn.codec.init(fn.ValueType.T) + + count, _, err := fn.Count.Read(sp) + if err != nil { + panic(err) + } + + if w.MaxTimestamp() < mtime.MaxTimestamp { + windowEnd := w.MaxTimestamp().ToTime() + if fn.AllowedLatenessMs > 0 { + windowEnd = windowEnd.Add(time.Duration(fn.AllowedLatenessMs) * time.Millisecond) + } + fn.WindowEnd.Set(tp, windowEnd, timers.WithNoOutputTimestamp()) + } + + if err := fn.Buffer.Add(sp, fn.codec.encode(value)); err != nil { + panic(err) + } + count++ + if err := fn.Count.Write(sp, count); err != nil { + panic(err) + } + + newBytes := int64(0) + if fn.BatchSizeBytes > 0 { + cur, _, err := fn.ByteSize.Read(sp) + if err != nil { + panic(err) + } + cur += sizeOf(fn.SizerKind, value) + if err := fn.ByteSize.Write(sp, cur); err != nil { + panic(err) + } + newBytes = cur + } + + if count == 1 { + fn.Buffering.Set(tp, time.Now().Add(time.Duration(fn.MaxBufferingMs)*time.Millisecond)) + if err := fn.TimerSet.Write(sp, true); err != nil { + panic(err) + } + } + + if fn.BatchSize > 0 && count >= fn.BatchSize { + fn.flush(sp, tp, key, emit) + return + } + if fn.BatchSizeBytes > 0 && newBytes >= fn.BatchSizeBytes { + fn.flush(sp, tp, key, emit) + return + } +} + +func (fn *groupIntoBatchesBufferedFn) OnTimer( + ctx context.Context, ts beam.EventTime, sp state.Provider, tp timers.Provider, + key typex.T, timer timers.Context, emit func(typex.T, []typex.V), +) { + fn.codec.init(fn.ValueType.T) + switch timer.Family { + case fn.Buffering.Family, fn.WindowEnd.Family: + fn.flush(sp, tp, key, emit) + default: + panic(fmt.Sprintf( + "batch.groupIntoBatchesBufferedFn: unexpected timer family %q", timer.Family)) + } +} + +func (fn *groupIntoBatchesBufferedFn) flush( + sp state.Provider, tp timers.Provider, key typex.T, emit func(typex.T, []typex.V), +) { + buf, ok, err := fn.Buffer.Read(sp) + if err != nil { + panic(err) + } + if !ok || len(buf) == 0 { + return + } + + out := make([]typex.V, len(buf)) + for i, b := range buf { + out[i] = fn.codec.decode(b) + } + emit(key, out) + + if err := fn.Buffer.Clear(sp); err != nil { + panic(err) + } + if err := fn.Count.Clear(sp); err != nil { + panic(err) + } + if fn.BatchSizeBytes > 0 { + if err := fn.ByteSize.Clear(sp); err != nil { + panic(err) + } + } + setBool, _, err := fn.TimerSet.Read(sp) + if err != nil { + panic(err) + } + if setBool { + fn.Buffering.Clear(tp) + if err := fn.TimerSet.Clear(sp); err != nil { + panic(err) + } + } +} + +func sizeOf(kind int32, v any) int64 { + switch kind { + case sizerNone: + return 0 + case sizerPrimitive: + if size, ok := defaultElementByteSize(v); ok { + return size + } + panic(fmt.Sprintf("batch: sizerPrimitive cannot size value of type %T", v)) + default: + panic(fmt.Sprintf("batch: unknown sizer kind %d", kind)) + } +} + +// wrapShardedKeyFn maps KV → KV. +type wrapShardedKeyFn[K any] struct{} + +func (*wrapShardedKeyFn[K]) ProcessElement( + key K, value typex.V, emit func(ShardedKey[K], typex.V), +) { + emit(ShardedKey[K]{Key: key, ShardID: makeShardID()}, value) +} + +var ( + workerUUIDOnce sync.Once + workerUUIDVal [16]byte + shardCounter atomic.Uint64 +) + +// makeShardID returns a 24-byte shard identifier: a 16-byte worker +// UUID fixed per process plus an 8-byte atomic counter, big-endian. +// The layout mirrors the Java and Python shapes exactly so the wire +// bytes of cross-language round-trips remain aligned. +func makeShardID() []byte { + workerUUIDOnce.Do(func() { + b, err := uuid.New().MarshalBinary() + if err != nil { + panic(fmt.Sprintf("batch: failed to marshal worker UUID: %v", err)) + } + copy(workerUUIDVal[:], b) + }) + out := make([]byte, 24) + copy(out[:16], workerUUIDVal[:]) + counter := shardCounter.Add(1) + binary.BigEndian.PutUint64(out[16:24], counter) + return out +} + +// writeVarInt writes a varint-encoded int64 to buf (unsigned, +// little-endian base-128). +func writeVarInt(buf *bytes.Buffer, v int64) { + u := uint64(v) + for u >= 0x80 { + buf.WriteByte(byte(u) | 0x80) + u >>= 7 + } + buf.WriteByte(byte(u)) +} + +// readVarInt reads a varint-encoded int64 from r. +func readVarInt(r *bytes.Reader) int64 { + var u uint64 + var s uint + for { + b, err := r.ReadByte() + if err != nil { + panic(err) + } + if b < 0x80 { + u |= uint64(b) << s + break + } + u |= uint64(b&0x7f) << s + s += 7 + } + return int64(u) +} + +func init() { + register.DoFn6x0[ + beam.Window, state.Provider, timers.Provider, + typex.T, typex.V, func(typex.T, []typex.V), + ](&groupIntoBatchesFn{}) + register.DoFn6x0[ + beam.Window, state.Provider, timers.Provider, + typex.T, typex.V, func(typex.T, []typex.V), + ](&groupIntoBatchesBufferedFn{}) + register.Emitter2[typex.T, []typex.V]() + + // Register common ShardedKey[K] types for WithShardedKey. + RegisterShardedKeyType[string]() + RegisterShardedKeyType[int]() + RegisterShardedKeyType[int64]() +} + +// GroupIntoBatches groups the values of the input PCollection> +// into batches of up to params.BatchSize elements (or +// params.BatchSizeBytes bytes) per key and emits them as +// PCollection>. +// +// The input must be KV-typed. The key coder must be deterministic; +// non-deterministic key coders would corrupt state keying. Panics at +// pipeline build time on invalid params, non-KV input, zero limits, or +// a non-deterministic key coder. +func GroupIntoBatches(s beam.Scope, params Params, col beam.PCollection) beam.PCollection { + s = s.Scope("batch.GroupIntoBatches") + + if err := params.validate(); err != nil { + panic(fmt.Errorf("GroupIntoBatches: %w", err)) + } + if !typex.IsKV(col.Type()) { + panic(fmt.Errorf( + "GroupIntoBatches: input PCollection must be KV-typed; got %v", col.Type())) + } + + keyFT := col.Type().Components()[0] + valFT := col.Type().Components()[1] + + if !beam.NewCoder(keyFT).IsDeterministic() { + panic(fmt.Errorf( + "GroupIntoBatches: key coder for type %v is not deterministic. "+ + "Register a deterministic custom coder with "+ + "coder.RegisterDeterministicCoder, or use a deterministic key "+ + "type (string, []byte, bool, integer, float)", keyFT.Type())) + } + + sizerKind := sizerNone + if params.BatchSizeBytes > 0 { + if !isBuiltinSizeable(valFT.Type()) { + panic(fmt.Errorf( + "GroupIntoBatches: BatchSizeBytes > 0 requires value type %v "+ + "to be a built-in primitive ([]byte, string, numeric, bool)", + valFT.Type())) + } + sizerKind = sizerPrimitive + } + + allowedLatenessMs := int64(col.WindowingStrategy().AllowedLateness) + valueType := beam.EncodedType{T: valFT.Type()} + + if params.MaxBufferingDuration > 0 { + fn := &groupIntoBatchesBufferedFn{ + Buffer: state.MakeBagState[[]byte]("batchBuffer"), + Count: state.MakeValueState[int64]("batchCount"), + ByteSize: state.MakeValueState[int64]("batchBytes"), + TimerSet: state.MakeValueState[bool]("batchTimerSet"), + Buffering: timers.InProcessingTime("batchBuffering"), + WindowEnd: timers.InEventTime("batchWindowEnd"), + ValueType: valueType, + BatchSize: params.BatchSize, + BatchSizeBytes: params.BatchSizeBytes, + MaxBufferingMs: params.MaxBufferingDuration.Milliseconds(), + AllowedLatenessMs: allowedLatenessMs, + SizerKind: sizerKind, + } + return beam.ParDo(s, fn, col) + } + + fn := &groupIntoBatchesFn{ + Buffer: state.MakeBagState[[]byte]("batchBuffer"), + Count: state.MakeValueState[int64]("batchCount"), + ByteSize: state.MakeValueState[int64]("batchBytes"), + WindowEnd: timers.InEventTime("batchWindowEnd"), + ValueType: valueType, + BatchSize: params.BatchSize, + BatchSizeBytes: params.BatchSizeBytes, + AllowedLatenessMs: allowedLatenessMs, + SizerKind: sizerKind, + } + + return beam.ParDo(s, fn, col) +} + +// GroupIntoBatchesWithShardedKey wraps each user key with a +// ShardedKey{Key: K, ShardID: [24]byte} and then applies +// GroupIntoBatches. Output is PCollection>. +// +// The key type K must have been registered via +// RegisterShardedKeyType[K] at init time. Common types (string, +// []byte, int, int64) are registered automatically. +// +// Sharding spreads the processing of a single hot logical key across +// multiple workers: each shard is independent state, so distributed +// runners can parallelize without the user's key type changing. +func GroupIntoBatchesWithShardedKey[K any](s beam.Scope, params Params, col beam.PCollection) beam.PCollection { + s = s.Scope("batch.GroupIntoBatchesWithShardedKey") + + if err := params.validate(); err != nil { + panic(fmt.Errorf("GroupIntoBatchesWithShardedKey: %w", err)) + } + if !typex.IsKV(col.Type()) { + panic(fmt.Errorf( + "GroupIntoBatchesWithShardedKey: input PCollection must be KV-typed; got %v", + col.Type())) + } + keyFT := col.Type().Components()[0] + var zero K + if keyFT.Type() != reflect.TypeOf(zero) { + panic(fmt.Errorf( + "GroupIntoBatchesWithShardedKey: type parameter K (%v) does not match input key type (%v)", + reflect.TypeOf(zero), keyFT.Type())) + } + + wrapped := beam.ParDo(s, &wrapShardedKeyFn[K]{}, col) + return GroupIntoBatches(s, params, wrapped) +} diff --git a/sdks/go/pkg/beam/transforms/batch/batch_prism_test.go b/sdks/go/pkg/beam/transforms/batch/batch_prism_test.go new file mode 100644 index 000000000000..158ea314dd0e --- /dev/null +++ b/sdks/go/pkg/beam/transforms/batch/batch_prism_test.go @@ -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 batch + +import ( + "os" + "sort" + "sync/atomic" + "testing" + + "github.com/apache/beam/sdks/v2/go/pkg/beam" + "github.com/apache/beam/sdks/v2/go/pkg/beam/options/jobopts" + "github.com/apache/beam/sdks/v2/go/pkg/beam/register" + _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/prism" + "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert" + "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" +) + +func TestMain(m *testing.M) { + f, _ := os.CreateTemp("", "dummy") + *jobopts.WorkerBinary = f.Name() + os.Exit(ptest.MainRetWithDefault(m, "prism")) +} + +// splitOnBar parses "key|value" strings into KV. +func splitOnBar(tuple string, emit func(string, string)) { + for i, r := range tuple { + if r == '|' { + emit(tuple[:i], tuple[i+1:]) + return + } + } +} + +func batchSize(_ string, batch []string) int { + return len(batch) +} + +func batchSizeSorted(_ string, batch []string) int { + sort.Strings(batch) + return len(batch) +} + +// intPair emits KV from a "key|int" string. +func intPair(tuple string, emit func(string, int)) { + for i, r := range tuple { + if r == '|' { + n := 0 + for _, c := range tuple[i+1:] { + n = n*10 + int(c-'0') + } + emit(tuple[:i], n) + return + } + } +} + +func intBatchSize(_ string, batch []int) int { return len(batch) } + +func init() { + register.Function2x0(splitOnBar) + register.Function2x0(intPair) + register.Function2x1(batchSize) + register.Function2x1(intBatchSize) + register.Function2x1(batchSizeSorted) + register.Emitter2[string, int]() +} + +// shardedBatchCount counts emitted ShardedKey batches via a side +// channel (no GBK). Uses a package-level atomic to avoid needing a +// Combine/GBK for aggregation, which triggers a separate Prism bug +// on deeply-chained stateful pipelines. +var shardedBatchCounter atomic.Int64 + +func shardedBatchSink(sk ShardedKey[string], batch []string) { + _ = sk + _ = batch + shardedBatchCounter.Add(1) +} + +func init() { + register.Function2x0(shardedBatchSink) +} + +// TAC-6 (BAC-4): GroupIntoBatchesWithShardedKey wraps each key with +// a ShardedKey and produces KV. We validate +// end-to-end on Prism using a terminal ParDo sink (not passert) to +// avoid an unrelated Prism GBK panic on deeply-chained pipelines. +func TestGroupIntoBatchesWithShardedKey_E2E(t *testing.T) { + shardedBatchCounter.Store(0) + + p, s := beam.NewPipelineWithRoot() + + tuples := make([]string, 0, 20) + for i := 0; i < 20; i++ { + tuples = append(tuples, "a|x") + } + raw := beam.CreateList(s, tuples) + kvs := beam.ParDo(s, splitOnBar, raw) + + batches := GroupIntoBatchesWithShardedKey[string](s, Params{BatchSize: 2}, kvs) + beam.ParDo0(s, shardedBatchSink, batches) + + ptest.RunAndValidate(t, p) + + got := shardedBatchCounter.Load() + // Each element gets a unique shardID (atomic counter), so under + // Prism single-process each shard has exactly 1 element — no + // batching occurs (BatchSize=2 is never reached per shard). + // On a distributed runner the same worker/goroutine would + // process multiple elements of the same key, sharing a shardID + // and thus producing real batches. Here we verify the pipeline + // executed and produced 20 shard-groups. + if got != 20 { + t.Errorf("expected 20 sharded batches (one per shard), got %d", got) + } +} + +// TestGroupIntoBatches_IntValues verifies that GroupIntoBatches works +// with a value type (int) that is not string — demonstrating the +// coder-driven generic value support (BAC-1 with non-string V). +func TestGroupIntoBatches_IntValues(t *testing.T) { + p, s := beam.NewPipelineWithRoot() + + raw := beam.CreateList(s, []string{ + "a|1", "a|2", "a|3", "a|4", + "b|5", "b|6", + }) + kvs := beam.ParDo(s, intPair, raw) + + batches := GroupIntoBatches(s, Params{BatchSize: 2}, kvs) + sizes := beam.ParDo(s, intBatchSize, batches) + + passert.Equals(s, sizes, 2, 2, 2) + + ptest.RunAndValidate(t, p) +} + +// TAC-1 (BAC-1): 1000 inputs over 10 keys with BatchSize 100 produces +// batches of exactly 100 elements for a single key. +func TestGroupIntoBatches_CountLimit(t *testing.T) { + p, s := beam.NewPipelineWithRoot() + + tuples := make([]string, 0, 1000) + for k := 0; k < 10; k++ { + for i := 0; i < 100; i++ { + tuples = append(tuples, string(rune('a'+k))+"|"+string(rune('0'+i%10))) + } + } + + raw := beam.CreateList(s, tuples) + kvs := beam.ParDo(s, splitOnBar, raw) + + batches := GroupIntoBatches(s, Params{BatchSize: 100}, kvs) + sizes := beam.ParDo(s, batchSize, batches) + + // 10 batches of 100. + wants := []any{} + for i := 0; i < 10; i++ { + wants = append(wants, 100) + } + passert.Equals(s, sizes, wants...) + + ptest.RunAndValidate(t, p) +} + +// TAC-4 (BAC-3): BatchSizeBytes threshold triggers a flush before the +// sum exceeds the limit. With BatchSizeBytes=10 and input strings of +// length 5 each, three 5-byte values first sum to 15 (> 10), so the +// flush happens after 2 elements. +func TestGroupIntoBatches_ByteLimit(t *testing.T) { + p, s := beam.NewPipelineWithRoot() + + raw := beam.CreateList(s, []string{ + "a|11111", "a|22222", "a|33333", "a|44444", // 4 * 5 bytes on key a + "b|55555", "b|66666", // 2 * 5 bytes on key b + }) + kvs := beam.ParDo(s, splitOnBar, raw) + + batches := GroupIntoBatches(s, Params{BatchSizeBytes: 10}, kvs) + sizes := beam.ParDo(s, batchSize, batches) + + // Each 2-element batch reaches 10 bytes and flushes: 2,2 for key a + // and 2 for key b = three flushes of size 2. + passert.Equals(s, sizes, 2, 2, 2) + + ptest.RunAndValidate(t, p) +} + +// TAC-7 (BAC-5) simplified in global window: batches only contain +// elements for a single key. Mixed-key batches would fail the +// key-equality assertion downstream. This test confirms the per-key +// groupism holds. +func TestGroupIntoBatches_PerKey(t *testing.T) { + p, s := beam.NewPipelineWithRoot() + + raw := beam.CreateList(s, []string{ + "a|1", "b|1", "a|2", "b|2", "a|3", "b|3", "a|4", "b|4", + }) + kvs := beam.ParDo(s, splitOnBar, raw) + + batches := GroupIntoBatches(s, Params{BatchSize: 2}, kvs) + sizes := beam.ParDo(s, batchSize, batches) + + // 8 inputs / BatchSize 2 over 2 keys → 4 batches of size 2. + passert.Equals(s, sizes, 2, 2, 2, 2) + + ptest.RunAndValidate(t, p) +} diff --git a/sdks/go/pkg/beam/transforms/batch/batch_test.go b/sdks/go/pkg/beam/transforms/batch/batch_test.go new file mode 100644 index 000000000000..0e0e00a80645 --- /dev/null +++ b/sdks/go/pkg/beam/transforms/batch/batch_test.go @@ -0,0 +1,47 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 batch + +import ( + "testing" + "time" +) + +func TestParams_validate(t *testing.T) { + cases := []struct { + name string + p Params + wantErr bool + }{ + {"zero_limits", Params{}, true}, + {"negative_size", Params{BatchSize: -1}, true}, + {"negative_bytes", Params{BatchSizeBytes: -1}, true}, + {"negative_duration", Params{BatchSize: 10, MaxBufferingDuration: -time.Second}, true}, + {"count_only", Params{BatchSize: 10}, false}, + {"bytes_only", Params{BatchSizeBytes: 1024}, false}, + {"both_and_duration", Params{BatchSize: 10, BatchSizeBytes: 1024, MaxBufferingDuration: time.Second}, false}, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + err := c.p.validate() + gotErr := err != nil + if gotErr != c.wantErr { + t.Errorf("validate() err = %v, wantErr = %v", err, c.wantErr) + } + }) + } +} diff --git a/sdks/go/pkg/beam/transforms/batch/doc.go b/sdks/go/pkg/beam/transforms/batch/doc.go new file mode 100644 index 000000000000..8bdbb17fbfd2 --- /dev/null +++ b/sdks/go/pkg/beam/transforms/batch/doc.go @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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-level doc examples for batch. Kept in the package itself so +// `go doc` surfaces them without a separate test package and a broader +// module import graph. +// +// These examples only construct a pipeline to illustrate API shape; they +// do not run one. + +package batch + +import ( + "fmt" + + "github.com/apache/beam/sdks/v2/go/pkg/beam" +) + +// ExampleGroupIntoBatches is an example of using the GroupIntoBatches +// transform. Each input element is a (user, event) pair. After +// GroupIntoBatches, each batch holds up to 100 events for a single user, +// ready to be written to a BigQuery sink that accepts bulk inserts. +func ExampleGroupIntoBatches() { + p := beam.NewPipeline() + s := p.Root() + + // Build KV PCollection via any source. The key + // coder (string) is deterministic so state keying is safe. + events := beam.CreateList(s, []string{"u1:login", "u1:click", "u2:login"}) + kvs := beam.ParDo(s, func(e string, emit func(string, string)) { + for i, r := 0, []rune(e); i < len(r); i++ { + if r[i] == ':' { + emit(string(r[:i]), string(r[i+1:])) + return + } + } + }, events) + + batches := GroupIntoBatches(s, Params{BatchSize: 100}, kvs) + + // Downstream: process each per-user batch. + _ = batches + fmt.Println("pipeline constructed") + + // Output: pipeline constructed +} diff --git a/sdks/go/pkg/beam/transforms/batch/size.go b/sdks/go/pkg/beam/transforms/batch/size.go new file mode 100644 index 000000000000..ff1499ddaa72 --- /dev/null +++ b/sdks/go/pkg/beam/transforms/batch/size.go @@ -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 batch + +import ( + "reflect" +) + +// defaultElementByteSize reports the byte cost of v for a fixed set of +// primitive types: it is the fallback used when the caller does not +// supply Params.ElementByteSize but BatchSizeBytes > 0. +// +// Returns (size, true) for supported types and (0, false) otherwise. +// For opaque types (user structs, interfaces, maps, non-byte slices, +// channels, functions) callers must supply their own sizer. +func defaultElementByteSize(v any) (int64, bool) { + switch x := v.(type) { + case []byte: + return int64(len(x)), true + case string: + return int64(len(x)), true + case bool: + return 1, true + case int8: + return 1, true + case uint8: + return 1, true + case int16: + return 2, true + case uint16: + return 2, true + case int32: + return 4, true + case uint32: + return 4, true + case float32: + return 4, true + case int: + return 8, true + case uint: + return 8, true + case int64: + return 8, true + case uint64: + return 8, true + case float64: + return 8, true + } + return 0, false +} + +// isBuiltinSizeable reports whether defaultElementByteSize can size an +// element of type t. Used at pipeline-build time to fail fast when +// BatchSizeBytes > 0 is requested without a user-supplied +// ElementByteSize and the value type is not one of the supported +// primitives. +// +// A []byte is recognized via reflect.Slice with Uint8 element kind; any +// other slice is not sizeable by the built-in fallback. +func isBuiltinSizeable(t reflect.Type) bool { + if t == nil { + return false + } + switch t.Kind() { + case reflect.String, + reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return true + case reflect.Slice: + return t.Elem().Kind() == reflect.Uint8 + } + return false +} diff --git a/sdks/go/pkg/beam/transforms/batch/size_test.go b/sdks/go/pkg/beam/transforms/batch/size_test.go new file mode 100644 index 000000000000..82d2d8dc0449 --- /dev/null +++ b/sdks/go/pkg/beam/transforms/batch/size_test.go @@ -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 batch + +import ( + "reflect" + "testing" +) + +func TestDefaultElementByteSize(t *testing.T) { + cases := []struct { + name string + v any + want int64 + ok bool + }{ + {"bytes_5", []byte("abcde"), 5, true}, + {"bytes_empty", []byte{}, 0, true}, + {"string_5", "abcde", 5, true}, + {"string_empty", "", 0, true}, + {"bool", true, 1, true}, + {"int8", int8(1), 1, true}, + {"uint8", uint8(1), 1, true}, + {"int16", int16(1), 2, true}, + {"uint16", uint16(1), 2, true}, + {"int32", int32(1), 4, true}, + {"uint32", uint32(1), 4, true}, + {"float32", float32(1.0), 4, true}, + {"int", int(1), 8, true}, + {"uint", uint(1), 8, true}, + {"int64", int64(1), 8, true}, + {"uint64", uint64(1), 8, true}, + {"float64", float64(1.0), 8, true}, + {"struct_unsupported", struct{ A int }{A: 1}, 0, false}, + {"map_unsupported", map[string]int{"a": 1}, 0, false}, + {"slice_int_unsupported", []int{1, 2, 3}, 0, false}, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + got, ok := defaultElementByteSize(c.v) + if ok != c.ok { + t.Errorf("ok = %v, want %v", ok, c.ok) + } + if got != c.want { + t.Errorf("size = %d, want %d", got, c.want) + } + }) + } +} + +func TestIsBuiltinSizeable(t *testing.T) { + cases := []struct { + name string + t reflect.Type + want bool + }{ + {"nil", nil, false}, + {"string", reflect.TypeOf(""), true}, + {"bytes", reflect.TypeOf([]byte(nil)), true}, + {"bool", reflect.TypeOf(true), true}, + {"int", reflect.TypeOf(int(0)), true}, + {"int64", reflect.TypeOf(int64(0)), true}, + {"float64", reflect.TypeOf(float64(0)), true}, + {"struct", reflect.TypeOf(struct{ A int }{}), false}, + {"map", reflect.TypeOf(map[string]int{}), false}, + {"slice_int", reflect.TypeOf([]int{}), false}, + {"slice_string", reflect.TypeOf([]string{}), false}, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + if got := isBuiltinSizeable(c.t); got != c.want { + t.Errorf("isBuiltinSizeable(%v) = %v, want %v", c.t, got, c.want) + } + }) + } +} 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/go/test/integration/io/xlang/debezium/debezium.go b/sdks/go/test/integration/io/xlang/debezium/debezium.go index e1b9bab963c3..a4046f018740 100644 --- a/sdks/go/test/integration/io/xlang/debezium/debezium.go +++ b/sdks/go/test/integration/io/xlang/debezium/debezium.go @@ -31,7 +31,7 @@ func ReadPipeline(addr, username, password, dbname, host, port string, connector connectorClass, reflectx.String, debeziumio.MaxRecord(maxrecords), debeziumio.MaxTimeToRun(120000), debeziumio.ConnectionProperties(connectionProperties), debeziumio.ExpansionAddr(addr)) - expectedJson := `{"metadata":{"connector":"postgresql","version":"3.1.3.Final","name":"beam-debezium-connector","database":"inventory","schema":"inventory","table":"customers"},"before":null,"after":{"fields":{"last_name":"Thomas","id":1001,"first_name":"Sally","email":"sally.thomas@acme.com"}}}` + expectedJson := `{"metadata":{"connector":"postgresql","version":"3.5.2.Final","name":"beam-debezium-connector","database":"inventory","schema":"inventory","table":"customers"},"before":null,"after":{"fields":{"last_name":"Thomas","id":1001,"first_name":"Sally","email":"sally.thomas@acme.com"}}}` expected := beam.Create(s, expectedJson) passert.Equals(s, result, expected) return p 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 8ccb64cae209..b234dd5d8f8a 100644 --- a/sdks/go/test/integration/io/xlang/debezium/debezium_test.go +++ b/sdks/go/test/integration/io/xlang/debezium/debezium_test.go @@ -33,7 +33,7 @@ import ( ) const ( - debeziumImage = "quay.io/debezium/example-postgres:3.1.3.Final" + debeziumImage = "quay.io/debezium/example-postgres:3.5.2.Final" debeziumPort = "5432/tcp" maxRetries = 5 ) 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 2ea84d427987..55302efc6853 100644 --- a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go +++ b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go @@ -32,7 +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/moby/moby/api/types/network" "github.com/testcontainers/testcontainers-go/wait" ) @@ -60,8 +60,8 @@ func setupTestContainer(ctx context.Context, t *testing.T, dbname, username, pas } hostname := "localhost" - dbURL := func(host string, port string) string { - cleanPort := strings.Split(port, "/")[0] + dbURL := func(host string, port network.Port) string { + cleanPort := port.Port() 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) diff --git a/sdks/go/test/run_validatesrunner_tests.sh b/sdks/go/test/run_validatesrunner_tests.sh index 9ea89fd22cfc..f1d681267691 100755 --- a/sdks/go/test/run_validatesrunner_tests.sh +++ b/sdks/go/test/run_validatesrunner_tests.sh @@ -286,6 +286,10 @@ if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "portable" || --artifact-port 0 & elif [[ "$RUNNER" == "spark" ]]; then "$JAVA_CMD" \ + --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 \ -jar $SPARK_JOB_SERVER_JAR \ --spark-master-url local \ --job-port $JOB_PORT \ diff --git a/sdks/java/container/boot.go b/sdks/java/container/boot.go index 3ce79e4927eb..6322c3004e59 100644 --- a/sdks/java/container/boot.go +++ b/sdks/java/container/boot.go @@ -105,8 +105,10 @@ func main() { logger.Fatalf(ctx, "Failed to convert pipeline options: %v", err) } + po := tools.ParseOptionsFromProto(info.GetPipelineOptions(), "") + // Inject artifact validation enabled state into context - ctx = artifact.WithArtifactValidation(ctx, !artifact.HasExperiment(info.GetPipelineOptions(), "disable_staged_file_integrity_checks")) + ctx = artifact.WithArtifactValidation(ctx, !po.HasExperiment("disable_staged_file_integrity_checks")) // (2) Retrieve the staged user jars. We ignore any disk limit, // because the staged jars are mandatory. @@ -195,9 +197,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 +226,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" diff --git a/sdks/java/container/common.gradle b/sdks/java/container/common.gradle index c81a33827bef..5b2322f61a35 100644 --- a/sdks/java/container/common.gradle +++ b/sdks/java/container/common.gradle @@ -83,6 +83,7 @@ task copyGolangLicenses(type: Copy) { task copyJdkOptions(type: Copy) { from "option-jamm.json" + from "option-arrow.json" from "java${imageJavaVersion}-security.properties" from "option-java${imageJavaVersion}-security.json" into "build/target/options" diff --git a/sdks/java/container/java17/option-arrow.json b/sdks/java/container/java17/option-arrow.json new file mode 100644 index 000000000000..a22b46ac1f78 --- /dev/null +++ b/sdks/java/container/java17/option-arrow.json @@ -0,0 +1,9 @@ +{ + "name": "arrow", + "enabled": true, + "options": { + "java_arguments": [ + "--add-opens=java.base/java.nio=ALL-UNNAMED" + ] + } +} diff --git a/sdks/java/container/java21/option-arrow.json b/sdks/java/container/java21/option-arrow.json new file mode 100644 index 000000000000..a22b46ac1f78 --- /dev/null +++ b/sdks/java/container/java21/option-arrow.json @@ -0,0 +1,9 @@ +{ + "name": "arrow", + "enabled": true, + "options": { + "java_arguments": [ + "--add-opens=java.base/java.nio=ALL-UNNAMED" + ] + } +} diff --git a/sdks/java/container/java25/option-arrow.json b/sdks/java/container/java25/option-arrow.json new file mode 100644 index 000000000000..a22b46ac1f78 --- /dev/null +++ b/sdks/java/container/java25/option-arrow.json @@ -0,0 +1,9 @@ +{ + "name": "arrow", + "enabled": true, + "options": { + "java_arguments": [ + "--add-opens=java.base/java.nio=ALL-UNNAMED" + ] + } +} diff --git a/sdks/java/container/license_scripts/dep_urls_java.yaml b/sdks/java/container/license_scripts/dep_urls_java.yaml index 29deb7165040..25e438f78541 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.85.0': license: "https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-opensource-java/master/LICENSE" type: "Apache License 2.0" paranamer: @@ -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.18.6': + '2.18.8': license: "https://raw.githubusercontent.com/FasterXML/jackson-bom/master/LICENSE" type: "Apache License 2.0" org.eclipse.jgit: @@ -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.56.0': - license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.56.0/LICENSE" + '1.62.0': + license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.62.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" + '1.62.0-alpha': + license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.62.0/LICENSE" type: "Apache License 2.0" zstd-jni: '1.5.2-5': diff --git a/sdks/java/core/jmh/src/main/java/org/apache/beam/sdk/jmh/util/package-info.java b/sdks/java/core/jmh/src/main/java/org/apache/beam/sdk/jmh/util/package-info.java index 647f31b72762..aaf7fc08f781 100644 --- a/sdks/java/core/jmh/src/main/java/org/apache/beam/sdk/jmh/util/package-info.java +++ b/sdks/java/core/jmh/src/main/java/org/apache/beam/sdk/jmh/util/package-info.java @@ -17,8 +17,4 @@ */ /** Benchmarks for core SDK utility classes. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.jmh.util; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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..eff031157e12 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. @@ -94,6 +107,12 @@ enum State { /** The job has been updated. */ UPDATED(true, true), + /** The job is draining: no longer accepting new input while finishing in-flight work. */ + DRAINING(false, false), + + /** The job has finished draining. */ + DRAINED(true, false), + /** The job state reported by a runner cannot be interpreted by the SDK. */ UNRECOGNIZED(false, false); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/annotations/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/annotations/package-info.java index 870e13d7c86b..ffb339e86e9f 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/annotations/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/annotations/package-info.java @@ -16,9 +16,5 @@ * limitations under the License. */ /** Defines annotations used across the SDK. */ -@DefaultAnnotation(NonNull.class) @Internal package org.apache.beam.sdk.annotations; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/coders/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/coders/package-info.java index c483da3713d6..e79de8f7a65a 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/coders/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/coders/package-info.java @@ -36,8 +36,4 @@ * String}, and {@code List}, as well as coders like {@link org.apache.beam.sdk.coders.AvroCoder} * that can be used to encode many custom types. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.coders; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/expansion/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/expansion/package-info.java index 84fd8332b2e4..51333b9eca1c 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/expansion/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/expansion/package-info.java @@ -16,8 +16,4 @@ * limitations under the License. */ /** Contains classes needed to expose transforms to other SDKs. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.expansion; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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/fn/splittabledofn/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/splittabledofn/package-info.java index 1de347b9d44b..d0d39bd22256 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/splittabledofn/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/fn/splittabledofn/package-info.java @@ -21,8 +21,4 @@ * href="https://s.apache.org/splittable-do-fn">splittable {@link * org.apache.beam.sdk.transforms.DoFn}. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.fn.splittabledofn; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/harness/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/harness/package-info.java index c9907138b6be..a4a713c94d2e 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/harness/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/harness/package-info.java @@ -17,10 +17,7 @@ */ /** Utilities for configuring worker environment. */ -@DefaultAnnotation(NonNull.class) @Internal package org.apache.beam.sdk.harness; -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; import org.apache.beam.sdk.annotations.Internal; -import org.checkerframework.checker.nullness.qual.NonNull; 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/main/java/org/apache/beam/sdk/io/fs/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/fs/package-info.java index bdd6687f4469..6ad5cac14344 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/fs/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/fs/package-info.java @@ -17,8 +17,4 @@ */ /** Apache Beam FileSystem interfaces and their default implementations. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.io.fs; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/package-info.java index 3f2433c1f264..a8d7424e3b0c 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/package-info.java @@ -34,8 +34,4 @@ * numbers.apply(TextIO.write().to("gs://my_bucket/path/to/numbers")); * } */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.io; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/range/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/range/package-info.java index 17fbd45a5e7b..441c15fb7d05 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/range/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/range/package-info.java @@ -21,8 +21,4 @@ * *

See {@link org.apache.beam.sdk.io.range.RangeTracker} to get started. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.io.range; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/metrics/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/metrics/package-info.java index 8602f758d473..52c1305fe062 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/metrics/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/metrics/package-info.java @@ -24,8 +24,4 @@ *

Runners should look at {@link org.apache.beam.sdk.metrics.MetricsContainer} for details on how * to support metrics. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.metrics; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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); } /** 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 7267dda9ed0b..531c86ef0438 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 @@ -114,6 +114,13 @@ enum LogLevel { void setLogMdc(boolean value); + @Description( + "This option controls if OpenTelemetry trace, spanId and sampled will be appended to log entries. This will allow to stitch traces to logs.") + @Default.Boolean(false) + boolean getLogOpenTelemetryTraceAndSpanId(); + + void setLogOpenTelemetryTraceAndSpanId(boolean value); + /** This option controls whether logging will be redirected through the FnApi. */ @Description( "Controls whether logging will be redirected through the FnApi. In normal usage, setting " diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/package-info.java index df2c3db3619c..60ebc83c918d 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/package-info.java @@ -26,8 +26,4 @@ *

Each Pipeline has a {@link org.apache.beam.sdk.PipelineRunner} to specify where and how it * should run after pipeline construction is complete. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/runners/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/runners/package-info.java index a7762c03fb12..97a7f8ab678f 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/runners/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/runners/package-info.java @@ -20,10 +20,7 @@ * *

Internals for use by runners. */ -@DefaultAnnotation(NonNull.class) @Internal package org.apache.beam.sdk.runners; -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; import org.apache.beam.sdk.annotations.Internal; -import org.checkerframework.checker.nullness.qual.NonNull; 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/SchemaTranslation.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaTranslation.java index d527b2949235..8122704e6436 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaTranslation.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaTranslation.java @@ -50,6 +50,7 @@ import org.apache.beam.sdk.schemas.logicaltypes.MicrosInstant; import org.apache.beam.sdk.schemas.logicaltypes.PythonCallable; import org.apache.beam.sdk.schemas.logicaltypes.SchemaLogicalType; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; import org.apache.beam.sdk.schemas.logicaltypes.UnknownLogicalType; import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; import org.apache.beam.sdk.schemas.logicaltypes.VariableString; @@ -115,6 +116,7 @@ private static String getLogicalTypeUrn(String identifier) { .put(FixedString.IDENTIFIER, FixedString.class) .put(VariableString.IDENTIFIER, VariableString.class) .put(Date.IDENTIFIER, Date.class) + .put(Timestamp.IDENTIFIER, Timestamp.class) .build(); public static SchemaApi.Schema schemaToProto(Schema schema, boolean serializeLogicalType) { diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaUtils.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaUtils.java index c8773ce2c232..88809182efcd 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaUtils.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaUtils.java @@ -238,6 +238,13 @@ static String toPrettyOptionsString(Schema.Options options, String prefix) { } static String toPrettyFieldValueString(Schema.FieldType fieldType, Object value, String prefix) { + if (value == null) { + // toPrettyRowString drops a row's own null fields, but a null nested inside an array, an + // iterable, a map or a row has a position and has to be rendered. The numeric and boolean + // branches below already render one as "null" via Objects.toString, so do the same for the + // remaining types rather than dereferencing the value. + return "null"; + } String nextPrefix = prefix + INDENT; switch (fieldType.getTypeName()) { case BYTE: diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/annotations/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/annotations/package-info.java index d87b591072b3..ec038f30a46b 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/annotations/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/annotations/package-info.java @@ -21,8 +21,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.annotations; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/io/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/io/package-info.java index aa2055005327..eeead7bf9345 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/io/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/io/package-info.java @@ -17,8 +17,4 @@ */ /** Provides abstractions for schema-aware IOs. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.io; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/io/payloads/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/io/payloads/package-info.java index 91fd27c3363b..07f47e122bcf 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/io/payloads/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/io/payloads/package-info.java @@ -17,8 +17,4 @@ */ /** Provides abstractions for schema-aware IOs. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.io.payloads; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/Timestamp.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/Timestamp.java index 87e47f5961e3..51fa9008b749 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/Timestamp.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/Timestamp.java @@ -22,6 +22,8 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import java.time.Instant; +import org.apache.beam.model.pipeline.v1.RunnerApi; +import org.apache.beam.model.pipeline.v1.SchemaApi; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.values.Row; import org.checkerframework.checker.nullness.qual.NonNull; @@ -56,7 +58,12 @@ * } */ public class Timestamp implements Schema.LogicalType { - public static final String IDENTIFIER = "beam:logical_type:timestamp:v1"; + public static final String IDENTIFIER = + SchemaApi.LogicalTypes.Enum.TIMESTAMP + .getValueDescriptor() + .getOptions() + .getExtension(RunnerApi.beamUrn); + static final int MIN_PRECISION = 0; static final int MAX_PRECISION = 9; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/package-info.java index 32af281c9cd5..1ae8448fb780 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/package-info.java @@ -20,8 +20,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.logicaltypes; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/package-info.java index 7d7ea7b13961..6852cdf91c71 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/package-info.java @@ -21,8 +21,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/parser/generated/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/parser/generated/package-info.java index 958360f14e31..5ce5774f6d87 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/parser/generated/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/parser/generated/package-info.java @@ -20,8 +20,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.parser.generated; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/parser/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/parser/package-info.java index f1c87597f5d2..51e67140cfcb 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/parser/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/parser/package-info.java @@ -20,8 +20,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.parser; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/transforms/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/transforms/package-info.java index 4c660fad7227..b25456eed42f 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/transforms/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/transforms/package-info.java @@ -20,8 +20,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.transforms; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/transforms/providers/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/transforms/providers/package-info.java index 6c5d1cb7c570..16e6dc0ea33b 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/transforms/providers/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/transforms/providers/package-info.java @@ -20,8 +20,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.transforms.providers; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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/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/main/java/org/apache/beam/sdk/schemas/utils/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/package-info.java index a1d0797a0d97..aebe3ea6e674 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/package-info.java @@ -20,8 +20,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.schemas.utils; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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() {} } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/state/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/state/package-info.java index c9cd01ad7d48..9682ccd6c1db 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/state/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/state/package-info.java @@ -17,8 +17,4 @@ */ /** Classes and interfaces for interacting with state. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.state; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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/testing/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/package-info.java index 61fd54f5831a..e66677df86ab 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/package-info.java @@ -19,8 +19,4 @@ * Defines utilities for unit testing Apache Beam pipelines. The tests for the {@code PTransform}s * and examples included in the Apache Beam SDK provide examples of using these utilities. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.testing; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/AsyncWrapper.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/AsyncWrapper.java new file mode 100644 index 000000000000..23372be2a2dc --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/AsyncWrapper.java @@ -0,0 +1,781 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.state.BagState; +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.transforms.reflect.DoFnInvoker; +import org.apache.beam.sdk.transforms.reflect.DoFnInvokers; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +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.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class that wraps a dofn and converts it from one which process elements synchronously to one + * which processes them asynchronously. + * + *

For synchronous dofns the default settings mean that many (100s) of elements will be processed + * in parallel and that processing an element will block all other work on that key. In addition + * runners are optimized for latencies less than a few seconds and longer operations can result in + * high retry rates. Async should be considered when the default parallelism is not correct and/or + * items are expected to take longer than a few seconds to process. + * + *

/* NOTE: 1) The wrapped syncFn REQUIRES thread-safety if BOTH parallelism > 1 and the DoFn is + * stateful. 2) Tagged output multi-outputs are unsupported. 3) StartBundle/finishBundle are invoked + * per element so any batching or aggregation logic will not behave as expected. + */ +public class AsyncWrapper extends DoFn, OutputT> { + + private static final Logger LOG = LoggerFactory.getLogger(AsyncWrapper.class); + + private static final int DEFAULT_MIN_BUFFER_CAPACITY = 10; + private static final int DEFAULT_TIMEOUT_SEC = 1; + private static final int DEFAULT_MAX_WAIT_TIME_MS = 500; + private static final int TEARDOWN_AWAIT_SEC = 5; + private static final int INITIAL_BACKOFF_SLEEP_MS = 10; + private static final int BACKPRESSURE_LOG_THRESHOLD_MS = 10000; + private static final double HASH_MODULO_LIMIT = 1000000.0; + private static final double MS_PER_SEC = 1000.0; + + @StateId("to_process") + private final StateSpec>> toProcessSpec; + + @TimerId("timer") + private final TimerSpec timerSpec = TimerSpecs.timer(TimeDomain.PROCESSING_TIME); + + private final DoFn syncFn; + private final int parallelism; + private final Duration timerFrequency; + private final int maxItemsToBuffer; + private final Duration timeout; + private final Duration maxWaitTime; + private final SerializableFunction idFn; + private final boolean useThreadPool; + private final String uuid; + + private transient volatile @Nullable PipelineOptions pipelineOptions; + + // Shared JVM-Wide States (Static Registries) + // Map-backed registry holding shared resources across serialized worker instances. Since runners + // clone DoFn instances on the same worker node, static maps ensure safe JVM-wide resource reuse. + private static final ConcurrentHashMap pool = new ConcurrentHashMap<>(); + // activeElements (processingElements) is global JVM memory (all keys) + private static final ConcurrentHashMap>> + processingElements = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap itemsInBuffer = + new ConcurrentHashMap<>(); + // Reference counts for cloned instances sharing the same UUID. Coordinates safe, + // leak-free thread pool shutdown during teardown without crashing active sibling clones. + private static final ConcurrentHashMap refCounts = + new ConcurrentHashMap<>(); + + // If contention becomes a bottleneck, this can be replaced with per-uuid locks + // in a ConcurrentHashMap + private static final ReentrantLock lock = new ReentrantLock(); + private static final boolean verboseLogging = false; + + private static class InFlightElement { + final @Nullable Object key; + final CompletableFuture> future; + + InFlightElement(@Nullable Object key, CompletableFuture> future) { + this.key = key; + this.future = future; + } + } + + // The In-Memory Accumulating Receiver + // Accumulates elements in-memory during asynchronous background worker execution. + // Buffered elements are only committed downstream once the parent task completes successfully + // and the timer fires. + private static class AccumulatingOutputReceiver implements OutputReceiver { + private final List outputs = new ArrayList<>(); + + AccumulatingOutputReceiver() {} + + @Override + public org.apache.beam.sdk.values.OutputBuilder builder(T value) { + return org.apache.beam.sdk.values.WindowedValues.builder() + .setValue(value) + .setReceiver(windowedValue -> outputs.add(windowedValue.getValue())); + } + + // Bypasses the nested anonymous OutputBuilder instantiation for standard outputs. + // JVM optimization to prevent garbage collection pressure under high pipeline throughput. + @Override + public void output(T output) { + outputs.add(output); + } + + @Override + public void outputWithTimestamp(T output, Instant timestamp) { + outputs.add(output); + } + + public List getOutputs() { + return outputs; + } + } + + public AsyncWrapper( + DoFn syncFn, + int parallelism, + Duration timerFrequency, + @Nullable Integer maxItemsToBuffer, + @Nullable Duration timeout, + @Nullable Duration maxWaitTime, + @Nullable SerializableFunction idFn, + boolean useThreadPool) { + this( + syncFn, + parallelism, + timerFrequency, + maxItemsToBuffer, + timeout, + maxWaitTime, + idFn, + useThreadPool, + null); + } + + public AsyncWrapper( + DoFn syncFn, + int parallelism, + Duration timerFrequency, + @Nullable Integer maxItemsToBuffer, + @Nullable Duration timeout, + @Nullable Duration maxWaitTime, + @Nullable SerializableFunction idFn, + boolean useThreadPool, + @Nullable Coder> coder) { + this.syncFn = syncFn; + this.parallelism = parallelism; + if (timerFrequency.getMillis() <= 0) { + throw new IllegalArgumentException("timerFrequency must be greater than zero"); + } + this.timerFrequency = timerFrequency; + this.maxItemsToBuffer = + (maxItemsToBuffer != null) + ? maxItemsToBuffer + : Math.max(parallelism * 2, DEFAULT_MIN_BUFFER_CAPACITY); + this.timeout = (timeout != null) ? timeout : Duration.standardSeconds(DEFAULT_TIMEOUT_SEC); + this.maxWaitTime = + (maxWaitTime != null) ? maxWaitTime : Duration.millis(DEFAULT_MAX_WAIT_TIME_MS); + this.idFn = + (idFn != null) + ? idFn + : (SerializableFunction) + input -> java.util.Objects.requireNonNull(input); + this.useThreadPool = useThreadPool; + this.uuid = UUID.randomUUID().toString(); + this.toProcessSpec = (coder != null) ? StateSpecs.bag(coder) : StateSpecs.bag(); + } + + private ExecutorService getThreadPool() { + ExecutorService threadPool = pool.get(uuid); + if (threadPool == null) { + throw new IllegalStateException("Thread pool not initialized for UUID: " + uuid); + } + return threadPool; + } + + @SuppressWarnings("unchecked") + private ConcurrentHashMap> getProcessingElements() { + ConcurrentHashMap> elements = processingElements.get(uuid); + if (elements == null) { + throw new IllegalStateException("Processing elements map not initialized for UUID: " + uuid); + } + return (ConcurrentHashMap>) (ConcurrentHashMap) elements; + } + + private AtomicInteger getItemsInBuffer() { + AtomicInteger buffer = itemsInBuffer.get(uuid); + if (buffer == null) { + throw new IllegalStateException("Buffer counter not initialized for UUID: " + uuid); + } + return buffer; + } + + // Setup is called by the runner exactly once on each worker node when this DoFn is initialized. + // It is responsible for setting up the wrapped synchronous DoFn + // and initializing the shared JVM-wide thread pool and registries. + @Setup + public void setup(PipelineOptions options) { + this.pipelineOptions = options; + + // Setup the wrapped DoFn + DoFnInvokers.invokerFor(syncFn) + .invokeSetup( + new DoFnInvoker.BaseArgumentProvider() { + @Override + public PipelineOptions pipelineOptions() { + return options; + } + + @Override + public String getErrorContext() { + return "AsyncWrapper/Setup"; + } + }); + + if (useThreadPool) { + LOG.info("Using thread pool for asynchronous execution with parallelism {}", parallelism); + } + + lock.lock(); + try { + if (useThreadPool) { + pool.computeIfAbsent(uuid, k -> Executors.newFixedThreadPool(parallelism)); + } + processingElements.computeIfAbsent(uuid, k -> new ConcurrentHashMap<>()); + itemsInBuffer.computeIfAbsent(uuid, k -> new AtomicInteger(0)); + refCounts.computeIfAbsent(uuid, k -> new AtomicInteger(0)).incrementAndGet(); + } finally { + lock.unlock(); + } + } + + // Clean up JVM-wide shared resources to prevent thread leaks on the worker + @Teardown + public void teardown() { + DoFnInvokers.invokerFor(syncFn).invokeTeardown(); + ExecutorService threadPool = null; + lock.lock(); + try { + AtomicInteger refCount = refCounts.get(uuid); + if (refCount != null && refCount.decrementAndGet() == 0) { + refCounts.remove(uuid); + threadPool = pool.remove(uuid); + processingElements.remove(uuid); + itemsInBuffer.remove(uuid); + } + } finally { + lock.unlock(); + } + if (threadPool != null) { + threadPool.shutdown(); + try { + if (!threadPool.awaitTermination(TEARDOWN_AWAIT_SEC, TimeUnit.SECONDS)) { + threadPool.shutdownNow(); + } + } catch (InterruptedException e) { + threadPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + // Asynchronous Scheduling & Deduplication + // Submits tasks to the background thread pool. If an element with the same ID is already + // in-flight, + // the submission is silently ignored to enforce exactly-once semantics. + private boolean scheduleIfRoom( + KV element, BoundedWindow window, Instant timestamp, boolean ignoreBuffer) { + lock.lock(); + try { + ConcurrentHashMap> activeElements = getProcessingElements(); + Object elementId = idFn.apply(element.getValue()); + + if (activeElements.containsKey(elementId)) { + logInfo("Item " + element + " already in processing elements"); + return true; + } + + int currentBuffer = getItemsInBuffer().get(); + if (currentBuffer < maxItemsToBuffer || ignoreBuffer) { + java.util.concurrent.Executor executor = + useThreadPool ? getThreadPool() : java.util.concurrent.ForkJoinPool.commonPool(); + + // Pending asynchronous task that will produce a list of outputs + CompletableFuture> future = + CompletableFuture.supplyAsync( + () -> { + try { + AccumulatingOutputReceiver receiver = + new AccumulatingOutputReceiver<>(); + DoFnInvoker invoker = DoFnInvokers.invokerFor(syncFn); + + DoFnInvoker.ArgumentProvider bundleArgProvider = + bundleArgProvider(receiver); + DoFnInvoker.ArgumentProvider processArgProvider = + processArgProvider(element, window, timestamp, receiver); + + invoker.invokeStartBundle(bundleArgProvider); + invoker.invokeProcessElement(processArgProvider); + invoker.invokeFinishBundle(bundleArgProvider); + + return receiver.getOutputs(); + } catch (Exception e) { + throw new CompletionException(e); + } + }, + executor); + + // Assigned to 'unused' to satisfy ErrorProne while preserving parent future for + // cancellation + CompletableFuture> unused = + future.whenComplete( + (res, ex) -> { + getItemsInBuffer().decrementAndGet(); + }); + + // Add element to active elements map and increment buffer counter + activeElements.put(elementId, new InFlightElement<>(element.getKey(), future)); + getItemsInBuffer().incrementAndGet(); + return true; + } + return false; + } finally { + lock.unlock(); + } + } + + private DoFnInvoker.ArgumentProvider bundleArgProvider( + AccumulatingOutputReceiver receiver) { + return new BundleArgProvider(receiver); + } + + private DoFnInvoker.ArgumentProvider processArgProvider( + KV element, + BoundedWindow window, + Instant timestamp, + OutputReceiver receiver) { + return new ProcessArgProvider(element, window, timestamp, receiver); + } + + // Named BaseArgumentProvider supplying bundle-level lifecycle context to the invoker. + private class BundleArgProvider extends DoFnInvoker.BaseArgumentProvider { + private final AccumulatingOutputReceiver receiver; + + BundleArgProvider(AccumulatingOutputReceiver receiver) { + this.receiver = receiver; + } + + @Override + public PipelineOptions pipelineOptions() { + PipelineOptions options = pipelineOptions; + if (options == null) { + throw new IllegalStateException("PipelineOptions not set"); + } + return options; + } + + @Override + public DoFn.FinishBundleContext finishBundleContext( + DoFn doFn) { + return new AsyncFinishBundleContext(doFn, receiver); + } + + @Override + public String getErrorContext() { + return "AsyncWrapper/Bundle"; + } + } + + // FinishBundleContext subclass bound to the enclosing DoFn instance to prevent compiler crashes. + private class AsyncFinishBundleContext extends DoFn.FinishBundleContext { + private final AccumulatingOutputReceiver receiver; + + AsyncFinishBundleContext( + DoFn doFn, AccumulatingOutputReceiver receiver) { + doFn.super(); + this.receiver = receiver; + } + + @Override + public PipelineOptions getPipelineOptions() { + PipelineOptions options = pipelineOptions; + if (options == null) { + throw new IllegalStateException("PipelineOptions not set"); + } + return options; + } + + @Override + public void output(OutputT output, Instant timestamp, BoundedWindow window) { + receiver.outputWithTimestamp(output, timestamp); + } + + @Override + public void output(TupleTag tag, T output, Instant timestamp, BoundedWindow window) { + throw new UnsupportedOperationException( + "Tagged output not supported in FinishBundleContext for AsyncWrapper"); + } + } + + // BaseArgumentProvider supplying element-level context to the invoker. + private class ProcessArgProvider extends DoFnInvoker.BaseArgumentProvider { + private final KV element; + private final BoundedWindow window; + private final Instant timestamp; + private final OutputReceiver receiver; + + ProcessArgProvider( + KV element, + BoundedWindow window, + Instant timestamp, + OutputReceiver receiver) { + this.element = element; + this.window = window; + this.timestamp = timestamp; + this.receiver = receiver; + } + + @Override + public InputT element(DoFn doFn) { + return element.getValue(); + } + + @Override + public OutputReceiver outputReceiver(DoFn doFn) { + return receiver; + } + + @Override + public BoundedWindow window() { + return window; + } + + @Override + public Instant timestamp(DoFn doFn) { + return timestamp; + } + + @Override + public PipelineOptions pipelineOptions() { + PipelineOptions options = pipelineOptions; + if (options == null) { + throw new IllegalStateException("PipelineOptions not set"); + } + return options; + } + + @Override + public String getErrorContext() { + return "AsyncWrapper/Process"; + } + } + + // Schedule an element to the thread pool, retries with backoff if the buffer is full. + private void scheduleItem(KV element, BoundedWindow window, Instant timestamp) { + boolean done = false; + long sleepTime = INITIAL_BACKOFF_SLEEP_MS; + long totalSleep = 0; + long timeoutMs = timeout.getMillis(); + + while (!done && totalSleep < timeoutMs) { + done = scheduleIfRoom(element, window, timestamp, false); + if (!done) { + long sleep = Math.min(maxWaitTime.getMillis(), sleepTime); + logBackpressure(element, sleep, totalSleep); + try { + Thread.sleep(sleep); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for space in buffer", e); + } + + // Prevents long overflow possibility + if (sleepTime < maxWaitTime.getMillis()) { + sleepTime *= 2; + } + + totalSleep += sleep; + } + } + // Timeout: element skips JVM pool but stays in BagState for timer to reschedule later. + } + + // Uses hashcode based jitter instead of random for deterministic rescheduling + // Satisfies lint check + private Instant nextTimeToFire(@Nullable K key) { + long seed = (key == null) ? 0 : key.hashCode(); + double fractionalOffset = Math.abs(seed % (long) HASH_MODULO_LIMIT) / HASH_MODULO_LIMIT; + double timerFrequencySec = timerFrequency.getMillis() / MS_PER_SEC; + double nowSec = System.currentTimeMillis() / MS_PER_SEC; + + double base = Math.floor((nowSec + timerFrequencySec) / timerFrequencySec) * timerFrequencySec; + double offset = fractionalOffset * timerFrequencySec; + + return Instant.ofEpochMilli((long) ((base + offset) * MS_PER_SEC)); + } + + @ProcessElement + public void processElement( + ProcessContext c, + BoundedWindow window, + @StateId("to_process") BagState> toProcessState, + @TimerId("timer") Timer timer) { + + KV element = c.element(); + scheduleItem(element, window, c.timestamp()); + toProcessState.add(element); + + Instant timeToFire = nextTimeToFire(element.getKey()); + timer.set(timeToFire); + } + + @OnTimer("timer") + public void onTimer( + OnTimerContext c, + @StateId("to_process") BagState> toProcessState, + @TimerId("timer") Timer timer, + OutputReceiver receiver) { + + commitFinishedItems(c.fireTimestamp(), toProcessState, timer, receiver); + } + + // Synchronizes local task results with the runner's persistent state container. + // Emits successfully completed elements, cancels rolled-back tasks, and reschedules lost work. + void commitFinishedItems( + Instant fireTimestamp, + BagState> toProcessState, + Timer timer, + OutputReceiver receiver) { + + Iterable> toProcessLocal = toProcessState.read(); + if (toProcessLocal == null || !toProcessLocal.iterator().hasNext()) { + // Early Exit: if BagState is empty, we skip checking activeElements for this key. + return; + } + + // Since fireTimestamp is key-scoped, we determine the current key from the first element in + // state + K key = null; + List> stateList = new ArrayList<>(); + for (KV element : toProcessLocal) { + stateList.add(element); + if (key == null) { + key = element.getKey(); + } + } + + logInfo("processing timer for key: " + key); + + ConcurrentHashMap> activeElements = getProcessingElements(); + List> toReturn = new ArrayList<>(); + List> toReschedule = new ArrayList<>(); + + int itemsFinished = 0; + int itemsNotYetFinished = 0; + int itemsRescheduled = 0; + + Set finishedElementIds = new HashSet<>(); + Set inFlightElementIds = new HashSet<>(); + Set rescheduledElementIds = new HashSet<>(); + + lock.lock(); + try { + Set stateElementIds = new HashSet<>(); + for (KV element : stateList) { + stateElementIds.add(idFn.apply(element.getValue())); + } + + List toCancelIds = new ArrayList<>(); + for (Map.Entry> entry : activeElements.entrySet()) { + InFlightElement inFlight = entry.getValue(); + if (java.util.Objects.equals(inFlight.key, key) + && !stateElementIds.contains(entry.getKey())) { + toCancelIds.add(entry.getKey()); + } + } + + for (Object cancelId : toCancelIds) { + InFlightElement inFlight = activeElements.get(cancelId); + if (inFlight != null) { + inFlight.future.cancel(true); + activeElements.remove(cancelId); + } + } + + for (KV element : stateList) { + Object elementId = idFn.apply(element.getValue()); + + // Skip processing if we already completed, rescheduled, or found this elementId active in + // this cycle + if (finishedElementIds.contains(elementId) + || rescheduledElementIds.contains(elementId) + || inFlightElementIds.contains(elementId)) { + continue; + } + + if (activeElements.containsKey(elementId)) { + InFlightElement inFlight = activeElements.get(elementId); + // Future is either completed, cancelled, or throws an exception + if (inFlight.future.isDone()) { + // Remove from local active map before checking the result + activeElements.remove(elementId); + try { + if (!inFlight.future.isCancelled()) { + toReturn.add(inFlight.future.get()); + // Only mark as finished if future was not cancelled + finishedElementIds.add(elementId); + itemsFinished++; + } + } catch (Exception e) { + LOG.error("Error executing async task for element {}", element, e); + throw new RuntimeException("Error executing async task for element " + element, e); + } + } else { + inFlightElementIds.add(elementId); + itemsNotYetFinished++; + } + } else { + logInfo( + "Item " + + element + + " found in state but not in local active elements, scheduling now"); + toReschedule.add(element); + rescheduledElementIds.add(elementId); + itemsRescheduled++; + } + } + } finally { + lock.unlock(); + } + + // Reschedule missing elements + for (KV element : toReschedule) { + scheduleItem(element, GlobalWindow.INSTANCE, fireTimestamp); + } + + // Update State: keep only unfinished items + toProcessState.clear(); + int itemsInProcessingState = 0; + for (KV element : stateList) { + Object elementId = idFn.apply(element.getValue()); + if (!finishedElementIds.contains(elementId)) { + toProcessState.add(element); + itemsInProcessingState++; + } + } + + // Emit completed outputs + // (Emit completed tasks immediately; do not wait for all active tasks to finish). + // Outputs use processing-time timestamps matching Python behavior + for (List outputs : toReturn) { + for (OutputT out : outputs) { + receiver.output(out); + } + } + + logInfo( + String.format( + "Items finished: %d, not yet finished: %d, rescheduled: %d, in processing state: %d", + itemsFinished, itemsNotYetFinished, itemsRescheduled, itemsInProcessingState)); + + if (itemsInProcessingState > 0) { + Instant timeToFire = nextTimeToFire(key); + timer.set(timeToFire); + } + } + + private void logInfo(String s) { + if (verboseLogging) { + LOG.info("{}", s); + } + } + + private void logBackpressure(KV element, long sleep, long totalSleep) { + if (verboseLogging || totalSleep > BACKPRESSURE_LOG_THRESHOLD_MS) { + LOG.info( + "buffer is full for item {}, {} waiting {} ms. Have waited for {} ms.", + element, + getItemsInBuffer().get(), + sleep, + totalSleep); + } + } + + // Package-private helper methods for testing direct execution without Pipeline / ProcessContext + // boilerplate + @VisibleForTesting + void processDirect( + KV element, + BoundedWindow window, + Instant timestamp, + BagState> toProcessState, + Timer timer) { + scheduleItem(element, window, timestamp); + toProcessState.add(element); + Instant timeToFire = nextTimeToFire(element.getKey()); + timer.set(timeToFire); + } + + @VisibleForTesting + List commitFinishedItemsDirect( + Instant fireTimestamp, BagState> toProcessState, Timer timer) { + AccumulatingOutputReceiver receiver = new AccumulatingOutputReceiver<>(); + commitFinishedItems(fireTimestamp, toProcessState, timer, receiver); + return receiver.getOutputs(); + } + + @VisibleForTesting + boolean isEmpty() { + return getItemsInBuffer().get() == 0; + } + + @VisibleForTesting + int getItemsInBufferCount() { + return getItemsInBuffer().get(); + } + + @VisibleForTesting + static void resetState() { + lock.lock(); + try { + for (Map.Entry entry : pool.entrySet()) { + entry.getValue().shutdownNow(); + } + pool.clear(); + processingElements.clear(); + itemsInBuffer.clear(); + refCounts.clear(); + } finally { + lock.unlock(); + } + } +} 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/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..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 @@ -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. */ @@ -355,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(); } @@ -364,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); } /** @@ -426,6 +446,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/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/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/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..7824e3b96a64 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 @@ -18,7 +18,10 @@ package org.apache.beam.sdk.transforms; import com.google.auto.service.AutoService; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; import org.apache.beam.model.pipeline.v1.RunnerApi; import org.apache.beam.sdk.annotations.Internal; @@ -182,13 +185,18 @@ public void processElement( @Element KV> kv, OutputReceiver> outputReceiver) { // todo #33176 specify additional metadata in the future - outputReceiver - .builder(KV.of(kv.getKey(), kv.getValue().getValue())) - .setTimestamp(kv.getValue().getTimestamp()) - .setWindow(kv.getValue().getWindow()) - .setPaneInfo(kv.getValue().getPaneInfo()) - .setCausedByDrain(kv.getValue().getCausedByDrain()) - .output(); + Context c = kv.getValue().getOpenTelemetryContext(); + try (Scope ignored = + Objects.requireNonNullElse(c, Context.root()).makeCurrent()) { + outputReceiver + .builder(KV.of(kv.getKey(), kv.getValue().getValue())) + .setTimestamp(kv.getValue().getTimestamp()) + .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..da6feef92d65 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 @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.transforms; +import io.opentelemetry.context.Context; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.VoidCoder; @@ -30,6 +31,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 +150,7 @@ public void processElement( BoundedWindow window, PaneInfo paneInfo, CausedByDrain causedByDrain, + ValueKind valueKind, OutputReceiver>> r) { r.output( KV.of( @@ -160,7 +163,9 @@ public void processElement( pc.currentRecordId(), pc.currentRecordOffset(), causedByDrain, - null))); + Context + .current(), // Otel context is not exposed via process context + 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/WithKeys.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/WithKeys.java index 96072d8ec29b..bdf9b07a4e6e 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/WithKeys.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/WithKeys.java @@ -17,9 +17,8 @@ */ package org.apache.beam.sdk.transforms; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; -import javax.annotation.CheckForNull; import org.apache.beam.sdk.coders.CannotProvideCoderException; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.CoderRegistry; @@ -56,9 +55,6 @@ * @param the type of the elements in the input {@code PCollection} and the values in the output * {@code PCollection} */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class WithKeys extends PTransform, PCollection>> { /** * Returns a {@code PTransform} that takes a {@code PCollection} and returns a {@code @@ -69,7 +65,7 @@ public class WithKeys extends PTransform, PCollection WithKeys of(SerializableFunction fn) { - checkNotNull( + checkArgumentNotNull( fn, "WithKeys constructed with null function. Did you mean WithKeys.of((Void) null)?"); return new WithKeys<>(fn, null); } @@ -79,20 +75,22 @@ public static WithKeys of(SerializableFunction fn) { * PCollection>}, where each of the values in the input {@code PCollection} has been * paired with the given key. */ - @SuppressWarnings("unchecked") - public static WithKeys of(final @Nullable K key) { - return new WithKeys<>( - value -> key, - (TypeDescriptor) - (key == null ? TypeDescriptors.voids() : TypeDescriptor.of(key.getClass()))); + public static WithKeys of(final K key) { + TypeDescriptor keyType; + if (key == null) { + keyType = (TypeDescriptor) TypeDescriptors.voids(); + } else { + keyType = (TypeDescriptor) TypeDescriptor.of(key.getClass()); + } + return new WithKeys<>(value -> key, keyType); // createWithConstantKey(key, keyType); } ///////////////////////////////////////////////////////////////////////////// private SerializableFunction fn; - @CheckForNull private transient TypeDescriptor keyType; + private transient @Nullable TypeDescriptor keyType; - private WithKeys(SerializableFunction fn, TypeDescriptor keyType) { + private WithKeys(SerializableFunction fn, @Nullable TypeDescriptor keyType) { this.fn = fn; this.keyType = keyType; } @@ -121,6 +119,7 @@ public KV apply(V element) { } })); + TypeDescriptor keyType = this.keyType; try { Coder keyCoder; CoderRegistry coderRegistry = in.getPipeline().getCoderRegistry(); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/display/DisplayData.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/display/DisplayData.java index 3be59e556158..dcc84ad8e1a0 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/display/DisplayData.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/display/DisplayData.java @@ -33,6 +33,7 @@ import java.util.Set; import org.apache.beam.sdk.options.ValueProvider; import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.util.common.ReflectHelpers; 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.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; @@ -617,7 +618,7 @@ FormattedItemValue format(Object value) { @Override FormattedItemValue format(Object value) { Class clazz = checkType(value, Class.class, JAVA_CLASS); - return new FormattedItemValue(clazz.getName(), clazz.getSimpleName()); + return new FormattedItemValue(clazz.getName(), ReflectHelpers.getSimpleName(clazz)); } }; @@ -753,10 +754,10 @@ private Builder include(Path path, HasDisplayData subComponent) { // Common case: AutoValue classes such as AutoValue_FooIO_Read. It's more useful // to show the user the FooIO.Read class, which is the direct superclass of the AutoValue // generated class. - if (namespace.getSimpleName().startsWith("AutoValue_")) { + if (ReflectHelpers.getSimpleName(namespace).startsWith("AutoValue_")) { namespace = namespace.getSuperclass(); } - if (namespace.isSynthetic() && namespace.getSimpleName().contains("$$Lambda")) { + if (namespace.isSynthetic() && ReflectHelpers.getSimpleName(namespace).contains("$$Lambda")) { try { String className = namespace.getCanonicalName(); // A local class, local interface, or anonymous class does not have a canonical name. diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/display/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/display/package-info.java index e17dddb5c276..7368aed4f71d 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/display/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/display/package-info.java @@ -23,8 +23,4 @@ * * @see org.apache.beam.sdk.transforms.display.HasDisplayData */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.transforms.display; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/errorhandling/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/errorhandling/package-info.java index c41e10143c12..a9e8975c513b 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/errorhandling/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/errorhandling/package-info.java @@ -16,8 +16,4 @@ * limitations under the License. */ /** Provides utilities for handling errors in Pipelines. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.transforms.errorhandling; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/join/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/join/package-info.java index 856558e86235..ca2f64053baa 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/join/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/join/package-info.java @@ -19,8 +19,4 @@ * Defines the {@link org.apache.beam.sdk.transforms.join.CoGroupByKey} transform for joining * multiple PCollections. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.transforms.join; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/package-info.java index 54e3a244caff..2ed963748fc8 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/package-info.java @@ -33,8 +33,4 @@ * package are composites, and users can also create composite PTransforms for their own * application-specific logic. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.transforms; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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..14f864c5b042 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 @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.method.MethodDescription; @@ -97,6 +98,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; @@ -112,6 +114,7 @@ import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.sdk.values.TypeDescriptors; 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.MapMaker; 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.primitives.Primitives; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -134,12 +137,15 @@ 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"; 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"; @@ -238,6 +244,22 @@ public String toString() { private final Map> byteBuddyInvokerConstructorCache = new ConcurrentHashMap<>(); + /** + * A weak, identity-keyed cache of the constructor selected for each {@link DoFn} instance. + * + *

Selecting a constructor resolves the {@link DoFn}'s input and output type descriptors. Some + * runners create a new invoker for the same {@link DoFn} at every bundle boundary, so doing that + * work on every invocation can be expensive. A deserialized {@link DoFn} is a new instance, so + * its constructor is selected again; later invoker creation for that instance reuses the cached + * selection. + * + *

The constructor values do not reference their {@link DoFn} keys, and weak keys allow unused + * instances to be collected. {@link MapMaker#weakKeys} also uses identity equality, which is + * required because separate instances of the same class may have different generic types. + */ + private final ConcurrentMap, Constructor> byteBuddyInvokerConstructorInstanceCache = + new MapMaker().weakKeys().makeMap(); + private ByteBuddyDoFnInvokerFactory() {} /** Returns the {@link DoFnInvoker} for the given {@link DoFn}. */ @@ -326,39 +348,15 @@ public DoFnInvoker newByteBuddyInvoker( signature.fnClass(), fn.getClass()); - // Extract input and output type descriptors to distinguish generic instantiations. - // Fall back to Object.class if unavailable. When type info is lost, different generic - // instantiations share an invoker, which is acceptable since the DoFn class in the cache - // key prevents collisions between different DoFn classes. - TypeDescriptor inputType; - try { - inputType = fn.getInputTypeDescriptor(); - } catch (Exception e) { - // Some DoFns (like MapElements) throw IllegalStateException if queried after - // serialization. - // In this case, we fall back to the raw class behavior (Object). - inputType = null; - } - if (inputType == null) { - inputType = (TypeDescriptor) TypeDescriptor.of(Object.class); - } - - TypeDescriptor outputType; - try { - outputType = fn.getOutputTypeDescriptor(); - } catch (Exception e) { - // Same as above: fall back to Object if type info is unavailable. - outputType = null; - } - if (outputType == null) { - outputType = (TypeDescriptor) TypeDescriptor.of(Object.class); - } + Constructor invokerConstructor = + byteBuddyInvokerConstructorInstanceCache.computeIfAbsent( + fn, unused -> getByteBuddyInvokerConstructor(signature, fn)); try { @SuppressWarnings("unchecked") DoFnInvokerBase> invoker = (DoFnInvokerBase>) - getByteBuddyInvokerConstructor(signature, inputType, outputType).newInstance(fn); + invokerConstructor.newInstance(fn); if (signature.onTimerMethods() != null) { for (OnTimerMethod onTimerMethod : signature.onTimerMethods().values()) { @@ -385,6 +383,39 @@ public DoFnInvoker newByteBuddyInvoker( } } + private Constructor getByteBuddyInvokerConstructor( + DoFnSignature signature, DoFn fn) { + // Extract input and output type descriptors to distinguish generic instantiations. + // Fall back to Object.class if unavailable. When type info is lost, different generic + // instantiations share an invoker, which is acceptable since the DoFn class in the cache + // key prevents collisions between different DoFn classes. + TypeDescriptor inputType; + try { + inputType = fn.getInputTypeDescriptor(); + } catch (Exception e) { + // Some DoFns (like MapElements) throw IllegalStateException if queried after + // serialization. + // In this case, we fall back to the raw class behavior (Object). + inputType = null; + } + if (inputType == null) { + inputType = (TypeDescriptor) TypeDescriptor.of(Object.class); + } + + TypeDescriptor outputType; + try { + outputType = fn.getOutputTypeDescriptor(); + } catch (Exception e) { + // Same as above: fall back to Object if type info is unavailable. + outputType = null; + } + if (outputType == null) { + outputType = (TypeDescriptor) TypeDescriptor.of(Object.class); + } + + return getByteBuddyInvokerConstructor(signature, inputType, outputType); + } + /** * Returns a generated constructor for a {@link DoFnInvoker} for the given {@link DoFnSignature} * and specific generic types. @@ -1117,6 +1148,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); @@ -1159,6 +1199,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 1f122f1bf661..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 @@ -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; @@ -184,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); @@ -232,6 +237,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 +372,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( @@ -437,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( @@ -528,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); @@ -573,6 +600,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..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) { @@ -351,6 +353,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 { @@ -389,6 +393,8 @@ public interface Cases { ResultT dispatch(OnTimerContextParameter p); + ResultT dispatch(OnWindowExpirationContextParameter p); + ResultT dispatch(WindowParameter p); ResultT dispatch(PaneInfoParameter p); @@ -417,6 +423,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. */ @@ -494,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); @@ -534,6 +547,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 +609,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 +645,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 +827,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..431ecea54b68 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; @@ -105,6 +106,8 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Utilities for working with {@link DoFnSignature}. See {@link #getSignature}. */ @Internal @@ -114,6 +117,8 @@ }) public class DoFnSignatures { + private static final Logger LOG = LoggerFactory.getLogger(DoFnSignatures.class); + private DoFnSignatures() {} /** @@ -143,6 +148,7 @@ private DoFnSignatures() {} Parameter.CurrentRecordIdParameter.class, Parameter.CurrentRecordOffsetParameter.class, Parameter.CausedByDrainParameter.class, + Parameter.ValueKindParameter.class, Parameter.BundleFinalizerParameter.class); private static final ImmutableList> @@ -162,6 +168,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 = @@ -194,7 +201,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 = @@ -212,7 +220,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 = @@ -223,7 +232,9 @@ private DoFnSignatures() {} Parameter.TaggedOutputReceiverParameter.class, Parameter.StateParameter.class, Parameter.TimestampParameter.class, - Parameter.KeyParameter.class); + Parameter.KeyParameter.class, + Parameter.SideInputParameter.class, + Parameter.OnWindowExpirationContextParameter.class); private static final Collection> ALLOWED_GET_INITIAL_RESTRICTION_PARAMETERS = @@ -1386,6 +1397,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( @@ -2356,12 +2372,57 @@ private static Map analyzeStateDeclarati (TypeDescriptor) TypeDescriptor.of(fnClazz).resolveType(unresolvedStateType); + // Warn if ValueState contains a collection type that could benefit from specialized state + warnIfValueStateContainsCollection(fnClazz, id, stateType); + declarations.put(id, DoFnSignature.StateDeclaration.create(id, field, stateType)); } return ImmutableMap.copyOf(declarations); } + /** + * Warns if a ValueState is declared with a collection type (Map, List, Set) that could benefit + * from using specialized state types (MapState, BagState, SetState) for better performance. + */ + private static void warnIfValueStateContainsCollection( + Class fnClazz, String stateId, TypeDescriptor stateType) { + if (!stateType.isSubtypeOf(TypeDescriptor.of(ValueState.class))) { + return; + } + + // Use TypeDescriptor.resolveType() to extract ValueState's type parameter + // This preserves generic type information better than raw Type manipulation + TypeDescriptor valueTypeDescriptor = + stateType.resolveType(ValueState.class.getTypeParameters()[0]); + + // Match on the collection's raw type. We intentionally do not skip types with unresolved + // parameters: for a parameterized collection such as ValueState> the collection's own + // raw type (Set) is known even though the element type T is a type variable, so we can still + // recommend SetState. A payload that is itself a bare type variable (e.g. ValueState) simply + // matches none of the branches below and produces no recommendation. + String recommendation = null; + if (valueTypeDescriptor.isSubtypeOf(TypeDescriptor.of(Map.class))) { + recommendation = "MapState"; + } else if (valueTypeDescriptor.isSubtypeOf(TypeDescriptor.of(List.class))) { + recommendation = "BagState or OrderedListState"; + } else if (valueTypeDescriptor.isSubtypeOf(TypeDescriptor.of(java.util.Set.class))) { + recommendation = "SetState"; + } + + if (recommendation != null) { + LOG.warn( + "DoFn {} declares ValueState '{}' with collection type {}. " + + "ValueState reads/writes the entire collection on each access. " + + "For large or frequently-updated collections, consider using {} instead " + + "(if supported by your runner).", + fnClazz.getSimpleName(), + stateId, + valueTypeDescriptor.getRawType().getSimpleName(), + recommendation); + } + } + private static @Nullable Method findAnnotatedMethod( ErrorReporter errors, Class anno, Class fnClazz, boolean required) { Collection matches = @@ -2409,7 +2470,8 @@ private static String format(TypeDescriptor t) { } private static String format(Class kls) { - return kls.getSimpleName().isEmpty() ? kls.getName() : kls.getSimpleName(); + String simpleName = ReflectHelpers.getSimpleName(kls); + return simpleName.isEmpty() ? kls.getName() : simpleName; } static class ErrorReporter { diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/package-info.java index 93277a711ca9..bdbbb6e5af5a 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/package-info.java @@ -22,10 +22,7 @@ * and creating {@link org.apache.beam.sdk.transforms.reflect.DoFnSignature}'s and {@link * org.apache.beam.sdk.transforms.reflect.DoFnInvoker}'s from them. */ -@DefaultAnnotation(NonNull.class) @Internal package org.apache.beam.sdk.transforms.reflect; -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; import org.apache.beam.sdk.annotations.Internal; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/splittabledofn/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/splittabledofn/package-info.java index e2722b41789e..4523032eeb82 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/splittabledofn/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/splittabledofn/package-info.java @@ -19,8 +19,4 @@ * Defines utilities related to splittable * {@link org.apache.beam.sdk.transforms.DoFn}. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.transforms.splittabledofn; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/windowing/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/windowing/package-info.java index 886b3f4fa20d..c842b3442a38 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/windowing/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/windowing/package-info.java @@ -42,8 +42,4 @@ * the system watermark passes the end of the window. See {@link * org.apache.beam.sdk.transforms.windowing.AfterWatermark} for details on the watermark. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.transforms.windowing; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/ApiSurface.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/ApiSurface.java index d72258cd9ac9..72a158f26132 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/ApiSurface.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/ApiSurface.java @@ -519,11 +519,16 @@ private boolean pruned(Type type) { return pruned(TypeToken.of(type).getRawType()); } + private static final ImmutableSet EXCLUDED_NAMES = + ImmutableSet.of( + "jdk.internal.HotSpotIntrinsicCandidate", + "jdk.internal.vm.annotation.IntrinsicCandidate"); + /** Whether a class and all that it references should be pruned from the graph. */ private boolean pruned(Class clazz) { return clazz.isPrimitive() || clazz.isArray() - || clazz.getCanonicalName().equals("jdk.internal.HotSpotIntrinsicCandidate") + || EXCLUDED_NAMES.contains(clazz.getCanonicalName()) || getPrunedPattern().matcher(clazz.getName()).matches(); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/RowJson.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/RowJson.java index c63f673ade21..ccfbd87d450d 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/RowJson.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/RowJson.java @@ -26,6 +26,7 @@ import static org.apache.beam.sdk.schemas.Schema.TypeName.INT16; import static org.apache.beam.sdk.schemas.Schema.TypeName.INT32; import static org.apache.beam.sdk.schemas.Schema.TypeName.INT64; +import static org.apache.beam.sdk.schemas.Schema.TypeName.MAP; import static org.apache.beam.sdk.schemas.Schema.TypeName.STRING; import static org.apache.beam.sdk.util.RowJsonValueExtractors.booleanValueExtractor; import static org.apache.beam.sdk.util.RowJsonValueExtractors.byteValueExtractor; @@ -57,6 +58,9 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.apache.beam.sdk.schemas.Schema; @@ -95,7 +99,8 @@ }) public class RowJson { private static final ImmutableSet SUPPORTED_TYPES = - ImmutableSet.of(BYTE, INT16, INT32, INT64, FLOAT, DOUBLE, BOOLEAN, STRING, DECIMAL, DATETIME); + ImmutableSet.of( + BYTE, INT16, INT32, INT64, FLOAT, DOUBLE, BOOLEAN, STRING, DECIMAL, DATETIME, MAP); private static final ImmutableSet KNOWN_LOGICAL_TYPE_IDENTIFIERS = ImmutableSet.of( SqlTypes.DATE.getIdentifier(), @@ -160,6 +165,14 @@ private static ImmutableList findUnsupportedFields( return findUnsupportedFields(fieldType.getCollectionElementType(), fieldName + "[]"); } + if (fieldTypeName.isMapType()) { + if (!STRING.equals(fieldType.getMapKeyType().getTypeName())) { + return ImmutableList.of( + new UnsupportedField(fieldName + ".key", fieldType.getMapKeyType().getTypeName())); + } + return findUnsupportedFields(fieldType.getMapValueType(), fieldName + "{}"); + } + if (fieldTypeName.isLogicalType()) { if (KNOWN_LOGICAL_TYPE_IDENTIFIERS.contains(fieldType.getLogicalType().getIdentifier())) { return ImmutableList.of(); @@ -303,6 +316,10 @@ private Object extractJsonNodeValue(FieldValue fieldValue) { return jsonArrayToList(fieldValue); } + if (fieldValue.isMapType()) { + return jsonObjectToMap(fieldValue); + } + if (fieldValue.typeName().isLogicalType()) { String identifier = fieldValue.type().getLogicalType().getIdentifier(); if (SqlTypes.DATE.getIdentifier().equals(identifier)) { @@ -365,6 +382,32 @@ private Object jsonArrayToList(FieldValue arrayFieldValue) { .collect(toImmutableList()); } + private Map jsonObjectToMap(FieldValue mapFieldValue) { + if (!mapFieldValue.isJsonObject()) { + throw new UnsupportedRowJsonException( + "Expected JSON object for field '" + + mapFieldValue.name() + + "'. Instead got " + + mapFieldValue.jsonNodeType().name()); + } + + Map result = new HashMap<>(); + Iterator> fields = mapFieldValue.jsonValue().fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + String key = field.getKey(); + JsonNode value = field.getValue(); + + Object extractedValue = + extractJsonNodeValue( + FieldValue.of( + mapFieldValue.name() + "['" + key + "']", mapFieldValue.mapValueType(), value)); + + result.put(key, extractedValue); + } + return result; + } + private static Object extractJsonPrimitiveValue(FieldValue fieldValue) { try { return JSON_VALUE_GETTERS.get(fieldValue.typeName()).extractValue(fieldValue.jsonValue()); @@ -440,6 +483,18 @@ Schema rowSchema() { return type().getRowSchema(); } + boolean isMapType() { + return TypeName.MAP.equals(type().getTypeName()); + } + + FieldType mapKeyType() { + return type().getMapKeyType(); + } + + FieldType mapValueType() { + return type().getMapValueType(); + } + static FieldValue of(String name, FieldType type, JsonNode jsonValue) { return new AutoValue_RowJson_RowJsonDeserializer_FieldValue(name, type, jsonValue); } @@ -538,6 +593,14 @@ private void writeValue(JsonGenerator gen, FieldType type, Object value) throws case ROW: writeRow((Row) value, type.getRowSchema(), gen); break; + case MAP: + gen.writeStartObject(); + for (Map.Entry entry : ((Map) value).entrySet()) { + gen.writeFieldName(entry.getKey().toString()); + writeValue(gen, type.getMapValueType(), entry.getValue()); + } + gen.writeEndObject(); + break; case LOGICAL_TYPE: String identifier = type.getLogicalType().getIdentifier(); if (SqlTypes.DATE.getIdentifier().equals(identifier)) { diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/RowJsonValueExtractors.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/RowJsonValueExtractors.java index f7a925d5c222..2179b20010dc 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/RowJsonValueExtractors.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/RowJsonValueExtractors.java @@ -189,7 +189,14 @@ static ValueExtractor decimalValueExtractor() { */ static ValueExtractor datetimeValueExtractor() { return ValidatingValueExtractor.builder() - .setExtractor(jsonNode -> DateTime.parse(jsonNode.textValue())) + .setExtractor( + jsonNode -> { + String text = jsonNode.textValue(); + if (text.contains(" ")) { + text = text.replace(' ', 'T'); + } + return DateTime.parse(text); + }) .setValidator(JsonNode::isTextual) .build(); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/common/ReflectHelpers.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/common/ReflectHelpers.java index c2d945bbaac1..7d5964cb83ca 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/common/ReflectHelpers.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/common/ReflectHelpers.java @@ -52,11 +52,29 @@ public class ReflectHelpers { private static final Joiner COMMA_SEPARATOR = Joiner.on(", "); + /** + * Returns the simple name of the given class, or a fallback simple name derived from the class + * name if {@link Class#getSimpleName()} throws an exception (such as an {@link + * IllegalAccessError} on Java 17+ for non-public inner classes). + */ + public static String getSimpleName(Class clazz) { + try { + return clazz.getSimpleName(); + } catch (Throwable t) { + if (clazz.isArray()) { + return getSimpleName(clazz.getComponentType()) + "[]"; + } + String name = clazz.getName(); + int idx = Math.max(name.lastIndexOf('.'), name.lastIndexOf('$')); + return idx != -1 ? name.substring(idx + 1) : name; + } + } + /** Returns a string representation of the signature of a {@link Method}. */ public static String formatMethod(Method input) { String parameterTypes = FluentIterable.from(asList(input.getParameterTypes())) - .transform(Class::getSimpleName) + .transform(ReflectHelpers::getSimpleName) .join(COMMA_SEPARATOR); return String.format("%s(%s)", input.getName(), parameterTypes); } @@ -100,7 +118,7 @@ private static void format(StringBuilder builder, Type t) { } private static void formatClass(StringBuilder builder, Class clazz) { - builder.append(clazz.getSimpleName()); + builder.append(getSimpleName(clazz)); } private static void formatTypeVariable(StringBuilder builder, TypeVariable t) { 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/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..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 @@ -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; @@ -505,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; @@ -520,6 +527,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 +561,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/util/construction/graph/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/graph/package-info.java index 9d5af85dc5a3..2e10f7a5d375 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/graph/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/graph/package-info.java @@ -17,8 +17,4 @@ */ /** Graph-manipulation utilities. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.util.construction.graph; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/package-info.java index 82d4d26c99d1..2b12cf1a5d04 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/construction/package-info.java @@ -17,8 +17,4 @@ */ /** Provides utilities for Beam runner authors, prior to execution. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.util.construction; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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 index 20120bf2172d..dee6ae837297 100644 --- 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 @@ -22,10 +22,12 @@ import io.opentelemetry.context.propagation.TextMapGetter; import io.opentelemetry.context.propagation.TextMapSetter; import org.apache.beam.model.fnexecution.v1.BeamFnApi; +import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.checkerframework.checker.nullness.qual.Nullable; -class OpenTelemetryContextPropagator { +@Internal +public class OpenTelemetryContextPropagator { private static final TextMapSetter SETTER = (carrier, key, value) -> { @@ -61,11 +63,11 @@ public Iterable keys(BeamFnApi.Elements.ElementMetadata carrier) { } }; - static void set(Context from, BeamFnApi.Elements.ElementMetadata.Builder builder) { + public static void set(Context from, BeamFnApi.Elements.ElementMetadata.Builder builder) { W3CTraceContextPropagator.getInstance().inject(from, builder, SETTER); } - static Context read(BeamFnApi.Elements.ElementMetadata from) { + public 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/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/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..9cbbd236c9d0 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 @@ -107,7 +107,6 @@ public static class Builder implements OutputBuilder { private @Nullable String recordId; private @Nullable Long recordOffset; private CausedByDrain causedByDrain = CausedByDrain.NORMAL; - private @Nullable Context openTelemetryContext; private ValueKind valueKind = ValueKind.INSERT; @Override @@ -160,8 +159,7 @@ public Builder setCausedByDrain(CausedByDrain causedByDrain) { } @Override - public Builder setOpenTelemetryContext(@Nullable Context openTelemetryContext) { - this.openTelemetryContext = openTelemetryContext; + public Builder setOpenTelemetryContext(@Nullable Context ignored) { return this; } @@ -200,7 +198,9 @@ public Instant getTimestamp() { @Override public @Nullable Context getOpenTelemetryContext() { - return openTelemetryContext; + // builder may have different context set at the beginning of parDo + // when building WindowedValue we should take current context from storage. + return Context.current(); } @Override @@ -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/main/java/org/apache/beam/sdk/values/package-info.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/package-info.java index a712e85dbf3d..86f617170287 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/package-info.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/package-info.java @@ -47,8 +47,4 @@ * *

For further details, see the documentation for each class in this package. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.values; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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/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)))); + } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/FileIOTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/FileIOTest.java index dffc4943bfab..c5a227d46b82 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/FileIOTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/FileIOTest.java @@ -232,9 +232,10 @@ public void testMatchAllDisallowEmptyNonWildcard() throws IOException { /** DoFn that copy test files from source to watch path. */ private static class CopyFilesFn extends DoFn, MatchResult.Metadata> { - public CopyFilesFn(Path sourcePath, Path watchPath) { + public CopyFilesFn(Path sourcePath, Path watchPath, long baseTimestampMillis) { this.sourcePathStr = sourcePath.toString(); this.watchPathStr = watchPath.toString(); + this.baseTimestampMillis = baseTimestampMillis; } @StateId("count") @@ -249,16 +250,24 @@ public void processElement(ProcessContext context, @StateId("count") ValueState< context.output(Objects.requireNonNull(context.element()).getValue()); CopyOption[] cpOptions = {StandardCopyOption.COPY_ATTRIBUTES}; - CopyOption[] updOptions = {StandardCopyOption.REPLACE_EXISTING}; + CopyOption[] updOptions = { + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES + }; final Path sourcePath = Paths.get(sourcePathStr); final Path watchPath = Paths.get(watchPathStr); if (0 == current) { Thread.sleep(100); + // Ensure overwrite updates get a distinct mtime even when COPY_ATTRIBUTES is enabled. + Files.setLastModifiedTime( + sourcePath.resolve("first"), FileTime.fromMillis(baseTimestampMillis + 2000)); Files.copy(sourcePath.resolve("first"), watchPath.resolve("first"), updOptions); Files.copy(sourcePath.resolve("second"), watchPath.resolve("second"), cpOptions); } else if (1 == current) { Thread.sleep(100); + FileTime updateTime = FileTime.fromMillis(baseTimestampMillis + 4000); + Files.setLastModifiedTime(sourcePath.resolve("first"), updateTime); + Files.setLastModifiedTime(sourcePath.resolve("second"), updateTime); Files.copy(sourcePath.resolve("first"), watchPath.resolve("first"), updOptions); Files.copy(sourcePath.resolve("second"), watchPath.resolve("second"), updOptions); Files.copy(sourcePath.resolve("third"), watchPath.resolve("third"), cpOptions); @@ -269,17 +278,62 @@ public void processElement(ProcessContext context, @StateId("count") ValueState< // Member variables need to be serializable. private final String sourcePathStr; private final String watchPathStr; + private final long baseTimestampMillis; + } + + private static class AfterNumberOfNewOutputs + implements Watch.Growth.TerminationCondition { + private final int numOutputs; + + public AfterNumberOfNewOutputs(int numOutputs) { + this.numOutputs = numOutputs; + } + + @Override + public org.apache.beam.sdk.coders.Coder getStateCoder() { + return VarIntCoder.of(); + } + + @Override + public Integer forNewInput(org.joda.time.Instant now, String input) { + return 0; + } + + @Override + public Integer onSeenNewOutput(org.joda.time.Instant now, Integer state) { + return (state == null ? 0 : state) + 1; + } + + @Override + public boolean canStopPolling(org.joda.time.Instant now, Integer state) { + return (state == null ? 0 : state) >= numOutputs; + } + + @Override + public String toString(Integer state) { + return "AfterNumberOfNewOutputs(" + state + "/" + numOutputs + ")"; + } } @Test @Category({NeedsRunner.class, UsesUnboundedSplittableParDo.class}) public void testMatchWatchForNewFiles() throws IOException, InterruptedException { + final int numFiles = 3; + final int numFirstFiles = 3; + final int numUpdatedFiles = 6; // first x3, second x2, third x1 + // Write some files to a "source" directory. final Path sourcePath = tmpFolder.getRoot().toPath().resolve("source"); sourcePath.toFile().mkdir(); Files.write(sourcePath.resolve("first"), new byte[42]); Files.write(sourcePath.resolve("second"), new byte[37]); Files.write(sourcePath.resolve("third"), new byte[99]); + // Keep controlled mtimes in the past so updates are distinct without future timestamps. + long baseTimestampMillis = System.currentTimeMillis() - Duration.standardMinutes(1).getMillis(); + FileTime baseTimestamp = FileTime.fromMillis(baseTimestampMillis); + Files.setLastModifiedTime(sourcePath.resolve("first"), baseTimestamp); + Files.setLastModifiedTime(sourcePath.resolve("second"), baseTimestamp); + Files.setLastModifiedTime(sourcePath.resolve("third"), baseTimestamp); // Create a "watch" directory that the pipeline will copy files into. final Path watchPath = tmpFolder.getRoot().toPath().resolve("watch"); @@ -291,7 +345,9 @@ public void testMatchWatchForNewFiles() throws IOException, InterruptedException .filepattern(watchPath.resolve("*").toString()) .continuously( Duration.millis(100), - Watch.Growth.afterTimeSinceNewOutput(Duration.standardSeconds(1)))); + Watch.Growth.eitherOf( + new AfterNumberOfNewOutputs(numFiles), + Watch.Growth.afterTimeSinceNewOutput(Duration.standardSeconds(10))))); PCollection matchAllMetadata = p.apply("create for matchAll new files", Create.of(watchPath.resolve("*").toString())) .apply( @@ -299,7 +355,9 @@ public void testMatchWatchForNewFiles() throws IOException, InterruptedException FileIO.matchAll() .continuously( Duration.millis(100), - Watch.Growth.afterTimeSinceNewOutput(Duration.standardSeconds(1)))); + Watch.Growth.eitherOf( + new AfterNumberOfNewOutputs(numFiles), + Watch.Growth.afterTimeSinceNewOutput(Duration.standardSeconds(10))))); PCollection matchUpdatedMetadata = p.apply( "match updated", @@ -307,7 +365,9 @@ public void testMatchWatchForNewFiles() throws IOException, InterruptedException .filepattern(watchPath.resolve("first").toString()) .continuously( Duration.millis(100), - Watch.Growth.afterTimeSinceNewOutput(Duration.standardSeconds(1)), + Watch.Growth.eitherOf( + new AfterNumberOfNewOutputs(numFirstFiles), + Watch.Growth.afterTimeSinceNewOutput(Duration.standardSeconds(10))), true)); PCollection matchAllUpdatedMetadata = p.apply("create for matchAll updated files", Create.of(watchPath.resolve("*").toString())) @@ -316,7 +376,9 @@ public void testMatchWatchForNewFiles() throws IOException, InterruptedException FileIO.matchAll() .continuously( Duration.millis(100), - Watch.Growth.afterTimeSinceNewOutput(Duration.standardSeconds(1)), + Watch.Growth.eitherOf( + new AfterNumberOfNewOutputs(numUpdatedFiles), + Watch.Growth.afterTimeSinceNewOutput(Duration.standardSeconds(10))), true)); // write one file at the beginning. This will trigger the first output for matchAll @@ -334,7 +396,7 @@ public void testMatchWatchForNewFiles() throws IOException, InterruptedException TypeDescriptors.strings(), TypeDescriptor.of(MatchResult.Metadata.class))) .via((metadata) -> KV.of("dumb key", metadata))) - .apply(ParDo.of(new CopyFilesFn(sourcePath, watchPath))); + .apply(ParDo.of(new CopyFilesFn(sourcePath, watchPath, baseTimestampMillis))); assertEquals(PCollection.IsBounded.UNBOUNDED, matchMetadata.isBounded()); assertEquals(PCollection.IsBounded.UNBOUNDED, matchAllMetadata.isBounded()); diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/options/PipelineOptionsFactoryTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/options/PipelineOptionsFactoryTest.java index 2755ade271dd..44de84996211 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/options/PipelineOptionsFactoryTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/options/PipelineOptionsFactoryTest.java @@ -668,12 +668,16 @@ public void testGettersAnnotatedWithInconsistentDefault() throws Exception { // Make sure the error message gives actionable details about what // annotations were contradictory. - // Note that the quotes in the unparsed string are present in Java 11 but absent in Java 8 + // Note that the quotes in the unparsed string are present in Java 11 but absent in Java 8, + // and "value=" element name prefix is absent in Java 21+ expectedException.expectMessage( anyOf( containsString("Default.String(value=\"abc\")"), - containsString("Default.String(value=abc)"))); - expectedException.expectMessage("Default.Integer(value=1"); + containsString("Default.String(value=abc)"), + containsString("Default.String(\"abc\")"), + containsString("Default.String(abc)"))); + expectedException.expectMessage( + anyOf(containsString("Default.Integer(value=1"), containsString("Default.Integer(1"))); // When we attempt to convert, we should error at this moment. options.as(GetterWithInconsistentDefaultType.class); @@ -686,11 +690,19 @@ public void testGettersAnnotatedWithInconsistentDefaultValue() throws Exception expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage( - "Property [object] is marked with contradictory annotations. Found [" - + "[Default.Integer(value=1) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" - + "$GetterWithDefault#getObject()], " - + "[Default.Integer(value=0) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" - + "$GetterWithInconsistentDefaultValue#getObject()]]."); + anyOf( + containsString( + "Property [object] is marked with contradictory annotations. Found [" + + "[Default.Integer(value=1) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" + + "$GetterWithDefault#getObject()], " + + "[Default.Integer(value=0) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" + + "$GetterWithInconsistentDefaultValue#getObject()]]."), + containsString( + "Property [object] is marked with contradictory annotations. Found [" + + "[Default.Integer(1) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" + + "$GetterWithDefault#getObject()], " + + "[Default.Integer(0) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" + + "$GetterWithInconsistentDefaultValue#getObject()]]."))); // When we attempt to convert, we should error at this moment. options.as(GetterWithInconsistentDefaultValue.class); @@ -703,11 +715,19 @@ public void testGettersAnnotatedWithInconsistentJsonIgnoreValue() throws Excepti expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage( - "Property [object] is marked with contradictory annotations. Found [" - + "[JsonIgnore(value=false) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" - + "$GetterWithInconsistentJsonIgnoreValue#getObject()], " - + "[JsonIgnore(value=true) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" - + "$GetterWithJsonIgnore#getObject()]]."); + anyOf( + containsString( + "Property [object] is marked with contradictory annotations. Found [" + + "[JsonIgnore(value=false) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" + + "$GetterWithInconsistentJsonIgnoreValue#getObject()], " + + "[JsonIgnore(value=true) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" + + "$GetterWithJsonIgnore#getObject()]]."), + containsString( + "Property [object] is marked with contradictory annotations. Found [" + + "[JsonIgnore(false) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" + + "$GetterWithInconsistentJsonIgnoreValue#getObject()], " + + "[JsonIgnore(true) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest" + + "$GetterWithJsonIgnore#getObject()]]."))); // When we attempt to convert, we should error at this moment. options.as(GetterWithInconsistentJsonIgnoreValue.class); @@ -731,12 +751,16 @@ public void testGettersWithMultipleDefaults() throws Exception { // Make sure the error message gives actionable details about what annotations were // contradictory. - // Note that the quotes in the unparsed string are present in Java 11 but absent in Java 8 + // Note that the quotes in the unparsed string are present in Java 11 but absent in Java 8, + // and "value=" element name prefix is absent in Java 21+ expectedException.expectMessage( anyOf( containsString("Default.String(value=\"abc\")"), - containsString("Default.String(value=abc)"))); - expectedException.expectMessage("Default.Integer(value=0)"); + containsString("Default.String(value=abc)"), + containsString("Default.String(\"abc\")"), + containsString("Default.String(abc)"))); + expectedException.expectMessage( + anyOf(containsString("Default.Integer(value=0)"), containsString("Default.Integer(0)"))); // When we attempt to create, we should error at this moment. PipelineOptionsFactory.as(GettersWithMultipleDefault.class); 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 df6c9cf18e5a..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; @@ -24,6 +25,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; @@ -32,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; @@ -46,9 +49,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; @@ -56,7 +65,9 @@ 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; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedArrayBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedArraysBean; @@ -68,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; @@ -131,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(); @@ -138,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")); @@ -159,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()); @@ -179,6 +192,98 @@ 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 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(); @@ -625,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 7a66decde017..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,16 +17,19 @@ */ 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; 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; @@ -34,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; @@ -46,20 +50,29 @@ 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.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; 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; @@ -76,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; @@ -180,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(); @@ -187,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")); @@ -208,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); @@ -228,6 +237,135 @@ 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 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(); @@ -781,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/SchemaTranslationTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaTranslationTest.java index 8a382c4ad1b8..925b1e02b786 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaTranslationTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaTranslationTest.java @@ -51,6 +51,7 @@ import org.apache.beam.sdk.schemas.logicaltypes.PythonCallable; import org.apache.beam.sdk.schemas.logicaltypes.SchemaLogicalType; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; import org.apache.beam.sdk.schemas.logicaltypes.UnknownLogicalType; import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; import org.apache.beam.sdk.schemas.logicaltypes.VariableString; @@ -460,6 +461,12 @@ public static Iterable data() { .add(FieldType.logicalType(FixedString.of(10))) .add(FieldType.logicalType(VariableString.of(10))) .add(FieldType.logicalType(FixedPrecisionNumeric.of(10))) + .add(FieldType.logicalType(Timestamp.MILLIS)) + .add(FieldType.logicalType(Timestamp.MICROS)) + .add(FieldType.logicalType(Timestamp.NANOS)) + // INT16/INT32 subseconds representation boundary. + .add(FieldType.logicalType(Timestamp.of(4))) + .add(FieldType.logicalType(Timestamp.of(5))) .add(FieldType.logicalType(new PortableNullArgLogicalType())) .add(FieldType.logicalType(new NullArgumentLogicalType())) .build(); diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaUtilsTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaUtilsTest.java index 45c06f65aaa7..c34832f58fb8 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaUtilsTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/SchemaUtilsTest.java @@ -18,8 +18,13 @@ package org.apache.beam.sdk.schemas; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.values.Row; import org.junit.Test; /** Tests for {@link org.apache.beam.sdk.schemas.SchemaUtils}. */ @@ -104,4 +109,42 @@ public void testWidenMap() { .build(); assertEquals(expected, SchemaUtils.mergeWideningNullable(schema1, schema2)); } + + @Test + public void testToPrettyStringRendersNullInsideArray() { + Schema schema = + Schema.builder().addArrayField("a", FieldType.STRING.withNullable(true)).build(); + Row row = Row.withSchema(schema).addValue(Arrays.asList("x", null)).build(); + assertTrue(row.toString(), row.toString().contains("null")); + } + + @Test + public void testToPrettyStringRendersNullMapValue() { + Schema schema = + Schema.builder() + .addMapField("m", FieldType.STRING, FieldType.STRING.withNullable(true)) + .build(); + Map map = new HashMap<>(); + map.put("k", null); + Row row = Row.withSchema(schema).addValue(map).build(); + assertTrue(row.toString(), row.toString().contains("null")); + } + + @Test + public void testToPrettyStringRendersNullRowInsideArray() { + Schema inner = Schema.builder().addStringField("s").build(); + Schema schema = + Schema.builder().addArrayField("a", FieldType.row(inner).withNullable(true)).build(); + Row row = Row.withSchema(schema).addValue(Arrays.asList((Row) null)).build(); + assertTrue(row.toString(), row.toString().contains("null")); + } + + @Test + public void testToPrettyStringRendersNullInsideArrayOfInts() { + Schema schema = Schema.builder().addArrayField("a", FieldType.INT32.withNullable(true)).build(); + Row row = Row.withSchema(schema).addValue(Arrays.asList(1, null)).build(); + String rendered = row.toString(); + assertTrue(rendered, rendered.contains("1")); + assertTrue(rendered, rendered.contains("null")); + } } 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/transforms/providers/JavaFilterTransformProviderTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/providers/JavaFilterTransformProviderTest.java index b269b12f1554..733a84e75258 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/providers/JavaFilterTransformProviderTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/providers/JavaFilterTransformProviderTest.java @@ -107,12 +107,18 @@ public void testErrorHandling() { PCollection errors = result.get("errors"); Schema errorSchema = errors.getSchema(); + // StringIndexOutOfBoundsException message format changed in Java 17+ + int javaVersion = + Integer.parseInt(System.getProperty("java.specification.version").replace("1.", "")); + String expectedErrorMessage = + javaVersion >= 17 ? "Index 7 out of bounds for length 5" : "String index out of range: 7"; + PAssert.that(errors) .containsInAnyOrder( Row.withSchema(errorSchema) .withFieldValue( "failed_row", Row.withSchema(inputSchema).addValues("short").build()) - .withFieldValue("error_message", "String index out of range: 7") + .withFieldValue("error_message", expectedErrorMessage) .build()); pipeline.run(); } 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..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 @@ -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,122 @@ 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(); + + @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 b82c4dc0e7e6..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 @@ -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,141 @@ 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(); + + @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/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 new file mode 100644 index 000000000000..d098104a8cf6 --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/AsyncWrapperTest.java @@ -0,0 +1,941 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.beam.sdk.state.BagState; +import org.apache.beam.sdk.state.ReadableState; +import org.apache.beam.sdk.state.Timer; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.values.KV; +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; + +/** Tests for verifying async processing structures and logic. */ +@RunWith(JUnit4.class) +public class AsyncWrapperTest implements Serializable { + + private final boolean useThreadPool = true; + + // Used for testing basic DoFn processing logic with optional latency. + private static class BasicDofn extends DoFn { + private final long sleepTimeMs; + private int processed = 0; + private final ReentrantLock lock = new ReentrantLock(); + + BasicDofn(long sleepTimeMs) { + this.sleepTimeMs = sleepTimeMs; + } + + BasicDofn() { + this(0); + } + + @ProcessElement + public void processElement(@Element String element, OutputReceiver receiver) { + if (sleepTimeMs > 0) { + try { + Thread.sleep(sleepTimeMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + lock.lock(); + try { + processed += 1; + } finally { + lock.unlock(); + } + receiver.output(element); + } + + int getProcessed() { + lock.lock(); + try { + return processed; + } finally { + lock.unlock(); + } + } + } + + // Used for testing multi element processing with optional finish bundle call. + private static class MultiElementDoFn extends DoFn { + @ProcessElement + public void processElement(@Element String element, OutputReceiver receiver) { + receiver.output(element); + receiver.output(element); + } + + @FinishBundle + public void finishBundle(FinishBundleContext c) { + c.output("bundle end", Instant.now(), GlobalWindow.INSTANCE); + } + } + + // Used for testing BagState thread safety. + private static class FakeBagState implements BagState { + private final List items; + private final ReentrantLock lock = new ReentrantLock(); + + FakeBagState(List initialItems) { + this.items = new ArrayList<>(initialItems); + } + + FakeBagState(T initialItem) { + this(new ArrayList<>(List.of(initialItem))); + } + + FakeBagState() { + this(new ArrayList<>()); + } + + @Override + public void add(T item) { + lock.lock(); + try { + items.add(item); + } finally { + lock.unlock(); + } + } + + @Override + public void clear() { + lock.lock(); + try { + items.clear(); + } finally { + lock.unlock(); + } + } + + @Override + public Iterable read() { + lock.lock(); + try { + return new ArrayList<>(items); + } finally { + lock.unlock(); + } + } + + @Override + public ReadableState isEmpty() { + return new ReadableState() { + @Override + public Boolean read() { + lock.lock(); + try { + return items.isEmpty(); + } finally { + lock.unlock(); + } + } + + @Override + public ReadableState readLater() { + return this; + } + }; + } + + @Override + public BagState readLater() { + return this; + } + } + + // 4. Used for testing Timer mock implementations. + private static class FakeTimer implements Timer { + private volatile Instant time = Instant.EPOCH; + + @Override + public void set(Instant absoluteTime) { + this.time = absoluteTime; + } + + @Override + public void setRelative() {} + + @Override + public void clear() { + this.time = Instant.EPOCH; + } + + @Override + public Timer offset(Duration offset) { + return this; + } + + @Override + public Timer align(Duration period) { + return this; + } + + @Override + public Timer withOutputTimestamp(Instant outputTime) { + return this; + } + + @Override + public Timer withNoOutputTimestamp() { + return this; + } + + @Override + public Instant getCurrentRelativeTime() { + return time; + } + } + + @Before + public void setUp() { + AsyncWrapper.resetState(); + } + + private void waitForEmpty(AsyncWrapper asyncWrapper) { + waitForEmpty(asyncWrapper, 10); + } + + private void waitForEmpty(AsyncWrapper asyncWrapper, int timeoutSeconds) { + 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(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + try { + Thread.sleep(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void checkOutput(List result, List expectedOutput) { + List resultStr = new ArrayList<>(); + for (T val : result) { + resultStr.add(val.toString()); + } + List expectedStr = new ArrayList<>(); + for (T val : expectedOutput) { + expectedStr.add(val.toString()); + } + Collections.sort(resultStr); + Collections.sort(expectedStr); + assertEquals(expectedStr, resultStr); + } + + private void checkItemsInBuffer(AsyncWrapper asyncWrapper, int expectedCount) { + assertEquals(expectedCount, asyncWrapper.getItemsInBufferCount()); + } + + // Test 1: testCustomIdFn + // Verifies custom ID extraction and deduplication of in-flight duplicate elements. + @Test + public void testCustomIdFn() { + class CustomIdObject implements Serializable { + final int elementId; + final String value; + + CustomIdObject(int elementId, String value) { + this.elementId = elementId; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomIdObject)) { + return false; + } + CustomIdObject that = (CustomIdObject) o; + return elementId == that.elementId; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(elementId); + } + + @Override + public String toString() { + return "CustomIdObject{id=" + elementId + ", val=" + value + "}"; + } + } + + class CustomIdDofn extends DoFn { + @ProcessElement + public void processElement(@Element CustomIdObject element, OutputReceiver receiver) { + receiver.output(element.value); + } + } + + CustomIdDofn dofn = new CustomIdDofn(); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, + 1, + Duration.standardSeconds(5), + null, + null, + null, + x -> x.elementId, + useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagState = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + + KV msg1 = KV.of("key1", new CustomIdObject(1, "a")); + KV msg2 = KV.of("key1", new CustomIdObject(1, "b")); + + asyncWrapper.processDirect(msg1, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + asyncWrapper.processDirect(msg2, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + waitForEmpty(asyncWrapper); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.singletonList("a")); + assertEquals(0, fakeBagState.items.size()); + } + + // Test 2: testBasic + // Verifies the standard end-to-end execution flow. Elements should be queued in persistent state + // and output correctly upon completion. + @Test + public void testBasic() { + BasicDofn dofn = new BasicDofn(); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagState = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + KV msg = KV.of("key1", "1"); + + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + assertEquals(1, fakeBagState.items.size()); + assertNotEquals(Instant.EPOCH, fakeTimer.getCurrentRelativeTime()); + + waitForEmpty(asyncWrapper); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.singletonList("1")); + assertEquals(1, dofn.getProcessed()); + assertEquals(0, fakeBagState.items.size()); + } + + // Test 3: testMultiKey + // Verifies key grouping isolation. Firing a timer for one partition key must not release + // or interfere with elements queued under a different partition key. + @Test + public void testMultiKey() { + for (boolean useThreadPool : new boolean[] {true, false}) { + BasicDofn dofn = new BasicDofn(); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagStateKey1 = new FakeBagState<>(); + FakeBagState> fakeBagStateKey2 = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + + KV msg1 = KV.of("key1", "1"); + KV msg2 = KV.of("key2", "2"); + + asyncWrapper.processDirect( + msg1, GlobalWindow.INSTANCE, Instant.now(), fakeBagStateKey1, fakeTimer); + asyncWrapper.processDirect( + msg2, GlobalWindow.INSTANCE, Instant.now(), fakeBagStateKey2, fakeTimer); + + waitForEmpty(asyncWrapper); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagStateKey2, fakeTimer); + checkOutput(result, Collections.singletonList("2")); + assertEquals(1, fakeBagStateKey1.items.size()); + assertEquals(0, fakeBagStateKey2.items.size()); + + result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagStateKey1, fakeTimer); + checkOutput(result, Collections.singletonList("1")); + assertEquals(0, fakeBagStateKey1.items.size()); + assertEquals(0, fakeBagStateKey2.items.size()); + } + } + + // Test 4: testLongItem + // Verifies that outputs are kept in-flight and not committed prematurely if the background + // execution task has not finished processing yet. + @Test + public void testLongItem() { + BasicDofn dofn = new BasicDofn(500); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagState = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + KV msg = KV.of("key1", "1"); + + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.emptyList()); + assertEquals(0, dofn.getProcessed()); + assertEquals(1, fakeBagState.items.size()); + + waitForEmpty(asyncWrapper, 2); + + result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.singletonList("1")); + assertEquals(1, dofn.getProcessed()); + assertEquals(0, fakeBagState.items.size()); + } + + // Test 5: testLostItem + // Verifies if the local worker's in-memory cache is empty but the runner's + // persistent state contains pending items. + // The wrapper must automatically detect the mismatch and reschedule execution. + @Test + public void testLostItem() { + BasicDofn dofn = new BasicDofn(); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeTimer fakeTimer = new FakeTimer(); + KV msg = KV.of("key1", "1"); + FakeBagState> fakeBagState = new FakeBagState<>(msg); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.emptyList()); + + waitForEmpty(asyncWrapper); + + result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.singletonList("1")); + } + + // Test 6: testCancelledItem + // Verifies active task cancellation if a pending element is deleted from the runner's state. + @Test + public void testCancelledItem() { + BasicDofn dofn = new BasicDofn(); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + KV msg1 = KV.of("key1", "1"); + KV msg2 = KV.of("key1", "2"); + FakeTimer fakeTimer = new FakeTimer(); + FakeBagState> fakeBagState = new FakeBagState<>(); + + asyncWrapper.processDirect(msg1, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + asyncWrapper.processDirect(msg2, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + waitForEmpty(asyncWrapper); + + fakeBagState.clear(); + fakeBagState.add(msg2); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.singletonList("2")); + assertEquals(0, fakeBagState.items.size()); + } + + // Test 7: testMultiElementDofn + // Verifies support for DoFns that emit multiple outputs per element, and correctly aggregates + // outputs produced during the finishBundle stage of the sync DoFn's lifecycle. + @Test + public void testMultiElementDofn() { + MultiElementDoFn dofn = new MultiElementDoFn(); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagState = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + KV msg = KV.of("key1", "1"); + + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + waitForEmpty(asyncWrapper); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Arrays.asList("1", "1", "bundle end")); + assertEquals(0, fakeBagState.items.size()); + } + + // Test 8: testDuplicates + // Verifies deduplication of duplicate elements under active processing. + // Identical elements should not spawn multiple concurrent background executions. + @Test + public void testDuplicates() { + BasicDofn dofn = new BasicDofn(10); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagState = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + KV msg = KV.of("key1", "1"); + + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + fakeBagState.clear(); + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + assertEquals(1, fakeBagState.items.size()); + + waitForEmpty(asyncWrapper); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.singletonList("1")); + assertEquals(0, fakeBagState.items.size()); + } + + // Test 9: testSlowDuplicates + // Verifies that duplicate elements sent after the in-memory buffer + // has cleared are correctly tracked and processed. + @Test + public void testSlowDuplicates() { + BasicDofn dofn = new BasicDofn(20); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagState = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + KV msg = KV.of("key1", "1"); + + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + fakeBagState.clear(); + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.emptyList()); + assertEquals(0, fakeBagState.items.size()); + + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + assertEquals(1, fakeBagState.items.size()); + waitForEmpty(asyncWrapper); + + result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.singletonList("1")); + assertEquals(0, fakeBagState.items.size()); + } + + // Test 10: testBufferCount + // Verifies accurate in-flight metrics tracking. + // The item count in the buffer must increment on task scheduling + // and decrement immediately upon execution completion. + @Test + public void testBufferCount() { + BasicDofn dofn = new BasicDofn(10); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + KV msg = KV.of("key1", "1"); + FakeTimer fakeTimer = new FakeTimer(); + FakeBagState> fakeBagState = new FakeBagState<>(); + + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + checkItemsInBuffer(asyncWrapper, 1); + + waitForEmpty(asyncWrapper); + checkItemsInBuffer(asyncWrapper, 0); + + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkItemsInBuffer(asyncWrapper, 0); + } + + // Test 11: testBufferStopsAcceptingItems + // Verifies queue boundaries and backpressure throttling. + // When concurrent threads push elements exceeding the capacity limit, + // the scheduler must block and delay submissions appropriately. + @Test + public void testBufferStopsAcceptingItems() { + BasicDofn dofn = new BasicDofn(500); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, + 1, + Duration.standardSeconds(5), + 5, // max buffer capacity + null, + null, + null, + useThreadPool); + asyncWrapper.setup(null); + + FakeTimer fakeTimer = new FakeTimer(); + FakeBagState> fakeBagState = new FakeBagState<>(); + + ExecutorService poolExecutor = Executors.newFixedThreadPool(10); + List expectedOutput = new ArrayList<>(); + List> futures = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + final int idx = i; + expectedOutput.add(String.valueOf(idx)); + futures.add( + poolExecutor.submit( + () -> { + KV item = KV.of("key", String.valueOf(idx)); + asyncWrapper.processDirect( + item, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + })); + } + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + assertEquals(5, asyncWrapper.getItemsInBufferCount()); + + waitForEmpty(asyncWrapper, 100); + + // Verify that all background tasks completed successfully without throwing exceptions + for (Future future : futures) { + try { + future.get(); // This will re-throw any exception that occurred in the background thread + } catch (Exception e) { + throw new AssertionError("Background task failed", e); + } + } + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + + waitForEmpty(asyncWrapper, 100); + + result.addAll( + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer)); + + checkOutput(result, expectedOutput); + checkItemsInBuffer(asyncWrapper, 0); + poolExecutor.shutdown(); + } + + // Test 12: testBufferWithCancellation + // Verifies actively cancelled elements are cleanly dropped from the buffer during throttling. + @Test + public void testBufferWithCancellation() { + BasicDofn dofn = new BasicDofn(10); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + KV msg1 = KV.of("key1", "1"); + KV msg2 = KV.of("key1", "2"); + FakeTimer fakeTimer = new FakeTimer(); + FakeBagState> fakeBagState = new FakeBagState<>(); + + asyncWrapper.processDirect(msg1, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + asyncWrapper.processDirect(msg2, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + checkItemsInBuffer(asyncWrapper, 2); + + fakeBagState.clear(); + fakeBagState.add(msg2); + + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkOutput(result, Collections.emptyList()); + assertEquals(1, fakeBagState.items.size()); + + waitForEmpty(asyncWrapper); + + result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + checkItemsInBuffer(asyncWrapper, 0); + checkOutput(result, Collections.singletonList("2")); + } + + // Test 13: testLoadCorrectness + // Verifies that the async wrapper processes large concurrent volumes + // across multiple keys correctly under heavy multi-threaded load. + @Test + public void testLoadCorrectness() { + BasicDofn dofn = new BasicDofn(10); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, + 1, + Duration.standardSeconds(5), + null, + null, + Duration.millis(10), + null, + useThreadPool); + asyncWrapper.setup(null); + + java.util.Map>> bagStates = new java.util.HashMap<>(); + java.util.Map timers = new java.util.HashMap<>(); + java.util.Map> expectedOutputs = new java.util.HashMap<>(); + + for (int i = 0; i < 10; i++) { + String key = "key" + i; + bagStates.put(key, new FakeBagState<>()); + timers.put(key, new FakeTimer()); + expectedOutputs.put(key, new ArrayList<>()); + } + + ExecutorService poolExecutor = Executors.newFixedThreadPool(10); + List> futures = new ArrayList<>(); + Random random = new Random(); + + for (int i = 0; i < 100; i++) { + final int val = i; + final String key = "key" + random.nextInt(10); + expectedOutputs.get(key).add(String.valueOf(val)); + + futures.add( + poolExecutor.submit( + () -> { + KV item = KV.of(key, String.valueOf(val)); + asyncWrapper.processDirect( + item, + GlobalWindow.INSTANCE, + Instant.now(), + bagStates.get(key), + timers.get(key)); + })); + try { + Thread.sleep(random.nextInt(2)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + try { + Thread.sleep(1000 + random.nextInt(1000)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Verify that all background tasks completed successfully + for (Future future : futures) { + try { + future.get(); + } catch (Exception e) { + throw new AssertionError("Background task failed", e); + } + } + + boolean done = false; + java.util.Map> results = new java.util.HashMap<>(); + for (int i = 0; i < 10; i++) { + results.put("key" + i, new ArrayList<>()); + } + + while (!done) { + done = true; + for (int i = 0; i < 10; i++) { + String key = "key" + i; + results + .get(key) + .addAll( + asyncWrapper.commitFinishedItemsDirect( + timers.get(key).getCurrentRelativeTime(), bagStates.get(key), timers.get(key))); + if (!bagStates.get(key).items.isEmpty()) { + done = false; + } else { + checkOutput(results.get(key), expectedOutputs.get(key)); + } + } + try { + Thread.sleep(10 + random.nextInt(20)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + for (int i = 0; i < 10; i++) { + String key = "key" + i; + checkOutput(results.get(key), expectedOutputs.get(key)); + assertEquals(0, bagStates.get(key).items.size()); + } + poolExecutor.shutdown(); + } + + // Test 14: testResetStateConcurrentTeardown + // Verifies safe resource cleanup during concurrent shutdown. + // Resetting the global shared execution state while workers are running + // must complete cleanly without thread or lock deadlocks. + @Test + public void testResetStateConcurrentTeardown() { + BasicDofn dofn = new BasicDofn(10); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagState = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + + asyncWrapper.processDirect( + KV.of("key1", "1"), GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + + try { + Thread.sleep(2); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Verify calling resetState() while background tasks are running finishes cleanly + AsyncWrapper.resetState(); + } + + // Test 15: testTransientRpcFailureRetry + // Verify DoFn exceptions wipe local active state so bundle retries reschedule work. + @Test + public void testTransientRpcFailureRetry() { + FlakyDoFn dofn = new FlakyDoFn(); + AsyncWrapper asyncWrapper = + new AsyncWrapper<>( + dofn, 1, Duration.standardSeconds(5), null, null, null, null, useThreadPool); + asyncWrapper.setup(null); + + FakeBagState> fakeBagState = new FakeBagState<>(); + FakeTimer fakeTimer = new FakeTimer(); + KV msg = KV.of("key1", "1"); + + asyncWrapper.processDirect(msg, GlobalWindow.INSTANCE, Instant.now(), fakeBagState, fakeTimer); + waitForEmpty(asyncWrapper); + + // Attempt 1 should throw RuntimeException wrapping ExecutionException + assertThrows( + RuntimeException.class, + () -> + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer)); + + // Verify the failed Future was removed from local active elements + assertEquals(0, asyncWrapper.getItemsInBufferCount()); + + // Simulate runner bundle retry: commitFinishedItemsDirect runs again with msg still in state. + // Because the dead future was removed, it will reschedule msg and succeed on Attempt 2. + waitForEmpty(asyncWrapper); + List result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + if (result.isEmpty()) { + waitForEmpty(asyncWrapper); + result = + asyncWrapper.commitFinishedItemsDirect( + fakeTimer.getCurrentRelativeTime(), fakeBagState, fakeTimer); + } + + checkOutput(result, Collections.singletonList("1")); + assertEquals(0, fakeBagState.items.size()); + } + + private static class FlakyDoFn extends DoFn { + private int attempts = 0; + private final ReentrantLock lock = new ReentrantLock(); + + @ProcessElement + public void processElement(@Element String element, OutputReceiver receiver) { + lock.lock(); + try { + attempts++; + if (attempts == 1) { + throw new RuntimeException("Transient RPC Error"); + } + } finally { + lock.unlock(); + } + receiver.output(element); + } + } +} 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"); + } + } } 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)); + } +} 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..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 @@ -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, @@ -7186,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/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/transforms/reflect/DoFnInvokersTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnInvokersTest.java index e9269deb18ea..9f4fdb6c9c28 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnInvokersTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnInvokersTest.java @@ -82,6 +82,7 @@ import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.sdk.values.TypeDescriptors; import org.apache.beam.sdk.values.WindowedValues; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; import org.junit.Before; import org.junit.Rule; @@ -1440,6 +1441,18 @@ public TypeDescriptor getInputTypeDescriptor() { public TypeDescriptor getOutputTypeDescriptor() { return typeDescriptor; } + + // Deliberately make separate instances equal. The per-instance cache must use identity + // because equal instances can have different generic types. + @Override + public boolean equals(@Nullable Object other) { + return other instanceof DynamicTypeDoFn; + } + + @Override + public int hashCode() { + return 0; + } } DoFn stringFn = new DynamicTypeDoFn<>(TypeDescriptors.strings()); @@ -1456,4 +1469,38 @@ public TypeDescriptor getOutputTypeDescriptor() { stringInvoker.getClass(), intInvoker.getClass()); } + + @Test + public void testTypeDescriptorResolutionIsCachedPerDoFnInstance() { + class CountingTypeDoFn extends DoFn { + private int inputTypeDescriptorCalls; + private int outputTypeDescriptorCalls; + + @ProcessElement + public void processElement(@Element String element, OutputReceiver out) { + out.output(element); + } + + @Override + public TypeDescriptor getInputTypeDescriptor() { + inputTypeDescriptorCalls++; + return TypeDescriptors.strings(); + } + + @Override + public TypeDescriptor getOutputTypeDescriptor() { + outputTypeDescriptorCalls++; + return TypeDescriptors.strings(); + } + } + + CountingTypeDoFn fn = new CountingTypeDoFn(); + + DoFnInvoker firstInvoker = DoFnInvokers.invokerFor(fn); + DoFnInvoker secondInvoker = DoFnInvokers.invokerFor(fn); + + assertSame(firstInvoker.getClass(), secondInvoker.getClass()); + assertEquals(1, fn.inputTypeDescriptorCalls); + assertEquals(1, fn.outputTypeDescriptorCalls); + } } 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..d7c42e421532 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 @@ -51,6 +51,7 @@ import org.apache.beam.sdk.state.TimerSpecs; import org.apache.beam.sdk.state.ValueState; import org.apache.beam.sdk.state.WatermarkHoldState; +import org.apache.beam.sdk.testing.ExpectedLogs; import org.apache.beam.sdk.testing.SerializableMatchers; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.Sum; @@ -137,6 +138,8 @@ public void process(@FireTimestamp Instant ts) {} @Rule public ExpectedException thrown = ExpectedException.none(); + @Rule public ExpectedLogs expectedLogs = ExpectedLogs.none(DoFnSignatures.class); + @Test public void testBasicDoFnProcessContext() throws Exception { DoFnSignature sig = @@ -1422,16 +1425,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 { @@ -1806,4 +1813,118 @@ public void onMyTimer() {} @Override public void processWithTimer(ProcessContext context, Timer timer) {} } + + // Test DoFns for ValueState collection warning tests + private static class DoFnWithMapValueState extends DoFn { + @StateId("mapState") + private final StateSpec>> mapState = + StateSpecs.value(); + + @ProcessElement + public void process() {} + } + + private static class DoFnWithListValueState extends DoFn { + @StateId("listState") + private final StateSpec>> listState = StateSpecs.value(); + + @ProcessElement + public void process() {} + } + + private static class DoFnWithSetValueState extends DoFn { + @StateId("setState") + private final StateSpec>> setState = StateSpecs.value(); + + @ProcessElement + public void process() {} + } + + private static class DoFnWithSimpleValueState extends DoFn { + @StateId("simpleState") + private final StateSpec> simpleState = StateSpecs.value(); + + @ProcessElement + public void process() {} + } + + private static class DoFnWithParameterizedSetValueState extends DoFn { + @StateId("parameterizedSetState") + private final StateSpec>> parameterizedSetState = + StateSpecs.value(); + + @ProcessElement + public void process() {} + } + + private static class DoFnWithParameterizedListValueState extends DoFn { + @StateId("parameterizedListState") + private final StateSpec>> parameterizedListState = + StateSpecs.value(); + + @ProcessElement + public void process() {} + } + + private static class DoFnWithTypeVariableValueState extends DoFn { + @StateId("typeVariableState") + private final StateSpec> typeVariableState = StateSpecs.value(); + + @ProcessElement + public void process() {} + } + + @Test + public void testValueStateWithMapLogsWarning() { + // This test verifies that the signature can be parsed for DoFns with collection ValueState. + // The warning is logged but doesn't prevent the signature from being created. + DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithMapValueState.class); + assertThat(signature.stateDeclarations().get("mapState"), notNullValue()); + } + + @Test + public void testValueStateWithListLogsWarning() { + DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithListValueState.class); + assertThat(signature.stateDeclarations().get("listState"), notNullValue()); + } + + @Test + public void testValueStateWithSetLogsWarning() { + DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithSetValueState.class); + assertThat(signature.stateDeclarations().get("setState"), notNullValue()); + } + + @Test + public void testValueStateWithSimpleTypeNoWarning() { + // Simple types should not trigger any warning + DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithSimpleValueState.class); + assertThat(signature.stateDeclarations().get("simpleState"), notNullValue()); + expectedLogs.verifyNotLogged("with collection type"); + } + + @Test + public void testValueStateWithParameterizedSetLogsWarning() { + // A parameterized collection such as ValueState> still has a known raw collection type + // (Set) even though the element type is a type variable, so it should still recommend SetState. + DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithParameterizedSetValueState.class); + assertThat(signature.stateDeclarations().get("parameterizedSetState"), notNullValue()); + expectedLogs.verifyWarn("SetState"); + } + + @Test + public void testValueStateWithParameterizedListLogsWarning() { + DoFnSignature signature = + DoFnSignatures.getSignature(DoFnWithParameterizedListValueState.class); + assertThat(signature.stateDeclarations().get("parameterizedListState"), notNullValue()); + expectedLogs.verifyWarn("BagState or OrderedListState"); + } + + @Test + public void testValueStateWithBareTypeVariableNoWarning() { + // A bare type variable payload (ValueState) has no raw collection type to inspect and must + // not produce a collection recommendation. + DoFnSignature signature = DoFnSignatures.getSignature(DoFnWithTypeVariableValueState.class); + assertThat(signature.stateDeclarations().get("typeVariableState"), notNullValue()); + expectedLogs.verifyNotLogged("with collection type"); + } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/RowJsonTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/RowJsonTest.java index 328765bf7f15..81f69b62c53b 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/RowJsonTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/RowJsonTest.java @@ -30,6 +30,8 @@ import java.math.BigDecimal; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.logicaltypes.VariableString; @@ -80,7 +82,8 @@ public static Collection data() { makeArrayOfArraysTestCase(), makeNestedRowTestCase(), makeDoublyNestedRowTestCase(), - makeNullsTestCase()); + makeNullsTestCase(), + makeMapFieldTestCase()); } private static Object[] makeFlatRowTestCase() { @@ -244,6 +247,21 @@ private static Object[] makeNullsTestCase() { return new Object[] {"Nulls", schema, rowString, expectedRow}; } + private static Object[] makeMapFieldTestCase() { + Schema schema = + Schema.builder().addMapField("f_map", FieldType.STRING, FieldType.INT32).build(); + + String rowString = "{\n" + "\"f_map\" : {\"key1\": 1, \"key2\": 2}\n" + "}"; + + Map expectedMap = new HashMap<>(); + expectedMap.put("key1", 1); + expectedMap.put("key2", 2); + + Row expectedRow = Row.withSchema(schema).addValues(expectedMap).build(); + + return new Object[] {"Map field", schema, rowString, expectedRow}; + } + @Test public void testDeserialize() throws IOException { Row parsedRow = @@ -564,6 +582,12 @@ public void testSupportedDatetimeConversions() throws Exception { testSupportedConversion(FieldType.DATETIME, quoted(DATETIME_STRING), DATETIME_VALUE); } + @Test + public void testSupportedDatetimeWithSpaceConversions() throws Exception { + String datetimeWithSpace = DATETIME_STRING.replace('T', ' '); + testSupportedConversion(FieldType.DATETIME, quoted(datetimeWithSpace), DATETIME_VALUE); + } + private void testSupportedConversion( FieldType fieldType, String jsonFieldValue, Object expectedRowFieldValue) throws Exception { 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/core/src/test/java/org/apache/beam/sdk/util/common/ReflectHelpersTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/common/ReflectHelpersTest.java index 5f20c4576fef..e999169abb7d 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/common/ReflectHelpersTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/common/ReflectHelpersTest.java @@ -137,15 +137,20 @@ public void testFormatAnnotationDefault() throws Exception { assertThat( ReflectHelpers.formatAnnotation(Options.class.getMethod("getString").getAnnotations()[0]), anyOf( - equalTo("Default.String(value=package.OuterClass$InnerClass#method())"), - equalTo("Default.String(value=\"package.OuterClass$InnerClass#method()\")"))); + equalTo("Default.String(value=package.OuterClass$InnerClass#method())"), // Java8 + equalTo("Default.String(value=\"package.OuterClass$InnerClass#method()\")"), // Java11 + equalTo("Default.String(\"package.OuterClass$InnerClass#method()\")") // Javaa21 + )); } @Test public void testFormatAnnotationJsonIgnore() throws Exception { - assertEquals( - "JsonIgnore(value=true)", - ReflectHelpers.formatAnnotation(Options.class.getMethod("getObject").getAnnotations()[0])); + assertThat( + ReflectHelpers.formatAnnotation(Options.class.getMethod("getObject").getAnnotations()[0]), + anyOf( + equalTo("JsonIgnore(value=true)"), // Java11 + equalTo("JsonIgnore(true)") // Java21 + )); } @Test 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/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/WindowIntoTransformProvider.java b/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/WindowIntoTransformProvider.java index d060d5916e9f..ca97ed287a0a 100644 --- a/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/WindowIntoTransformProvider.java +++ b/sdks/java/expansion-service/src/main/java/org/apache/beam/sdk/expansion/service/WindowIntoTransformProvider.java @@ -88,6 +88,7 @@ public List outputCollectionNames() { @DefaultSchema(AutoValueSchema.class) @AutoValue + @SuppressWarnings("mutable") public abstract static class Configuration { @SuppressWarnings({"AutoValueMutable", "mutable"}) diff --git a/sdks/java/extensions/arrow/build.gradle b/sdks/java/extensions/arrow/build.gradle index 4fc663ddbc28..4e9f238adf45 100644 --- a/sdks/java/extensions/arrow/build.gradle +++ b/sdks/java/extensions/arrow/build.gradle @@ -31,4 +31,9 @@ dependencies { testImplementation library.java.junit testImplementation library.java.hamcrest testRuntimeOnly library.java.slf4j_simple -} \ No newline at end of file +} + +test { + // Open java.nio for Apache Arrow direct memory access under Java 17+ + jvmArgs '--add-opens=java.base/java.nio=ALL-UNNAMED' +} 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..f1850df2e958 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 @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.channels.Channels; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -35,6 +36,7 @@ import org.apache.arrow.vector.ipc.ReadChannel; import org.apache.arrow.vector.ipc.message.ArrowRecordBatch; import org.apache.arrow.vector.ipc.message.MessageSerializer; +import org.apache.arrow.vector.types.FloatingPointPrecision; import org.apache.arrow.vector.types.TimeUnit; import org.apache.arrow.vector.types.pojo.ArrowType; import org.apache.arrow.vector.util.Text; @@ -57,6 +59,57 @@ */ public class ArrowConversion { + /** Get Arrow Field from Beam Field. */ + private static org.apache.arrow.vector.types.pojo.Field toArrowField(Field field) { + FieldType beamFieldType = field.getType(); + ArrowType arrowType; + // TODO: Support aggregate and logical Beam field types. + switch (beamFieldType.getTypeName()) { + case BYTE: + arrowType = new ArrowType.Int(8, true); + break; + case INT16: + arrowType = new ArrowType.Int(16, true); + break; + case INT32: + arrowType = new ArrowType.Int(32, true); + break; + case INT64: + arrowType = new ArrowType.Int(64, true); + break; + case FLOAT: + arrowType = new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE); + break; + case DOUBLE: + arrowType = new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE); + break; + case STRING: + arrowType = ArrowType.Utf8.INSTANCE; + break; + case BOOLEAN: + arrowType = ArrowType.Bool.INSTANCE; + break; + case BYTES: + arrowType = ArrowType.Binary.INSTANCE; + break; + case DATETIME: + arrowType = new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC"); + break; + default: + throw new IllegalArgumentException( + String.format( + "Arrow schema conversion does not support Beam type '%s' for field '%s'.", + beamFieldType.getTypeName(), field.getName())); + } + + org.apache.arrow.vector.types.pojo.FieldType arrowFieldType = + beamFieldType.getNullable() + ? org.apache.arrow.vector.types.pojo.FieldType.nullable(arrowType) + : org.apache.arrow.vector.types.pojo.FieldType.notNullable(arrowType); + return new org.apache.arrow.vector.types.pojo.Field( + field.getName(), arrowFieldType, Collections.emptyList()); + } + /** Get Beam Field from Arrow Field. */ private static Field toBeamField(org.apache.arrow.vector.types.pojo.Field field) { FieldType beamFieldType = toFieldType(field.getFieldType(), field.getChildren()); @@ -533,7 +586,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; } @@ -544,6 +599,14 @@ private ArrowConversion() {} /** Converts Arrow schema to Beam row schema. */ public static class ArrowSchemaTranslator { + /** Converts a supported Beam row schema to an Arrow schema. */ + public static org.apache.arrow.vector.types.pojo.Schema toArrowSchema(Schema schema) { + return new org.apache.arrow.vector.types.pojo.Schema( + schema.getFields().stream() + .map(ArrowConversion::toArrowField) + .collect(Collectors.toList())); + } + public static Schema toBeamSchema(org.apache.arrow.vector.types.pojo.Schema schema) { return toBeamSchema(schema.getFields()); } diff --git a/sdks/java/extensions/arrow/src/test/java/org/apache/beam/sdk/extensions/arrow/ArrowConversionTest.java b/sdks/java/extensions/arrow/src/test/java/org/apache/beam/sdk/extensions/arrow/ArrowConversionTest.java index 8c2975766507..be5c7120343e 100644 --- a/sdks/java/extensions/arrow/src/test/java/org/apache/beam/sdk/extensions/arrow/ArrowConversionTest.java +++ b/sdks/java/extensions/arrow/src/test/java/org/apache/beam/sdk/extensions/arrow/ArrowConversionTest.java @@ -83,6 +83,39 @@ public void toBeamSchema_convertsSimpleArrowSchema() { assertThat(ArrowConversion.ArrowSchemaTranslator.toBeamSchema(arrowSchema), equalTo(expected)); } + @Test + public void toArrowSchema_convertsSimpleBeamSchema() { + Schema beamSchema = + Schema.builder() + .addByteField("int8") + .addInt16Field("int16") + .addInt32Field("int32") + .addInt64Field("int64") + .addFloatField("float32") + .addDoubleField("float64") + .addNullableField("string", FieldType.STRING) + .addBooleanField("boolean") + .addByteArrayField("bytes") + .addDateTimeField("timestamp") + .build(); + + org.apache.arrow.vector.types.pojo.Schema expected = + new org.apache.arrow.vector.types.pojo.Schema( + ImmutableList.of( + field("int8", new ArrowType.Int(8, true)), + field("int16", new ArrowType.Int(16, true)), + field("int32", new ArrowType.Int(32, true)), + field("int64", new ArrowType.Int(64, true)), + field("float32", new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE)), + field("float64", new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)), + field("string", true, ArrowType.Utf8.INSTANCE), + field("boolean", ArrowType.Bool.INSTANCE), + field("bytes", ArrowType.Binary.INSTANCE), + field("timestamp", new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")))); + + assertThat(ArrowConversion.ArrowSchemaTranslator.toArrowSchema(beamSchema), equalTo(expected)); + } + @Test public void rowIterator() { org.apache.arrow.vector.types.pojo.Schema schema = diff --git a/sdks/java/extensions/avro/build.gradle b/sdks/java/extensions/avro/build.gradle index 718f07c03660..6c21dbb7c08b 100644 --- a/sdks/java/extensions/avro/build.gradle +++ b/sdks/java/extensions/avro/build.gradle @@ -58,6 +58,13 @@ test { } } +tasks.withType(Test).configureEach { + // Open modules for Kryo reflection and memory access under Java 17+ + jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.base/java.nio=ALL-UNNAMED' +} + dependencies { implementation library.java.byte_buddy implementation library.java.vendored_guava_32_1_2_jre diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/coders/package-info.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/coders/package-info.java index a6ba616f091d..9964f2ef0e83 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/coders/package-info.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/coders/package-info.java @@ -19,8 +19,4 @@ * Defines {@link org.apache.beam.sdk.coders.Coder Coders} to specify how data is encoded to and * decoded from byte strings using Apache Avro. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.avro.coders; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/io/package-info.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/io/package-info.java index c0b6b28ba01b..7845ce0d2960 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/io/package-info.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/io/package-info.java @@ -16,8 +16,4 @@ * limitations under the License. */ /** Defines transforms for reading and writing Avro storage format. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.avro.io; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/package-info.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/package-info.java index f1c61f986f5e..04845c36b0ee 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/package-info.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/package-info.java @@ -19,8 +19,4 @@ * Defines {@link org.apache.beam.sdk.schemas.Schema} and other classes for representing schema'd * data in a {@link org.apache.beam.sdk.Pipeline} using Apache Avro. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.avro; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/io/payloads/package-info.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/io/payloads/package-info.java index 715b3fab4fdf..40e5f095d75f 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/io/payloads/package-info.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/io/payloads/package-info.java @@ -17,8 +17,4 @@ */ /** Provides abstractions for schema-aware AvroIO. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.avro.schemas.io.payloads; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/package-info.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/package-info.java index 836b26f1e7b7..5ea137e22bd9 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/package-info.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/package-info.java @@ -19,8 +19,4 @@ * Defines {@link org.apache.beam.sdk.schemas.Schema} and other classes for representing schema'd * data in a {@link org.apache.beam.sdk.Pipeline} using Apache Avro. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.avro.schemas; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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..3c9064466d52 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); } } @@ -1363,7 +1380,7 @@ private static org.apache.avro.Schema getFieldSchema( return Days.daysBetween(Instant.EPOCH, (Instant) value).getDays(); } else if (SqlTypes.DATE.getIdentifier().equals(identifier)) { // portable SqlTypes.DATE is backed by java.time.LocalDate - return ((java.time.LocalDate) value).toEpochDay(); + return (int) ((java.time.LocalDate) value).toEpochDay(); } else if ("TIME".equals(identifier)) { return (int) ((Instant) value).getMillis(); } else if (SqlTypes.TIMESTAMP.getIdentifier().equals(identifier)) { diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/package-info.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/package-info.java index d9a74eb62174..97a920f2982f 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/package-info.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/package-info.java @@ -16,8 +16,4 @@ * limitations under the License. */ /** Defines utilities for deailing with schemas using Apache Avro. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.avro.schemas.utils; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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 6db5206c3cf6..d2452452f14f 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 @@ -1077,7 +1077,9 @@ public void testSqlTypesToGenericRecord() { org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(beamSchema); GenericRecord expectedRecord = - new GenericRecordBuilder(avroSchema).set("local_date", localDate.toEpochDay()).build(); + new GenericRecordBuilder(avroSchema) + .set("local_date", (int) localDate.toEpochDay()) + .build(); assertEquals(expectedRecord, AvroUtils.toGenericRecord(rowData, avroSchema)); } diff --git a/sdks/java/extensions/google-cloud-platform-core/build.gradle b/sdks/java/extensions/google-cloud-platform-core/build.gradle index f1bfb63c7a3a..78cfe4739ec5 100644 --- a/sdks/java/extensions/google-cloud-platform-core/build.gradle +++ b/sdks/java/extensions/google-cloud-platform-core/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation library.java.http_core implementation library.java.http_client implementation library.java.jackson_annotations + implementation library.java.jackson_core implementation library.java.jackson_databind permitUnusedDeclared library.java.jackson_databind // BEAM-11761 testImplementation project(path: ":sdks:java:core", configuration: "shadowTest") diff --git a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/options/GcsOptions.java b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/options/GcsOptions.java index 2da382a5b674..134c4cb3f281 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/options/GcsOptions.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/options/GcsOptions.java @@ -18,6 +18,15 @@ package org.apache.beam.sdk.extensions.gcp.options; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.cloud.hadoop.gcsio.GoogleCloudStorageReadOptions; import com.google.cloud.hadoop.util.AsyncWriteChannelOptions; import java.util.HashMap; @@ -49,12 +58,16 @@ public interface GcsOptions extends ApplicationNameOptions, GcpOptions, Pipeline class GcsReadOptionsFactory implements DefaultValueFactory { @Override public GoogleCloudStorageReadOptions create(PipelineOptions options) { - return GoogleCloudStorageReadOptions.DEFAULT; + // In gcs-connector v3, GoogleCloudStorageReadOptions.DEFAULT changed fadvise from SEQUENTIAL + // to AUTO. Beam workloads default to SEQUENTIAL to preserve expected sequential read + // throughput and caching behavior. + return GcsReadOptionsSerializer.DEFAULT_OPTIONS; } } /** @deprecated This option will be removed in a future release. */ - @JsonIgnore + @JsonSerialize(using = GcsReadOptionsSerializer.class) + @JsonDeserialize(using = GcsReadOptionsDeserializer.class) @Description( "The GoogleCloudStorageReadOptions instance that should be used to read from Google Cloud Storage.") @Default.InstanceFactory(GcsReadOptionsFactory.class) @@ -286,3 +299,71 @@ boolean exceedsEntryLimit() { } } } + +class GcsReadOptionsSerializer extends JsonSerializer { + static final GoogleCloudStorageReadOptions DEFAULT_OPTIONS = + GoogleCloudStorageReadOptions.DEFAULT + .toBuilder() + .setFadvise(GoogleCloudStorageReadOptions.Fadvise.SEQUENTIAL) + .build(); + + @Override + public void serialize( + GoogleCloudStorageReadOptions value, JsonGenerator gen, SerializerProvider serializers) + throws java.io.IOException { + // Note: We only support a partial set of options to propagate to remote + // workers. Setting the unsupported ones will not have any effect and will fall + // back to default in remote workers. Support for additional options can be + // added on a need basis. The full list of options can be seen in + // com.google.cloud.hadoop.gcsio.GoogleCloudStorageReadOptions. + gen.writeStartObject(); + if (value.getFadvise() != null && value.getFadvise() != DEFAULT_OPTIONS.getFadvise()) { + gen.writeStringField("fadvise", value.getFadvise().name()); + } + if (value.isFastFailOnNotFoundEnabled() != DEFAULT_OPTIONS.isFastFailOnNotFoundEnabled()) { + gen.writeBooleanField("fastFailOnNotFoundEnabled", value.isFastFailOnNotFoundEnabled()); + } + if (value.getMinRangeRequestSize() != DEFAULT_OPTIONS.getMinRangeRequestSize()) { + gen.writeNumberField("minRangeRequestSize", value.getMinRangeRequestSize()); + } + if (value.getInplaceSeekLimit() != DEFAULT_OPTIONS.getInplaceSeekLimit()) { + gen.writeNumberField("inplaceSeekLimit", value.getInplaceSeekLimit()); + } + if (value.isGrpcReadZeroCopyEnabled() != DEFAULT_OPTIONS.isGrpcReadZeroCopyEnabled()) { + gen.writeBooleanField("grpcReadZeroCopyEnabled", value.isGrpcReadZeroCopyEnabled()); + } + gen.writeEndObject(); + } +} + +class GcsReadOptionsDeserializer extends JsonDeserializer { + @Override + public GoogleCloudStorageReadOptions deserialize(JsonParser p, DeserializationContext ctxt) + throws java.io.IOException { + JsonNode root = p.readValueAsTree(); + GoogleCloudStorageReadOptions.Builder builder = + GcsReadOptionsSerializer.DEFAULT_OPTIONS.toBuilder(); + + if (root != null && root.isObject()) { + if (root.hasNonNull("fadvise")) { + builder.setFadvise( + GoogleCloudStorageReadOptions.Fadvise.valueOf(root.get("fadvise").asText())); + } + if (root.hasNonNull("fastFailOnNotFoundEnabled")) { + builder.setFastFailOnNotFoundEnabled(root.get("fastFailOnNotFoundEnabled").asBoolean()); + } else if (root.hasNonNull("fastFailOnNotFound")) { + builder.setFastFailOnNotFoundEnabled(root.get("fastFailOnNotFound").asBoolean()); + } + if (root.hasNonNull("minRangeRequestSize")) { + builder.setMinRangeRequestSize(root.get("minRangeRequestSize").asLong()); + } + if (root.hasNonNull("inplaceSeekLimit")) { + builder.setInplaceSeekLimit(root.get("inplaceSeekLimit").asLong()); + } + if (root.hasNonNull("grpcReadZeroCopyEnabled")) { + builder.setGrpcReadZeroCopyEnabled(root.get("grpcReadZeroCopyEnabled").asBoolean()); + } + } + return builder.build(); + } +} 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..cfeb12dcae5c 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); ///////////////////////////////////////////////////////////////////////////// @@ -259,12 +261,18 @@ public boolean shouldRetry(IOException e) { this.credentials = credentials; this.maxBytesRewrittenPerCall = null; this.numRewriteTokensUsed = null; - googleCloudStorageOptions = + GoogleCloudStorageOptions.Builder optionsBuilder = GoogleCloudStorageOptions.builder() .setAppName("Beam") .setReadChannelOptions(gcsReadOptions) - .setGrpcEnabled(shouldUseGrpc) - .build(); + .setGrpcEnabled(shouldUseGrpc); + if (storageClient.getRootUrl() != null) { + optionsBuilder.setStorageRootUrl(storageClient.getRootUrl()); + } + if (storageClient.getServicePath() != null) { + optionsBuilder.setStorageServicePath(storageClient.getServicePath()); + } + googleCloudStorageOptions = optionsBuilder.build(); try { googleCloudStorage = createGoogleCloudStorage(googleCloudStorageOptions, storageClient, credentials); @@ -493,6 +501,11 @@ private Long toFileSize(StorageObjectOrIOException storageObjectOrIOException) } } + @VisibleForTesting + GoogleCloudStorage getGoogleCloudStorage() { + return googleCloudStorage; + } + @VisibleForTesting void setCloudStorageImpl(GoogleCloudStorage g) { googleCloudStorage = g; @@ -726,9 +739,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()) diff --git a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/GcpCoreApiSurfaceTest.java b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/GcpCoreApiSurfaceTest.java index 8af5e2260fc5..cd51d4fd9d28 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/GcpCoreApiSurfaceTest.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/GcpCoreApiSurfaceTest.java @@ -51,6 +51,8 @@ public void testGcpCoreApiSurface() throws Exception { final Set>> allowedClasses = ImmutableSet.of( classesInPackage("com.fasterxml.jackson.annotation"), + classesInPackage("com.fasterxml.jackson.core"), + classesInPackage("com.fasterxml.jackson.databind"), classesInPackage("com.google.api.client.googleapis"), classesInPackage("com.google.api.client.http"), classesInPackage("com.google.api.client.json"), diff --git a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/options/GcsOptionsTest.java b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/options/GcsOptionsTest.java index c499290b851d..912f0110e9ce 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/options/GcsOptionsTest.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/options/GcsOptionsTest.java @@ -18,11 +18,16 @@ package org.apache.beam.sdk.extensions.gcp.options; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.hadoop.gcsio.GoogleCloudStorageReadOptions; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.junit.Test; import org.junit.runner.RunWith; @@ -75,4 +80,46 @@ public void testEntriesWithErrors() throws Exception { IllegalArgumentException.class, () -> PipelineOptionsFactory.fromArgs(TOO_MANY_ENTRIES_WITH_JOB).as(GcsOptions.class)); } + + @Test + public void testGoogleCloudStorageReadOptionsSerialization() throws Exception { + GcsOptions options = PipelineOptionsFactory.as(GcsOptions.class); + GoogleCloudStorageReadOptions readOptions = + GoogleCloudStorageReadOptions.builder() + .setFadvise(GoogleCloudStorageReadOptions.Fadvise.RANDOM) + .setFastFailOnNotFoundEnabled(false) + .setMinRangeRequestSize(12345L) + .build(); + options.setGoogleCloudStorageReadOptions(readOptions); + + ObjectMapper mapper = new ObjectMapper(); + String serialized = mapper.writeValueAsString(options); + GcsOptions deserialized = + mapper.readValue(serialized, PipelineOptions.class).as(GcsOptions.class); + + GoogleCloudStorageReadOptions deserializedReadOptions = + deserialized.getGoogleCloudStorageReadOptions(); + + assertNotNull(deserializedReadOptions); + assertEquals( + GoogleCloudStorageReadOptions.Fadvise.RANDOM, deserializedReadOptions.getFadvise()); + assertFalse(deserializedReadOptions.isFastFailOnNotFoundEnabled()); + assertEquals(12345L, deserializedReadOptions.getMinRangeRequestSize()); + } + + @Test + public void testDefaultGoogleCloudStorageReadOptionsSerialization() throws Exception { + GcsOptions options = PipelineOptionsFactory.as(GcsOptions.class); + ObjectMapper mapper = new ObjectMapper(); + String serialized = mapper.writeValueAsString(options); + GcsOptions deserialized = + mapper.readValue(serialized, PipelineOptions.class).as(GcsOptions.class); + + GoogleCloudStorageReadOptions deserializedReadOptions = + deserialized.getGoogleCloudStorageReadOptions(); + + assertNotNull(deserializedReadOptions); + assertEquals( + GoogleCloudStorageReadOptions.Fadvise.SEQUENTIAL, deserializedReadOptions.getFadvise()); + } } 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 d32ca162e3fd..2f77f15dcffc 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 @@ -63,6 +63,7 @@ import com.google.auth.Credentials; import com.google.cloud.hadoop.gcsio.CreateObjectOptions; import com.google.cloud.hadoop.gcsio.GoogleCloudStorage; +import com.google.cloud.hadoop.gcsio.GoogleCloudStorageImpl; import com.google.cloud.hadoop.gcsio.GoogleCloudStorageOptions; import com.google.cloud.hadoop.gcsio.GoogleCloudStorageReadOptions; import com.google.cloud.hadoop.gcsio.StorageResourceId; @@ -1866,6 +1867,18 @@ public void testReadMetricsAreNotCollectedWhenNotEnabledOpenWithOptions() throws testReadMetrics(false, GoogleCloudStorageReadOptions.DEFAULT); } + @Test + public void testGcsEndpoint() throws IOException { + GcsOptions pipelineOptions = PipelineOptionsFactory.as(GcsOptions.class); + pipelineOptions.setGcsEndpoint("http://localhost:4443/storage/v1/"); + + GcsUtil gcsUtil = pipelineOptions.getGcsUtil(); + GoogleCloudStorageImpl gcsImpl = + (GoogleCloudStorageImpl) gcsUtil.delegate.getGoogleCloudStorage(); + assertEquals("http://localhost:4443/", gcsImpl.getOptions().getStorageRootUrl()); + assertEquals("storage/v1/", gcsImpl.getOptions().getStorageServicePath()); + } + /** A helper to wrap a {@link GenericJson} object in a content stream. */ private static InputStream toStream(String content) throws IOException { return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); 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..a1e18805bf3f --- /dev/null +++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle @@ -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. + */ + +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.mockito_junit_jupiter + 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..8cdbe00af6ef --- /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,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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. + *
  • {@code none} - Disables 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..453fb5717008 --- /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,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.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; +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 java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; +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 + */ +@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"; + 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 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 + * 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) { + 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( + (spanExporter, configProperties) -> + customizeSpanExporter(spanExporter, credentialsSupplier, configProperties)) + .addMetricExporterCustomizer( + (metricExporter, configProperties) -> + customizeMetricExporter(metricExporter, credentialsSupplier, configProperties)) + .addResourceCustomizer( + (resource, configProperties) -> + customizeResource(resource, credentialsSupplier, configProperties)); + } + + @Override + public int order() { + return Integer.MAX_VALUE - 1; + } + + private static SpanExporter customizeSpanExporter( + SpanExporter exporter, + java.util.function.Supplier credentialsSupplier, + ConfigProperties configProperties) { + if (isSignalTargeted(SIGNAL_TYPE_TRACES, 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 static MetricExporter customizeMetricExporter( + MetricExporter exporter, + java.util.function.Supplier credentialsSupplier, + ConfigProperties configProperties) { + if (isSignalTargeted(SIGNAL_TYPE_METRICS, 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 userSpecifiedTargetedSignals = + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getConfiguredValueWithFallback( + configProperties, () -> SIGNAL_TYPE_ALL); + 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 static SpanExporter addAuthorizationHeaders( + SpanExporter exporter, GoogleCredentials credentials, ConfigProperties configProperties) { + if (exporter instanceof OtlpHttpSpanExporter) { + OtlpHttpSpanExporterBuilder builder = + ((OtlpHttpSpanExporter) exporter) + .toBuilder() + .setHeaders(() -> getRequiredHeaderMap(credentials, configProperties)); + return builder.build(); + } else if (exporter instanceof OtlpGrpcSpanExporter) { + OtlpGrpcSpanExporterBuilder builder = + ((OtlpGrpcSpanExporter) exporter) + .toBuilder() + .setHeaders(() -> getRequiredHeaderMap(credentials, configProperties)); + return builder.build(); + } + return exporter; + } + + // Adds authorization headers to the calls made by the OtlpGrpcMetricExporter and + // OtlpHttpMetricExporter. + private static MetricExporter addAuthorizationHeaders( + MetricExporter exporter, GoogleCredentials credentials, ConfigProperties configProperties) { + if (exporter instanceof OtlpHttpMetricExporter) { + OtlpHttpMetricExporterBuilder builder = + ((OtlpHttpMetricExporter) exporter) + .toBuilder() + .setHeaders(() -> getRequiredHeaderMap(credentials, configProperties)); + return builder.build(); + } else if (exporter instanceof OtlpGrpcMetricExporter) { + OtlpGrpcMetricExporterBuilder builder = + ((OtlpGrpcMetricExporter) exporter) + .toBuilder() + .setHeaders(() -> getRequiredHeaderMap(credentials, configProperties)); + return builder.build(); + } + return exporter; + } + + private static Map getRequiredHeaderMap( + GoogleCredentials credentials, ConfigProperties configProperties) { + Map> gcpHeaders; + try { + // this also refreshes the credentials, if required + gcpHeaders = credentials.getRequestMetadata(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); + } + Map flattenedHeaders = + gcpHeaders.entrySet().stream() + .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 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 = credentialsSupplier.get().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/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..92aa72c2a79d --- /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,1310 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.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.LongPointData; +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.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; +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.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"; + 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 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); + } + + // 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")); + } + } + } + + // TODO: Use parameterized test for testing metrics customizer for http & grpc. + @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 (LongPointData longPointData : metricData.getLongSumData().getPoints()) { + assertThat(longPointData.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 (LongPointData longPointData : metricData.getLongSumData().getPoints()) { + assertThat(longPointData.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); + Assert.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()); + } + } + } + + @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 { + + // 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 + Assert.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(defaultOtelPropertiesSpanExporter) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics") + .setUserSpecifiedOtelProperties(defaultOtelPropertiesMetricExporter) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("all") + .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(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics, traces") + .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(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("") + .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(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("all") + .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", + "none", + "otel.metrics.exporter", + "none", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metric, trace") + .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") + .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(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())); + } + + /** + * 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(), defaultOtelPropertiesSpanExporter); + } + + @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, defaultOtelPropertiesMetricExporter); + } + + @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/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; + } } diff --git a/sdks/java/extensions/sql/iceberg/build.gradle b/sdks/java/extensions/sql/iceberg/build.gradle index 893a485e7d86..97a128d69e9e 100644 --- a/sdks/java/extensions/sql/iceberg/build.gradle +++ b/sdks/java/extensions/sql/iceberg/build.gradle @@ -22,8 +22,8 @@ plugins { id 'org.apache.beam.module' } applyJavaNature( automaticModuleName: 'org.apache.beam.sdk.extensions.sql.meta.provider.hcatalog', - // iceberg requires Java11+ - requireJavaVersion: JavaVersion.VERSION_11, + // iceberg requires Java17+ + requireJavaVersion: JavaVersion.VERSION_17, ) dependencies { 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 678c76153c5c..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 @@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; import org.apache.beam.sdk.extensions.sql.TableUtils; import org.apache.beam.sdk.extensions.sql.impl.TableName; import org.apache.beam.sdk.extensions.sql.meta.BeamSqlTable; @@ -60,12 +61,14 @@ public void createTable(Table table) { getProvider(table.getType()).createTable(table); } else { String identifier = getIdentifier(table); + Map props = + TableUtils.getObjectMapper() + .convertValue(table.getProperties(), new TypeReference>() {}) + .entrySet().stream() + .filter(p -> !p.getKey().startsWith("beam.")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); try { - Map properties = - TableUtils.getObjectMapper() - .convertValue(table.getProperties(), new TypeReference>() {}); - catalogConfig.createTable( - identifier, table.getSchema(), table.getPartitionFields(), properties); + catalogConfig.createTable(identifier, table.getSchema(), table.getPartitionFields(), props); } catch (TableAlreadyExistsException e) { LOG.info( "Iceberg table '{}' already exists at location '{}'.", table.getName(), identifier); @@ -91,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)); @@ -113,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; } @@ -130,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/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergTable.java b/sdks/java/extensions/sql/iceberg/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergTable.java index b68aa34a1777..2cdcaa9b63a2 100644 --- a/sdks/java/extensions/sql/iceberg/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergTable.java +++ b/sdks/java/extensions/sql/iceberg/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergTable.java @@ -19,6 +19,7 @@ import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.HashMap; import java.util.List; @@ -56,6 +57,8 @@ class IcebergTable extends SchemaBaseBeamTable { @VisibleForTesting static final String CATALOG_PROPERTIES_FIELD = "catalog_properties"; @VisibleForTesting static final String HADOOP_CONFIG_PROPERTIES_FIELD = "config_properties"; @VisibleForTesting static final String CATALOG_NAME_FIELD = "catalog_name"; + static final String BEAM_WRITE_PROPERTY = "beam.write."; + static final String BEAM_READ_PROPERTY = "beam.read."; @VisibleForTesting static final String TRIGGERING_FREQUENCY_FIELD = "triggering_frequency_seconds"; @@ -71,9 +74,21 @@ class IcebergTable extends SchemaBaseBeamTable { this.tableIdentifier = tableIdentifier; this.catalogConfig = catalogConfig; ObjectNode properties = table.getProperties(); - if (properties.has(TRIGGERING_FREQUENCY_FIELD)) { - this.triggeringFrequency = properties.get(TRIGGERING_FREQUENCY_FIELD).asInt(); + for (Map.Entry property : properties.properties()) { + String name = property.getKey().toLowerCase(); + if (name.startsWith(BEAM_WRITE_PROPERTY)) { + String prop = name.substring(BEAM_WRITE_PROPERTY.length()); + if (prop.equalsIgnoreCase(TRIGGERING_FREQUENCY_FIELD)) { + this.triggeringFrequency = property.getValue().asInt(); + } else { + throw new IllegalArgumentException("Unknown Beam write property: " + name); + } + } else if (name.startsWith(BEAM_READ_PROPERTY)) { + // none supported yet + throw new IllegalArgumentException("Unknown Beam read property: " + name); + } } + this.partitionFields = table.getPartitionFields(); } diff --git a/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/BeamSqlCliIcebergTest.java b/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/BeamSqlCliIcebergTest.java index 9ac96652d340..567c3bbc7fae 100644 --- a/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/BeamSqlCliIcebergTest.java +++ b/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/BeamSqlCliIcebergTest.java @@ -18,7 +18,6 @@ package org.apache.beam.sdk.extensions.sql.meta.provider.iceberg; import static java.lang.String.format; -import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -223,9 +222,8 @@ public void testCrossCatalogTableWriteAndRead() throws IOException { PCollection output = BeamSqlRelUtils.toPCollection(p3, insertNode3); // validate read contents - Schema expectedSchema = - checkStateNotNull(catalog.catalogConfig.loadTable(tableIdentifier)).getSchema(); - assertEquals(expectedSchema, output.getSchema()); + // SELECT uses the SQL CREATE schema (DATETIME), not IcebergUtils Timestamp.MICROS. + Schema expectedSchema = output.getSchema(); PAssert.that(output) .containsInAnyOrder( Row.withSchema(expectedSchema) 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/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergReadWriteIT.java b/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergReadWriteIT.java index 417db09a2210..3a791c6fe88c 100644 --- a/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergReadWriteIT.java +++ b/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/IcebergReadWriteIT.java @@ -27,7 +27,6 @@ import static org.apache.beam.sdk.schemas.Schema.FieldType.STRING; import static org.apache.beam.sdk.schemas.Schema.FieldType.array; import static org.apache.beam.sdk.schemas.Schema.FieldType.row; -import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; @@ -200,8 +199,6 @@ public void runSqlWriteAndRead(boolean withPartitionFields) assertEquals("my_catalog." + tableIdentifier, icebergTable.name()); assertTrue(icebergTable.location().startsWith(warehouse)); assertEquals(expectedSpec, icebergTable.spec()); - Schema expectedSchema = checkStateNotNull(metastore.getTable(tableName)).getSchema(); - assertEquals(expectedSchema, IcebergUtils.icebergSchemaToBeamSchema(icebergTable.schema())); // 4) write to underlying Iceberg table String insertStatement = diff --git a/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/PubsubToIcebergIT.java b/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/PubsubToIcebergIT.java index 900fdae743a1..8b250af2754a 100644 --- a/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/PubsubToIcebergIT.java +++ b/sdks/java/extensions/sql/iceberg/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/iceberg/PubsubToIcebergIT.java @@ -156,7 +156,7 @@ public void testSimpleInsertWithPartitionedFields() throws Exception { + ") \n" + "TYPE 'iceberg' \n" + "PARTITIONED BY('id', 'truncate(name, 3)') \n" - + "TBLPROPERTIES '{ \"triggering_frequency_seconds\" : 10 }'"; + + "TBLPROPERTIES '{ \"beam.write.triggering_frequency_seconds\" : 10 }'"; String insertStatement = format("INSERT INTO %s \n", tableIdentifier) + "SELECT \n" @@ -211,7 +211,7 @@ public void testSimpleInsertFlat() throws Exception { + " name VARCHAR \n " + ") \n" + "TYPE 'iceberg' \n" - + "TBLPROPERTIES '{ \"triggering_frequency_seconds\" : 10 }'"; + + "TBLPROPERTIES '{ \"beam.write.triggering_frequency_seconds\" : 10 }'"; String insertStatement = format("INSERT INTO %s \n", tableIdentifier) + "SELECT \n" 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/BeamSqlEnv.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/BeamSqlEnv.java index d84783118bbd..8d8d48dd5774 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/BeamSqlEnv.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/BeamSqlEnv.java @@ -49,6 +49,7 @@ import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.plan.RelOptUtil; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.Function; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlKind; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.tools.RuleSet; import org.checkerframework.checker.nullness.qual.Nullable; @@ -132,6 +133,18 @@ public Map getPipelineOptions() { return connection.getPipelineOptionsMap(); } + /** + * Registers a custom {@link SqlOperator} into the current session's operator table. This allows + * registering functions with non-fixed operand types (e.g. VARIADIC), which the schema-function + * auto-wrapping mechanism cannot express. + * + *

Only safe to call before the first SQL query is planned in this environment (otherwise the + * parser/validator operator table may have already been built without it). + */ + public void registerSqlOperator(SqlOperator operator) { + connection.getExtraOperatorTable().add(operator); + } + public String explain(String sqlString) throws ParseException { try { return RelOptUtil.toString(planner.convertToBeamRel(sqlString, QueryParameters.ofNone())); diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/CalciteQueryPlanner.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/CalciteQueryPlanner.java index 606a3c5f71a2..cedfc5dc6d1d 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/CalciteQueryPlanner.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/CalciteQueryPlanner.java @@ -51,6 +51,11 @@ import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.metadata.ReflectiveRelMetadataProvider; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.metadata.RelMetadataProvider; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.metadata.RelMetadataQuery; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.type.RelDataType; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rex.RexBuilder; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rex.RexDynamicParam; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rex.RexNode; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rex.RexShuttle; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.SchemaPlus; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlNode; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperatorTable; @@ -153,7 +158,15 @@ public FrameworkConfig defaultConfig(JdbcConnection connection, Collection\n{}", BeamSqlRelUtils.explainLazily(root.rel)); RelTraitSet desiredTraits = - root.rel + relNode .getTraitSet() .replace(BeamLogicalConvention.INSTANCE) .replace(root.collation) .simplify(); // beam physical plan - root.rel + relNode .getCluster() .setMetadataProvider( ChainedRelMetadataProvider.of( ImmutableList.of( NonCumulativeCostImpl.SOURCE, RelMdNodeStats.SOURCE, - root.rel.getCluster().getMetadataProvider()))); + relNode.getCluster().getMetadataProvider()))); - root.rel.getCluster().setMetadataQuerySupplier(BeamRelMetadataQuery::instance); + relNode.getCluster().setMetadataQuerySupplier(BeamRelMetadataQuery::instance); RelMetadataQuery.THREAD_PROVIDERS.set( - JaninoRelMetadataProvider.of(root.rel.getCluster().getMetadataProvider())); - root.rel.getCluster().invalidateMetadataQuery(); - beamRelNode = (BeamRelNode) planner.transform(0, desiredTraits, root.rel); + JaninoRelMetadataProvider.of(relNode.getCluster().getMetadataProvider())); + relNode.getCluster().invalidateMetadataQuery(); + beamRelNode = (BeamRelNode) planner.transform(0, desiredTraits, relNode); LOG.info("BEAMPlan>\n{}", BeamSqlRelUtils.explainLazily(beamRelNode)); } catch (RelConversionException | CannotPlanException e) { throw new SqlConversionException( @@ -225,6 +245,15 @@ public BeamRelNode convertToBeamRel(String sqlStatement, QueryParameters queryPa return beamRelNode; } + private static RelNode bindParameters(RelNode rel, RexShuttle binder) { + RelNode newRel = rel.accept(binder); + java.util.List newInputs = new java.util.ArrayList<>(); + for (RelNode input : newRel.getInputs()) { + newInputs.add(bindParameters(input, binder)); + } + return newRel.copy(newRel.getTraitSet(), newInputs); + } + // It needs to be public so that the generated code in Calcite can access it. public static class NonCumulativeCostImpl implements MetadataHandler { @@ -265,4 +294,58 @@ public RelOptCost getNonCumulativeCost(RelNode rel, RelMetadataQuery mq) { return ((BeamRelNode) rel).beamComputeSelfCost(rel.getCluster().getPlanner(), bmq); } } + + private static class ParameterBinder extends RexShuttle { + private final RexBuilder rexBuilder; + private final List positionalParams; + + ParameterBinder(RexBuilder rexBuilder, QueryParameters params) { + this.rexBuilder = rexBuilder; + this.positionalParams = params.getKind() == Kind.POSITIONAL ? params.positional() : null; + } + + @Override + public RexNode visitDynamicParam(RexDynamicParam dynamicParam) { + if (positionalParams != null) { + int index = dynamicParam.getIndex(); + if (index < 0 || index >= positionalParams.size()) { + throw new IllegalArgumentException( + "Index out of bounds for positional parameter: " + index); + } + Object val = positionalParams.get(index); + return makeLiteral(cleanValue(val), dynamicParam.getType()); + } + return super.visitDynamicParam(dynamicParam); + } + + private RexNode makeLiteral(Object val, RelDataType type) { + if (val == null) { + return rexBuilder.makeNullLiteral(type); + } + return rexBuilder.makeLiteral(val, type, true); + } + + @SuppressWarnings("JavaUtilDate") // explicit java.util.Date support + private Object cleanValue(Object value) { + if (value instanceof org.joda.time.ReadableInstant) { + return ((org.joda.time.ReadableInstant) value).getMillis(); + } + if (value instanceof java.time.LocalDate) { + return (int) ((java.time.LocalDate) value).toEpochDay(); + } + if (value instanceof java.time.LocalTime) { + return (int) (((java.time.LocalTime) value).toNanoOfDay() / 1_000_000L); + } + if (value instanceof java.time.LocalDateTime) { + return ((java.time.LocalDateTime) value).toInstant(java.time.ZoneOffset.UTC).toEpochMilli(); + } + if (value instanceof java.sql.Timestamp) { + return ((java.sql.Timestamp) value).getTime(); + } + if (value instanceof java.util.Date) { + return ((java.util.Date) value).getTime(); + } + return value; + } + } } diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/JdbcConnection.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/JdbcConnection.java index 9c54f059f214..cbdb1e999534 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/JdbcConnection.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/JdbcConnection.java @@ -31,6 +31,8 @@ import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.jdbc.CalciteSchema; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.Schema; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.SchemaPlus; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.util.ListSqlOperatorTable; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.checkerframework.checker.nullness.qual.Nullable; @@ -51,11 +53,31 @@ public class JdbcConnection extends CalciteConnectionWrapper { private Map pipelineOptionsMap; private @Nullable PipelineOptions pipelineOptions; + /** + * A mutable, session-scoped operator table that callers can populate with custom {@link + * SqlOperator}s after the connection (and its planner) is built. {@link + * CalciteQueryPlanner#defaultConfig} chains this table at the front of the validator/converter + * operator table, so operators added here resolve in subsequent SQL. This is the channel for + * functions that must declare a non-fixed (e.g. {@code VARIADIC}) operand checker, which the + * schema-function auto-wrapping in {@code CalciteCatalogReader.toOp} (always fixed-parameter + * {@code OperandMetadataImpl}) cannot express. The list is held by reference in the planner + * config, so additions made after the planner is built are visible. + */ + private final ListSqlOperatorTable extraOperatorTable = new ListSqlOperatorTable(); + private JdbcConnection(CalciteConnection connection) throws SQLException { super(connection); this.pipelineOptionsMap = Collections.emptyMap(); } + /** + * The session-scoped, mutable operator table chained at the front of the planner's operator + * table. Add custom {@link SqlOperator}s here to make them resolvable in subsequent SQL. + */ + public ListSqlOperatorTable getExtraOperatorTable() { + return extraOperatorTable; + } + /** * Wraps and initializes the initial connection created by Calcite. * 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/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/impl/rel/BeamCalcRel.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRel.java index fad96abb29a5..b9525bd07dd0 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRel.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRel.java @@ -133,6 +133,23 @@ public class BeamCalcRel extends AbstractBeamCalcRel { private static final TupleTag rows = new TupleTag() {}; private static final TupleTag errors = new TupleTag() {}; + /** + * Converts a {@link java.time.Instant} from a Timestamp logical type to Calcite TIMESTAMP millis. + * Calcite's TIMESTAMP is millisecond-based, so sub-millisecond values are rejected rather than + * silently truncated. + */ + public static long timestampToCalciteMillis(java.time.Instant instant) { + long millis = instant.toEpochMilli(); + // toEpochMilli truncates; reject rather than silently drop sub-millisecond precision. + if (!instant.equals(java.time.Instant.ofEpochMilli(millis))) { + throw new UnsupportedOperationException( + "Beam SQL cannot convert Timestamp values with sub-millisecond precision through" + + " Calcite (millis-based TIMESTAMP). Got: " + + instant); + } + return millis; + } + public BeamCalcRel(RelOptCluster cluster, RelTraitSet traits, RelNode input, RexProgram program) { super(cluster, traits, input, program); } @@ -439,6 +456,12 @@ static Object toBeamObject(Object value, FieldType fieldType, boolean verifyValu LocalDate.ofEpochDay(((Number) value).longValue() / MILLIS_PER_DAY), LocalTime.ofNanoOfDay( (((Number) value).longValue() % MILLIS_PER_DAY) * NANOS_PER_MILLISECOND)); + } else if (org.apache.beam.sdk.schemas.logicaltypes.Timestamp.IDENTIFIER.equals( + identifier)) { + if (value instanceof Timestamp) { + value = SqlFunctions.toLong((Timestamp) value); + } + return java.time.Instant.ofEpochMilli(((Number) value).longValue()); } else { if (logicalType instanceof PassThroughLogicalType) { return toBeamObject(value, logicalType.getBaseType(), verifyValues); @@ -591,6 +614,15 @@ private static Expression getBeamField( fieldName, Expressions.constant(LocalDateTime.class)), LocalDateTime.class); + } else if (org.apache.beam.sdk.schemas.logicaltypes.Timestamp.IDENTIFIER.equals( + identifier)) { + return Expressions.convert_( + Expressions.call( + expression, + "getLogicalTypeValue", + fieldName, + Expressions.constant(java.time.Instant.class)), + java.time.Instant.class); } else if (FixedPrecisionNumeric.IDENTIFIER.equals(identifier)) { return Expressions.call(expression, "getDecimal", fieldName); } else if (logicalType instanceof PassThroughLogicalType) { @@ -684,6 +716,14 @@ private static Expression toCalciteValue( Expressions.multiply(dateValue, Expressions.constant(MILLIS_PER_DAY)), Expressions.divide(timeValue, Expressions.constant(NANOS_PER_MILLISECOND))); return nullOr(value, returnValue); + } else if (org.apache.beam.sdk.schemas.logicaltypes.Timestamp.IDENTIFIER.equals( + identifier)) { + return nullOr( + value, + Expressions.call( + BeamCalcRel.class, + "timestampToCalciteMillis", + Expressions.convert_(value, java.time.Instant.class))); } else if (FixedPrecisionNumeric.IDENTIFIER.equals(identifier)) { return Expressions.convert_(value, BigDecimal.class); } else if (logicalType instanceof PassThroughLogicalType) { 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/rel/package-info.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/package-info.java index 7a973d9c8b33..f3f87d8ea28d 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/package-info.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/package-info.java @@ -20,8 +20,4 @@ * BeamSQL specified nodes, to replace {@link * org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.RelNode}. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.sql.impl.rel; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rule/package-info.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rule/package-info.java index 8a7179731138..4bb2be1f3e73 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rule/package-info.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rule/package-info.java @@ -20,8 +20,4 @@ * {@link org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.plan.RelOptRule} to generate * {@link org.apache.beam.sdk.extensions.sql.impl.rel.BeamRelNode}. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.sql.impl.rule; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/package-info.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/package-info.java index c9d277bddc40..93fcb88de4f6 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/package-info.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/transform/agg/package-info.java @@ -17,8 +17,4 @@ */ /** Implementation of standard SQL aggregation functions, e.g. VAR_POP, VAR_SAMP. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.sql.impl.transform.agg; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; 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..2627f1c0f868 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 @@ -29,6 +29,7 @@ import org.apache.beam.sdk.schemas.Schema.TypeName; import org.apache.beam.sdk.schemas.logicaltypes.PassThroughLogicalType; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; import org.apache.beam.sdk.util.Preconditions; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.avatica.util.ByteString; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.type.RelDataType; @@ -75,7 +76,8 @@ public static boolean isDateTimeType(FieldType fieldType) { return logicalId.equals(SqlTypes.DATE.getIdentifier()) || logicalId.equals(SqlTypes.TIME.getIdentifier()) || logicalId.equals(TimeWithLocalTzType.IDENTIFIER) - || logicalId.equals(SqlTypes.DATETIME.getIdentifier()); + || logicalId.equals(SqlTypes.DATETIME.getIdentifier()) + || logicalId.equals(Timestamp.IDENTIFIER); } return false; } @@ -114,6 +116,8 @@ public static boolean isStringType(FieldType fieldType) { FieldType.logicalType(SqlTypes.TIME).withNullable(true); public static final FieldType TIME_WITH_LOCAL_TZ = FieldType.logicalType(new TimeWithLocalTzType()); + // TODO: Default SQL TIMESTAMP to Timestamp.MICROS (or equivalent) instead of FieldType.DATETIME + // once Beam SQL / Calcite can preserve microsecond precision end-to-end. public static final FieldType TIMESTAMP = FieldType.DATETIME; public static final FieldType NULLABLE_TIMESTAMP = FieldType.DATETIME.withNullable(true); public static final FieldType TIMESTAMP_WITH_LOCAL_TZ = FieldType.logicalType(SqlTypes.DATETIME); @@ -170,6 +174,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 @@ -203,6 +226,8 @@ public static SqlTypeName toSqlTypeName(FieldType type) { if (logicalType instanceof PassThroughLogicalType) { // for pass through logical type, just return its base type return toSqlTypeName(logicalType.getBaseType()); + } else if (Timestamp.IDENTIFIER.equals(logicalType.getIdentifier())) { + return SqlTypeName.TIMESTAMP; } else if ("SqlCharType".equals(logicalType.getIdentifier())) { LOG.warn( "SqlCharType is used in Schema. It was removed in Beam 2.44.0 and should be" @@ -365,7 +390,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 +423,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/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/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/mongodb/package-info.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/mongodb/package-info.java index 3162a6547166..2f24e3ae050b 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/mongodb/package-info.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/mongodb/package-info.java @@ -17,8 +17,4 @@ */ /** Table schema for MongoDb. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.sql.meta.provider.mongodb; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/pubsub/package-info.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/pubsub/package-info.java index ef6642780029..2b480e59ff5a 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/pubsub/package-info.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/pubsub/package-info.java @@ -17,8 +17,4 @@ */ /** Table schema for {@link org.apache.beam.sdk.io.gcp.pubsub.PubsubIO}. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.extensions.sql.meta.provider.pubsub; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamComplexTypeTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamComplexTypeTest.java index 5ef081b92c3f..062651161f59 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamComplexTypeTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamComplexTypeTest.java @@ -37,6 +37,7 @@ import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; import org.apache.beam.sdk.schemas.logicaltypes.FixedString; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; import org.apache.beam.sdk.schemas.logicaltypes.VariableString; import org.apache.beam.sdk.testing.PAssert; @@ -797,4 +798,35 @@ public void testUnknownLogicalType() { assertEquals(inputRow.getSchema(), outputRow.getSchema()); pipeline.run().waitUntilFinish(Duration.standardMinutes(1)); } + + @Test + public void testSqlTimestampLogicalType() { + // Calcite TIMESTAMP is millis-based; SQL projection of Timestamp.MICROS uses + // FieldType.DATETIME. + Schema inputSchema = + Schema.builder() + .addField("ts", FieldType.logicalType(Timestamp.MICROS)) + .addNullableField("nullable_ts", FieldType.logicalType(Timestamp.MICROS)) + .build(); + + java.time.Instant ts = java.time.Instant.parse("2025-07-31T20:17:40.123Z"); + Row inputRow = Row.withSchema(inputSchema).addValues(ts, null).build(); + + PCollection outputRow = + pipeline + .apply(Create.of(inputRow)) + .setRowSchema(inputSchema) + .apply(SqlTransform.query("SELECT ts, nullable_ts FROM PCOLLECTION")); + + Schema outputSchema = + Schema.builder() + .addDateTimeField("ts") + .addNullableField("nullable_ts", FieldType.DATETIME) + .build(); + Row expectedRow = + Row.withSchema(outputSchema).addValues(new Instant(ts.toEpochMilli()), null).build(); + + PAssert.that(outputRow).containsInAnyOrder(expectedRow); + pipeline.run().waitUntilFinish(Duration.standardMinutes(2)); + } } diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlAliasTest b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlAliasTest.java similarity index 92% rename from sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlAliasTest rename to sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlAliasTest.java index 790312b7e756..de3c8e6f301e 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlAliasTest +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlAliasTest.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.extensions.sql; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.Serializable; import java.util.HashMap; import java.util.List; @@ -33,8 +35,6 @@ import org.apache.beam.sdk.values.Row; import org.junit.Rule; import org.junit.Test; -import org.testcontainers.shaded.com.fasterxml.jackson.databind.MapperFeature; -import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; public class BeamSqlAliasTest implements Serializable { @@ -42,10 +42,10 @@ public class BeamSqlAliasTest implements Serializable { @Test public void testSqlWithAliasIsNotIgnoredWithOptimizers() { - String ID = "id"; - String EVENT = "event"; + final String id = "id"; + final String event = "event"; - Schema inputType = Schema.builder().addStringField(ID).addStringField(EVENT).build(); + Schema inputType = Schema.builder().addStringField(id).addStringField(event).build(); String sql = "select event as event_name, count(*) as c\n" + "from PCOLLECTION\n" + "group by event"; @@ -91,4 +91,4 @@ public void processElement(DoFn.ProcessContext c) pipeline.run().waitUntilFinish(); } -} \ No newline at end of file +} 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(); 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/BeamSqlDslParametersTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlDslParametersTest.java new file mode 100644 index 000000000000..9166fd16e0a6 --- /dev/null +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlDslParametersTest.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.sdk.extensions.sql; + +import static org.apache.beam.sdk.extensions.sql.utils.DateTimeUtils.parseTimestampWithoutTimeZone; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Arrays; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.joda.time.Instant; +import org.junit.Test; + +/** Tests for query parameters in Beam SQL. */ +public class BeamSqlDslParametersTest extends BeamSqlDslBase { + + @Test + public void testPositionalParameters() { + String sql = "SELECT f_int, f_string FROM PCOLLECTION WHERE f_int = ? AND f_string = ?"; + + PCollection result = + boundedInput1.apply( + "testPositionalParameters", + SqlTransform.query(sql).withPositionalParameters(Arrays.asList(1, "string_row1"))); + + Row expectedRow = + Row.withSchema(Schema.builder().addInt32Field("f_int").addStringField("f_string").build()) + .addValues(1, "string_row1") + .build(); + + PAssert.that(result).containsInAnyOrder(expectedRow); + + pipeline.run(); + } + + @Test + public void testDateTimeParameters() { + String sql = + "SELECT f_int FROM PCOLLECTION WHERE f_date = ? AND f_time = ? AND f_datetime = ? AND f_timestamp = ?"; + + PCollection result = + boundedInput1.apply( + "testDateTimeParameters", + SqlTransform.query(sql) + .withPositionalParameters( + Arrays.asList( + LocalDate.of(2017, 1, 1), + LocalTime.of(1, 1, 3), + LocalDateTime.of(2017, 1, 1, 1, 1, 3), + new Instant(parseTimestampWithoutTimeZone("2017-01-01 01:01:03"))))); + + Row expectedRow = + Row.withSchema(Schema.builder().addInt32Field("f_int").build()).addValues(1).build(); + + PAssert.that(result).containsInAnyOrder(expectedRow); + + pipeline.run(); + } +} diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/BeamSqlEnvRegisterOperatorTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/BeamSqlEnvRegisterOperatorTest.java new file mode 100644 index 000000000000..a7ff3584f2ba --- /dev/null +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/BeamSqlEnvRegisterOperatorTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.apache.beam.sdk.extensions.sql.impl.rel.BaseRelTest; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlFunction; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlFunctionCategory; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlIdentifier; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlKind; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlSyntax; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.type.OperandTypes; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.type.ReturnTypes; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.validate.SqlNameMatcher; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.validate.SqlNameMatchers; +import org.junit.Test; + +/** + * Tests for {@link BeamSqlEnv#registerSqlOperator(SqlOperator)} and the session-scoped operator + * table in {@link JdbcConnection}. + */ +public class BeamSqlEnvRegisterOperatorTest extends BaseRelTest { + + /** A simple custom function for testing, not present in the default operator table. */ + private static final String CUSTOM_FUNCTION_NAME = "BEAM_TEST_CUSTOM_FUNC"; + + private static SqlFunction createCustomFunction() { + return new SqlFunction( + CUSTOM_FUNCTION_NAME, + SqlKind.OTHER_FUNCTION, + ReturnTypes.INTEGER, + null, + OperandTypes.NUMERIC, + SqlFunctionCategory.USER_DEFINED_FUNCTION); + } + + @Test + public void testExtraOperatorTableStartsEmpty() { + // A fresh env should have an empty extra operator table + BeamSqlEnv freshEnv = BeamSqlEnv.readOnly("empty_test", new java.util.HashMap<>()); + assertTrue( + "Extra operator table should be empty on a fresh env", + freshEnv.connection.getExtraOperatorTable().getOperatorList().isEmpty()); + } + + @Test + public void testRegisterSqlOperator_addsToExtraOperatorTable() { + SqlFunction customFunc = createCustomFunction(); + env.registerSqlOperator(customFunc); + + // Verify the operator is in the extra operator table + assertTrue( + "Registered operator should be present in the extra operator table", + env.connection.getExtraOperatorTable().getOperatorList().contains(customFunc)); + assertEquals(1, env.connection.getExtraOperatorTable().getOperatorList().size()); + } + + @Test + public void testRegisterSqlOperator_makesOperatorResolvableByName() { + SqlFunction customFunc = createCustomFunction(); + env.registerSqlOperator(customFunc); + + // Verify the operator is resolvable by name in the extra operator table + SqlNameMatcher nameMatcher = SqlNameMatchers.withCaseSensitive(false); + java.util.List resolvedOps = new java.util.ArrayList<>(); + env.connection + .getExtraOperatorTable() + .lookupOperatorOverloads( + new SqlIdentifier(CUSTOM_FUNCTION_NAME, SqlParserPos.ZERO), + SqlFunctionCategory.USER_DEFINED_FUNCTION, + SqlSyntax.FUNCTION, + resolvedOps, + nameMatcher); + + assertFalse( + "Custom operator should be resolvable by name after registration", resolvedOps.isEmpty()); + assertTrue( + "The resolved operator should be the one we registered", resolvedOps.contains(customFunc)); + } +} 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/BeamCalcRelTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRelTest.java index 8aeb77fc0490..019832ffe964 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRelTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRelTest.java @@ -17,7 +17,11 @@ */ package org.apache.beam.sdk.extensions.sql.impl.rel; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + import java.math.BigDecimal; +import java.time.Instant; import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.extensions.sql.impl.BeamTableStatistics; import org.apache.beam.sdk.extensions.sql.impl.planner.BeamRelMetadataQuery; @@ -250,4 +254,20 @@ public void testNoFieldAccess() throws IllegalAccessException { pipeline.run().waitUntilFinish(); } + + @Test + public void testTimestampToCalciteMillisAcceptsMillisecondPrecision() { + Instant instant = Instant.parse("2025-07-31T20:17:40.123Z"); + assertEquals(instant.toEpochMilli(), BeamCalcRel.timestampToCalciteMillis(instant)); + } + + @Test + public void testTimestampToCalciteMillisRejectsSubMillisecondPrecision() { + Instant instant = Instant.parse("2025-07-31T20:17:40.123456Z"); + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> BeamCalcRel.timestampToCalciteMillis(instant)); + Assert.assertTrue(thrown.getMessage().contains("sub-millisecond")); + } } 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/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; 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()); + } } 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..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 @@ -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 @@ -2231,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; @@ -2286,10 +2312,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(); + } } /** @@ -2333,6 +2369,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() @@ -2435,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; @@ -2460,6 +2507,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; @@ -2620,6 +2676,11 @@ public BoundedWindow window() { return currentWindow; } + @Override + public T sideInput(PCollectionView view) { + return stateAccessor.get(view, currentWindow); + } + @Override public CausedByDrain causedByDrain() { return causedByDrain; @@ -2771,6 +2832,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(); 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..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 @@ -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) @@ -296,6 +305,23 @@ 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); + } 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, ThrowingFunction> @@ -307,7 +333,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..b744da10cf6f 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(); } } } @@ -756,6 +767,7 @@ public BeamFnApi.InstructionResponse.Builder trySplit(InstructionRequest request /** Shutdown the bundles, running the tearDown() functions. */ public void shutdown() throws Exception { bundleProcessorCache.shutdown(); + beamFnDataClient.close(); } @VisibleForTesting @@ -875,7 +887,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..458ed1209f43 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 @@ -17,21 +17,22 @@ */ package org.apache.beam.fn.harness.data; +import java.io.Closeable; +import java.io.IOException; 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 * provide a receiver of outbound elements. Callers can register themselves as receivers for inbound * elements or can get a handle for a receiver of outbound elements. */ -public interface BeamFnDataClient { +public interface BeamFnDataClient extends Closeable { /** * Registers a receiver for the provided instruction id. * @@ -47,6 +48,7 @@ public interface BeamFnDataClient { */ void registerReceiver( String instructionId, + String dataStreamId, List apiServiceDescriptors, CloseableFnDataReceiver receiver); @@ -58,7 +60,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 +71,12 @@ 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); + + @Override + default void close() throws IOException { + // Default to no-op + } } 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..79e34fe5765f 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,44 @@ 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 void close() { + for (BeamFnDataGrpcMultiplexer client : multiplexerCache.values()) { + try { + client.close(); + } catch (Exception e) { + LOG.warn("Failed to close multiplexer", e); + } + } + multiplexerCache.clear(); + } + + @Override + 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")); 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) { diff --git a/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/sqs/SqsIOWriteBatchesTest.java b/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/sqs/SqsIOWriteBatchesTest.java index e92720bfb5a5..90ea28d60447 100644 --- a/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/sqs/SqsIOWriteBatchesTest.java +++ b/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/sqs/SqsIOWriteBatchesTest.java @@ -21,8 +21,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.CompletableFuture.completedFuture; import static java.util.concurrent.CompletableFuture.supplyAsync; +import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; import static java.util.stream.IntStream.range; +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.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -30,6 +32,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -38,6 +41,7 @@ import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -263,10 +267,16 @@ public void testWriteBatchesWithTimeout() { p.run().waitUntilFinish(); - SendMessageBatchRequestEntry[] entries = entries(range(0, 5)); - // due to added delay, batches are timed out on arrival of every 3rd msg - verify(sqs).sendMessageBatch(request("queue", entries[0], entries[1], entries[2])); - verify(sqs).sendMessageBatch(request("queue", entries[3], entries[4])); + // Nominally batches time out on arrival of every 3rd message ([0,1,2], [3,4]). The exact + // grouping depends on wall clock time and is unreliable on loaded machines (#38946), so + // verify timing-independent invariants instead: expired batches are flushed on append, so + // with >= 100ms between messages no batch can exceed 3 entries before exceeding the 150ms + // timeout, forcing at least 2 batches for 5 messages. + Map> requests = captureBatchRequests(atLeast(2)); + assertThat(requests.keySet()).containsExactly("queue"); + assertMessageBodies(requestsFor(requests, "queue"), range(0, 5)); + assertThat(requestsFor(requests, "queue")) + .allSatisfy(req -> assertThat(req.entries().size()).isLessThanOrEqualTo(3)); } @Test @@ -285,11 +295,16 @@ public void testWriteBatchesWithStrictTimeout() { p.run().waitUntilFinish(); - SendMessageBatchRequestEntry[] entries = entries(range(0, 5)); - // using strict timeouts batches, batches are timed out by a separate thread - verify(sqs).sendMessageBatch(request("queue", entries[0], entries[1])); - verify(sqs).sendMessageBatch(request("queue", entries[2], entries[3])); - verify(sqs).sendMessageBatch(request("queue", entries[4])); + // Nominally the separate timeout thread flushes [0,1], [2,3], [4]. The exact grouping + // depends on wall clock time and is unreliable on loaded machines (#38946), so verify + // timing-independent invariants instead. Expired batches are also flushed on append + // (independently of the timeout thread), so no batch can exceed 3 entries and 5 messages + // require at least 2 batches. + Map> requests = captureBatchRequests(atLeast(2)); + assertThat(requests.keySet()).containsExactly("queue"); + assertMessageBodies(requestsFor(requests, "queue"), range(0, 5)); + assertThat(requestsFor(requests, "queue")) + .allSatisfy(req -> assertThat(req.entries().size()).isLessThanOrEqualTo(3)); } @Test @@ -337,11 +352,18 @@ public void testWriteBatchesToDynamicWithTimeout() { p.run().waitUntilFinish(); - SendMessageBatchRequestEntry[] entries = entries(range(0, 5)); - // due to added delay, dynamic batches are timed out on arrival of every 2nd msg (per batch) - verify(sqs).sendMessageBatch(request("even", entries[0], entries[2])); - verify(sqs).sendMessageBatch(request("uneven", entries[1], entries[3])); - verify(sqs).sendMessageBatch(request("even", entries[4])); + // Nominally dynamic batches are timed out on arrival of every 2nd message per batch + // ([0,2], [1,3], [4]). The exact grouping depends on wall clock time and is unreliable on + // loaded machines (#38946), so verify timing-independent invariants instead: per queue, + // consecutive messages are >= 200ms apart, so expired batches are flushed on append once + // reaching 2 entries, requiring at least 3 batches overall. + Map> requests = captureBatchRequests(atLeast(3)); + assertThat(requests.keySet()).containsExactlyInAnyOrder("even", "uneven"); + assertMessageBodies(requestsFor(requests, "even"), range(0, 5).filter(i -> i % 2 == 0)); + assertMessageBodies(requestsFor(requests, "uneven"), range(0, 5).filter(i -> i % 2 == 1)); + requests.values().stream() + .flatMap(List::stream) + .forEach(req -> assertThat(req.entries().size()).isLessThanOrEqualTo(2)); } @Test @@ -360,14 +382,19 @@ public void testWriteBatchesToDynamicWithStrictTimeout() { p.run().waitUntilFinish(); - SendMessageBatchRequestEntry[] entries = entries(range(0, 5)); - // using strict timeouts batches, batches are timed out by a separate thread before any 2nd - // entry - verify(sqs).sendMessageBatch(request("even", entries[0])); - verify(sqs).sendMessageBatch(request("uneven", entries[1])); - verify(sqs).sendMessageBatch(request("even", entries[2])); - verify(sqs).sendMessageBatch(request("uneven", entries[3])); - verify(sqs).sendMessageBatch(request("even", entries[4])); + // Nominally the separate timeout thread flushes every batch before a 2nd entry arrives + // (5 singleton batches). The exact grouping depends on wall clock time and is unreliable + // on loaded machines (#38946), so verify timing-independent invariants instead. Expired + // batches are also flushed on append (independently of the timeout thread) and per queue + // consecutive messages are >= 200ms apart, so no batch can exceed 2 entries and at least + // 3 batches are required overall. + Map> requests = captureBatchRequests(atLeast(3)); + assertThat(requests.keySet()).containsExactlyInAnyOrder("even", "uneven"); + assertMessageBodies(requestsFor(requests, "even"), range(0, 5).filter(i -> i % 2 == 0)); + assertMessageBodies(requestsFor(requests, "uneven"), range(0, 5).filter(i -> i % 2 == 1)); + requests.values().stream() + .flatMap(List::stream) + .forEach(req -> assertThat(req.entries().size()).isLessThanOrEqualTo(2)); } @Test @@ -406,6 +433,32 @@ private SendMessageBatchRequest anyRequest() { return any(); } + /** Captures all batch requests, verifying the given mode, and groups them by queue url. */ + private Map> captureBatchRequests( + org.mockito.verification.VerificationMode mode) { + ArgumentCaptor captor = + ArgumentCaptor.forClass(SendMessageBatchRequest.class); + verify(sqs, mode).sendMessageBatch(captor.capture()); + return captor.getAllValues().stream().collect(groupingBy(SendMessageBatchRequest::queueUrl)); + } + + /** Returns the (non-null) batch requests for a queue, failing if there were none. */ + private List requestsFor( + Map> requests, String queue) { + return checkStateNotNull(requests.get(queue), "no batch requests for queue %s", queue); + } + + /** Asserts that the requests contain exactly the expected message bodies, each exactly once. */ + private void assertMessageBodies(List requests, IntStream expectedMsgs) { + assertThat( + requests.stream() + .flatMap(req -> req.entries().stream()) + .map(SendMessageBatchRequestEntry::messageBody) + .collect(toList())) + .containsExactlyInAnyOrderElementsOf( + expectedMsgs.mapToObj(Integer::toString).collect(toList())); + } + private SendMessageBatchRequest request(String queue, SendMessageBatchRequestEntry... entries) { return SendMessageBatchRequest.builder() .queueUrl(queue) diff --git a/sdks/java/io/arrow-flight/build.gradle b/sdks/java/io/arrow-flight/build.gradle new file mode 100644 index 000000000000..6d7a6765f8c0 --- /dev/null +++ b/sdks/java/io/arrow-flight/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. + */ + +plugins { id 'org.apache.beam.module' } +applyJavaNature(automaticModuleName: 'org.apache.beam.sdk.io.arrowflight') + +description = "Apache Beam :: SDKs :: Java :: IO :: Arrow Flight" +ext.summary = "IO to read and write data using Apache Arrow Flight RPC." + +dependencies { + implementation project(path: ":sdks:java:core", configuration: "shadow") + implementation project(path: ":sdks:java:extensions:arrow") + implementation library.java.joda_time + implementation library.java.slf4j_api + implementation library.java.vendored_guava_32_1_2_jre + implementation(library.java.arrow_flight_core) + implementation(library.java.arrow_memory_core) + implementation(library.java.arrow_vector) + testImplementation library.java.hamcrest + testImplementation library.java.junit + testImplementation(library.java.arrow_memory_netty) + testRuntimeOnly library.java.slf4j_simple + testRuntimeOnly project(path: ":runners:direct-java", configuration: "shadow") +} + +test { + // Keep aligned with ArrowFlightIO runtime guidance for Java 17+. + jvmArgs '--add-opens=java.base/java.nio=ALL-UNNAMED' +} diff --git a/sdks/java/io/arrow-flight/src/main/java/org/apache/beam/sdk/io/arrowflight/ArrowFlightIO.java b/sdks/java/io/arrow-flight/src/main/java/org/apache/beam/sdk/io/arrowflight/ArrowFlightIO.java new file mode 100644 index 000000000000..f14e10631d65 --- /dev/null +++ b/sdks/java/io/arrow-flight/src/main/java/org/apache/beam/sdk/io/arrowflight/ArrowFlightIO.java @@ -0,0 +1,840 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.arrowflight; + +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 com.google.auto.value.AutoValue; +import java.io.IOException; +import java.io.Serializable; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import org.apache.arrow.flight.AsyncPutListener; +import org.apache.arrow.flight.CallOption; +import org.apache.arrow.flight.FlightCallHeaders; +import org.apache.arrow.flight.FlightClient; +import org.apache.arrow.flight.FlightDescriptor; +import org.apache.arrow.flight.FlightEndpoint; +import org.apache.arrow.flight.FlightInfo; +import org.apache.arrow.flight.FlightStream; +import org.apache.arrow.flight.HeaderCallOption; +import org.apache.arrow.flight.Location; +import org.apache.arrow.flight.Ticket; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.BigIntVector; +import org.apache.arrow.vector.BitVector; +import org.apache.arrow.vector.FieldVector; +import org.apache.arrow.vector.Float4Vector; +import org.apache.arrow.vector.Float8Vector; +import org.apache.arrow.vector.IntVector; +import org.apache.arrow.vector.SmallIntVector; +import org.apache.arrow.vector.TimeStampMilliTZVector; +import org.apache.arrow.vector.TinyIntVector; +import org.apache.arrow.vector.VarBinaryVector; +import org.apache.arrow.vector.VarCharVector; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.extensions.arrow.ArrowConversion; +import org.apache.beam.sdk.io.BoundedSource; +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.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.transforms.display.DisplayData; +import org.apache.beam.sdk.values.PBegin; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PDone; +import org.apache.beam.sdk.values.Row; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * IO to read and write data using Apache + * Arrow Flight. + * + *

Arrow Flight is an RPC framework for transferring Arrow-formatted data over gRPC. + * + *

Reading from an Arrow Flight server

+ * + *

{@link ArrowFlightIO#read()} returns a bounded {@link PCollection} of {@link Row} elements. + * Each row is converted from Arrow record batches using the existing {@link ArrowConversion} + * extension. + * + *

{@code
+ * PCollection rows = pipeline.apply(
+ *     ArrowFlightIO.read()
+ *         .withHost("localhost")
+ *         .withPort(47470)
+ *         .withCommand("SELECT * FROM my_table"));
+ * }
+ * + *

Writing to an Arrow Flight server

+ * + *

{@link ArrowFlightIO#write()} accepts a {@link PCollection} of {@link Row} elements and + * streams them to a Flight server using {@code doPut}. + * + *

{@code
+ * rows.apply(
+ *     ArrowFlightIO.write()
+ *         .withHost("localhost")
+ *         .withPort(47470)
+ *         .withDescriptor("my_table")
+ *         .withBatchSize(1024));
+ * }
+ * + *

Java runtime configuration

+ * + *

On Java 17 or later, Arrow memory access requires {@code + * --add-opens=java.base/java.nio=ALL-UNNAMED} on JVMs executing this connector. Beam SDK containers + * enable it by default. For other environments, add it to the local or runner JVM and pass {@code + * --JdkAddOpenModules=java.base/java.nio=ALL-UNNAMED} to configure SDK harness JVMs. See the Arrow Java installation guide. + */ +public class ArrowFlightIO { + + private static final Logger LOG = LoggerFactory.getLogger(ArrowFlightIO.class); + + private ArrowFlightIO() {} + + private static byte[] copyToken(byte[] token) { + return Arrays.copyOf(checkNotNull(token, "token"), token.length); + } + + private static CallOption[] callOptions(byte @Nullable [] token) { + if (token == null) { + return new CallOption[0]; + } + FlightCallHeaders headers = new FlightCallHeaders(); + headers.insert("authorization", "Bearer " + new String(token, StandardCharsets.UTF_8)); + return new CallOption[] {new HeaderCallOption(headers)}; + } + + private static void validateWriteSchema(Schema schema) { + try { + ArrowConversion.ArrowSchemaTranslator.toArrowSchema(schema); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("ArrowFlightIO.write(): " + e.getMessage(), e); + } + } + + public static Read read() { + return new AutoValue_ArrowFlightIO_Read.Builder().setPort(47470).setUseTls(false).build(); + } + + public static Write write() { + return new AutoValue_ArrowFlightIO_Write.Builder() + .setPort(47470) + .setUseTls(false) + .setBatchSize(1024) + .build(); + } + + /** + * Creates a {@link FlightClient} from the given connection parameters. + * + *

The client uses a {@link RootAllocator} for Arrow memory management and connects to the + * specified host and port using either plaintext or TLS. + */ + static FlightClient createClient( + BufferAllocator allocator, String host, int port, boolean useTls) { + Location location; + if (useTls) { + location = Location.forGrpcTls(host, port); + } else { + location = Location.forGrpcInsecure(host, port); + } + return FlightClient.builder(allocator, location).build(); + } + + /** A serializable wrapper around Flight endpoint information for use in BoundedSource splits. */ + static class SerializableEndpoint implements Serializable { + private static final long serialVersionUID = 1L; + + private final byte[] ticketBytes; + private final @Nullable String host; + private final int port; + + SerializableEndpoint(byte[] ticketBytes, @Nullable String host, int port) { + this.ticketBytes = ticketBytes; + this.host = host; + this.port = port; + } + + static SerializableEndpoint fromFlightEndpoint( + FlightEndpoint endpoint, String defaultHost, int defaultPort) { + byte[] ticket = endpoint.getTicket().getBytes(); + List locations = endpoint.getLocations(); + if (locations != null && !locations.isEmpty()) { + URI uri = locations.get(0).getUri(); + return new SerializableEndpoint(ticket, uri.getHost(), uri.getPort()); + } + return new SerializableEndpoint(ticket, defaultHost, defaultPort); + } + + Ticket getTicket() { + return new Ticket(ticketBytes); + } + + String getHost(String defaultHost) { + return host != null ? host : defaultHost; + } + + int getPort(int defaultPort) { + return port > 0 ? port : defaultPort; + } + } + + // ======================== READ ======================== + + @AutoValue + public abstract static class Read extends PTransform> { + + abstract @Nullable String host(); + + abstract int port(); + + abstract boolean useTls(); + + abstract @Nullable String command(); + + @SuppressWarnings("mutable") + abstract byte @Nullable [] token(); + + abstract Builder builder(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setHost(String host); + + abstract Builder setPort(int port); + + abstract Builder setUseTls(boolean useTls); + + abstract Builder setCommand(String command); + + abstract Builder setToken(byte[] token); + + abstract Read build(); + } + + /** Sets the Flight server host. */ + public Read withHost(String host) { + return builder().setHost(host).build(); + } + + /** Sets the Flight server port. */ + public Read withPort(int port) { + return builder().setPort(port).build(); + } + + /** Enables TLS for the connection. */ + public Read withUseTls(boolean useTls) { + return builder().setUseTls(useTls).build(); + } + + /** Sets the command (e.g., a SQL query or table name) to request from the Flight server. */ + public Read withCommand(String command) { + return builder().setCommand(command).build(); + } + + /** Sets a bearer token for authentication. */ + public Read withToken(byte[] token) { + return builder().setToken(copyToken(token)).build(); + } + + @Override + public PCollection expand(PBegin input) { + checkArgument(host() != null, "withHost() is required"); + checkArgument(command() != null, "withCommand() is required"); + + Schema beamSchema; + try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE); + FlightClient client = + createClient(allocator, checkNotNull(host(), "host"), port(), useTls())) { + FlightInfo info = + client.getInfo( + FlightDescriptor.command( + checkNotNull(command(), "command").getBytes(StandardCharsets.UTF_8)), + callOptions()); + beamSchema = ArrowConversion.ArrowSchemaTranslator.toBeamSchema(info.getSchema()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while fetching Flight schema", e); + } + + return input + .apply(org.apache.beam.sdk.io.Read.from(new FlightBoundedSource(this, beamSchema))) + .setRowSchema(beamSchema); + } + + CallOption[] callOptions() { + return ArrowFlightIO.callOptions(token()); + } + + @Override + public void populateDisplayData(DisplayData.Builder builder) { + super.populateDisplayData(builder); + builder.addIfNotNull(DisplayData.item("host", host())); + builder.add(DisplayData.item("port", port())); + builder.add(DisplayData.item("useTls", useTls())); + builder.addIfNotNull(DisplayData.item("command", command())); + } + } + + /** A {@link BoundedSource} that reads rows from Arrow Flight endpoints. */ + static class FlightBoundedSource extends BoundedSource { + private final Read spec; + private final Schema beamSchema; + private final @Nullable SerializableEndpoint endpoint; + + FlightBoundedSource(Read spec, Schema beamSchema) { + this(spec, beamSchema, null); + } + + FlightBoundedSource(Read spec, Schema beamSchema, @Nullable SerializableEndpoint endpoint) { + this.spec = spec; + this.beamSchema = beamSchema; + this.endpoint = endpoint; + } + + @Override + public List> split( + long desiredBundleSizeBytes, PipelineOptions options) throws Exception { + if (endpoint != null) { + return Collections.singletonList(this); + } + + List> sources = new ArrayList<>(); + try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE); + FlightClient client = + createClient( + allocator, checkNotNull(spec.host(), "host"), spec.port(), spec.useTls())) { + FlightInfo info = + client.getInfo( + FlightDescriptor.command( + checkNotNull(spec.command(), "command").getBytes(StandardCharsets.UTF_8)), + spec.callOptions()); + for (FlightEndpoint fe : info.getEndpoints()) { + SerializableEndpoint se = + SerializableEndpoint.fromFlightEndpoint( + fe, checkNotNull(spec.host(), "host"), spec.port()); + sources.add(new FlightBoundedSource(spec, beamSchema, se)); + } + } + + if (sources.isEmpty()) { + sources.add(this); + } + return sources; + } + + @Override + public long getEstimatedSizeBytes(PipelineOptions options) throws Exception { + if (endpoint != null) { + return -1; + } + try (BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE); + FlightClient client = + createClient( + allocator, checkNotNull(spec.host(), "host"), spec.port(), spec.useTls())) { + FlightInfo info = + client.getInfo( + FlightDescriptor.command( + checkNotNull(spec.command(), "command").getBytes(StandardCharsets.UTF_8)), + spec.callOptions()); + return info.getBytes(); + } + } + + @Override + public BoundedReader createReader(PipelineOptions options) { + return new FlightBoundedReader(this); + } + + @Override + public void validate() { + checkArgument(spec.host() != null, "host is required"); + checkArgument(spec.command() != null, "command is required"); + } + + @Override + public Coder getOutputCoder() { + return RowCoder.of(beamSchema); + } + } + + /** Reader that streams Arrow record batches from a Flight endpoint and emits Beam Rows. */ + @SuppressWarnings("initialization.fields.uninitialized") + static class FlightBoundedReader extends BoundedSource.BoundedReader { + private static final Counter RECORDS_READ = Metrics.counter(ArrowFlightIO.class, "recordsRead"); + + private final FlightBoundedSource source; + private transient BufferAllocator allocator; + private transient FlightClient client; + private transient FlightStream stream; + private transient Iterator currentBatchIterator; + private transient Row current; + private FlightBoundedSource currentSource; + + FlightBoundedReader(FlightBoundedSource source) { + this.source = source; + this.currentSource = source; + } + + @Override + public boolean start() throws IOException { + allocator = new RootAllocator(Long.MAX_VALUE); + Read spec = source.spec; + + String defaultHost = checkNotNull(spec.host(), "host"); + SerializableEndpoint endpoint = source.endpoint; + if (endpoint == null) { + try (FlightClient discoveryClient = + createClient(allocator, defaultHost, spec.port(), spec.useTls())) { + FlightInfo info = + discoveryClient.getInfo( + FlightDescriptor.command( + checkNotNull(spec.command(), "command").getBytes(StandardCharsets.UTF_8)), + spec.callOptions()); + List endpoints = info.getEndpoints(); + if (endpoints.isEmpty()) { + return false; + } + endpoint = + SerializableEndpoint.fromFlightEndpoint(endpoints.get(0), defaultHost, spec.port()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while discovering Flight endpoints", e); + } + currentSource = new FlightBoundedSource(spec, source.beamSchema, endpoint); + } + + client = + createClient( + allocator, + endpoint.getHost(defaultHost), + endpoint.getPort(spec.port()), + spec.useTls()); + stream = client.getStream(endpoint.getTicket(), spec.callOptions()); + currentBatchIterator = Collections.emptyIterator(); + return advance(); + } + + @Override + public boolean advance() throws IOException { + while (true) { + if (currentBatchIterator.hasNext()) { + current = currentBatchIterator.next(); + RECORDS_READ.inc(); + return true; + } + if (stream.next()) { + VectorSchemaRoot root = stream.getRoot(); + if (root.getRowCount() > 0) { + Iterator lazyIterator = + ArrowConversion.rowsFromRecordBatch(source.beamSchema, root); + List materializedRows = new ArrayList<>(); + while (lazyIterator.hasNext()) { + Row lazyRow = lazyIterator.next(); + materializedRows.add( + Row.withSchema(source.beamSchema).addValues(lazyRow.getValues()).build()); + } + currentBatchIterator = materializedRows.iterator(); + } + } else { + return false; + } + } + } + + @Override + public Row getCurrent() { + return current; + } + + @Override + public void close() throws IOException { + try { + if (stream != null) { + stream.close(); + } + } catch (Exception e) { + LOG.warn("Error closing FlightStream", e); + } + try { + if (client != null) { + client.close(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warn("Interrupted closing FlightClient", e); + } + try { + if (allocator != null) { + allocator.close(); + } + } catch (Exception e) { + LOG.warn("Error closing BufferAllocator", e); + } + } + + @Override + public BoundedSource getCurrentSource() { + return currentSource; + } + } + + // ======================== WRITE ======================== + + // TODO: Return a write result with failed rows for dead-letter handling instead of PDone. + @AutoValue + public abstract static class Write extends PTransform, PDone> { + + abstract @Nullable String host(); + + abstract int port(); + + abstract boolean useTls(); + + abstract @Nullable String descriptor(); + + abstract int batchSize(); + + @SuppressWarnings("mutable") + abstract byte @Nullable [] token(); + + abstract Builder builder(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setHost(String host); + + abstract Builder setPort(int port); + + abstract Builder setUseTls(boolean useTls); + + abstract Builder setDescriptor(String descriptor); + + abstract Builder setBatchSize(int batchSize); + + abstract Builder setToken(byte[] token); + + abstract Write build(); + } + + /** Sets the Flight server host. */ + public Write withHost(String host) { + return builder().setHost(host).build(); + } + + /** Sets the Flight server port. */ + public Write withPort(int port) { + return builder().setPort(port).build(); + } + + /** Enables TLS for the connection. */ + public Write withUseTls(boolean useTls) { + return builder().setUseTls(useTls).build(); + } + + /** Sets the Flight descriptor (table name or path) for the write target. */ + public Write withDescriptor(String descriptor) { + return builder().setDescriptor(descriptor).build(); + } + + /** Sets the batch size for writing. Rows are buffered and flushed in batches. */ + public Write withBatchSize(int batchSize) { + checkArgument(batchSize > 0, "batchSize must be positive"); + return builder().setBatchSize(batchSize).build(); + } + + /** Sets a bearer token for authentication. */ + public Write withToken(byte[] token) { + return builder().setToken(copyToken(token)).build(); + } + + CallOption[] callOptions() { + return ArrowFlightIO.callOptions(token()); + } + + @Override + public PDone expand(PCollection input) { + checkArgument(host() != null, "withHost() is required"); + checkArgument(descriptor() != null, "withDescriptor() is required"); + Schema inputSchema = checkNotNull(input.getSchema(), "input schema"); + validateWriteSchema(inputSchema); + + input.apply(ParDo.of(new FlightWriteFn(this, inputSchema))); + return PDone.in(input.getPipeline()); + } + + @Override + public void populateDisplayData(DisplayData.Builder builder) { + super.populateDisplayData(builder); + builder.addIfNotNull(DisplayData.item("host", host())); + builder.add(DisplayData.item("port", port())); + builder.add(DisplayData.item("useTls", useTls())); + builder.addIfNotNull(DisplayData.item("descriptor", descriptor())); + builder.add(DisplayData.item("batchSize", batchSize())); + } + } + + /** DoFn that buffers Beam Rows and streams them as Arrow record batches to a Flight server. */ + @SuppressWarnings("initialization.fields.uninitialized") + static class FlightWriteFn extends DoFn { + private static final Counter RECORDS_WRITTEN = + Metrics.counter(ArrowFlightIO.class, "recordsWritten"); + private static final Counter BATCHES_WRITTEN = + Metrics.counter(ArrowFlightIO.class, "batchesWritten"); + + private final Write spec; + private final Schema beamSchema; + private transient @Nullable BufferAllocator allocator; + private transient @Nullable FlightClient client; + private transient FlightClient.@Nullable ClientStreamListener listener; + private transient @Nullable VectorSchemaRoot root; + private transient List batch; + + FlightWriteFn(Write spec, Schema beamSchema) { + this.spec = spec; + this.beamSchema = beamSchema; + } + + @StartBundle + public void startBundle() { + batch = new ArrayList<>(); + } + + @ProcessElement + public void processElement(@Element Row row) { + checkArgument( + row.getSchema().equivalent(beamSchema), + "ArrowFlightIO.write() requires all rows to use the same schema."); + batch.add(row); + if (batch.size() >= spec.batchSize()) { + flush(); + } + } + + @FinishBundle + public void finishBundle() { + RuntimeException failure = null; + try { + flush(); + } catch (RuntimeException e) { + failure = e; + } + + try { + closeConnection(); + } catch (RuntimeException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + + if (failure != null) { + throw failure; + } + } + + @Teardown + public void teardown() { + try { + closeConnection(); + } catch (RuntimeException e) { + LOG.warn("Error closing Flight write connection during teardown", e); + } + } + + private void ensureConnection() { + if (client == null) { + BufferAllocator currentAllocator = new RootAllocator(Long.MAX_VALUE); + allocator = currentAllocator; + FlightClient currentClient = + createClient( + currentAllocator, checkNotNull(spec.host(), "host"), spec.port(), spec.useTls()); + client = currentClient; + + org.apache.arrow.vector.types.pojo.Schema arrowSchema = + ArrowConversion.ArrowSchemaTranslator.toArrowSchema(beamSchema); + VectorSchemaRoot currentRoot = VectorSchemaRoot.create(arrowSchema, currentAllocator); + root = currentRoot; + + FlightDescriptor descriptor = + FlightDescriptor.path(checkNotNull(spec.descriptor(), "descriptor")); + listener = + currentClient.startPut( + descriptor, currentRoot, new AsyncPutListener(), spec.callOptions()); + } + } + + @SuppressWarnings("nullness") + private void flush() { + if (batch == null || batch.isEmpty()) { + return; + } + ensureConnection(); + + for (int colIdx = 0; colIdx < beamSchema.getFieldCount(); colIdx++) { + FieldVector vector = root.getVector(colIdx); + vector.allocateNew(); + Schema.Field field = beamSchema.getField(colIdx); + for (int rowIdx = 0; rowIdx < batch.size(); rowIdx++) { + Object value = batch.get(rowIdx).getValue(colIdx); + if (value == null) { + vector.setNull(rowIdx); + } else { + setVectorValue(vector, rowIdx, value, field.getType()); + } + } + vector.setValueCount(batch.size()); + } + root.setRowCount(batch.size()); + + listener.putNext(); + RECORDS_WRITTEN.inc(batch.size()); + BATCHES_WRITTEN.inc(); + root.clear(); + batch.clear(); + } + + @SuppressWarnings("nullness") + private void setVectorValue( + FieldVector vector, int index, Object value, Schema.FieldType type) { + switch (type.getTypeName()) { + case BYTE: + ((TinyIntVector) vector).setSafe(index, ((Number) value).byteValue()); + break; + case INT16: + ((SmallIntVector) vector).setSafe(index, ((Number) value).shortValue()); + break; + case INT32: + ((IntVector) vector).setSafe(index, ((Number) value).intValue()); + break; + case INT64: + ((BigIntVector) vector).setSafe(index, ((Number) value).longValue()); + break; + case FLOAT: + ((Float4Vector) vector).setSafe(index, ((Number) value).floatValue()); + break; + case DOUBLE: + ((Float8Vector) vector).setSafe(index, ((Number) value).doubleValue()); + break; + case BOOLEAN: + ((BitVector) vector).setSafe(index, ((Boolean) value) ? 1 : 0); + break; + case STRING: + ((VarCharVector) vector) + .setSafe(index, value.toString().getBytes(StandardCharsets.UTF_8)); + break; + case BYTES: + ((VarBinaryVector) vector).setSafe(index, (byte[]) value); + break; + case DATETIME: + long millis; + if (value instanceof org.joda.time.ReadableInstant) { + millis = ((org.joda.time.ReadableInstant) value).getMillis(); + } else { + millis = ((Number) value).longValue(); + } + ((TimeStampMilliTZVector) vector).setSafe(index, millis); + break; + default: + throw new IllegalArgumentException( + "Unsupported Beam type for ArrowFlightIO.write(): " + type.getTypeName()); + } + } + + private void closeConnection() { + RuntimeException failure = null; + FlightClient.ClientStreamListener currentListener = listener; + listener = null; + try { + if (currentListener != null) { + currentListener.completed(); + currentListener.getResult(); + } + } catch (RuntimeException e) { + failure = e; + } + + VectorSchemaRoot currentRoot = root; + root = null; + try { + if (currentRoot != null) { + currentRoot.close(); + } + } catch (Exception e) { + if (failure == null) { + failure = new RuntimeException("Error closing VectorSchemaRoot", e); + } else { + failure.addSuppressed(e); + } + } + + FlightClient currentClient = client; + client = null; + try { + if (currentClient != null) { + currentClient.close(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + RuntimeException closeFailure = new RuntimeException("Interrupted closing FlightClient", e); + if (failure == null) { + failure = closeFailure; + } else { + failure.addSuppressed(closeFailure); + } + } + + BufferAllocator currentAllocator = allocator; + allocator = null; + try { + if (currentAllocator != null) { + currentAllocator.close(); + } + } catch (Exception e) { + if (failure == null) { + failure = new RuntimeException("Error closing BufferAllocator", e); + } else { + failure.addSuppressed(e); + } + } + + if (failure != null) { + throw failure; + } + } + } +} diff --git a/sdks/java/io/arrow-flight/src/main/java/org/apache/beam/sdk/io/arrowflight/package-info.java b/sdks/java/io/arrow-flight/src/main/java/org/apache/beam/sdk/io/arrowflight/package-info.java new file mode 100644 index 000000000000..6988db88c3a0 --- /dev/null +++ b/sdks/java/io/arrow-flight/src/main/java/org/apache/beam/sdk/io/arrowflight/package-info.java @@ -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. + */ + +/** + * I/O connector for Apache Arrow + * Flight. + * + *

Arrow Flight is a high-performance RPC framework for fast data transport using the Apache + * Arrow columnar format over gRPC. This connector enables Beam pipelines to read from and write to + * Arrow Flight-compatible data systems. + * + * @see org.apache.beam.sdk.io.arrowflight.ArrowFlightIO + */ +package org.apache.beam.sdk.io.arrowflight; diff --git a/sdks/java/io/arrow-flight/src/test/java/org/apache/beam/sdk/io/arrowflight/ArrowFlightIOTest.java b/sdks/java/io/arrow-flight/src/test/java/org/apache/beam/sdk/io/arrowflight/ArrowFlightIOTest.java new file mode 100644 index 000000000000..7ef67b5f6249 --- /dev/null +++ b/sdks/java/io/arrow-flight/src/test/java/org/apache/beam/sdk/io/arrowflight/ArrowFlightIOTest.java @@ -0,0 +1,330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.arrowflight; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.arrow.flight.Action; +import org.apache.arrow.flight.ActionType; +import org.apache.arrow.flight.CallStatus; +import org.apache.arrow.flight.Criteria; +import org.apache.arrow.flight.FlightConstants; +import org.apache.arrow.flight.FlightDescriptor; +import org.apache.arrow.flight.FlightEndpoint; +import org.apache.arrow.flight.FlightInfo; +import org.apache.arrow.flight.FlightProducer; +import org.apache.arrow.flight.FlightServer; +import org.apache.arrow.flight.FlightStream; +import org.apache.arrow.flight.Location; +import org.apache.arrow.flight.PutResult; +import org.apache.arrow.flight.Result; +import org.apache.arrow.flight.ServerHeaderMiddleware; +import org.apache.arrow.flight.Ticket; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.VarCharVector; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.FieldType; +import org.apache.beam.sdk.Pipeline; +import org.apache.beam.sdk.Pipeline.PipelineExecutionException; +import org.apache.beam.sdk.io.BoundedSource; +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.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.Row; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ArrowFlightIO}. */ +@RunWith(JUnit4.class) +public class ArrowFlightIOTest { + + @Rule public final TestPipeline pipeline = TestPipeline.create(); + + private BufferAllocator allocator; + private FlightServer server; + private TestFlightProducer producer; + private int port; + + @Before + public void setUp() throws Exception { + allocator = new RootAllocator(Long.MAX_VALUE); + producer = new TestFlightProducer(allocator); + + // Bind to any available port + Location location = Location.forGrpcInsecure("localhost", 0); + server = FlightServer.builder(allocator, location, producer).build(); + server.start(); + + port = server.getPort(); + } + + @After + public void tearDown() throws Exception { + if (server != null) { + server.close(); + } + if (allocator != null) { + allocator.close(); + } + } + + @Test + public void testRead() { + PCollection output = + pipeline.apply( + "Read from Flight", + ArrowFlightIO.read().withHost("localhost").withPort(port).withCommand("test_query")); + + Schema expectedSchema = Schema.builder().addStringField("name").build(); + Row expectedRow1 = Row.withSchema(expectedSchema).addValue("Alice").build(); + Row expectedRow2 = Row.withSchema(expectedSchema).addValue("Bob").build(); + + PAssert.that(output).containsInAnyOrder(expectedRow1, expectedRow2); + + pipeline.run().waitUntilFinish(); + } + + @Test + public void testCurrentSourceDoesNotResplitAfterReadStarts() throws Exception { + producer.endpointCount = 2; + PipelineOptions options = PipelineOptionsFactory.create(); + Schema schema = Schema.builder().addStringField("name").build(); + ArrowFlightIO.Read read = + ArrowFlightIO.read().withHost("localhost").withPort(port).withCommand("test_query"); + ArrowFlightIO.FlightBoundedSource source = new ArrowFlightIO.FlightBoundedSource(read, schema); + + assertEquals(2, source.split(0, options).size()); + try (BoundedSource.BoundedReader reader = source.createReader(options)) { + assertTrue(reader.start()); + assertEquals(1, reader.getCurrentSource().split(0, options).size()); + } + } + + @Test + public void testWrite() throws Exception { + Schema expectedSchema = Schema.builder().addStringField("name").build(); + Row row1 = Row.withSchema(expectedSchema).addValue("Charlie").build(); + Row row2 = Row.withSchema(expectedSchema).addValue("Dave").build(); + + pipeline + .apply(Create.of(row1, row2).withRowSchema(expectedSchema)) + .apply( + "Write to Flight", + ArrowFlightIO.write() + .withHost("localhost") + .withPort(port) + .withDescriptor("test_table")); + + pipeline.run().waitUntilFinish(); + + assertEquals(2, producer.writtenRecords.get()); + } + + @Test + public void testWriteWithToken() throws Exception { + producer.requiredAuthorizationHeader = "Bearer test-token"; + + Schema expectedSchema = Schema.builder().addStringField("name").build(); + Row row = Row.withSchema(expectedSchema).addValue("Charlie").build(); + + pipeline + .apply(Create.of(row).withRowSchema(expectedSchema)) + .apply( + "Write to Flight with Token", + ArrowFlightIO.write() + .withHost("localhost") + .withPort(port) + .withDescriptor("test_table") + .withToken("test-token".getBytes(StandardCharsets.UTF_8))); + + pipeline.run().waitUntilFinish(); + + assertEquals("Bearer test-token", producer.lastAuthorizationHeader.get()); + assertEquals(1, producer.writtenRecords.get()); + } + + @Test + public void testWritePropagatesServerErrors() { + producer.failWrites = true; + + Schema expectedSchema = Schema.builder().addStringField("name").build(); + Row row = Row.withSchema(expectedSchema).addValue("Charlie").build(); + + pipeline + .apply(Create.of(row).withRowSchema(expectedSchema)) + .apply( + "Write to Failing Flight Server", + ArrowFlightIO.write() + .withHost("localhost") + .withPort(port) + .withDescriptor("test_table")); + + PipelineExecutionException exception = + assertThrows(PipelineExecutionException.class, () -> pipeline.run().waitUntilFinish()); + assertThat(exception.getMessage(), containsString("Rejected write")); + } + + @Test + public void testWriteRejectsUnsupportedSchema() { + Schema unsupportedSchema = + Schema.builder().addArrayField("names", Schema.FieldType.STRING).build(); + Row row = + Row.withSchema(unsupportedSchema).addArray(Collections.singletonList("Charlie")).build(); + Pipeline testPipeline = Pipeline.create(); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + testPipeline + .apply(Create.of(row).withRowSchema(unsupportedSchema)) + .apply( + ArrowFlightIO.write() + .withHost("localhost") + .withPort(port) + .withDescriptor("test_table"))); + + assertThat(exception.getMessage(), containsString("does not support Beam type 'ARRAY'")); + } + + /** A simple FlightProducer that returns predefined data for reads and counts writes. */ + private static class TestFlightProducer implements FlightProducer { + + private final BufferAllocator allocator; + final AtomicInteger writtenRecords = new AtomicInteger(); + final AtomicReference lastAuthorizationHeader = new AtomicReference<>(); + volatile int endpointCount = 1; + volatile boolean failWrites; + volatile String requiredAuthorizationHeader; + + TestFlightProducer(BufferAllocator allocator) { + this.allocator = allocator; + } + + @Override + public void getStream(CallContext context, Ticket ticket, ServerStreamListener listener) { + org.apache.arrow.vector.types.pojo.Schema schema = + new org.apache.arrow.vector.types.pojo.Schema( + Collections.singletonList( + new Field( + "name", FieldType.nullable(new ArrowType.Utf8()), Collections.emptyList()))); + + try (VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) { + listener.start(root); + + VarCharVector vector = (VarCharVector) root.getVector("name"); + vector.allocateNew(); + vector.setSafe(0, "Alice".getBytes(StandardCharsets.UTF_8)); + vector.setSafe(1, "Bob".getBytes(StandardCharsets.UTF_8)); + vector.setValueCount(2); + root.setRowCount(2); + + listener.putNext(); + listener.completed(); + } catch (Exception e) { + listener.error(e); + } + } + + @Override + public void listFlights( + CallContext context, Criteria criteria, StreamListener listener) { + listener.onCompleted(); + } + + @Override + public FlightInfo getFlightInfo(CallContext context, FlightDescriptor descriptor) { + org.apache.arrow.vector.types.pojo.Schema schema = + new org.apache.arrow.vector.types.pojo.Schema( + Collections.singletonList( + new Field( + "name", FieldType.nullable(new ArrowType.Utf8()), Collections.emptyList()))); + List endpoints = new ArrayList<>(); + for (int i = 0; i < endpointCount; i++) { + endpoints.add( + new FlightEndpoint( + new Ticket(descriptor.getCommand()), Location.forGrpcInsecure("localhost", 0))); + } + return new FlightInfo(schema, descriptor, endpoints, -1, -1); + } + + @Override + public Runnable acceptPut( + CallContext context, FlightStream flightStream, StreamListener ackStream) { + ServerHeaderMiddleware headerMiddleware = context.getMiddleware(FlightConstants.HEADER_KEY); + lastAuthorizationHeader.set( + headerMiddleware == null ? null : headerMiddleware.headers().get("authorization")); + + return () -> { + try { + if (requiredAuthorizationHeader != null + && !requiredAuthorizationHeader.equals(lastAuthorizationHeader.get())) { + ackStream.onError( + CallStatus.UNAUTHENTICATED + .withDescription("Missing or invalid authorization header") + .toRuntimeException()); + return; + } + while (flightStream.next()) { + VectorSchemaRoot root = flightStream.getRoot(); + writtenRecords.addAndGet(root.getRowCount()); + } + if (failWrites) { + ackStream.onError( + CallStatus.INTERNAL.withDescription("Rejected write").toRuntimeException()); + return; + } + ackStream.onCompleted(); + } catch (Exception e) { + ackStream.onError(e); + } + }; + } + + @Override + public void doAction(CallContext context, Action action, StreamListener listener) { + listener.onCompleted(); + } + + @Override + public void listActions(CallContext context, StreamListener listener) { + listener.onCompleted(); + } + } +} diff --git a/sdks/java/io/cassandra/build.gradle b/sdks/java/io/cassandra/build.gradle index 5256ac1f2148..43ac8bc8bf85 100644 --- a/sdks/java/io/cassandra/build.gradle +++ b/sdks/java/io/cassandra/build.gradle @@ -69,3 +69,24 @@ configurations.all (Configuration it) -> { force 'com.google.guava:guava:25.1-jre' } } + +def testJavaVer = project.findProperty('testJavaVersion') ?: JavaVersion.current().getMajorVersion() +def isJava17OrHigher = testJavaVer.toInteger() >= 17 + +test { + if (isJava17OrHigher) { + systemProperty 'java.security.manager', 'allow' + } + // Open and export modules for embedded Cassandra under Java 17+ + jvmArgs '--add-exports=java.base/sun.nio.ch=ALL-UNNAMED', + '--add-exports=java.rmi/sun.rmi.registry=ALL-UNNAMED', + '--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED', + '--add-opens=java.base/sun.nio.ch=ALL-UNNAMED', + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', + '--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.net=ALL-UNNAMED', + '--add-opens=java.base/java.nio=ALL-UNNAMED' +} diff --git a/sdks/java/io/cdap/build.gradle b/sdks/java/io/cdap/build.gradle index 6ccb10a5c891..cc7e3ba12cde 100644 --- a/sdks/java/io/cdap/build.gradle +++ b/sdks/java/io/cdap/build.gradle @@ -85,3 +85,13 @@ dependencies { testImplementation project(path: ":runners:direct-java", configuration: "shadow") testImplementation project(path: ":sdks:java:io:common") } + +test { + // Open java.lang for Gson reflection on StackTraceElement under Java 17+ + jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED' +} + +// spark3_streaming depends on old 'org.lz4:lz4-java'; conflict with kafka-clients:3.9.2 +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} \ No newline at end of file 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<>(); diff --git a/sdks/java/io/components/src/main/java/org/apache/beam/sdk/io/components/throttling/AdaptiveThrottler.java b/sdks/java/io/components/src/main/java/org/apache/beam/sdk/io/components/throttling/AdaptiveThrottler.java new file mode 100644 index 000000000000..b2c94a5de359 --- /dev/null +++ b/sdks/java/io/components/src/main/java/org/apache/beam/sdk/io/components/throttling/AdaptiveThrottler.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.sdk.io.components.throttling; + +import java.util.Random; +import org.apache.beam.sdk.transforms.Sum; +import org.apache.beam.sdk.util.MovingFunction; + +/** + * Implements adaptive throttling. + * + *

See + * https://landing.google.com/sre/book/chapters/handling-overload.html#client-side-throttling-a7sYUg + * for a full discussion of the use case and algorithm applied. + */ +public class AdaptiveThrottler { + + // The target minimum number of requests per samplePeriodMs, even if no + // requests succeed. Must be greater than 0, else we could throttle to zero. + // Because every decision is probabilistic, there is no guarantee that the + // request rate in any given interval will not be zero. (This is the +1 from + // the formula in + // https://landing.google.com/sre/book/chapters/handling-overload.html ) + public static final int MIN_REQUESTS = 1; + + private final MovingFunction allRequests; + private final MovingFunction successfulRequests; + private final double overloadRatio; + private final Random random; + + /** + * Initializes AdaptiveThrottler. + * + * @param samplePeriodMs length of history to consider, in ms, to set throttling. + * @param sampleUpdateMs granularity of time buckets that we store data in, in ms. + * @param overloadRatio the target ratio between requests sent and successful requests. + */ + public AdaptiveThrottler(long samplePeriodMs, long sampleUpdateMs, double overloadRatio) { + this(samplePeriodMs, sampleUpdateMs, overloadRatio, new Random()); + } + + // visible for testing + AdaptiveThrottler(long samplePeriodMs, long sampleUpdateMs, double overloadRatio, Random random) { + if (overloadRatio <= 1.0) { + throw new IllegalArgumentException("overloadRatio must be greater than 1.0"); + } + this.allRequests = new MovingFunction(samplePeriodMs, sampleUpdateMs, 1, 1, Sum.ofLongs()); + this.successfulRequests = + new MovingFunction(samplePeriodMs, sampleUpdateMs, 1, 1, Sum.ofLongs()); + this.overloadRatio = overloadRatio; + this.random = random; + } + + protected double throttlingProbability(long nowMsSinceEpoch) { + long allReqs = allRequests.get(nowMsSinceEpoch); + if (!allRequests.isSignificant()) { + return 0.0; + } + long successfulReqs = successfulRequests.get(nowMsSinceEpoch); + double prob = (allReqs - overloadRatio * successfulReqs) / (allReqs + MIN_REQUESTS); + return Math.max(0.0, prob); + } + + /** + * Determines whether one RPC attempt should be throttled. + * + *

This should be called once each time the caller intends to send an RPC; if it returns true, + * drop or delay that request (calling this function again after the delay). + * + * @param nowMsSinceEpoch time in ms since the epoch + * @return true if the caller should throttle or delay the request. + */ + public synchronized boolean throttleRequest(long nowMsSinceEpoch) { + double prob = throttlingProbability(nowMsSinceEpoch); + allRequests.add(nowMsSinceEpoch, 1); + return random.nextDouble() < prob; + } + + /** + * Notifies the throttler of a successful request. + * + *

Must be called once for each request (for which throttleRequest was previously called) that + * succeeded. + * + * @param nowMsSinceEpoch time in ms since the epoch + */ + public synchronized void successfulRequest(long nowMsSinceEpoch) { + successfulRequests.add(nowMsSinceEpoch, 1); + } +} diff --git a/sdks/java/io/components/src/main/java/org/apache/beam/sdk/io/components/throttling/ReactiveThrottler.java b/sdks/java/io/components/src/main/java/org/apache/beam/sdk/io/components/throttling/ReactiveThrottler.java new file mode 100644 index 000000000000..2c828f9c9187 --- /dev/null +++ b/sdks/java/io/components/src/main/java/org/apache/beam/sdk/io/components/throttling/ReactiveThrottler.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.sdk.io.components.throttling; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A wrapper around the AdaptiveThrottler that also handles logging and signaling throttling to the + * SDK harness using the provided namespace. + * + *

For usage, instantiate one instance of a ReactiveThrottler class for a PTransform. When making + * remote calls to a service, preface that call with the throttle() method to potentially + * pre-emptively throttle the request. This will throttle future calls based on the failure rate of + * preceding calls, with higher failure rates leading to longer periods of throttling to allow + * system recovery. capture the timestamp of the attempted request, then execute the request code. + * On a success, call successfulRequest(timestamp) to report the success to the throttler. + */ +public class ReactiveThrottler extends AdaptiveThrottler { + private static final Logger LOG = LoggerFactory.getLogger(ReactiveThrottler.class); + private static final long SECONDS_TO_MILLISECONDS = 1000L; + + private final ThrottlingSignaler throttlingSignaler; + private final int throttleDelaySecs; + + /** + * Initializes the ReactiveThrottler. + * + * @param samplePeriodMs length of history to consider, in ms, to set throttling. + * @param sampleUpdateMs granularity of time buckets that we store data in, in ms. + * @param overloadRatio the target ratio between requests sent and successful requests. + * @param namespace the namespace to use for logging and signaling throttling is occurring. + * @param throttleDelaySecs the amount of time in seconds to wait after preemptively throttled + * requests. + */ + public ReactiveThrottler( + long samplePeriodMs, + long sampleUpdateMs, + double overloadRatio, + String namespace, + int throttleDelaySecs) { + super(samplePeriodMs, sampleUpdateMs, overloadRatio); + if (throttleDelaySecs <= 0) { + throw new IllegalArgumentException("throttleDelaySecs must be greater than 0"); + } + this.throttlingSignaler = new ThrottlingSignaler(namespace); + this.throttleDelaySecs = throttleDelaySecs; + } + + /** + * Stops request code from advancing while the underlying AdaptiveThrottler is signaling to + * preemptively throttle the request. Automatically handles logging the throttling and signaling + * to the SDK harness that the request is being throttled. This should be called in any context + * where a call to a remote service is being contacted prior to the call being performed. + */ + public void throttle() throws InterruptedException { + if (throttleRequest(System.currentTimeMillis())) { + LOG.debug("Delaying request for {} seconds due to previous failures", throttleDelaySecs); + Thread.sleep(throttleDelaySecs * SECONDS_TO_MILLISECONDS); + throttlingSignaler.signalThrottling(throttleDelaySecs * SECONDS_TO_MILLISECONDS); + } + } +} diff --git a/sdks/java/io/components/src/test/java/org/apache/beam/sdk/io/components/throttling/AdaptiveThrottlerTest.java b/sdks/java/io/components/src/test/java/org/apache/beam/sdk/io/components/throttling/AdaptiveThrottlerTest.java new file mode 100644 index 000000000000..3e798c7a0d55 --- /dev/null +++ b/sdks/java/io/components/src/test/java/org/apache/beam/sdk/io/components/throttling/AdaptiveThrottlerTest.java @@ -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. + */ +package org.apache.beam.sdk.io.components.throttling; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Random; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AdaptiveThrottlerTest { + + private static final long START_TIME = 1500000000000L; + private static final long SAMPLE_PERIOD = 60000L; + private static final long BUCKET = 1000L; + private static final double OVERLOAD_RATIO = 2.0; + + private AdaptiveThrottler throttler; + + @Before + public void setUp() { + throttler = new AdaptiveThrottler(SAMPLE_PERIOD, BUCKET, OVERLOAD_RATIO); + } + + @Test + public void testNoInitialThrottling() { + assertEquals(0.0, throttler.throttlingProbability(START_TIME), 0.0); + } + + @Test + public void testNoThrottlingIfNoErrors() { + for (long t = START_TIME; t < START_TIME + 20; t++) { + assertFalse(throttler.throttleRequest(t)); + throttler.successfulRequest(t); + } + assertEquals(0.0, throttler.throttlingProbability(START_TIME + 20), 0.0); + } + + @Test + public void testNoThrottlingAfterErrorsExpire() { + for (long t = START_TIME; t < START_TIME + SAMPLE_PERIOD; t += 100) { + throttler.throttleRequest(t); + // No successful request + } + assertTrue(throttler.throttlingProbability(START_TIME + SAMPLE_PERIOD) > 0); + + for (long t = START_TIME + SAMPLE_PERIOD; t < START_TIME + SAMPLE_PERIOD * 2; t += 100) { + throttler.throttleRequest(t); + throttler.successfulRequest(t); + } + + assertEquals(0.0, throttler.throttlingProbability(START_TIME + SAMPLE_PERIOD * 2), 0.0); + } + + @Test + public void testThrottlingAfterErrors() { + // Inject a mocked Random + throttler = new AdaptiveThrottler(SAMPLE_PERIOD, BUCKET, OVERLOAD_RATIO, new MockRandom()); + + for (long t = START_TIME; t < START_TIME + 20; t++) { + boolean throttled = throttler.throttleRequest(t); + // 1/3rd of requests succeeding. + if (t % 3 == 1) { + throttler.successfulRequest(t); + } + + if (t > START_TIME + 10) { + // Roughly 1/3rd succeeding, 1/3rd failing, 1/3rd throttled. + assertEquals(0.33, throttler.throttlingProbability(t), 0.1); + // Given mocked random, expects 10..13 throttled, 14+ unthrottled + assertEquals(t < START_TIME + 14, throttled); + } + } + } + + private static class MockRandom extends Random { + private int callCount = 0; + + @Override + public double nextDouble() { + // Return 0.0, 0.1, ..., 0.9, 0.0, 0.1 ... + double val = (callCount % 10) / 10.0; + callCount++; + return val; + } + } +} diff --git a/sdks/java/io/csv/src/test/java/org/apache/beam/sdk/io/csv/CsvIOParseTest.java b/sdks/java/io/csv/src/test/java/org/apache/beam/sdk/io/csv/CsvIOParseTest.java index a517cef3d51f..6d04ba4fafdf 100644 --- a/sdks/java/io/csv/src/test/java/org/apache/beam/sdk/io/csv/CsvIOParseTest.java +++ b/sdks/java/io/csv/src/test/java/org/apache/beam/sdk/io/csv/CsvIOParseTest.java @@ -238,6 +238,7 @@ public void givenMultipleCustomParsingLambdas_parsesPOJOs() { "instant", input -> DateTimeFormat.forPattern("yyyy-MM-dd@HH:mm:ss") + .withZoneUTC() .parseDateTime(input) .toInstant()) .withCustomRecordParsing("instantList", instantListParsingLambda()); @@ -308,7 +309,10 @@ private static SerializableFunction> instantListParsingLam List output = new ArrayList<>(); for (String cell : cells) { output.add( - DateTimeFormat.forPattern("HH-mm-ss-yyyy-MM-dd").parseDateTime(cell).toInstant()); + DateTimeFormat.forPattern("HH-mm-ss-yyyy-MM-dd") + .withZoneUTC() + .parseDateTime(cell) + .toInstant()); } return output; }; 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/debezium/build.gradle b/sdks/java/io/debezium/build.gradle index c488ac17d990..ad5410b37dee 100644 --- a/sdks/java/io/debezium/build.gradle +++ b/sdks/java/io/debezium/build.gradle @@ -46,10 +46,13 @@ dependencies { permitUnusedDeclared library.java.jackson_dataformat_csv // Kafka connect dependencies - implementation "org.apache.kafka:connect-api:3.9.0" + implementation "org.apache.kafka:connect-api:4.1.2" + implementation "org.apache.kafka:kafka-clients:4.1.2" // Debezium dependencies - implementation group: 'io.debezium', name: 'debezium-core', version: '3.1.3.Final' + implementation group: 'io.debezium', name: 'debezium-core', version: '3.5.2.Final' + implementation group: 'io.debezium', name: 'debezium-config', version: '3.5.2.Final' + implementation group: 'io.debezium', name: 'debezium-connector-common', version: '3.5.2.Final' // Test dependencies testImplementation project(path: ":sdks:java:core", configuration: "shadowTest") @@ -64,12 +67,12 @@ dependencies { testImplementation "org.testcontainers:kafka" testImplementation "org.testcontainers:mysql" testImplementation "org.testcontainers:postgresql" - testImplementation "io.debezium:debezium-testing-testcontainers:3.1.3.Final" + testImplementation "io.debezium:debezium-testing-testcontainers:3.5.2.Final" testImplementation 'com.zaxxer:HikariCP:5.1.0' // Debezium connector implementations for testing - testImplementation group: 'io.debezium', name: 'debezium-connector-mysql', version: '3.1.3.Final' - testImplementation group: 'io.debezium', name: 'debezium-connector-postgres', version: '3.1.3.Final' + testImplementation group: 'io.debezium', name: 'debezium-connector-mysql', version: '3.5.2.Final' + testImplementation group: 'io.debezium', name: 'debezium-connector-postgres', version: '3.5.2.Final' } // Pin the Antlr version to 4.10 @@ -83,7 +86,9 @@ configurations.all { 'com.fasterxml.jackson.core:jackson-core:2.17.1', 'com.fasterxml.jackson.core:jackson-annotations:2.17.1', 'com.fasterxml.jackson.core:jackson-databind:2.17.1', - 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1' + 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1', + 'org.apache.kafka:kafka-clients:4.1.2', + 'org.postgresql:postgresql:42.7.7' } } diff --git a/sdks/java/io/debezium/expansion-service/build.gradle b/sdks/java/io/debezium/expansion-service/build.gradle index 82a34b5c0665..fe820b62c138 100644 --- a/sdks/java/io/debezium/expansion-service/build.gradle +++ b/sdks/java/io/debezium/expansion-service/build.gradle @@ -39,7 +39,7 @@ dependencies { runtimeOnly library.java.slf4j_jdk14 // Debezium runtime dependencies - def debezium_version = '3.1.3.Final' + def debezium_version = '3.5.2.Final' runtimeOnly group: 'io.debezium', name: 'debezium-connector-mysql', version: debezium_version runtimeOnly group: 'io.debezium', name: 'debezium-connector-postgres', version: debezium_version runtimeOnly group: 'io.debezium', name: 'debezium-connector-sqlserver', version: debezium_version @@ -55,7 +55,9 @@ configurations.all { 'com.fasterxml.jackson.core:jackson-core:2.17.1', 'com.fasterxml.jackson.core:jackson-annotations:2.17.1', 'com.fasterxml.jackson.core:jackson-databind:2.17.1', - 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1' + 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.1', + 'org.apache.kafka:kafka-clients:4.1.2', + 'org.postgresql:postgresql:42.7.7' } } diff --git a/sdks/java/io/debezium/src/README.md b/sdks/java/io/debezium/src/README.md index 535213218856..677f42b59b02 100644 --- a/sdks/java/io/debezium/src/README.md +++ b/sdks/java/io/debezium/src/README.md @@ -25,7 +25,7 @@ DebeziumIO is an Apache Beam connector that lets users connect their Events-Driv ### Getting Started -DebeziumIO uses [Debezium Connectors v3.1](https://debezium.io/documentation/reference/3.1/connectors/) to connect to Apache Beam. All you need to do is choose the Debezium Connector that suits your Debezium setup and pick a [Serializable Function](https://beam.apache.org/releases/javadoc/2.65.0/org/apache/beam/sdk/transforms/SerializableFunction.html), then you will be able to connect to Apache Beam and start building your own Pipelines. +DebeziumIO uses [Debezium Connectors v3.5](https://debezium.io/documentation/reference/3.5/connectors/) to connect to Apache Beam. All you need to do is choose the Debezium Connector that suits your Debezium setup and pick a [Serializable Function](https://beam.apache.org/releases/javadoc/2.65.0/org/apache/beam/sdk/transforms/SerializableFunction.html), then you will be able to connect to Apache Beam and start building your own Pipelines. These connectors have been successfully tested and are known to work fine: * MySQL Connector @@ -65,7 +65,7 @@ You can also add more configuration, such as Connector-specific Properties with |Method|Params|Description| |-|-|-| |`.withConnectionProperty(propName, propValue)`|_String_, _String_|Adds a custom property to the connector.| -> **Note:** For more information on custom properties, see your [Debezium Connector](https://debezium.io/documentation/reference/3.1/connectors/) specific documentation. +> **Note:** For more information on custom properties, see your [Debezium Connector](https://debezium.io/documentation/reference/3.5/connectors/) specific documentation. Example of a MySQL Debezium Connector setup: ``` @@ -160,8 +160,8 @@ By default, DebeziumIO initializes it with the former, though user may choose th ### Requirements and Supported versions - JDK v17 -- Debezium Connectors v3.1 -- Apache Beam 2.66 +- Debezium Connectors v3.5 +- Apache Beam 2.76 ## Running Unit Tests diff --git a/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumIO.java b/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumIO.java index 6c31d5a02349..b89b3644f615 100644 --- a/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumIO.java +++ b/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumIO.java @@ -36,7 +36,6 @@ import org.apache.beam.sdk.values.PCollection; 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.collect.Lists; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; import org.apache.kafka.connect.source.SourceConnector; import org.apache.kafka.connect.source.SourceRecord; import org.checkerframework.checker.nullness.qual.Nullable; @@ -75,7 +74,7 @@ * .withConnectorClass(MySqlConnector.class) * .withConnectionProperty("database.server.id", "184054") * .withConnectionProperty("database.server.name", "serverid") - * .withConnectionProperty("database.history", DebeziumSDFDatabaseHistory.class.getName()) + * .withConnectionProperty("schema.history.internal", DebeziumSDFDatabaseHistory.class.getName()) * .withConnectionProperty("include.schema.changes", "false"); * * PipelineOptions options = PipelineOptionsFactory.create(); @@ -313,9 +312,9 @@ protected Schema getRecordSchema() { new KafkaSourceConsumerFn.OffsetTracker( new KafkaSourceConsumerFn.OffsetHolder(null, null, 0))); - Map connectorConfig = - Maps.newHashMap(getConnectorConfiguration().getConfigurationMap()); - connectorConfig.put("snapshot.mode", "schema_only"); + // Deliberately runs with the connector's configured snapshot mode: schema inference samples + // an actual data record, which a schema-only snapshot ("no_data", formerly "schema_only") + // would never emit. SourceRecord sampledRecord = fn.getOneRecord(getConnectorConfiguration().getConfigurationMap()); fn.reset(); @@ -641,10 +640,10 @@ public Map getConfigurationMap() { configuration.computeIfAbsent(entry.getKey(), k -> entry.getValue()); } - // Set default Database History impl. if not provided implementation and Kafka topic prefix, - // if not provided + // Set default schema history impl. if not provided implementation and Kafka topic prefix, + // if not provided. Before Debezium 2.0 this key was named "database.history". configuration.computeIfAbsent( - "database.history", + "schema.history.internal", k -> KafkaSourceConsumerFn.DebeziumSDFDatabaseHistory.class.getName()); configuration.computeIfAbsent("topic.prefix", k -> "beam-debezium-connector"); configuration.computeIfAbsent( diff --git a/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformProvider.java b/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformProvider.java index d1d5103cc496..923f845bfa4f 100644 --- a/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformProvider.java +++ b/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformProvider.java @@ -20,12 +20,13 @@ import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import org.apache.beam.sdk.coders.RowCoder; +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; @@ -73,6 +74,19 @@ protected DebeziumReadSchemaTransformProvider( this.testLimitMilliseconds = timeLimitMs; } + private static Connectors parseConnector(String value) { + try { + return Connectors.valueOf(value); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Unsupported connector '" + + value + + "'. Supported connectors are: " + + Arrays.toString(Connectors.values()), + e); + } + } + @Override protected @NonNull @Initialized Class configurationClass() { @@ -82,21 +96,15 @@ protected DebeziumReadSchemaTransformProvider( @Override protected @NonNull @Initialized SchemaTransform from( DebeziumReadSchemaTransformConfiguration configuration) { - // TODO(pabloem): Validate configuration parameters to ensure formatting is correct. + + configuration.validate(); + Connectors connector = parseConnector(configuration.getDatabase()); + return new SchemaTransform() { @Override public PCollectionRowTuple expand(PCollectionRowTuple input) { // TODO(pabloem): Test this behavior - Collection connectors = - Arrays.stream(Connectors.values()).map(Object::toString).collect(Collectors.toSet()); - if (!connectors.contains(configuration.getDatabase())) { - throw new IllegalArgumentException( - "Unsupported database " - + configuration.getDatabase() - + ". Unable to select a JDBC driver for it. Supported Databases are: " - + String.join(", ", connectors)); - } - Class connectorClass = Connectors.valueOf(configuration.getDatabase()).getConnector(); + Class connectorClass = connector.getConnector(); DebeziumIO.ConnectorConfiguration connectorConfiguration = DebeziumIO.ConnectorConfiguration.create() .withUsername(configuration.getUsername()) @@ -123,9 +131,9 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { configuration.getDebeziumConnectionProperties(); if (debeziumConnectionProperties != null) { for (String connectionProperty : debeziumConnectionProperties) { - String[] parts = connectionProperty.split("=", -1); - String key = parts[0]; - String value = parts[1]; + int separator = connectionProperty.indexOf('='); + String key = connectionProperty.substring(0, separator); + String value = connectionProperty.substring(separator + 1); connectorConfiguration = connectorConfiguration.withConnectionProperty(key, value); } } @@ -138,6 +146,15 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { readTransform .withMaxNumberOfRecords(testLimitRecords) .withMaxTimeToRun(testLimitMilliseconds); + } else { + Integer maxNumberOfRecords = configuration.getMaxNumberOfRecords(); + if (maxNumberOfRecords != null) { + readTransform = readTransform.withMaxNumberOfRecords(maxNumberOfRecords); + } + Long maxTimeToRun = configuration.getMaxTimeToRun(); + if (maxTimeToRun != null) { + readTransform = readTransform.withMaxTimeToRun(maxTimeToRun); + } } // TODO(pabloem): Database connection issues can be debugged here. @@ -172,22 +189,65 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { return Collections.singletonList("output"); } + @DefaultSchema(AutoValueSchema.class) @AutoValue public abstract static class DebeziumReadSchemaTransformConfiguration { + + @SchemaFieldDescription("Maximum number of records to read before stopping.") + public abstract @Nullable Integer getMaxNumberOfRecords(); + + @SchemaFieldDescription("Maximum time in milliseconds to run before stopping.") + public abstract @Nullable Long getMaxTimeToRun(); + + @SchemaFieldDescription("Username used to connect to the source database.") public abstract String getUsername(); + @SchemaFieldDescription("Password used to connect to the source database.") public abstract String getPassword(); + @SchemaFieldDescription("Hostname of the source database.") public abstract String getHost(); + @SchemaFieldDescription("Port of the source database.") public abstract int getPort(); + @SchemaFieldDescription("Fully qualified table name included in the Debezium change stream.") public abstract String getTable(); + @SchemaFieldDescription( + "Debezium connector type. Supported values: MYSQL, POSTGRES, SQLSERVER, ORACLE, and DB2.") public abstract @NonNull String getDatabase(); + @SchemaFieldDescription("Additional Debezium connection properties in key=value format.") public abstract @Nullable List getDebeziumConnectionProperties(); + public void validate() { + if (getHost().isEmpty()) { + throw new IllegalArgumentException("host must not be empty."); + } + + if (getPort() <= 0 || getPort() > 65535) { + throw new IllegalArgumentException( + "port must be between 1 and 65535, but was " + getPort() + "."); + } + + if (getTable().isEmpty()) { + throw new IllegalArgumentException("table must not be empty."); + } + + List connectionProperties = getDebeziumConnectionProperties(); + if (connectionProperties != null) { + for (String property : connectionProperties) { + if (property == null || property.indexOf('=') <= 0) { + throw new IllegalArgumentException( + "Invalid Debezium connection property '" + + property + + "'. Expected key=value format."); + } + } + } + } + public static Builder builder() { return new AutoValue_DebeziumReadSchemaTransformProvider_DebeziumReadSchemaTransformConfiguration .Builder(); @@ -195,6 +255,10 @@ public static Builder builder() { @AutoValue.Builder public abstract static class Builder { + public abstract Builder setMaxNumberOfRecords(@Nullable Integer maxNumberOfRecords); + + public abstract Builder setMaxTimeToRun(@Nullable Long maxTimeToRun); + public abstract Builder setUsername(String username); public abstract Builder setPassword(String password); diff --git a/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/KafkaSourceConsumerFn.java b/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/KafkaSourceConsumerFn.java index d298ddd9cafb..89fc2a5c085d 100644 --- a/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/KafkaSourceConsumerFn.java +++ b/sdks/java/io/debezium/src/main/java/org/apache/beam/io/debezium/KafkaSourceConsumerFn.java @@ -350,6 +350,11 @@ public OffsetStorageReader offsetStorageReader() { LOG.debug("------------- Creating an offset storage reader"); return new DebeziumSourceOffsetStorageReader(initialOffset); } + + @Override + public org.apache.kafka.common.metrics.PluginMetrics pluginMetrics() { + return null; + } } private static class DebeziumSourceOffsetStorageReader implements OffsetStorageReader { diff --git a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOMySqlConnectorIT.java b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOMySqlConnectorIT.java index 3fe86a29cce5..e0f811a8495d 100644 --- a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOMySqlConnectorIT.java +++ b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOMySqlConnectorIT.java @@ -74,7 +74,7 @@ public class DebeziumIOMySqlConnectorIT { @ClassRule public static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer<>( - DockerImageName.parse("quay.io/debezium/example-mysql:3.1.3.Final") + DockerImageName.parse("quay.io/debezium/example-mysql:3.5.2.Final") .asCompatibleSubstituteFor("mysql")) .withPassword("debezium") .withUsername("mysqluser") @@ -277,8 +277,8 @@ public void testDebeziumIOMySql() { .withMaxNumberOfRecords(30) .withCoder(StringUtf8Coder.of())); String expected = - "{\"metadata\":{\"connector\":\"mysql\",\"version\":\"3.1.3.Final\",\"name\":\"beam-debezium-connector\"," - + "\"database\":\"inventory\",\"schema\":\"binlog.000002\",\"table\":\"addresses\"},\"before\":null," + "{\"metadata\":{\"connector\":\"mysql\",\"version\":\"3.5.2.Final\",\"name\":\"beam-debezium-connector\"," + + "\"database\":\"inventory\",\"schema\":\"mysql-bin.000003\",\"table\":\"addresses\"},\"before\":null," + "\"after\":{\"fields\":{\"zip\":\"76036\",\"city\":\"Euless\"," + "\"street\":\"3183 Moore Avenue\",\"id\":10,\"state\":\"Texas\",\"customer_id\":1001," + "\"type\":\"SHIPPING\"}}}"; diff --git a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOPostgresSqlConnectorIT.java b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOPostgresSqlConnectorIT.java index 87b9bbb92e5e..0a76415d23ba 100644 --- a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOPostgresSqlConnectorIT.java +++ b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOPostgresSqlConnectorIT.java @@ -56,7 +56,7 @@ public class DebeziumIOPostgresSqlConnectorIT { @ClassRule public static final PostgreSQLContainer POSTGRES_SQL_CONTAINER = new PostgreSQLContainer<>( - DockerImageName.parse("quay.io/debezium/example-postgres:3.1.3.Final") + DockerImageName.parse("quay.io/debezium/example-postgres:3.5.2.Final") .asCompatibleSubstituteFor("postgres")) .withPassword("dbz") .withUsername("debezium") @@ -180,7 +180,7 @@ public void testDebeziumIOPostgresSql() { .withMaxNumberOfRecords(30) .withCoder(StringUtf8Coder.of())); String expected = - "{\"metadata\":{\"connector\":\"postgresql\",\"version\":\"3.1.3.Final\",\"name\":\"beam-debezium-connector\"," + "{\"metadata\":{\"connector\":\"postgresql\",\"version\":\"3.5.2.Final\",\"name\":\"beam-debezium-connector\"," + "\"database\":\"inventory\",\"schema\":\"inventory\",\"table\":\"customers\"},\"before\":null," + "\"after\":{\"fields\":{\"last_name\":\"Thomas\",\"id\":1001,\"first_name\":\"Sally\"," + "\"email\":\"sally.thomas@acme.com\"}}}"; diff --git a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOTest.java b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOTest.java index 80509f5bb911..074f0ba5b526 100644 --- a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOTest.java +++ b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumIOTest.java @@ -52,7 +52,8 @@ public class DebeziumIOTest implements Serializable { .withConnectionProperty("database.server.id", "184054") .withConnectionProperty("database.server.name", "dbserver1") .withConnectionProperty( - "database.history", KafkaSourceConsumerFn.DebeziumSDFDatabaseHistory.class.getName()) + "schema.history.internal", + KafkaSourceConsumerFn.DebeziumSDFDatabaseHistory.class.getName()) .withConnectionProperty("include.schema.changes", "false"); @Test diff --git a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformProviderTest.java b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformProviderTest.java new file mode 100644 index 000000000000..07bd91a5f522 --- /dev/null +++ b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformProviderTest.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.io.debezium; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; + +import java.util.Arrays; +import java.util.Collections; +import org.hamcrest.Matchers; +import org.junit.Test; + +public class DebeziumReadSchemaTransformProviderTest { + + private static DebeziumReadSchemaTransformProvider.DebeziumReadSchemaTransformConfiguration + .Builder + validConfiguration() { + return DebeziumReadSchemaTransformProvider.DebeziumReadSchemaTransformConfiguration.builder() + .setUsername("user") + .setPassword("password") + .setHost("localhost") + .setPort(5432) + .setDatabase("POSTGRES") + .setTable("inventory.customers") + .setDebeziumConnectionProperties(Collections.emptyList()); + } + + @Test + public void testValidConfiguration() { + validConfiguration().build().validate(); + } + + @Test + public void testEmptyHost() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> validConfiguration().setHost("").build().validate()); + + assertThat(exception.getMessage(), Matchers.containsString("host must not be empty")); + } + + @Test + public void testInvalidPort() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> validConfiguration().setPort(0).build().validate()); + + assertThat(exception.getMessage(), Matchers.containsString("port must be between")); + } + + @Test + public void testEmptyTable() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> validConfiguration().setTable("").build().validate()); + + assertThat(exception.getMessage(), Matchers.containsString("table must not be empty")); + } + + @Test + public void testInvalidConnectionProperty() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + validConfiguration() + .setDebeziumConnectionProperties(Collections.singletonList("invalid-property")) + .build() + .validate()); + + assertThat(exception.getMessage(), Matchers.containsString("Expected key=value format")); + } + + @Test + public void testNullConnectionProperty() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + validConfiguration() + .setDebeziumConnectionProperties(Arrays.asList("valid=value", null)) + .build() + .validate()); + + assertThat(exception.getMessage(), Matchers.containsString("Expected key=value format")); + } + + @Test + public void testConnectionPropertyValueContainingEquals() { + validConfiguration() + .setDebeziumConnectionProperties( + Collections.singletonList("some.property=value=containing=equals")) + .build() + .validate(); + } + + @Test + public void testUnsupportedConnector() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + new DebeziumReadSchemaTransformProvider() + .from(validConfiguration().setDatabase("UNKNOWN").build())); + + assertThat(exception.getMessage(), Matchers.containsString("Unsupported connector 'UNKNOWN'")); + assertThat(exception.getMessage(), Matchers.containsString("MYSQL")); + assertThat(exception.getMessage(), Matchers.containsString("POSTGRES")); + } + + @Test + public void testProviderContract() { + DebeziumReadSchemaTransformProvider provider = new DebeziumReadSchemaTransformProvider(); + + assertThat(provider.inputCollectionNames(), Matchers.empty()); + assertThat(provider.outputCollectionNames(), Matchers.contains("output")); + assertThat( + provider.identifier(), + Matchers.equalTo("beam:schematransform:org.apache.beam:debezium_read:v1")); + } +} diff --git a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformTest.java b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformTest.java index 2fc8996ba55e..b961ad84a7b8 100644 --- a/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformTest.java +++ b/sdks/java/io/debezium/src/test/java/org/apache/beam/io/debezium/DebeziumReadSchemaTransformTest.java @@ -18,6 +18,7 @@ package org.apache.beam.io.debezium; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import io.debezium.DebeziumException; @@ -60,7 +61,7 @@ public class DebeziumReadSchemaTransformTest { @ClassRule public static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer<>( - DockerImageName.parse("debezium/example-mysql:1.4") + DockerImageName.parse("quay.io/debezium/example-mysql:3.5.2.Final") .asCompatibleSubstituteFor("mysql")) .withPassword("debezium") .withUsername("mysqluser") @@ -118,6 +119,17 @@ private PTransform makePtransform( .build()); } + // Since Debezium 3.5 connection failures surface as a RetriableException wrapping the + // DebeziumException instead of a top-level DebeziumException. + private static DebeziumException findDebeziumException(Throwable thrown) { + Throwable cause = thrown; + while (cause != null && !(cause instanceof DebeziumException)) { + cause = cause.getCause(); + } + assertNotNull("Expected DebeziumException in cause chain", cause); + return (DebeziumException) cause; + } + @Test public void testNoProblem() { Pipeline readPipeline = Pipeline.create(); @@ -142,9 +154,9 @@ public void testNoProblem() { @Test public void testWrongUser() { Pipeline readPipeline = Pipeline.create(); - DebeziumException ex = + Exception thrown = assertThrows( - DebeziumException.class, + Exception.class, () -> { PCollectionRowTuple.empty(readPipeline) .apply( @@ -156,6 +168,7 @@ public void testWrongUser() { "localhost")) .get("output"); }); + DebeziumException ex = findDebeziumException(thrown); assertThat(ex.getCause().getMessage(), Matchers.containsString("password")); assertThat(ex.getCause().getMessage(), Matchers.containsString("wrongUser")); } @@ -163,9 +176,9 @@ public void testWrongUser() { @Test public void testWrongPassword() { Pipeline readPipeline = Pipeline.create(); - DebeziumException ex = + Exception thrown = assertThrows( - DebeziumException.class, + Exception.class, () -> { PCollectionRowTuple.empty(readPipeline) .apply( @@ -177,6 +190,7 @@ public void testWrongPassword() { "localhost")) .get("output"); }); + DebeziumException ex = findDebeziumException(thrown); assertThat(ex.getCause().getMessage(), Matchers.containsString("password")); assertThat(ex.getCause().getMessage(), Matchers.containsString(userName)); } @@ -184,14 +198,15 @@ public void testWrongPassword() { @Test public void testWrongPort() { Pipeline readPipeline = Pipeline.create(); - DebeziumException ex = + Exception thrown = assertThrows( - DebeziumException.class, + Exception.class, () -> { PCollectionRowTuple.empty(readPipeline) .apply(makePtransform(userName, password, database, 12345, "localhost")) .get("output"); }); + DebeziumException ex = findDebeziumException(thrown); Throwable lowestCause = ex.getCause(); while (lowestCause.getCause() != null) { lowestCause = lowestCause.getCause(); diff --git a/sdks/java/io/delta/build.gradle b/sdks/java/io/delta/build.gradle new file mode 100644 index 000000000000..a0de04ed8544 --- /dev/null +++ b/sdks/java/io/delta/build.gradle @@ -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. + */ + +import groovy.json.JsonOutput + +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." + +// 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") + implementation project(path: ":model:pipeline", 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 + + 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" + + runtimeOnly library.java.hadoop_aws + + // 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") + testImplementation project(":sdks:java:io:google-cloud-platform") + testImplementation project(":sdks:java:extensions:google-cloud-platform-core") + testImplementation library.java.google_cloud_storage + testImplementation library.java.testcontainers_base + testImplementation library.java.testcontainers_localstack + testImplementation library.java.hadoop_aws + testRuntimeOnly project(path: ":runners:google-cloud-dataflow-java") + testRuntimeOnly library.java.slf4j_simple +} + +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" +} + +def gcpProject = project.findProperty('gcpProject') ?: 'apache-beam-testing' +def gcpTempLocation = project.findProperty('gcpTempLocation') ?: 'gs://apache-beam-testing-delta-lake/temp' + +// Used by Dataflow integration tests. +task dataflowIntegrationTest(type: Test) { + group = "Verification" + evaluationDependsOn(":runners:google-cloud-dataflow-java") + dependsOn ":runners:google-cloud-dataflow-java:buildAndPushDockerJavaContainer" + finalizedBy ":runners:google-cloud-dataflow-java:cleanUpDockerJavaImages" + def dockerJavaImageName = project.project(':runners:google-cloud-dataflow-java').ext.dockerJavaImageName + + def args = [ + "--runner=TestDataflowRunner", + "--region=us-central1", + "--project=${gcpProject}", + "--tempLocation=${gcpTempLocation}", + "--tempRoot=${gcpTempLocation}", + "--sdkContainerImage=${dockerJavaImageName}", + "--experiments=use_runner_v2,use_staged_dataflow_worker_jar" + ] + systemProperty "beamTestPipelineOptions", JsonOutput.toJson(args) + + // Disable Gradle cache: these ITs interact with live service that should always be considered "out of date" + outputs.upToDateWhen { false } + + include '**/*IT.class' + exclude '**/DeltaIOS3IT.class' + + maxParallelForks 4 + classpath = sourceSets.test.runtimeClasspath + testClassesDirs = sourceSets.test.output.classesDirs +} + +task integrationTest(type: Test) { + group = "Verification" + description = "Runs integration tests locally." + outputs.upToDateWhen { false } + include '**/*IT.class' + classpath = sourceSets.test.runtimeClasspath + testClassesDirs = sourceSets.test.output.classesDirs + + systemProperty "org.slf4j.simpleLogger.log.org.apache.hadoop.fs.s3a", "info" + systemProperty "org.slf4j.simpleLogger.log.software.amazon.awssdk", "info" +} 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/CreateCDCReadTasksDoFn.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateCDCReadTasksDoFn.java new file mode 100644 index 000000000000..9a0049a8651e --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateCDCReadTasksDoFn.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.CommitRange; +import io.delta.kernel.CommitRangeBuilder; +import io.delta.kernel.Scan; +import io.delta.kernel.Snapshot; +import io.delta.kernel.Table; +import io.delta.kernel.TableManager; +import io.delta.kernel.data.ColumnarBatch; +import io.delta.kernel.data.Row; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.internal.DeltaLogActionUtils.DeltaAction; +import io.delta.kernel.internal.TableImpl; +import io.delta.kernel.internal.actions.AddCDCFile; +import io.delta.kernel.internal.actions.AddFile; +import io.delta.kernel.internal.util.VectorUtils; +import io.delta.kernel.utils.CloseableIterator; +import java.time.Instant; +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 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 plans read tasks for Change Data Feed. */ +class CreateCDCReadTasksDoFn extends DoFn { + private static final long MAX_TASK_SIZE_BYTES = 1024L * 1024L * 1024L; // 1 GB + private final @Nullable Map hadoopConfig; + private final @Nullable Long startVersion; + private final @Nullable String startTimestamp; + private final @Nullable Long endVersion; + private final @Nullable String endTimestamp; + + public CreateCDCReadTasksDoFn( + @Nullable Map hadoopConfig, + @Nullable Long startVersion, + @Nullable String startTimestamp, + @Nullable Long endVersion, + @Nullable String endTimestamp) { + this.hadoopConfig = hadoopConfig; + this.startVersion = startVersion; + this.startTimestamp = startTimestamp; + this.endVersion = endVersion; + this.endTimestamp = endTimestamp; + } + + @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); + TableImpl tableImpl = (TableImpl) table; + + // 1. Resolve starting and ending versions + long resolvedStartVersion; + if (startVersion != null) { + resolvedStartVersion = startVersion; + } else if (startTimestamp != null) { + long startMillis = Instant.parse(startTimestamp).toEpochMilli(); + resolvedStartVersion = tableImpl.getVersionAtOrAfterTimestamp(engine, startMillis); + } else { + throw new IllegalArgumentException("Starting version or timestamp must be specified."); + } + + long resolvedEndVersion; + if (endVersion != null) { + resolvedEndVersion = endVersion; + } else if (endTimestamp != null) { + long endMillis = Instant.parse(endTimestamp).toEpochMilli(); + resolvedEndVersion = tableImpl.getVersionBeforeOrAtTimestamp(engine, endMillis); + } else { + resolvedEndVersion = table.getLatestSnapshot(engine).getVersion(); + } + + if (resolvedStartVersion > resolvedEndVersion) { + throw new IllegalArgumentException( + String.format( + "Resolved start version %d is greater than resolved end version %d", + resolvedStartVersion, resolvedEndVersion)); + } + + // 2. Load snapshot at resolvedEndVersion to get the scanStateRow + // We use endVersion's schema because it represents the latest schema in the + // read range + // which handles schema evolution (older files will just lack new columns). + Snapshot endSnapshot = table.getSnapshotAsOfVersion(engine, resolvedEndVersion); + Scan scan = endSnapshot.getScanBuilder().build(); + Row scanState = scan.getScanState(engine); + SerializableRow serializableScanState = new SerializableRow(scanState); + + // 3. Load snapshot at resolvedStartVersion to initialize the CommitRange + Snapshot startSnapshot = table.getSnapshotAsOfVersion(engine, resolvedStartVersion); + + CommitRangeBuilder rangeBuilder = + TableManager.loadCommitRange( + tablePath, CommitRangeBuilder.CommitBoundary.atVersion(resolvedStartVersion)); + rangeBuilder.withEndBoundary(CommitRangeBuilder.CommitBoundary.atVersion(resolvedEndVersion)); + CommitRange range = rangeBuilder.build(engine); + + // We need both CDC and ADD actions. + // If a commit version has CDC files, we only read CDC files. + // If a commit version has no CDC files, we read ADD files (inserts). + Set actionSet = new HashSet<>(); + actionSet.add(DeltaAction.CDC); + actionSet.add(DeltaAction.ADD); + + // 4. Iterate over commits in the range and group actions by version + try (CloseableIterator batchIter = + range.getActions(engine, startSnapshot, actionSet)) { + Map commitActionsMap = new HashMap<>(); + + while (batchIter.hasNext()) { + ColumnarBatch batch = batchIter.next(); + int versionIdx = batch.getSchema().indexOf("version"); + int timestampIdx = batch.getSchema().indexOf("timestamp"); + int cdcIdx = batch.getSchema().indexOf("cdc"); + int addIdx = batch.getSchema().indexOf("add"); + + for (int i = 0; i < batch.getSize(); i++) { + long version = batch.getColumnVector(versionIdx).getLong(i); + long timestamp = batch.getColumnVector(timestampIdx).getLong(i); + + CommitActionsInfo info = + commitActionsMap.computeIfAbsent( + version, k -> new CommitActionsInfo(version, timestamp)); + + if (cdcIdx >= 0 && !batch.getColumnVector(cdcIdx).isNullAt(i)) { + Row cdcRow = + (Row) + VectorUtils.getValueAsObject( + batch.getColumnVector(cdcIdx), + batch.getSchema().at(cdcIdx).getDataType(), + i); + info.cdcInfo.add(cdcRow); + } + if (addIdx >= 0 && !batch.getColumnVector(addIdx).isNullAt(i)) { + Row addRow = + (Row) + VectorUtils.getValueAsObject( + batch.getColumnVector(addIdx), + batch.getSchema().at(addIdx).getDataType(), + i); + AddFile addFile = new AddFile(addRow); + // Only consider add files that change data (ignore OPTIMIZE etc.) + if (addFile.getDataChange()) { + info.insertInfo.add(addRow); + } + } + } + } + + // 5. Emit tasks for each version + List currentGroup = new ArrayList<>(); + long currentGroupSize = 0L; + + // TODO: to prevent OOMs in true streaming executions, update DeltaReadTask to include a group + // of files. + + // Sort versions to process them in order + List versions = new ArrayList<>(commitActionsMap.keySet()); + Collections.sort(versions); + + for (long version : versions) { + CommitActionsInfo info = commitActionsMap.get(version); + if (info == null) { + throw new IllegalStateException("CommitActionsInfo was not found for version " + version); + } + boolean hasCDC = !info.cdcInfo.isEmpty(); + + List rowsToProcess = hasCDC ? info.cdcInfo : info.insertInfo; + + for (Row fileRow : rowsToProcess) { + String relPath; + long size; + Map partitionValues; + + if (hasCDC) { + relPath = fileRow.getString(AddCDCFile.FULL_SCHEMA.indexOf("path")); + size = fileRow.getLong(AddCDCFile.FULL_SCHEMA.indexOf("size")); + partitionValues = + VectorUtils.toJavaMap( + fileRow.getMap(AddCDCFile.FULL_SCHEMA.indexOf("partitionValues"))); + } else { + AddFile addFile = new AddFile(fileRow); + relPath = addFile.getPath(); + size = addFile.getSize(); + partitionValues = VectorUtils.toJavaMap(addFile.getPartitionValues()); + } + + String fullPath = new org.apache.hadoop.fs.Path(tablePath, relPath).toString(); + List rowGroupSizes = getRowGroupSizes(fullPath, conf); + + DeltaCDCReadTask task = + new DeltaCDCReadTask( + fullPath, + size, + partitionValues, + info.version, + info.timestamp, + hasCDC, + rowGroupSizes, + serializableScanState); + + if (size >= MAX_TASK_SIZE_BYTES) { + if (!currentGroup.isEmpty()) { + emitGroup(currentGroup, out); + currentGroup = new ArrayList<>(); + currentGroupSize = 0L; + } + out.output(task); + } else { + if (currentGroupSize + size > MAX_TASK_SIZE_BYTES) { + emitGroup(currentGroup, out); + currentGroup = new ArrayList<>(); + currentGroup.add(task); + currentGroupSize = size; + } else { + currentGroup.add(task); + currentGroupSize += size; + } + } + } + } + + if (!currentGroup.isEmpty()) { + emitGroup(currentGroup, out); + } + } + } + + private void emitGroup(List group, OutputReceiver out) { + for (DeltaCDCReadTask task : group) { + out.output(task); + } + } + + private List getRowGroupSizes(String pathStr, Configuration conf) { + List sizes = new ArrayList<>(); + 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; + } + + private static class CommitActionsInfo { + final long version; + + final long timestamp; + final List cdcInfo = new ArrayList<>(); + final List insertInfo = new ArrayList<>(); + + CommitActionsInfo(long version, long timestamp) { + this.version = version; + this.timestamp = timestamp; + } + } +} 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/DeltaCDCReadTask.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCReadTask.java new file mode 100644 index 000000000000..24c594343def --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCReadTask.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.Map; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A serializable task containing the necessary metadata to read a CDF (Change Data Feed) file. This + * can be either a CDC parquet file or a regular data file (representing inserts) from a commit. + */ +public class DeltaCDCReadTask implements Serializable { + private static final long serialVersionUID = 1L; + + private final String path; + private final long size; + private final Map partitionValues; + private final long version; + private final long timestamp; + private final boolean isCDC; + private final List rowGroupSizes; + private final SerializableRow scanStateRow; + + public DeltaCDCReadTask( + String path, + long size, + Map partitionValues, + long version, + long timestamp, + boolean isCDC, + List rowGroupSizes, + SerializableRow scanStateRow) { + this.path = path; + this.size = size; + this.partitionValues = partitionValues; + this.version = version; + this.timestamp = timestamp; + this.isCDC = isCDC; + this.rowGroupSizes = rowGroupSizes; + this.scanStateRow = scanStateRow; + } + + public String getPath() { + return path; + } + + public long getSize() { + return size; + } + + public Map getPartitionValues() { + return partitionValues; + } + + public long getVersion() { + return version; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isCDC() { + return isCDC; + } + + public List getRowGroupSizes() { + return rowGroupSizes; + } + + public SerializableRow getScanStateRow() { + return scanStateRow; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DeltaCDCReadTask)) { + return false; + } + DeltaCDCReadTask that = (DeltaCDCReadTask) o; + return size == that.size + && version == that.version + && timestamp == that.timestamp + && isCDC == that.isCDC + && Objects.equals(path, that.path) + && Objects.equals(partitionValues, that.partitionValues) + && Objects.equals(rowGroupSizes, that.rowGroupSizes) + && Objects.equals(scanStateRow, that.scanStateRow); + } + + @Override + public int hashCode() { + return Objects.hash( + path, size, partitionValues, version, timestamp, isCDC, rowGroupSizes, scanStateRow); + } + + @Override + public String toString() { + return String.format( + "DeltaCDCReadTask{path='%s', size=%d, partitionValues=%s, version=%d, timestamp=%d, isCDC=%b, " + + "rowGroupSizes=%s, scanStateRow=%s}", + path, size, partitionValues, version, timestamp, isCDC, rowGroupSizes, scanStateRow); + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCSourceDoFn.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCSourceDoFn.java new file mode 100644 index 000000000000..97d9c914a086 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCDCSourceDoFn.java @@ -0,0 +1,367 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 io.delta.kernel.internal.DeltaErrors.wrapEngineException; + +import io.delta.kernel.Scan; +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.expressions.ExpressionEvaluator; +import io.delta.kernel.expressions.Literal; +import io.delta.kernel.internal.InternalScanFileUtils; +import io.delta.kernel.internal.data.GenericRow; +import io.delta.kernel.internal.data.ScanStateRow; +import io.delta.kernel.internal.util.Utils; +import io.delta.kernel.internal.util.VectorUtils; +import io.delta.kernel.types.LongType; +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.util.HashMap; +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.beam.sdk.values.ValueKind; +import org.apache.hadoop.conf.Configuration; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A Splittable DoFn that processes {@link DeltaCDCReadTask} elements and reads Change Data Feed + * files, converting rows to Beam Rows. + */ +@DoFn.BoundedPerElement +class DeltaCDCSourceDoFn extends DoFn { + @Nullable Map hadoopConfig; + private final @Nullable List metadataColumns; + private transient @Nullable Engine engine; + private transient @Nullable Configuration conf; + + public DeltaCDCSourceDoFn( + @Nullable Map hadoopConfig, @Nullable List metadataColumns) { + this.hadoopConfig = hadoopConfig; + this.metadataColumns = metadataColumns; + } + + 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; + } + + private List getRowGroupSizes(DeltaCDCReadTask task) { + return task.getRowGroupSizes(); + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction(@Element DeltaCDCReadTask task) { + List rowGroupSizes = getRowGroupSizes(task); + return new OffsetRange(0L, rowGroupSizes.size()); + } + + @NewTracker + public DeltaReadTaskTracker newTracker( + @Restriction OffsetRange restriction, @Element DeltaCDCReadTask task) { + return new DeltaReadTaskTracker(restriction, getRowGroupSizes(task)); + } + + @Setup + public void setUp() { + engine = DefaultEngine.create(getConfiguration()); + } + + @ProcessElement + public void processElement( + @Element DeltaCDCReadTask task, + RestrictionTracker tracker, + OutputReceiver out) + throws Exception { + + Engine currentEngine = engine; + if (currentEngine == null) { + throw new IllegalArgumentException("Expected the engine to not be null"); + } + + SerializableRow originalScanStateRow = task.getScanStateRow(); + StructType logicalTableSchema = ScanStateRow.getLogicalSchema(originalScanStateRow); + Schema baseSchema = DeltaIO.ReadRows.convertToBeamSchema(logicalTableSchema); + Schema publicBeamSchema = DeltaIO.buildPublicBeamSchema(baseSchema, metadataColumns); + StructType physicalTableSchema = ScanStateRow.getPhysicalDataReadSchema(originalScanStateRow); + + StructType scanStateSchema = originalScanStateRow.getSchema(); + + // 1. Build modified scanState and scanFile rows depending on whether we read a CDC file or ADD + // file. + io.delta.kernel.data.Row scanStateRow; + StructType readPhysicalSchema; + StructType readLogicalSchema; + Schema beamSchema; + + if (task.isCDC()) { + readLogicalSchema = appendCDFColumns(logicalTableSchema); + readPhysicalSchema = appendCDFColumns(physicalTableSchema); + beamSchema = DeltaIO.ReadRows.convertToBeamSchema(readLogicalSchema); + + HashMap valueMap = new HashMap<>(); + + // Tracking row level lineage is not needed. + Map config = + new HashMap<>(ScanStateRow.getConfiguration(originalScanStateRow)); + config.put("delta.enableRowTracking", "false"); + + valueMap.put( + scanStateSchema.indexOf("configuration"), VectorUtils.stringStringMapValue(config)); + valueMap.put(scanStateSchema.indexOf("logicalSchemaJson"), readLogicalSchema.toJson()); + valueMap.put(scanStateSchema.indexOf("physicalSchemaJson"), readPhysicalSchema.toJson()); + valueMap.put( + scanStateSchema.indexOf("partitionColumns"), + originalScanStateRow.getArray(scanStateSchema.indexOf("partitionColumns"))); + valueMap.put( + scanStateSchema.indexOf("minReaderVersion"), + originalScanStateRow.getInt(scanStateSchema.indexOf("minReaderVersion"))); + valueMap.put( + scanStateSchema.indexOf("minWriterVersion"), + originalScanStateRow.getInt(scanStateSchema.indexOf("minWriterVersion"))); + valueMap.put( + scanStateSchema.indexOf("tablePath"), + originalScanStateRow.getString(scanStateSchema.indexOf("tablePath"))); + + scanStateRow = new ScanStateRow(valueMap); + } else { + // For ADD files, we read the table schema and append the CDF columns manually afterwards. + // readLogicalSchema = logicalTableSchema; + readPhysicalSchema = physicalTableSchema; + beamSchema = DeltaIO.ReadRows.convertToBeamSchema(appendCDFColumns(logicalTableSchema)); + scanStateRow = originalScanStateRow; + } + + io.delta.kernel.data.Row scanFileRow = + generateScanFileRow(task.getPath(), task.getPartitionValues()); + FileStatus fileStatus = FileStatus.of(task.getPath(), task.getSize(), task.getTimestamp()); + + BeamParquetHandler parquetHandler = + new BeamParquetHandler(getConfiguration(), currentEngine.getParquetHandler(), tracker); + BeamEngine beamEngine = new BeamEngine(currentEngine, parquetHandler); + + long currentStartRgIndex = 0L; + + try (CloseableIterator fileReadResults = + parquetHandler.readParquetFiles( + Utils.singletonCloseableIterator(fileStatus), + readPhysicalSchema, + Optional.empty(), + currentStartRgIndex)) { + + 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(); + } + }; + + try (CloseableIterator logicalBatches = + Scan.transformPhysicalData(beamEngine, scanStateRow, scanFileRow, physicalData)) { + + while (logicalBatches.hasNext()) { + FilteredColumnarBatch batch = logicalBatches.next(); + + if (!task.isCDC()) { + // For ADD files, we need to append the constant CDF columns: + // _change_type = "insert", _commit_version = task.version, _commit_timestamp = + // task.timestamp + ColumnarBatch logicalBatch = + appendConstantCDFColumns( + currentEngine, batch.getData(), task.getVersion(), task.getTimestamp()); + // Make sure we use selection vector to considered filtered out or deleted rows. + batch = new FilteredColumnarBatch(logicalBatch, batch.getSelectionVector()); + } + + try (CloseableIterator logicalRows = batch.getRows()) { + while (logicalRows.hasNext()) { + io.delta.kernel.data.Row deltaRow = logicalRows.next(); + Row beamRow = DeltaSourceDoFn.toBeamRow(deltaRow, beamSchema); + String changeType = beamRow.getString(DeltaIO.CHANGE_TYPE_COLUMN); + if (changeType == null) { + throw new IllegalStateException( + "Field " + DeltaIO.CHANGE_TYPE_COLUMN + " must not be null."); + } + ValueKind kind = getValueKind(changeType); + Row publicRow = projectRow(beamRow, publicBeamSchema, task); + out.builder(publicRow).setValueKind(kind).output(); + } + } + } + } + } + } + + private static Row projectRow(Row row, Schema targetSchema, DeltaCDCReadTask task) { + Row.Builder builder = Row.withSchema(targetSchema); + for (Schema.Field field : targetSchema.getFields()) { + Object value = row.getValue(field.getName()); + if (value == null) { + if (field.getName().equals(DeltaIO.COMMIT_VERSION_COLUMN)) { + value = task.getVersion(); + } else if (field.getName().equals(DeltaIO.COMMIT_TIMESTAMP_COLUMN)) { + value = new org.joda.time.Instant(task.getTimestamp()); + } + } + builder.addValue(value); + } + return builder.build(); + } + + private static ValueKind getValueKind(String changeType) { + // Maps Delta CDC change types to Beam's ValueKind enum. + // https://docs.delta.io/delta-change-data-feed/#what-is-the-schema-for-the-change-data-feed + switch (changeType) { + case "insert": + return ValueKind.INSERT; + case "delete": + return ValueKind.DELETE; + case "update_preimage": + return ValueKind.UPDATE_BEFORE; + case "update_postimage": + return ValueKind.UPDATE_AFTER; + default: + throw new IllegalArgumentException("Unsupported change type: " + changeType); + } + } + + private static StructType appendCDFColumns(StructType schema) { + return schema + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING, true) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG, true) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP, true); + } + + private ColumnarBatch appendConstantCDFColumns( + Engine engine, ColumnarBatch batch, long version, long timestamp) { + StructType schemaForEval = batch.getSchema(); + + ExpressionEvaluator changeTypeGenerator = + wrapEngineException( + () -> + engine + .getExpressionHandler() + .getEvaluator(schemaForEval, Literal.ofString("insert"), StringType.STRING), + "Get the expression evaluator for change type"); + + ExpressionEvaluator commitVersionGenerator = + wrapEngineException( + () -> + engine + .getExpressionHandler() + .getEvaluator(schemaForEval, Literal.ofLong(version), LongType.LONG), + "Get the expression evaluator for commit version"); + + ExpressionEvaluator commitTimestampGenerator = + wrapEngineException( + () -> + engine + .getExpressionHandler() + // Microseconds since epoch is expected for TimestampType + .getEvaluator( + schemaForEval, + Literal.ofTimestamp(timestamp * 1000L), + TimestampType.TIMESTAMP), + "Get the expression evaluator for commit timestamp"); + + ColumnVector changeTypeVector = + wrapEngineException( + () -> changeTypeGenerator.eval(batch), "Evaluating change type expression"); + + ColumnVector commitVersionVector = + wrapEngineException( + () -> commitVersionGenerator.eval(batch), "Evaluating commit version expression"); + + ColumnVector commitTimestampVector = + wrapEngineException( + () -> commitTimestampGenerator.eval(batch), "Evaluating commit timestamp expression"); + + int numCols = batch.getSchema().length(); + return batch + .withNewColumn( + numCols, + new StructField(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING, false), + changeTypeVector) + .withNewColumn( + numCols + 1, + new StructField(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG, false), + commitVersionVector) + .withNewColumn( + numCols + 2, + new StructField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP, false), + commitTimestampVector); + } + + @SuppressWarnings("nullness") + private static io.delta.kernel.data.Row generateScanFileRow( + String path, Map partitionValues) { + StructType addFileSchema = + (StructType) InternalScanFileUtils.SCAN_FILE_SCHEMA.get("add").getDataType(); + MapValue partMapValue = VectorUtils.stringStringMapValue(partitionValues); + + Map addFileMap = new HashMap<>(); + addFileMap.put(addFileSchema.indexOf("path"), path); + addFileMap.put(addFileSchema.indexOf("partitionValues"), partMapValue); + addFileMap.put(addFileSchema.indexOf("size"), 0L); + addFileMap.put(addFileSchema.indexOf("modificationTime"), 0L); + addFileMap.put(addFileSchema.indexOf("dataChange"), true); + addFileMap.put(addFileSchema.indexOf("deletionVector"), null); + + io.delta.kernel.data.Row addFile = new GenericRow(addFileSchema, addFileMap); + + StructType scanFileSchema = InternalScanFileUtils.SCAN_FILE_SCHEMA; + Map scanFileMap = new HashMap<>(); + scanFileMap.put(scanFileSchema.indexOf("add"), addFile); + scanFileMap.put(scanFileSchema.indexOf("tableRoot"), "/"); + + return new GenericRow(scanFileSchema, scanFileMap); + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCdcReadSchemaTransformProvider.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCdcReadSchemaTransformProvider.java new file mode 100644 index 000000000000..f35a7a52b053 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaCdcReadSchemaTransformProvider.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.delta; + +import static org.apache.beam.sdk.io.delta.DeltaCdcReadSchemaTransformProvider.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.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#readChanges}. Reads change 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 DeltaCdcReadSchemaTransformProvider + extends TypedSchemaTransformProvider { + static final String OUTPUT_TAG = "output"; + + @Override + protected SchemaTransform from(Configuration configuration) { + return new DeltaCdcReadSchemaTransform(configuration); + } + + @Override + public List outputCollectionNames() { + return Collections.singletonList(OUTPUT_TAG); + } + + @Override + public String identifier() { + return getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_CDC_READ); + } + + static class DeltaCdcReadSchemaTransform extends SchemaTransform { + private final Configuration configuration; + + DeltaCdcReadSchemaTransform(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.ReadChanges read = DeltaIO.readChanges().from(configuration.getTable()); + Long startVersion = configuration.getStartVersion(); + if (startVersion != null) { + read = read.withStartVersion(startVersion); + } + String startTimestamp = configuration.getStartTimestamp(); + if (startTimestamp != null) { + read = read.withStartTimestamp(startTimestamp); + } + Long endVersion = configuration.getEndVersion(); + if (endVersion != null) { + read = read.withEndVersion(endVersion); + } + String endTimestamp = configuration.getEndTimestamp(); + if (endTimestamp != null) { + read = read.withEndTimestamp(endTimestamp); + } + Map hadoopConfig = configuration.getHadoopConfig(); + if (hadoopConfig != null) { + read = read.withConfig(hadoopConfig); + } + List includeMetadataColumns = configuration.getIncludeMetadataColumns(); + if (includeMetadataColumns != null && !includeMetadataColumns.isEmpty()) { + read = read.withMetadataColumns(includeMetadataColumns.toArray(new String[0])); + } + + 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_DeltaCdcReadSchemaTransformProvider_Configuration.Builder(); + } + + @SchemaFieldDescription("Identifier of the Delta Lake table.") + abstract String getTable(); + + @SchemaFieldDescription( + "Start version of the Delta Lake table to read changes from. Either this or the start timestamp has to be provided.") + @Nullable + abstract Long getStartVersion(); + + @SchemaFieldDescription( + "Start timestamp of the Delta Lake table to read changes from. Should be specified in the ISO 8601 standard. Either this or the start version has to be provided.") + @Nullable + abstract String getStartTimestamp(); + + @SchemaFieldDescription("End version of the Delta Lake table to read changes up to.") + @Nullable + abstract Long getEndVersion(); + + @SchemaFieldDescription( + "End timestamp of the Delta Lake table to read changes up to. Should be specified in the ISO 8601 standard.") + @Nullable + abstract String getEndTimestamp(); + + @SchemaFieldDescription("Properties passed to the Hadoop Configuration.") + @Nullable + abstract Map getHadoopConfig(); + + @SchemaFieldDescription( + "Metadata columns to include in the output rows. Supported columns are: _change_type, _commit_version, and _commit_timestamp.") + @Nullable + abstract List getIncludeMetadataColumns(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setTable(String table); + + abstract Builder setStartVersion(Long startVersion); + + abstract Builder setStartTimestamp(String startTimestamp); + + abstract Builder setEndVersion(Long endVersion); + + abstract Builder setEndTimestamp(String endTimestamp); + + abstract Builder setHadoopConfig(Map hadoopConfig); + + abstract Builder setIncludeMetadataColumns(List includeMetadataColumns); + + abstract Configuration build(); + } + } +} 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..3a53b5c76202 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 io.delta.kernel.Snapshot; +import io.delta.kernel.Table; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.internal.TableImpl; +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.Arrays; +import java.util.List; +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; + +/** + * A connector that reads from Delta Lake tables. + * + *

Configuration Options

+ * + * Please check the Managed IO + * configuration page for more details. + * + *

This is work in progress and is subject to change. For more details and to track progress, see + * Issue 21100. + */ +@Internal +public class DeltaIO { + + public static final String CHANGE_TYPE_COLUMN = "_change_type"; + public static final String COMMIT_VERSION_COLUMN = "_commit_version"; + public static final String COMMIT_TIMESTAMP_COLUMN = "_commit_timestamp"; + + /** + * Reads rows from a Delta Lake table. + * + *

Normally, it is recommended to use {@link org.apache.beam.sdk.managed.Managed#read(String)} + * with {@code Managed.DELTA_LAKE} instead of directly using this transform. + */ + public static ReadRows readRows() { + return new AutoValue_DeltaIO_ReadRows.Builder().build(); + } + + /** Reads change data feed (CDC) from a Delta Lake table. */ + public static ReadChanges readChanges() { + return new AutoValue_DeltaIO_ReadChanges.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) { + String path = getTablePath(); + if (path == null) { + throw new IllegalArgumentException("Table path must be set."); + } + if (getTimestamp() != null) { + throw new UnsupportedOperationException( + "Reading from a specific timestamp is not supported yet"); + } + + if (getVersion() != null) { + throw new UnsupportedOperationException( + "Reading from a specific version is not supported yet"); + } + + 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( + Schema.Field.of(field.getName(), convertToBeamFieldType(field.getDataType())) + .withNullable(field.isNullable())); + } + 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()); + } + } + } + + static Schema buildPublicBeamSchema(Schema baseSchema, @Nullable List metadataColumns) { + if (metadataColumns == null || metadataColumns.isEmpty()) { + return baseSchema; + } + Schema.Builder builder = Schema.builder(); + for (Schema.Field field : baseSchema.getFields()) { + builder.addField(field); + } + for (String col : metadataColumns) { + if (col.equals(CHANGE_TYPE_COLUMN)) { + builder.addField(CHANGE_TYPE_COLUMN, Schema.FieldType.STRING); + } else if (col.equals(COMMIT_VERSION_COLUMN)) { + builder.addField(COMMIT_VERSION_COLUMN, Schema.FieldType.INT64); + } else if (col.equals(COMMIT_TIMESTAMP_COLUMN)) { + builder.addField(COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME); + } + } + return builder.build(); + } + + @AutoValue + public abstract static class ReadChanges extends PTransform> { + public abstract @Nullable String getTablePath(); + + public abstract @Nullable Long getStartVersion(); + + public abstract @Nullable String getStartTimestamp(); + + public abstract @Nullable Long getEndVersion(); + + public abstract @Nullable String getEndTimestamp(); + + public abstract @Nullable List getMetadataColumns(); + + public abstract @Nullable Map getHadoopConfig(); + + abstract Builder toBuilder(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setTablePath(String tablePath); + + abstract Builder setStartVersion(@Nullable Long startVersion); + + abstract Builder setStartTimestamp(@Nullable String startTimestamp); + + abstract Builder setEndVersion(@Nullable Long endVersion); + + abstract Builder setEndTimestamp(@Nullable String endTimestamp); + + abstract Builder setMetadataColumns(@Nullable List metadataColumns); + + abstract Builder setHadoopConfig(@Nullable Map hadoopConfig); + + abstract ReadChanges build(); + } + + public ReadChanges from(String tablePath) { + return toBuilder().setTablePath(tablePath).build(); + } + + public ReadChanges withStartVersion(long startVersion) { + return toBuilder().setStartVersion(startVersion).build(); + } + + public ReadChanges withStartTimestamp(String startTimestamp) { + return toBuilder().setStartTimestamp(startTimestamp).build(); + } + + public ReadChanges withEndVersion(long endVersion) { + return toBuilder().setEndVersion(endVersion).build(); + } + + public ReadChanges withEndTimestamp(String endTimestamp) { + return toBuilder().setEndTimestamp(endTimestamp).build(); + } + + public ReadChanges withMetadataColumns(String... metadataColumns) { + for (String col : metadataColumns) { + if (!col.equals(CHANGE_TYPE_COLUMN) + && !col.equals(COMMIT_VERSION_COLUMN) + && !col.equals(COMMIT_TIMESTAMP_COLUMN)) { + throw new IllegalArgumentException( + String.format( + "Unsupported metadata column %s. Supported columns are: %s, %s, and %s.", + col, CHANGE_TYPE_COLUMN, COMMIT_VERSION_COLUMN, COMMIT_TIMESTAMP_COLUMN)); + } + } + return toBuilder().setMetadataColumns(Arrays.asList(metadataColumns)).build(); + } + + public ReadChanges withConfig(Map config) { + return toBuilder().setHadoopConfig(config).build(); + } + + @Override + public PCollection expand(PBegin input) { + String path = getTablePath(); + if (path == null) { + throw new IllegalArgumentException("Table path must be set."); + } + if (getStartVersion() == null && getStartTimestamp() == null) { + // TODO: for unbounded reads, support using current HEAD or the latest snapshot + // as the default starting point. + throw new IllegalArgumentException("Either startVersion or startTimestamp must be set."); + } + if (getStartVersion() != null && getStartTimestamp() != null) { + throw new IllegalArgumentException("Cannot set both startVersion and startTimestamp."); + } + if (getEndVersion() != null && getEndTimestamp() != null) { + throw new IllegalArgumentException("Cannot set both endVersion and endTimestamp."); + } + + 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); + + TableImpl tableImpl = (TableImpl) table; + + long resolvedEndVersion; + Long endVersionVal = getEndVersion(); + String endTimestampVal = getEndTimestamp(); + if (endVersionVal != null) { + resolvedEndVersion = endVersionVal; + } else if (endTimestampVal != null) { + long endMillis = java.time.Instant.parse(endTimestampVal).toEpochMilli(); + resolvedEndVersion = tableImpl.getVersionBeforeOrAtTimestamp(engine, endMillis); + } else { + resolvedEndVersion = table.getLatestSnapshot(engine).getVersion(); + } + + Snapshot endSnapshot = table.getSnapshotAsOfVersion(engine, resolvedEndVersion); + StructType deltaSchema = endSnapshot.getSchema(); + if (deltaSchema == null) { + throw new IllegalStateException("Table schema is null."); + } + Schema baseSchema = ReadRows.convertToBeamSchema(deltaSchema); + Schema publicBeamSchema = buildPublicBeamSchema(baseSchema, getMetadataColumns()); + + return input + .apply("Create Path", Create.of(path)) + .apply( + "Plan CDF Files", + ParDo.of( + new CreateCDCReadTasksDoFn( + hadoopConfig, + getStartVersion(), + getStartTimestamp(), + getEndVersion(), + getEndTimestamp()))) + .apply( + "Read CDF Data", ParDo.of(new DeltaCDCSourceDoFn(hadoopConfig, getMetadataColumns()))) + .setRowSchema(publicBeamSchema); + } + } +} 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..48dc3a2c7488 --- /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(Long version); + + abstract Builder setTimestamp(String timestamp); + + abstract Builder setHadoopConfig(Map hadoopConfig); + + abstract Configuration build(); + } + } +} 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..fca19e30cb8f --- /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`. + 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/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/package-info.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/package-info.java similarity index 85% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/package-info.java rename to sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/package-info.java index 9759071b397e..c765a7fb7655 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/artifacts/matchers/package-info.java +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/package-info.java @@ -16,5 +16,9 @@ * limitations under the License. */ -/** Package for Artifact Truth matchers / subjects to have reusable assertions. */ -package org.apache.beam.it.gcp.artifacts.matchers; +/** + * 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/DeltaIOIT.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOIT.java new file mode 100644 index 000000000000..ad526008b20e --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOIT.java @@ -0,0 +1,428 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import io.delta.kernel.DataWriteContext; +import io.delta.kernel.Operation; +import io.delta.kernel.Table; +import io.delta.kernel.Transaction; +import io.delta.kernel.TransactionBuilder; +import io.delta.kernel.TransactionCommitResult; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.ColumnarBatch; +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.defaults.internal.data.DefaultColumnarBatch; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import io.delta.kernel.utils.CloseableIterable; +import io.delta.kernel.utils.CloseableIterator; +import io.delta.kernel.utils.DataFileStatus; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.beam.sdk.managed.Managed; +import org.apache.beam.sdk.options.ExperimentalOptions; +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.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +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.apache.hadoop.conf.Configuration; +import org.joda.time.Instant; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Integration tests for {@link DeltaIO}. */ +@RunWith(JUnit4.class) +public class DeltaIOIT { + private static final Logger LOG = LoggerFactory.getLogger(DeltaIOIT.class); + + private static final String DEFAULT_BUCKET = "apache-beam-testing-delta-lake"; + @Rule public final TestPipeline readPipeline = TestPipeline.create(); + @Rule public final TestName testName = new TestName(); + + private String bucket; + private String repoPath; + private String repoPrefix; + private Storage storage; + private String version0FilePath; + + private static final Schema ROW_SCHEMA = + Schema.builder().addInt32Field("id").addStringField("name").build(); + + private static final List TEST_ROWS = + IntStream.range(0, 100) + .mapToObj(i -> Row.withSchema(ROW_SCHEMA).addValues(i, "name_" + i).build()) + .collect(Collectors.toList()); + + @Before + public void setup() throws Exception { + storage = StorageOptions.newBuilder().build().getService(); + long salt = System.currentTimeMillis(); + + String tempLocation = readPipeline.getOptions().getTempLocation(); + if (tempLocation != null && tempLocation.startsWith("gs://")) { + org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath gcsPath = + org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath.fromUri(tempLocation); + bucket = gcsPath.getBucket(); + repoPrefix = gcsPath.getObject() + "/delta_io_it/" + testName.getMethodName() + "-" + salt; + } else { + bucket = DEFAULT_BUCKET; + repoPrefix = "delta_io_it/" + testName.getMethodName() + "-" + salt; + } + repoPath = "gs://" + bucket + "/" + repoPrefix; + + LOG.info("Generating Delta Lake repository at {}", repoPath); + + Configuration configuration = new Configuration(); + configuration.set("fs.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem"); + configuration.set( + "fs.AbstractFileSystem.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS"); + configuration.set("fs.gs.auth.type", "APPLICATION_DEFAULT"); + String project = + readPipeline + .getOptions() + .as(org.apache.beam.sdk.extensions.gcp.options.GcpOptions.class) + .getProject(); + if (project != null) { + configuration.set("fs.gs.project.id", project); + } + + Engine engine = DefaultEngine.create(configuration); + Table table = Table.forPath(engine, repoPath); + + StructType deltaSchema = + new StructType().add("id", IntegerType.INTEGER).add("name", StringType.STRING); + + TransactionBuilder txnBuilder = + table.createTransactionBuilder(engine, "DeltaIOIT", Operation.CREATE_TABLE); + txnBuilder = + txnBuilder + .withSchema(engine, deltaSchema) + .withTableProperties( + engine, Collections.singletonMap("delta.enableChangeDataFeed", "true")); + Transaction txn = txnBuilder.build(engine); + io.delta.kernel.data.Row txnState = txn.getTransactionState(engine); + + ColumnVector idVector = + new ColumnVector() { + @Override + public DataType getDataType() { + return IntegerType.INTEGER; + } + + @Override + public int getSize() { + return TEST_ROWS.size(); + } + + @Override + public void close() {} + + @Override + public boolean isNullAt(int rowId) { + return TEST_ROWS.get(rowId).getValue("id") == null; + } + + @Override + public int getInt(int rowId) { + return TEST_ROWS.get(rowId).getInt32("id"); + } + }; + + ColumnVector nameVector = + new ColumnVector() { + @Override + public DataType getDataType() { + return StringType.STRING; + } + + @Override + public int getSize() { + return TEST_ROWS.size(); + } + + @Override + public void close() {} + + @Override + public boolean isNullAt(int rowId) { + return TEST_ROWS.get(rowId).getValue("name") == null; + } + + @Override + public String getString(int rowId) { + return TEST_ROWS.get(rowId).getString("name"); + } + }; + + ColumnVector[] vectors = new ColumnVector[] {idVector, nameVector}; + ColumnarBatch columnarBatch = new DefaultColumnarBatch(TEST_ROWS.size(), deltaSchema, vectors); + FilteredColumnarBatch filteredBatch = + new FilteredColumnarBatch(columnarBatch, Optional.empty()); + + CloseableIterator data = + io.delta.kernel.internal.util.Utils.toCloseableIterator( + Collections.singletonList(filteredBatch).iterator()); + + CloseableIterator physicalData = + Transaction.transformLogicalData(engine, txnState, data, Collections.emptyMap()); + + DataWriteContext writeContext = + Transaction.getWriteContext(engine, txnState, Collections.emptyMap()); + + CloseableIterator dataFiles = + engine + .getParquetHandler() + .writeParquetFiles( + writeContext.getTargetDirectory(), + physicalData, + writeContext.getStatisticsColumns()); + + CloseableIterator dataActions = + Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext); + + List addActionsList = new ArrayList<>(); + while (dataActions.hasNext()) { + addActionsList.add(dataActions.next()); + } + + if (!addActionsList.isEmpty()) { + io.delta.kernel.data.Row action = addActionsList.get(0); + int addOrdinal = action.getSchema().indexOf("add"); + if (addOrdinal < 0) { + throw new IllegalStateException( + "Expected append action to contain 'add' field, but it didn't: " + action.getSchema()); + } + io.delta.kernel.data.Row addAction = action.getStruct(addOrdinal); + if (addAction == null) { + throw new IllegalStateException("Action 'add' struct is null"); + } + int pathOrdinal = addAction.getSchema().indexOf("path"); + if (pathOrdinal < 0) { + throw new IllegalStateException( + "'add' action schema does not contain 'path': " + addAction.getSchema()); + } + version0FilePath = addAction.getString(pathOrdinal); + } + + CloseableIterable dataActionsIterable = + CloseableIterable.inMemoryIterable( + io.delta.kernel.internal.util.Utils.toCloseableIterator(addActionsList.iterator())); + + TransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable); + + if (commitResult.getVersion() < 0) { + throw new RuntimeException("Table creation/write failed"); + } + + LOG.info("Successfully generated Delta Lake repository"); + } + + @After + public void teardown() { + if (storage == null) { + return; + } + LOG.info("Cleaning up Delta Lake repository at {}", repoPath); + try { + Iterable blobs = + storage.list(bucket, Storage.BlobListOption.prefix(repoPrefix)).getValues(); + blobs.forEach(b -> storage.delete(b.getBlobId())); + } catch (Exception e) { + LOG.warn("Failed to clean up GCS repository at {}", repoPath, e); + } + } + + @Test + public void testReadDeltaLakeTable() { + ExperimentalOptions options = readPipeline.getOptions().as(ExperimentalOptions.class); + ExperimentalOptions.addExperiment(options, "use_runner_v2"); + + Map hadoopConfig = new HashMap<>(); + hadoopConfig.put("fs.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem"); + hadoopConfig.put( + "fs.AbstractFileSystem.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS"); + String project = + readPipeline + .getOptions() + .as(org.apache.beam.sdk.extensions.gcp.options.GcpOptions.class) + .getProject(); + if (project != null) { + hadoopConfig.put("fs.gs.project.id", project); + } + + PCollection output = + readPipeline + .apply( + Managed.read(Managed.DELTA_LAKE) + .withConfig(ImmutableMap.of("table", repoPath, "hadoop_config", hadoopConfig))) + .getSinglePCollection(); + + PAssert.that(output).containsInAnyOrder(TEST_ROWS); + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesDeltaLake() throws Exception { + ExperimentalOptions options = readPipeline.getOptions().as(ExperimentalOptions.class); + List experiments = options.getExperiments(); + if (experiments != null) { + List modifiableExperiments = new java.util.ArrayList<>(experiments); + // TODO: remove this when Runner v2 supports elements that includes CDC metadata + // (ValueKind). + modifiableExperiments.remove("use_runner_v2"); + options.setExperiments(modifiableExperiments); + } + + Map hadoopConfig = new HashMap<>(); + hadoopConfig.put("fs.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem"); + hadoopConfig.put( + "fs.AbstractFileSystem.gs.impl", "com.google.cloud.hadoop.fs.gcs.GoogleHadoopFS"); + hadoopConfig.put("fs.gs.auth.type", "APPLICATION_DEFAULT"); + String project = + readPipeline + .getOptions() + .as(org.apache.beam.sdk.extensions.gcp.options.GcpOptions.class) + .getProject(); + if (project != null) { + hadoopConfig.put("fs.gs.project.id", project); + } + + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + for (Map.Entry entry : hadoopConfig.entrySet()) { + conf.set(entry.getKey(), entry.getValue()); + } + Engine engine = DefaultEngine.create(conf); + + StructType deltaSchema = + new StructType().add("id", IntegerType.INTEGER).add("name", StringType.STRING); + + // 1. Write version 1 containing cdc actions for testing updates and deletes + Schema cdcWriteSchema = + Schema.builder() + .addField("id", Schema.FieldType.INT32) + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64) + .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("id", IntegerType.INTEGER) + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP); + + Row cdcRow1 = + Row.withSchema(cdcWriteSchema) + .addValues(0, "name_0", "delete", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow2 = + Row.withSchema(cdcWriteSchema) + .addValues(1, "name_1", "update_preimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow3 = + Row.withSchema(cdcWriteSchema) + .addValues(1, "name_1_updated", "update_postimage", 1L, new Instant(123456789000L)) + .build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + repoPath, + 1L, + System.currentTimeMillis(), + deltaSchema, + null, + version0FilePath, + java.util.Arrays.asList(cdcRow1, cdcRow2, cdcRow3), + cdcWriteDeltaSchema); + + // 2. Read CDF data from table using Managed.read(Managed.DELTA_LAKE_CDC) + Map readConfig = new HashMap<>(); + readConfig.put("table", repoPath); + readConfig.put("start_version", 0L); + readConfig.put("hadoop_config", hadoopConfig); + readConfig.put( + "include_metadata_columns", + java.util.Arrays.asList( + DeltaIO.CHANGE_TYPE_COLUMN, + DeltaIO.COMMIT_VERSION_COLUMN, + DeltaIO.COMMIT_TIMESTAMP_COLUMN)); + + PCollection output = + readPipeline + .apply(Managed.read(Managed.DELTA_LAKE_CDC).withConfig(readConfig)) + .getSinglePCollection(); + + PCollection formattedOutput = + output.apply("Format Row with Metadata", ParDo.of(new FormatITRowWithMetadata())); + + // Generate expected outputs for version 0 (inserts of id 0-99) + List expectedOutputs = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + expectedOutputs.add(String.format("%d:name_%d:insert:v0", i, i)); + } + // Expected outputs for version 1 + expectedOutputs.add("0:name_0:delete:v1"); + expectedOutputs.add("1:name_1:update_preimage:v1"); + expectedOutputs.add("1:name_1_updated:update_postimage:v1"); + + PAssert.that(formattedOutput).containsInAnyOrder(expectedOutputs); + + readPipeline.run().waitUntilFinish(); + } + + private static final class FormatITRowWithMetadata extends DoFn { + @ProcessElement + public void process(@Element Row row, OutputReceiver out) { + out.output( + String.format( + "%d:%s:%s:v%d", + row.getInt32("id"), + row.getString("name"), + row.getString(DeltaIO.CHANGE_TYPE_COLUMN), + row.getInt64(DeltaIO.COMMIT_VERSION_COLUMN))); + } + } +} diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOS3IT.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOS3IT.java new file mode 100644 index 000000000000..c3b1a6dd0c32 --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOS3IT.java @@ -0,0 +1,278 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.DataWriteContext; +import io.delta.kernel.Operation; +import io.delta.kernel.Table; +import io.delta.kernel.Transaction; +import io.delta.kernel.TransactionBuilder; +import io.delta.kernel.TransactionCommitResult; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.ColumnarBatch; +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.defaults.internal.data.DefaultColumnarBatch; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructType; +import io.delta.kernel.utils.CloseableIterable; +import io.delta.kernel.utils.CloseableIterator; +import io.delta.kernel.utils.DataFileStatus; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +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; +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.apache.hadoop.conf.Configuration; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.localstack.LocalStackContainer; +import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +/** Integration tests for {@link DeltaIO} on AWS S3 using Localstack. */ +@RunWith(JUnit4.class) +public class DeltaIOS3IT { + private static final Logger LOG = LoggerFactory.getLogger(DeltaIOS3IT.class); + + @ClassRule + public static LocalStackContainer localstack = + new LocalStackContainer(DockerImageName.parse("localstack/localstack:0.13.1")) + .withServices(LocalStackContainer.Service.S3) + .withLogConsumer(frame -> System.out.print("[LocalStack S3] " + frame.getUtf8String())); + + @Rule public final TestPipeline readPipeline = TestPipeline.create(); + @Rule public final TestName testName = new TestName(); + + private String bucketName; + private String repoPath; + private Configuration configuration; + private S3Client s3Client; + + private static final Schema ROW_SCHEMA = + Schema.builder().addInt32Field("id").addStringField("name").build(); + + private static final List TEST_ROWS = + IntStream.range(0, 100) + .mapToObj(i -> Row.withSchema(ROW_SCHEMA).addValues(i, "name_" + i).build()) + .collect(Collectors.toList()); + + @Before + public void setup() throws Exception { + bucketName = "beam-delta-s3-it-" + System.currentTimeMillis(); + repoPath = "s3a://" + bucketName + "/delta_io_it/" + testName.getMethodName(); + + LOG.info("Creating localstack S3 client and bucket: {}", bucketName); + s3Client = + S3Client.builder() + .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3)) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create( + localstack.getAccessKey(), localstack.getSecretKey()))) + .region(Region.of(localstack.getRegion())) + .build(); + + s3Client.createBucket(b -> b.bucket(bucketName)); + + LOG.info("Generating Delta Lake repository at {}", repoPath); + + configuration = new Configuration(); + configuration.set("fs.s3a.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem"); + configuration.set("fs.AbstractFileSystem.s3a.impl", "org.apache.hadoop.fs.s3a.S3A"); + configuration.set( + "fs.s3a.endpoint", + localstack.getEndpointOverride(LocalStackContainer.Service.S3).toString()); + configuration.set("fs.s3a.access.key", localstack.getAccessKey()); + configuration.set("fs.s3a.secret.key", localstack.getSecretKey()); + configuration.set("fs.s3a.audit.enabled", "false"); + configuration.set( + "fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider"); + + Engine engine = DefaultEngine.create(configuration); + Table table = Table.forPath(engine, repoPath); + + StructType deltaSchema = + new StructType().add("id", IntegerType.INTEGER).add("name", StringType.STRING); + + TransactionBuilder txnBuilder = + table.createTransactionBuilder(engine, "DeltaIOS3IT", Operation.CREATE_TABLE); + txnBuilder = txnBuilder.withSchema(engine, deltaSchema); + Transaction txn = txnBuilder.build(engine); + io.delta.kernel.data.Row txnState = txn.getTransactionState(engine); + + ColumnVector idVector = + new ColumnVector() { + @Override + public DataType getDataType() { + return IntegerType.INTEGER; + } + + @Override + public int getSize() { + return TEST_ROWS.size(); + } + + @Override + public void close() {} + + @Override + public boolean isNullAt(int rowId) { + return TEST_ROWS.get(rowId).getValue("id") == null; + } + + @Override + public int getInt(int rowId) { + return TEST_ROWS.get(rowId).getInt32("id"); + } + }; + + ColumnVector nameVector = + new ColumnVector() { + @Override + public DataType getDataType() { + return StringType.STRING; + } + + @Override + public int getSize() { + return TEST_ROWS.size(); + } + + @Override + public void close() {} + + @Override + public boolean isNullAt(int rowId) { + return TEST_ROWS.get(rowId).getValue("name") == null; + } + + @Override + public String getString(int rowId) { + return TEST_ROWS.get(rowId).getString("name"); + } + }; + + ColumnVector[] vectors = new ColumnVector[] {idVector, nameVector}; + ColumnarBatch columnarBatch = new DefaultColumnarBatch(TEST_ROWS.size(), deltaSchema, vectors); + FilteredColumnarBatch filteredBatch = + new FilteredColumnarBatch(columnarBatch, Optional.empty()); + + CloseableIterator data = + io.delta.kernel.internal.util.Utils.toCloseableIterator( + Collections.singletonList(filteredBatch).iterator()); + + CloseableIterator physicalData = + Transaction.transformLogicalData(engine, txnState, data, Collections.emptyMap()); + + DataWriteContext writeContext = + Transaction.getWriteContext(engine, txnState, Collections.emptyMap()); + + CloseableIterator dataFiles = + engine + .getParquetHandler() + .writeParquetFiles( + writeContext.getTargetDirectory(), + physicalData, + writeContext.getStatisticsColumns()); + + CloseableIterator dataActions = + Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext); + + CloseableIterable dataActionsIterable = + CloseableIterable.inMemoryIterable(dataActions); + + TransactionCommitResult commitResult = txn.commit(engine, dataActionsIterable); + + if (commitResult.getVersion() < 0) { + throw new RuntimeException("Table creation/write failed"); + } + + LOG.info("Successfully generated Delta Lake repository on S3"); + } + + @After + public void teardown() { + if (repoPath != null && configuration != null) { + LOG.info("Cleaning up Delta Lake repository at {}", repoPath); + try { + org.apache.hadoop.fs.Path path = new org.apache.hadoop.fs.Path(repoPath); + org.apache.hadoop.fs.FileSystem fs = path.getFileSystem(configuration); + fs.delete(path, true); + } catch (Exception e) { + LOG.warn("Failed to clean up S3 repository at {}", repoPath, e); + } + } + if (s3Client != null && bucketName != null) { + try { + s3Client.deleteBucket(b -> b.bucket(bucketName)); + } catch (Exception e) { + LOG.warn("Failed to delete bucket {}", bucketName, e); + } + s3Client.close(); + } + } + + @Test + public void testReadDeltaLakeTableS3() { + Map hadoopConfig = new HashMap<>(); + hadoopConfig.put("fs.s3a.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem"); + hadoopConfig.put("fs.AbstractFileSystem.s3a.impl", "org.apache.hadoop.fs.s3a.S3A"); + hadoopConfig.put( + "fs.s3a.endpoint", + localstack.getEndpointOverride(LocalStackContainer.Service.S3).toString()); + hadoopConfig.put("fs.s3a.access.key", localstack.getAccessKey()); + hadoopConfig.put("fs.s3a.secret.key", localstack.getSecretKey()); + hadoopConfig.put("fs.s3a.audit.enabled", "false"); + hadoopConfig.put( + "fs.s3a.aws.credentials.provider", "org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider"); + + PCollection output = + readPipeline + .apply( + Managed.read(Managed.DELTA_LAKE) + .withConfig(ImmutableMap.of("table", repoPath, "hadoop_config", hadoopConfig))) + .getSinglePCollection(); + + PAssert.that(output).containsInAnyOrder(TEST_ROWS); + readPipeline.run().waitUntilFinish(); + } +} 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..0db0aef9e080 --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java @@ -0,0 +1,1718 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.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.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.Collections; +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.managed.Managed; +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.DoFn; +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.PCollectionRowTuple; +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.collect.ImmutableMap; +import org.joda.time.Instant; +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 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"; + 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()); + } + + @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(); + + 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(); + + File parquetFile = new File(tableDir, "part-00000.parquet"); + byte[] fileBytes = Files.readAllBytes(parquetFile.toPath()); + + // 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 + public void testManagedDeltaRead() throws Exception { + File tableDir = tempFolder.newFolder("managed-delta-table"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row row = Row.withSchema(schema).addValues("test-name").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 123456789L, + deltaSchema, + Collections.singletonList(row)); + + // 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 { + 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 testConvertToBeamSchemaPreservesNullability() { + StructType deltaSchema = + new StructType( + java.util.Arrays.asList( + new StructField("nullable_string", StringType.STRING, true), + new StructField("non_nullable_integer", IntegerType.INTEGER, false))); + + Schema expectedSchema = + Schema.builder() + .addField( + Schema.Field.of("nullable_string", Schema.FieldType.STRING).withNullable(true)) + .addField( + Schema.Field.of("non_nullable_integer", Schema.FieldType.INT32).withNullable(false)) + .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 + } + } + } + + @Test + public void testReadChanges() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + Row tableRow2 = Row.withSchema(tableSchema).addValues("row-2").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1, tableRow2)); + + // 2. Write cdc parquet file for Version 1 (commit with cdc actions) + Schema cdcWriteSchema = + Schema.builder() + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64) + .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP); + + Row cdcRow1 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1", "update_preimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow2 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1-updated", "update_postimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow3 = + Row.withSchema(cdcWriteSchema) + .addValues("row-2", "delete", 1L, new Instant(123456789000L)) + .build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 1L, + 200000000000L, + deltaSchema, + null, + null, + java.util.Arrays.asList(cdcRow1, cdcRow2, cdcRow3), + cdcWriteDeltaSchema); + + // 3. Read CDF data from table using ReadChanges + PCollection output = + readPipeline.apply( + DeltaIO.readChanges().from(tableDir.getAbsolutePath()).withStartVersion(0L)); + + PCollection formattedOutput = + output.apply("Format ValueKind and Row", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(formattedOutput) + .containsInAnyOrder( + "INSERT:row-1", + "INSERT:row-2", + "UPDATE_BEFORE:row-1", + "UPDATE_AFTER:row-1-updated", + "DELETE:row-2"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesAndNormalReadWithCDCAndAppend() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-cdc-and-append"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + Row tableRow2 = Row.withSchema(tableSchema).addValues("row-2").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1, tableRow2)); + + // 2. Write cdc and append parquet files for Version 1 (commit with cdc and add actions) + Schema cdcWriteSchema = + Schema.builder() + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64) + .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP); + + Row cdcRow = + Row.withSchema(cdcWriteSchema) + .addValues("row-3", "insert", 1L, new Instant(123456789000L)) + .build(); + + Row appendRow = Row.withSchema(tableSchema).addValues("row-3").build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 1L, + 200000000000L, + deltaSchema, + java.util.Arrays.asList(appendRow), + null, + java.util.Arrays.asList(cdcRow), + cdcWriteDeltaSchema); + + // 3. Read CDF data from table using ReadChanges + PCollection outputCDC = + readPipeline.apply( + "Read Changes", + DeltaIO.readChanges().from(tableDir.getAbsolutePath()).withStartVersion(0L)); + + PCollection formattedOutputCDC = + outputCDC.apply("Format CDC Row", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(formattedOutputCDC) + .containsInAnyOrder("INSERT:row-1", "INSERT:row-2", "INSERT:row-3"); + + // 4. Read latest snapshot using normal read via writePipeline + PCollection outputNormal = + writePipeline.apply("Read Normal", DeltaIO.readRows().from(tableDir.getAbsolutePath())); + + PCollection formattedOutputNormal = + outputNormal.apply("Format Normal Row", ParDo.of(new FormatRowName())); + + PAssert.that(formattedOutputNormal).containsInAnyOrder("row-1", "row-2", "row-3"); + + readPipeline.run().waitUntilFinish(); + writePipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesWithSchemaTransformProvider() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-provider"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + Row tableRow2 = Row.withSchema(tableSchema).addValues("row-2").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1, tableRow2)); + + // 2. Write cdc parquet file for Version 1 (commit with cdc actions) + Schema cdcWriteSchema = + Schema.builder() + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64) + .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP); + + Row cdcRow1 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1", "update_preimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow2 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1-updated", "update_postimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow3 = + Row.withSchema(cdcWriteSchema) + .addValues("row-2", "delete", 1L, new Instant(123456789000L)) + .build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 1L, + 200000000000L, + deltaSchema, + null, + null, + java.util.Arrays.asList(cdcRow1, cdcRow2, cdcRow3), + cdcWriteDeltaSchema); + + // 3. Read CDF data from table using DeltaCdcReadSchemaTransformProvider + DeltaCdcReadSchemaTransformProvider.Configuration config = + DeltaCdcReadSchemaTransformProvider.Configuration.builder() + .setTable(tableDir.getAbsolutePath()) + .setStartVersion(0L) + .build(); + + PCollection output = + PCollectionRowTuple.empty(readPipeline) + .apply(new DeltaCdcReadSchemaTransformProvider().from(config)) + .get(DeltaCdcReadSchemaTransformProvider.OUTPUT_TAG); + + PCollection formattedOutput = + output.apply("Format ValueKind and Row", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(formattedOutput) + .containsInAnyOrder( + "INSERT:row-1", + "INSERT:row-2", + "UPDATE_BEFORE:row-1", + "UPDATE_AFTER:row-1-updated", + "DELETE:row-2"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesWithMetadataColumns() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-metadata"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + Row tableRow2 = Row.withSchema(tableSchema).addValues("row-2").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1, tableRow2)); + + // 2. Write cdc parquet file for Version 1 (commit with cdc actions) + Schema cdcWriteSchema = + Schema.builder() + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64) + .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP); + + Row cdcRow1 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1", "update_preimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow2 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1-updated", "update_postimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow3 = + Row.withSchema(cdcWriteSchema) + .addValues("row-2", "delete", 1L, new Instant(123456789000L)) + .build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 1L, + 200000000000L, + deltaSchema, + null, + null, + java.util.Arrays.asList(cdcRow1, cdcRow2, cdcRow3), + cdcWriteDeltaSchema); + + // 3. Read CDF data from table using DeltaCdcReadSchemaTransformProvider requesting metadata + // columns + DeltaCdcReadSchemaTransformProvider.Configuration config = + DeltaCdcReadSchemaTransformProvider.Configuration.builder() + .setTable(tableDir.getAbsolutePath()) + .setStartVersion(0L) + .setIncludeMetadataColumns( + java.util.Arrays.asList( + DeltaIO.CHANGE_TYPE_COLUMN, + DeltaIO.COMMIT_VERSION_COLUMN, + DeltaIO.COMMIT_TIMESTAMP_COLUMN)) + .build(); + + PCollection output = + PCollectionRowTuple.empty(readPipeline) + .apply(new DeltaCdcReadSchemaTransformProvider().from(config)) + .get(DeltaCdcReadSchemaTransformProvider.OUTPUT_TAG); + + PCollection formattedOutput = + output.apply("Format Row with Metadata", ParDo.of(new FormatRowWithMetadata())); + + PAssert.that(formattedOutput) + .containsInAnyOrder( + "row-1:insert:v0:t100000000000", + "row-2:insert:v0:t100000000000", + "row-1:update_preimage:v1:t123456789000", + "row-1-updated:update_postimage:v1:t123456789000", + "row-2:delete:v1:t123456789000"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesWithMissingMetadataColumns() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-missing-metadata"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + Row tableRow2 = Row.withSchema(tableSchema).addValues("row-2").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1, tableRow2)); + + // 2. Write cdc parquet file for Version 1 (commit with cdc actions), but OMIT version and + // timestamp columns! + Schema cdcWriteSchema = + Schema.builder() + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING); + + Row cdcRow1 = Row.withSchema(cdcWriteSchema).addValues("row-1", "update_preimage").build(); + Row cdcRow2 = + Row.withSchema(cdcWriteSchema).addValues("row-1-updated", "update_postimage").build(); + Row cdcRow3 = Row.withSchema(cdcWriteSchema).addValues("row-2", "delete").build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 1L, + 200000000000L, + deltaSchema, + null, + null, + java.util.Arrays.asList(cdcRow1, cdcRow2, cdcRow3), + cdcWriteDeltaSchema); + + // 3. Read CDF data from table requesting metadata columns + DeltaCdcReadSchemaTransformProvider.Configuration config = + DeltaCdcReadSchemaTransformProvider.Configuration.builder() + .setTable(tableDir.getAbsolutePath()) + .setStartVersion(0L) + .setIncludeMetadataColumns( + java.util.Arrays.asList( + DeltaIO.CHANGE_TYPE_COLUMN, + DeltaIO.COMMIT_VERSION_COLUMN, + DeltaIO.COMMIT_TIMESTAMP_COLUMN)) + .build(); + + PCollection output = + PCollectionRowTuple.empty(readPipeline) + .apply(new DeltaCdcReadSchemaTransformProvider().from(config)) + .get(DeltaCdcReadSchemaTransformProvider.OUTPUT_TAG); + + PCollection formattedOutput = + output.apply("Format Row with Metadata", ParDo.of(new FormatRowWithMetadata())); + + PAssert.that(formattedOutput) + .containsInAnyOrder( + "row-1:insert:v0:t100000000000", + "row-2:insert:v0:t100000000000", + "row-1:update_preimage:v1:t200000000000", + "row-1-updated:update_postimage:v1:t200000000000", + "row-2:delete:v1:t200000000000"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesWithSubsetOfMetadataColumns() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-subset-metadata"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1)); + + // 2. Read CDF data from table requesting ONLY _change_type + DeltaCdcReadSchemaTransformProvider.Configuration config = + DeltaCdcReadSchemaTransformProvider.Configuration.builder() + .setTable(tableDir.getAbsolutePath()) + .setStartVersion(0L) + .setIncludeMetadataColumns( + java.util.Collections.singletonList(DeltaIO.CHANGE_TYPE_COLUMN)) + .build(); + + PCollection output = + PCollectionRowTuple.empty(readPipeline) + .apply(new DeltaCdcReadSchemaTransformProvider().from(config)) + .get(DeltaCdcReadSchemaTransformProvider.OUTPUT_TAG); + + // Verify schema does not contain version or timestamp + org.junit.Assert.assertTrue(output.getSchema().hasField(DeltaIO.CHANGE_TYPE_COLUMN)); + org.junit.Assert.assertFalse(output.getSchema().hasField(DeltaIO.COMMIT_VERSION_COLUMN)); + org.junit.Assert.assertFalse(output.getSchema().hasField(DeltaIO.COMMIT_TIMESTAMP_COLUMN)); + + PCollection formattedOutput = + output.apply("Format Row", ParDo.of(new FormatRowSubsetMetadata())); + + PAssert.that(formattedOutput).containsInAnyOrder("row-1:insert"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesWithCommitVersionMetadataColumn() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-version-metadata"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1)); + + // 2. Read CDF data from table requesting ONLY _commit_version + DeltaCdcReadSchemaTransformProvider.Configuration config = + DeltaCdcReadSchemaTransformProvider.Configuration.builder() + .setTable(tableDir.getAbsolutePath()) + .setStartVersion(0L) + .setIncludeMetadataColumns( + java.util.Collections.singletonList(DeltaIO.COMMIT_VERSION_COLUMN)) + .build(); + + PCollection output = + PCollectionRowTuple.empty(readPipeline) + .apply(new DeltaCdcReadSchemaTransformProvider().from(config)) + .get(DeltaCdcReadSchemaTransformProvider.OUTPUT_TAG); + + // Verify schema contains version but not change type or timestamp + org.junit.Assert.assertFalse(output.getSchema().hasField(DeltaIO.CHANGE_TYPE_COLUMN)); + org.junit.Assert.assertTrue(output.getSchema().hasField(DeltaIO.COMMIT_VERSION_COLUMN)); + org.junit.Assert.assertFalse(output.getSchema().hasField(DeltaIO.COMMIT_TIMESTAMP_COLUMN)); + + PCollection formattedOutput = + output.apply("Format Row", ParDo.of(new FormatRowVersionMetadata())); + + PAssert.that(formattedOutput).containsInAnyOrder("row-1:0"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesWithCommitTimestampMetadataColumn() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-timestamp-metadata"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1)); + + // 2. Read CDF data from table requesting ONLY _commit_timestamp + DeltaCdcReadSchemaTransformProvider.Configuration config = + DeltaCdcReadSchemaTransformProvider.Configuration.builder() + .setTable(tableDir.getAbsolutePath()) + .setStartVersion(0L) + .setIncludeMetadataColumns( + java.util.Collections.singletonList(DeltaIO.COMMIT_TIMESTAMP_COLUMN)) + .build(); + + PCollection output = + PCollectionRowTuple.empty(readPipeline) + .apply(new DeltaCdcReadSchemaTransformProvider().from(config)) + .get(DeltaCdcReadSchemaTransformProvider.OUTPUT_TAG); + + // Verify schema contains timestamp but not change type or version + org.junit.Assert.assertFalse(output.getSchema().hasField(DeltaIO.CHANGE_TYPE_COLUMN)); + org.junit.Assert.assertFalse(output.getSchema().hasField(DeltaIO.COMMIT_VERSION_COLUMN)); + org.junit.Assert.assertTrue(output.getSchema().hasField(DeltaIO.COMMIT_TIMESTAMP_COLUMN)); + + PCollection formattedOutput = + output.apply("Format Row", ParDo.of(new FormatRowTimestampMetadata())); + + PAssert.that(formattedOutput).containsInAnyOrder("row-1:100000000000"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesWithManaged() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-managed"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + // 1. Write parquet files for Version 0 (insert-only commit) + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + Row tableRow2 = Row.withSchema(tableSchema).addValues("row-2").build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1, tableRow2)); + + // 2. Write cdc parquet file for Version 1 (commit with cdc actions) + Schema cdcWriteSchema = + Schema.builder() + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64) + .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP); + + Row cdcRow1 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1", "update_preimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow2 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1-updated", "update_postimage", 1L, new Instant(123456789000L)) + .build(); + Row cdcRow3 = + Row.withSchema(cdcWriteSchema) + .addValues("row-2", "delete", 1L, new Instant(123456789000L)) + .build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 1L, + 200000000000L, + deltaSchema, + null, + null, + java.util.Arrays.asList(cdcRow1, cdcRow2, cdcRow3), + cdcWriteDeltaSchema); + + // 3. Read CDF data from table using Managed.read(Managed.DELTA_LAKE_CDC) + Map config = new HashMap<>(); + config.put("table", tableDir.getAbsolutePath()); + config.put("start_version", 0L); + + PCollection output = + readPipeline + .apply(Managed.read(Managed.DELTA_LAKE_CDC).withConfig(config)) + .getSinglePCollection(); + + PCollection formattedOutput = + output.apply("Format ValueKind and Row", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(formattedOutput) + .containsInAnyOrder( + "INSERT:row-1", + "INSERT:row-2", + "UPDATE_BEFORE:row-1", + "UPDATE_AFTER:row-1-updated", + "DELETE:row-2"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesRanges() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-ranges"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + // 1. Write parquet files for Version 0 (insert-only commit) + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + Row tableRow2 = Row.withSchema(tableSchema).addValues("row-2").build(); + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1, tableRow2)); + + // 2. Write parquet files for Version 1 (commit with updates and deletes) + Schema cdcWriteSchema = + Schema.builder() + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64) + .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP); + + Row cdcRow1 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1", "update_preimage", 1L, new Instant(200000000000L)) + .build(); + Row cdcRow2 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1-updated", "update_postimage", 1L, new Instant(200000000000L)) + .build(); + Row cdcRow3 = + Row.withSchema(cdcWriteSchema) + .addValues("row-2", "delete", 1L, new Instant(200000000000L)) + .build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 1L, + 200000000000L, + deltaSchema, + null, + null, + java.util.Arrays.asList(cdcRow1, cdcRow2, cdcRow3), + cdcWriteDeltaSchema); + + // 3. Write parquet files for Version 2 (insert-only commit) + Row tableRow3 = Row.withSchema(tableSchema).addValues("row-3").build(); + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 2L, + 300000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow3)); + + // Test 1: Read changes between start version 0 and end version 2 + PCollection outputVersions = + readPipeline.apply( + "Read Changes Version Range", + DeltaIO.readChanges() + .from(tableDir.getAbsolutePath()) + .withStartVersion(0L) + .withEndVersion(2L)); + + PCollection formattedVersions = + outputVersions.apply("Format Version Output", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(formattedVersions) + .containsInAnyOrder( + "INSERT:row-1", + "INSERT:row-2", + "UPDATE_BEFORE:row-1", + "UPDATE_AFTER:row-1-updated", + "DELETE:row-2", + "INSERT:row-3"); + + // Test 2: Read changes between start timestamp (after version 0) and end timestamp (after + // version 2) + String startTimestamp = java.time.Instant.ofEpochMilli(150000000000L).toString(); + String endTimestamp = java.time.Instant.ofEpochMilli(350000000000L).toString(); + + PCollection outputTimestamps = + filteringPipeline.apply( + "Read Changes Timestamp Range", + DeltaIO.readChanges() + .from(tableDir.getAbsolutePath()) + .withStartTimestamp(startTimestamp) + .withEndTimestamp(endTimestamp)); + + PCollection formattedTimestamps = + outputTimestamps.apply("Format Timestamp Output", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(formattedTimestamps) + .containsInAnyOrder( + "UPDATE_BEFORE:row-1", "UPDATE_AFTER:row-1-updated", "DELETE:row-2", "INSERT:row-3"); + + readPipeline.run().waitUntilFinish(); + filteringPipeline.run().waitUntilFinish(); + } + + @Test + public void testReadChangesPartialRange() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-changes-partial-range"); + Engine engine = DefaultEngine.create(new org.apache.hadoop.conf.Configuration()); + + Schema tableSchema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + StructType deltaSchema = new StructType().add("name", StringType.STRING); + + // 1. Write parquet files for Version 0 (insert-only commit) + Row tableRow1 = Row.withSchema(tableSchema).addValues("row-1").build(); + Row tableRow2 = Row.withSchema(tableSchema).addValues("row-2").build(); + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 0L, + 100000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow1, tableRow2)); + + // 2. Write parquet files for Version 1 (commit with updates and deletes) + Schema cdcWriteSchema = + Schema.builder() + .addField("name", Schema.FieldType.STRING) + .addField(DeltaIO.CHANGE_TYPE_COLUMN, Schema.FieldType.STRING) + .addField(DeltaIO.COMMIT_VERSION_COLUMN, Schema.FieldType.INT64) + .addField(DeltaIO.COMMIT_TIMESTAMP_COLUMN, Schema.FieldType.DATETIME) + .build(); + StructType cdcWriteDeltaSchema = + new StructType() + .add("name", StringType.STRING) + .add(DeltaIO.CHANGE_TYPE_COLUMN, StringType.STRING) + .add(DeltaIO.COMMIT_VERSION_COLUMN, LongType.LONG) + .add(DeltaIO.COMMIT_TIMESTAMP_COLUMN, TimestampType.TIMESTAMP); + + Row cdcRow1 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1", "update_preimage", 1L, new Instant(200000000000L)) + .build(); + Row cdcRow2 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1-updated", "update_postimage", 1L, new Instant(200000000000L)) + .build(); + Row cdcRow3 = + Row.withSchema(cdcWriteSchema) + .addValues("row-2", "delete", 1L, new Instant(200000000000L)) + .build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 1L, + 200000000000L, + deltaSchema, + null, + null, + java.util.Arrays.asList(cdcRow1, cdcRow2, cdcRow3), + cdcWriteDeltaSchema); + + // 3. Write parquet files for Version 2 (insert-only commit) + Row tableRow3 = Row.withSchema(tableSchema).addValues("row-3").build(); + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 2L, + 300000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow3)); + + // 4. Write parquet files for Version 3 (commit with updates and deletes) + Row cdcRow4 = + Row.withSchema(cdcWriteSchema) + .addValues("row-3", "update_preimage", 3L, new Instant(400000000000L)) + .build(); + Row cdcRow5 = + Row.withSchema(cdcWriteSchema) + .addValues("row-3-updated", "update_postimage", 3L, new Instant(400000000000L)) + .build(); + Row cdcRow6 = + Row.withSchema(cdcWriteSchema) + .addValues("row-1-updated", "delete", 3L, new Instant(400000000000L)) + .build(); + + DeltaWriteTestUtils.writeCdcCommit( + engine, + tableDir.getAbsolutePath(), + 3L, + 400000000000L, + deltaSchema, + null, + null, + java.util.Arrays.asList(cdcRow4, cdcRow5, cdcRow6), + cdcWriteDeltaSchema); + + // 5. Write parquet files for Version 4 (insert-only commit) + Row tableRow4 = Row.withSchema(tableSchema).addValues("row-4").build(); + DeltaWriteTestUtils.writeAppendCommit( + engine, + tableDir.getAbsolutePath(), + 4L, + 500000000000L, + deltaSchema, + java.util.Arrays.asList(tableRow4)); + + // Read changes between start version 1 and end version 3 + PCollection outputVersions = + readPipeline.apply( + "Read Changes Partial Version Range", + DeltaIO.readChanges() + .from(tableDir.getAbsolutePath()) + .withStartVersion(1L) + .withEndVersion(3L)); + + PCollection formattedVersions = + outputVersions.apply("Format Version Output", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(formattedVersions) + .containsInAnyOrder( + "UPDATE_BEFORE:row-1", + "UPDATE_AFTER:row-1-updated", + "DELETE:row-2", + "INSERT:row-3", + "UPDATE_BEFORE:row-3", + "UPDATE_AFTER:row-3-updated", + "DELETE:row-1-updated"); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testS3SchemeRegistrationWithAnonymousCredentials() { + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + conf.set("fs.s3.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem"); + conf.set("fs.AbstractFileSystem.s3a.impl", "org.apache.hadoop.fs.s3a.S3A"); + conf.set( + "fs.s3a.aws.credentials.provider", + "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider"); + + Engine engine = DefaultEngine.create(conf); + Table table = Table.forPath(engine, "s3://fake-bucket/table"); + Exception e = Assert.assertThrows(Exception.class, () -> table.getLatestSnapshot(engine)); + String msg = e.toString(); + Assert.assertFalse( + "Should not throw UnsupportedFileSystemException. Error was: " + msg, + msg.contains("UnsupportedFileSystemException") || msg.contains("No FileSystem for scheme")); + } + + private static final class FormatValueKindAndRow extends DoFn { + @ProcessElement + public void process( + @Element Row row, ValueKind valueKind, OutputReceiver outputReceiver) { + outputReceiver.output(valueKind.name() + ":" + row.getString("name")); + } + } + + private static final class FormatRowWithMetadata extends DoFn { + @ProcessElement + public void process(@Element Row row, OutputReceiver out) { + out.output( + String.format( + "%s:%s:v%d:t%d", + row.getString("name"), + row.getString(DeltaIO.CHANGE_TYPE_COLUMN), + row.getInt64(DeltaIO.COMMIT_VERSION_COLUMN), + row.getDateTime(DeltaIO.COMMIT_TIMESTAMP_COLUMN).getMillis())); + } + } + + private static final class FormatRowSubsetMetadata extends DoFn { + @ProcessElement + public void process(@Element Row row, OutputReceiver out) { + out.output(row.getString("name") + ":" + row.getString(DeltaIO.CHANGE_TYPE_COLUMN)); + } + } + + private static final class FormatRowName extends DoFn { + @ProcessElement + public void process(@Element Row row, OutputReceiver out) { + out.output(row.getString("name")); + } + } + + private static final class FormatRowVersionMetadata extends DoFn { + @ProcessElement + public void process(@Element Row row, OutputReceiver out) { + out.output(row.getString("name") + ":" + row.getInt64(DeltaIO.COMMIT_VERSION_COLUMN)); + } + } + + private static final class FormatRowTimestampMetadata extends DoFn { + @ProcessElement + public void process(@Element Row row, OutputReceiver out) { + out.output( + row.getString("name") + + ":" + + row.getDateTime(DeltaIO.COMMIT_TIMESTAMP_COLUMN).getMillis()); + } + } +} diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProviderTest.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProviderTest.java new file mode 100644 index 000000000000..77aef7bce494 --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProviderTest.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.io.delta.DeltaReadSchemaTransformProvider.OUTPUT_TAG; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +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.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.Create; +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.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DeltaReadSchemaTransformProvider}. */ +@RunWith(JUnit4.class) +public class DeltaReadSchemaTransformProviderTest { + + @Rule public TestPipeline writePipeline = TestPipeline.create(); + @Rule public TestPipeline readPipeline = TestPipeline.create(); + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Test + public void testBuildTransformWithRow() { + java.util.Map hadoopConfig = new java.util.HashMap<>(); + hadoopConfig.put("fs.gs.project.id", "test-project"); + + Row config = + Row.withSchema(new DeltaReadSchemaTransformProvider().configurationSchema()) + .withFieldValue("table", "/path/to/table") + .withFieldValue("version", 5L) + .withFieldValue("timestamp", "2026-06-04T12:00:00Z") + .withFieldValue("hadoop_config", hadoopConfig) + .build(); + + new DeltaReadSchemaTransformProvider().from(config); + } + + @Test + public void testSimpleScan() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-simple"); + + // 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(); + + File parquetFile = new File(tableDir, "part-00000.parquet"); + byte[] fileBytes = Files.readAllBytes(parquetFile.toPath()); + + // 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 DeltaReadSchemaTransformProvider + Configuration readConfig = Configuration.builder().setTable(tableDir.getAbsolutePath()).build(); + + PCollection output = + PCollectionRowTuple.empty(readPipeline) + .apply(new DeltaReadSchemaTransformProvider().from(readConfig)) + .get(OUTPUT_TAG); + + PAssert.that(output).containsInAnyOrder(row); + + readPipeline.run().waitUntilFinish(); + } +} diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaWriteTestUtils.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaWriteTestUtils.java new file mode 100644 index 000000000000..4ae75bcd47cd --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaWriteTestUtils.java @@ -0,0 +1,371 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.DataWriteContext; +import io.delta.kernel.Operation; +import io.delta.kernel.Table; +import io.delta.kernel.Transaction; +import io.delta.kernel.TransactionBuilder; +import io.delta.kernel.TransactionCommitResult; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.ColumnarBatch; +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.defaults.internal.data.DefaultColumnarBatch; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.internal.data.GenericRow; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.DataType; +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 io.delta.kernel.utils.CloseableIterable; +import io.delta.kernel.utils.CloseableIterator; +import io.delta.kernel.utils.DataFileStatus; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nullable; +import org.apache.beam.sdk.values.Row; +import org.joda.time.Instant; + +/** Utility class for writing test commits (appends and CDC actions) to Delta tables in tests. */ +final class DeltaWriteTestUtils { + + private DeltaWriteTestUtils() {} + + private static final StructType CDC_ACTION_SCHEMA = + new StructType() + .add("path", StringType.STRING, false) + .add("partitionValues", new MapType(StringType.STRING, StringType.STRING, false), false) + .add("size", LongType.LONG, false) + .add("dataChange", BooleanType.BOOLEAN, false); + + private static StructType getCustomSingleActionSchema() { + StructType originalSchema = io.delta.kernel.internal.actions.SingleAction.FULL_SCHEMA; + List fields = new ArrayList<>(); + for (StructField field : originalSchema.fields()) { + if (field.getName().equals("cdc")) { + fields.add(new StructField("cdc", CDC_ACTION_SCHEMA, true)); + } else { + fields.add(field); + } + } + return new StructType(fields); + } + + private static io.delta.kernel.data.Row createSingleAction( + StructType customSingleActionSchema, String actionName, io.delta.kernel.data.Row actionRow) { + Map values = new HashMap<>(); + values.put(customSingleActionSchema.indexOf(actionName), actionRow); + return new GenericRow(customSingleActionSchema, values); + } + + private static io.delta.kernel.data.Row createRemoveAction( + StructType removeSchema, String path, long deletionTimestamp) { + Map values = new HashMap<>(); + values.put(removeSchema.indexOf("path"), path); + values.put(removeSchema.indexOf("deletionTimestamp"), deletionTimestamp); + values.put(removeSchema.indexOf("dataChange"), true); + values.put(removeSchema.indexOf("size"), 100L); + return new GenericRow(removeSchema, values); + } + + private static io.delta.kernel.data.Row createCdcAction( + StructType cdcSchema, String path, long size) { + Map values = new HashMap<>(); + values.put(cdcSchema.indexOf("path"), path); + values.put( + cdcSchema.indexOf("partitionValues"), + io.delta.kernel.internal.util.VectorUtils.stringStringMapValue(Collections.emptyMap())); + values.put(cdcSchema.indexOf("size"), size); + values.put(cdcSchema.indexOf("dataChange"), true); + return new GenericRow(cdcSchema, values); + } + + private static ColumnVector createColumnVector( + List rows, int fieldIndex, DataType dataType) { + return new ColumnVector() { + @Override + public DataType getDataType() { + return dataType; + } + + @Override + public int getSize() { + return rows.size(); + } + + @Override + public void close() {} + + @Override + public boolean isNullAt(int rowId) { + return rows.get(rowId).getValue(fieldIndex) == null; + } + + @Override + public boolean getBoolean(int rowId) { + return rows.get(rowId).getBoolean(fieldIndex); + } + + @Override + public int getInt(int rowId) { + return rows.get(rowId).getInt32(fieldIndex); + } + + @Override + public long getLong(int rowId) { + if (dataType instanceof TimestampType) { + Instant instant = rows.get(rowId).getDateTime(fieldIndex).toInstant(); + return instant.getMillis() * 1000L; + } + return rows.get(rowId).getInt64(fieldIndex); + } + + @Override + public String getString(int rowId) { + return rows.get(rowId).getString(fieldIndex); + } + }; + } + + /** + * Writes a Delta commit containing append actions. + * + * @param engine the Delta Lake {@link Engine} instance to use + * @param tablePath the path of the Delta table to write to + * @param expectedVersion the expected version of the commit to be created + * @param timestamp the timestamp of the commit file + * @param deltaSchema the schema of the Delta table + * @param beamRows the rows to write + * @return the list of names of the written Parquet data files + * @throws Exception if any error occurs during write or commit + */ + static List writeAppendCommit( + Engine engine, + String tablePath, + long expectedVersion, + long timestamp, + StructType deltaSchema, + List beamRows) + throws Exception { + + Table table = Table.forPath(engine, tablePath); + TransactionBuilder txnBuilder = + table.createTransactionBuilder(engine, "DeltaTestUtils", Operation.WRITE); + if (expectedVersion == 0) { + txnBuilder = + txnBuilder + .withSchema(engine, deltaSchema) + .withTableProperties( + engine, Collections.singletonMap("delta.enableChangeDataFeed", "true")); + } + Transaction txn = txnBuilder.build(engine); + io.delta.kernel.data.Row txnState = txn.getTransactionState(engine); + + ColumnVector[] vectors = new ColumnVector[deltaSchema.fields().size()]; + for (int i = 0; i < deltaSchema.fields().size(); i++) { + StructField field = deltaSchema.fields().get(i); + vectors[i] = createColumnVector(beamRows, i, field.getDataType()); + } + ColumnarBatch columnarBatch = new DefaultColumnarBatch(beamRows.size(), deltaSchema, vectors); + FilteredColumnarBatch filteredBatch = + new FilteredColumnarBatch(columnarBatch, Optional.empty()); + CloseableIterator data = + io.delta.kernel.internal.util.Utils.toCloseableIterator( + Collections.singletonList(filteredBatch).iterator()); + CloseableIterator physicalData = + Transaction.transformLogicalData(engine, txnState, data, Collections.emptyMap()); + DataWriteContext writeContext = + Transaction.getWriteContext(engine, txnState, Collections.emptyMap()); + CloseableIterator dataFiles = + engine + .getParquetHandler() + .writeParquetFiles( + writeContext.getTargetDirectory(), + physicalData, + writeContext.getStatisticsColumns()); + + List writtenFiles = new ArrayList<>(); + List filesList = new ArrayList<>(); + while (dataFiles.hasNext()) { + DataFileStatus file = dataFiles.next(); + filesList.add(file); + writtenFiles.add(new File(file.getPath()).getName()); + } + CloseableIterator dataFilesCopy = + io.delta.kernel.internal.util.Utils.toCloseableIterator(filesList.iterator()); + + CloseableIterator dataActions = + Transaction.generateAppendActions(engine, txnState, dataFilesCopy, writeContext); + + TransactionCommitResult result = + txn.commit(engine, CloseableIterable.inMemoryIterable(dataActions)); + org.junit.Assert.assertEquals(expectedVersion, result.getVersion()); + if (!tablePath.startsWith("gs://") + && !tablePath.startsWith("s3://") + && !tablePath.startsWith("hdfs://")) { + File commitFile = + new File(new File(tablePath, "_delta_log"), String.format("%020d.json", expectedVersion)); + commitFile.setLastModified(timestamp); + } + return writtenFiles; + } + + /** + * Writes a Delta commit containing CDC actions (simulating updates/deletes). + * + *

Note on why this is manual: In a standard Spark or Flink writer, setting the table property + * {@code "delta.enableChangeDataFeed" = "true"} automatically instructs the engine to compute and + * write the change data files to {@code _change_data/} and append the {@code cdc} actions to the + * commit log whenever DML statements (like UPDATE/DELETE) are executed. + * + *

However, we are using the Delta Lake Kernel API which does not contain an SQL execution + * engine or a DML parser. Thus, it cannot automatically compute which rows were deleted or + * updated. To generate a realistic integration test dataset, we must manually construct these + * change records, write them into the GCS {@code _change_data/} directory using the low-level + * parquet handler, and manually register them as {@code cdc} actions in the committed + * transaction. + * + * @param engine the Delta Lake {@link Engine} instance to use + * @param tablePath the path of the Delta table to write to + * @param expectedVersion the expected version of the commit to be created + * @param timestamp the timestamp of the commit file + * @param deltaSchema the schema of the Delta table + * @param addBeamRows the optional list of rows to add in this commit + * @param removePath the optional path of the file to remove in this commit + * @param cdcBeamRows the optional list of CDC rows to write + * @param cdcWriteSchema the schema used for writing the CDC files + * @throws Exception if any error occurs during write or commit + */ + static void writeCdcCommit( + Engine engine, + String tablePath, + long expectedVersion, + long timestamp, + StructType deltaSchema, + @Nullable List addBeamRows, + @Nullable String removePath, + @Nullable List cdcBeamRows, + StructType cdcWriteSchema) + throws Exception { + + Table table = Table.forPath(engine, tablePath); + TransactionBuilder txnBuilder = + table.createTransactionBuilder(engine, "DeltaTestUtils", Operation.WRITE); + Transaction txn = txnBuilder.build(engine); + io.delta.kernel.data.Row txnState = txn.getTransactionState(engine); + + StructType customSingleActionSchema = getCustomSingleActionSchema(); + List commitActions = new ArrayList<>(); + + if (addBeamRows != null && !addBeamRows.isEmpty()) { + ColumnVector[] vectors = new ColumnVector[deltaSchema.fields().size()]; + for (int i = 0; i < deltaSchema.fields().size(); i++) { + StructField field = deltaSchema.fields().get(i); + vectors[i] = createColumnVector(addBeamRows, i, field.getDataType()); + } + ColumnarBatch columnarBatch = + new DefaultColumnarBatch(addBeamRows.size(), deltaSchema, vectors); + FilteredColumnarBatch filteredBatch = + new FilteredColumnarBatch(columnarBatch, Optional.empty()); + CloseableIterator data = + io.delta.kernel.internal.util.Utils.toCloseableIterator( + Collections.singletonList(filteredBatch).iterator()); + CloseableIterator physicalData = + Transaction.transformLogicalData(engine, txnState, data, Collections.emptyMap()); + DataWriteContext writeContext = + Transaction.getWriteContext(engine, txnState, Collections.emptyMap()); + CloseableIterator dataFiles = + engine + .getParquetHandler() + .writeParquetFiles( + writeContext.getTargetDirectory(), + physicalData, + writeContext.getStatisticsColumns()); + CloseableIterator addActions = + Transaction.generateAppendActions(engine, txnState, dataFiles, writeContext); + while (addActions.hasNext()) { + commitActions.add(addActions.next()); + } + } + + if (removePath != null) { + StructType removeSchema = + (StructType) + io.delta.kernel.internal.actions.SingleAction.FULL_SCHEMA + .fields() + .get(io.delta.kernel.internal.actions.SingleAction.REMOVE_FILE_ORDINAL) + .getDataType(); + io.delta.kernel.data.Row removeAction = + createRemoveAction(removeSchema, removePath, timestamp); + commitActions.add(createSingleAction(customSingleActionSchema, "remove", removeAction)); + } + + if (cdcBeamRows != null && !cdcBeamRows.isEmpty()) { + ColumnVector[] vectors = new ColumnVector[cdcWriteSchema.fields().size()]; + for (int i = 0; i < cdcWriteSchema.fields().size(); i++) { + StructField field = cdcWriteSchema.fields().get(i); + vectors[i] = createColumnVector(cdcBeamRows, i, field.getDataType()); + } + ColumnarBatch columnarBatch = + new DefaultColumnarBatch(cdcBeamRows.size(), cdcWriteSchema, vectors); + FilteredColumnarBatch filteredBatch = + new FilteredColumnarBatch(columnarBatch, Optional.empty()); + CloseableIterator data = + io.delta.kernel.internal.util.Utils.toCloseableIterator( + Collections.singletonList(filteredBatch).iterator()); + + String cdcDir = new org.apache.hadoop.fs.Path(tablePath, "_change_data").toString(); + + CloseableIterator cdcFiles = + engine.getParquetHandler().writeParquetFiles(cdcDir, data, Collections.emptyList()); + + StructType cdcActionSchema = CDC_ACTION_SCHEMA; + while (cdcFiles.hasNext()) { + DataFileStatus cdcFile = cdcFiles.next(); + String relativeCdcPath = "_change_data/" + new File(cdcFile.getPath()).getName(); + io.delta.kernel.data.Row cdcAction = + createCdcAction(cdcActionSchema, relativeCdcPath, cdcFile.getSize()); + commitActions.add(createSingleAction(customSingleActionSchema, "cdc", cdcAction)); + } + } + + TransactionCommitResult result = + txn.commit( + engine, + CloseableIterable.inMemoryIterable( + io.delta.kernel.internal.util.Utils.toCloseableIterator(commitActions.iterator()))); + org.junit.Assert.assertEquals(expectedVersion, result.getVersion()); + if (!tablePath.startsWith("gs://") + && !tablePath.startsWith("s3://") + && !tablePath.startsWith("hdfs://")) { + File commitFile = + new File(new File(tablePath, "_delta_log"), String.format("%020d.json", expectedVersion)); + commitFile.setLastModified(timestamp); + } + } +} 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() {} + } +} diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index 1045ad4aeed0..a0c2930c1ff9 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. @@ -65,6 +65,7 @@ configurations.runtimeClasspath { 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' + resolutionStrategy.force 'org.apache.hadoop:hadoop-aws:3.3.6' } shadowJar { @@ -72,7 +73,6 @@ shadowJar { attributes(["Multi-Release": true]) } mergeServiceFiles() - outputs.upToDateWhen { false } } description = "Apache Beam :: SDKs :: Java :: IO :: Expansion Service" @@ -96,6 +96,9 @@ 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 project(":sdks:java:io:delta") runtimeOnly library.java.kafka_clients runtimeOnly library.java.slf4j_jdk14 diff --git a/sdks/java/io/google-cloud-platform/build.gradle b/sdks/java/io/google-cloud-platform/build.gradle index 2686297c0001..23859ded50e0 100644 --- a/sdks/java/io/google-cloud-platform/build.gradle +++ b/sdks/java/io/google-cloud-platform/build.gradle @@ -30,8 +30,14 @@ applyJavaNature( description = "Apache Beam :: SDKs :: Java :: IO :: Google Cloud Platform" ext.summary = "IO library to read and write Google Cloud Platform systems from Beam." +tasks.withType(Test).configureEach { + // Open java.nio for Apache Arrow direct memory access under Java 17+ + jvmArgs '--add-opens=java.base/java.nio=ALL-UNNAMED' +} + dependencies { implementation(enforcedPlatform(library.java.google_cloud_platform_libraries_bom)) + implementation platform(library.java.opentelemetry_bom) implementation project(path: ":model:pipeline", configuration: "shadow") implementation project(":runners:core-java") implementation project(path: ":sdks:java:core", configuration: "shadow") @@ -41,6 +47,7 @@ dependencies { permitUnusedDeclared project(":sdks:java:expansion-service") // BEAM-11761 implementation project(":sdks:java:extensions:google-cloud-platform-core") implementation project(":sdks:java:extensions:protobuf") + implementation project(":sdks:java:io:components") implementation project(":sdks:java:extensions:arrow") implementation library.java.avro implementation library.java.bigdataoss_util @@ -58,6 +65,8 @@ dependencies { implementation library.java.google_api_client implementation library.java.google_api_common implementation library.java.google_api_services_bigquery + implementation library.java.opentelemetry_api + implementation library.java.opentelemetry_context implementation library.java.google_api_services_healthcare implementation library.java.google_api_services_pubsub implementation library.java.google_api_services_storage @@ -274,7 +283,8 @@ task bigQueryEarlyRolloutIntegrationTest(type: Test, dependsOn: processTestResou include '**/StorageApiDirectWriteProtosIT.class' include '**/StorageApiSinkFailedRowsIT.class' include '**/StorageApiSinkRowUpdateIT.class' - include '**/StorageApiSinkSchemaUpdateIT.class' + include '**/StorageApiSinkSchemaUpdateWithInputSchemaIT.class' + include '**/StorageApiSinkSchemaUpdateWithoutInputSchemaIT.class' include '**/TableRowToStorageApiProtoIT.class' // file loads include '**/BigQuerySchemaUpdateOptionsIT.class' diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIO.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIO.java index b222b358f547..c2ac4efb5b5d 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIO.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIO.java @@ -2105,9 +2105,13 @@ public static void readSource( // the same order. BoundedSource.BoundedReader reader = streamSource.createReader(options); + T current = null; + boolean hasCurrent = false; try { if (reader.start()) { - outputReceiver.get(rowTag).output(reader.getCurrent()); + current = + java.util.Objects.requireNonNull(reader.getCurrent(), "Reader returned null element"); + hasCurrent = true; } else { return; } @@ -2120,11 +2124,17 @@ public static void readSource( (Exception) e.getCause(), "Unable to parse record reading from BigQuery"); } + if (hasCurrent) { + outputReceiver.get(rowTag).output(current); + } while (true) { + current = null; + hasCurrent = false; try { if (reader.advance()) { - outputReceiver.get(rowTag).output(reader.getCurrent()); + current = reader.getCurrent(); + hasCurrent = true; } else { return; } @@ -2137,6 +2147,9 @@ public static void readSource( (Exception) e.getCause(), "Unable to parse record reading from BigQuery"); } + if (hasCurrent) { + outputReceiver.get(rowTag).output(current); + } } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryUtils.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryUtils.java index d9805a6f4e06..5ba2d17c127a 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryUtils.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryUtils.java @@ -932,6 +932,12 @@ public static Row toBeamRow(Schema rowSchema, TableSchema bqSchema, TableRow jso return java.time.Instant.parse(jsonBQString); } } else if (fieldType.isLogicalType(Timestamp.IDENTIFIER)) { + if (!jsonBQString.contains("UTC")) { + BigDecimal bd = new BigDecimal(jsonBQString); + long seconds = bd.longValue(); + long nanos = bd.subtract(BigDecimal.valueOf(seconds)).movePointRight(9).longValue(); + return java.time.Instant.ofEpochSecond(seconds, nanos); + } return VAR_PRECISION_FORMATTER.parse(jsonBQString, java.time.Instant::from); } } 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 2dfc8b2f1c00..e639b5812d24 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 @@ -792,9 +792,17 @@ long flush( // Maximum number of times we retry before we fail the work item. if (failedContext.failureCount > allowedRetry) { - throw new RuntimeException( + String errorMessage = String.format( - "More than %d attempts to call AppendRows failed.", allowedRetry)); + "More than %d attempts to call AppendRows failed. Last encountered error: %s", + allowedRetry, error != null ? error.toString() : "unknown"); + if (statusCode == Status.Code.PERMISSION_DENIED + || statusCode == Status.Code.NOT_FOUND) { + errorMessage += + ". Please check if the destination table exists and if the service account has the " + + "bigquery.tables.updateData permission."; + } + throw new RuntimeException(errorMessage, error); } // The following errors are known to be persistent, so always fail the work item in 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 b644d7aa752c..188f393dbe8c 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 @@ -1066,7 +1066,25 @@ public void process( if (numAppends > 0) { initializeContexts.accept(contexts); - retryManager.run(true); + try { + retryManager.run(true); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw e; + } catch (Exception e) { + Status.Code statusCode = Status.fromThrowable(e).getCode(); + String errorMessage = + String.format( + "More than %d attempts to call AppendRows failed. Last encountered error: %s", + maxRetries, e.toString()); + if (statusCode == Status.Code.PERMISSION_DENIED + || statusCode == Status.Code.NOT_FOUND) { + errorMessage += + ". Please check if the destination table exists and if the service account has the " + + "bigquery.tables.updateData permission."; + } + throw new RuntimeException(errorMessage, e); + } appendSplitDistribution.update(numAppends); if (autoUpdateSchema) { 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/main/java/org/apache/beam/sdk/io/gcp/datastore/AdaptiveThrottler.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/AdaptiveThrottler.java deleted file mode 100644 index 15c4e678bcb4..000000000000 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/AdaptiveThrottler.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.sdk.io.gcp.datastore; - -import java.util.Random; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.util.MovingFunction; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; - -/** - * An implementation of client-side adaptive throttling. See - * https://landing.google.com/sre/book/chapters/handling-overload.html#client-side-throttling-a7sYUg - * for a full discussion of the use case and algorithm applied. - */ -class AdaptiveThrottler { - private final MovingFunction successfulRequests; - private final MovingFunction allRequests; - - /** - * The target ratio between requests sent and successful requests. This is "K" in the formula in - * https://landing.google.com/sre/book/chapters/handling-overload.html - */ - private final double overloadRatio; - - /** - * The target minimum number of requests per samplePeriodMs, even if no requests succeed. Must be - * greater than 0, else we could throttle to zero. Because every decision is probabilistic, there - * is no guarantee that the request rate in any given interval will not be zero. (This is the +1 - * from the formula in https://landing.google.com/sre/book/chapters/handling-overload.html - */ - private static final double MIN_REQUESTS = 1; - - private final Random random; - - /** - * @param samplePeriodMs the time window to keep of request history to inform throttling - * decisions. - * @param sampleUpdateMs the length of buckets within this time window. - * @param overloadRatio the target ratio between requests sent and successful requests. You should - * always set this to more than 1, otherwise the client would never try to send more requests - * than succeeded in the past - so it could never recover from temporary setbacks. - */ - public AdaptiveThrottler(long samplePeriodMs, long sampleUpdateMs, double overloadRatio) { - this(samplePeriodMs, sampleUpdateMs, overloadRatio, new Random()); - } - - @VisibleForTesting - AdaptiveThrottler(long samplePeriodMs, long sampleUpdateMs, double overloadRatio, Random random) { - allRequests = - new MovingFunction( - samplePeriodMs, - sampleUpdateMs, - 1 /* numSignificantBuckets */, - 1 /* numSignificantSamples */, - Sum.ofLongs()); - successfulRequests = - new MovingFunction( - samplePeriodMs, - sampleUpdateMs, - 1 /* numSignificantBuckets */, - 1 /* numSignificantSamples */, - Sum.ofLongs()); - this.overloadRatio = overloadRatio; - this.random = random; - } - - @VisibleForTesting - double throttlingProbability(long nowMsSinceEpoch) { - if (!allRequests.isSignificant()) { - return 0; - } - long allRequestsNow = allRequests.get(nowMsSinceEpoch); - long successfulRequestsNow = successfulRequests.get(nowMsSinceEpoch); - return Math.max( - 0, - (allRequestsNow - overloadRatio * successfulRequestsNow) / (allRequestsNow + MIN_REQUESTS)); - } - - /** - * Call this before sending a request to the remote service; if this returns true, drop the - * request (treating it as a failure or trying it again at a later time). - */ - public boolean throttleRequest(long nowMsSinceEpoch) { - double delayProbability = throttlingProbability(nowMsSinceEpoch); - // Note that we increment the count of all requests here, even if we return true - so even if we - // tell the client not to send a request at all, it still counts as a failed request. - allRequests.add(nowMsSinceEpoch, 1); - - return (random.nextDouble() < delayProbability); - } - - /** Call this after {@link throttleRequest} if your request was successful. */ - public void successfulRequest(long nowMsSinceEpoch) { - successfulRequests.add(nowMsSinceEpoch, 1); - } -} diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/DatastoreV1.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/DatastoreV1.java index c9507475648d..6966de5b484b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/DatastoreV1.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/DatastoreV1.java @@ -77,6 +77,7 @@ import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; import org.apache.beam.sdk.extensions.gcp.util.RetryHttpRequestInitializer; +import org.apache.beam.sdk.io.components.throttling.AdaptiveThrottler; import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Distribution; import org.apache.beam.sdk.metrics.Metrics; diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/healthcare/FhirIO.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/healthcare/FhirIO.java index 70be676c75fe..1162db9e1a28 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/healthcare/FhirIO.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/healthcare/FhirIO.java @@ -602,8 +602,9 @@ public void instantiateHealthcareClient() throws IOException { @ProcessElement public void processElement(ProcessContext context) { String resourceId = context.element(); + String resource = null; try { - context.output(fetchResource(this.client, resourceId)); + resource = java.util.Objects.requireNonNull(fetchResource(this.client, resourceId)); } catch (Exception e) { READ_RESOURCE_ERRORS.inc(); LOG.warn( @@ -612,6 +613,9 @@ public void processElement(ProcessContext context) { e); context.output(FhirIO.Read.DEAD_LETTER, HealthcareIOError.of(resourceId, e)); } + if (resource != null) { + context.output(resource); + } } private String fetchResource(HealthcareApiClient client, String resourceName) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/healthcare/HL7v2IO.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/healthcare/HL7v2IO.java index 3647ef7671eb..496b39a923f7 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/healthcare/HL7v2IO.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/healthcare/HL7v2IO.java @@ -365,11 +365,15 @@ public void instantiateHealthcareClient() throws IOException { @ProcessElement public void processElement(ProcessContext context) { String msgId = context.element(); + HL7v2Message message = null; try { - context.output(client.fetchMessage(msgId)); + message = java.util.Objects.requireNonNull(client.fetchMessage(msgId)); } catch (Exception e) { context.output(HL7v2IO.Read.DEAD_LETTER, HealthcareIOError.of(msgId, e)); } + if (message != null) { + context.output(message); + } } } } @@ -487,15 +491,20 @@ public void instantiateHealthcareClient() throws IOException { @ProcessElement public void processElement(ProcessContext context) { String msgId = context.element().getHl7v2MessageId(); + HL7v2ReadResponse response = null; try { - HL7v2ReadResponse response = - HL7v2ReadResponse.of(context.element().getMetadata(), client.fetchMessage(msgId)); - context.output(response); + response = + java.util.Objects.requireNonNull( + HL7v2ReadResponse.of( + context.element().getMetadata(), client.fetchMessage(msgId))); } catch (Exception e) { HealthcareIOError error = HealthcareIOError.of(context.element(), e); context.output(HL7v2IO.HL7v2Read.DEAD_LETTER, error); } + if (response != null) { + context.output(response); + } } } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/AddTimestampAttribute.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/AddTimestampAttribute.java index 0ecafc82f90b..2fd3032c0da1 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/AddTimestampAttribute.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/AddTimestampAttribute.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.gcp.pubsub; import static org.apache.beam.sdk.io.gcp.pubsub.PubsubMessageToRow.TIMESTAMP_FIELD; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; import org.apache.beam.sdk.schemas.transforms.DropFields; import org.apache.beam.sdk.transforms.PTransform; @@ -31,9 +32,6 @@ * Adds a timestamp attribute if desired and filters it out of the underlying row if no timestamp * attribute exists. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) class AddTimestampAttribute extends PTransform, PCollection> { private static final Logger LOG = LoggerFactory.getLogger(AddTimestampAttribute.class); private final boolean useTimestampAttribute; @@ -48,7 +46,14 @@ public PCollection expand(PCollection input) { // element's event time. PubSubIO will populate the attribute from there. PCollection withTimestamp = useTimestampAttribute - ? input.apply(WithTimestamps.of((row) -> row.getDateTime(TIMESTAMP_FIELD).toInstant())) + ? input.apply( + WithTimestamps.of( + (row) -> + checkArgumentNotNull( + row.getDateTime(TIMESTAMP_FIELD), + "Field '%s' must be present and non-null in the input row when writing to PubSub with a timestamp attribute.", + TIMESTAMP_FIELD) + .toInstant())) : input; PCollection rows; 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..3bcc83aed78b 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 @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; + import com.google.auto.service.AutoService; import java.util.Map; import org.apache.beam.sdk.expansion.ExternalTransformRegistrar; @@ -35,9 +37,6 @@ /** Exposes {@link PubsubIO.Write} as an external transform for cross-language usage. */ @AutoService(ExternalTransformRegistrar.class) -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public final class ExternalWrite implements ExternalTransformRegistrar { public ExternalWrite() {} @@ -50,9 +49,10 @@ public ExternalWrite() {} /** Parameters class to expose the transform to an external SDK. */ public static class Configuration { - private String topic; + private @Nullable String topic; private @Nullable String idAttribute; private @Nullable String timestampAttribute; + private boolean publishWithOrderingKey = false; public void setTopic(String topic) { this.topic = topic; @@ -65,6 +65,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 @@ -75,8 +79,9 @@ public WriteBuilder() {} public PTransform, PDone> buildExternal(Configuration config) { PubsubIO.Write.Builder writeBuilder = PubsubIO.Write.newBuilder(new ParsePubsubMessageProtoAsPayloadFromWindowedValue()); - if (config.topic != null) { - StaticValueProvider topic = StaticValueProvider.of(config.topic); + String topicStr = config.topic; + if (topicStr != null) { + StaticValueProvider topic = StaticValueProvider.of(topicStr); writeBuilder.setTopicProvider(NestedValueProvider.of(topic, PubsubTopic::fromPath)); } if (config.idAttribute != null) { @@ -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(); } @@ -96,7 +102,8 @@ public static class ParsePubsubMessageProtoAsPayloadFromWindowedValue @Override public PubsubMessage apply(ValueInSingleWindow input) { - return INNER.apply(input.getValue()); + return INNER.apply( + checkArgumentNotNull(input.getValue(), "Windowed value input cannot be null")); } } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/NestedRowToMessage.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/NestedRowToMessage.java index 9c56410cda6b..50325f7b39e2 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/NestedRowToMessage.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/NestedRowToMessage.java @@ -22,6 +22,7 @@ import static org.apache.beam.sdk.io.gcp.pubsub.PubsubSchemaIOProvider.ATTRIBUTE_ARRAY_FIELD_TYPE; import static org.apache.beam.sdk.io.gcp.pubsub.PubsubSchemaIOProvider.ATTRIBUTE_MAP_FIELD_TYPE; 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.Collection; @@ -34,16 +35,17 @@ import org.apache.beam.sdk.transforms.SimpleFunction; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.checkerframework.checker.nullness.qual.Nullable; class NestedRowToMessage extends SimpleFunction { private static final long serialVersionUID = 65176815766314684L; - private final PayloadSerializer serializer; + private final @Nullable PayloadSerializer serializer; private final SerializableFunction> attributesExtractor; private final SerializableFunction payloadExtractor; @SuppressWarnings("methodref.receiver.bound") - NestedRowToMessage(PayloadSerializer serializer, Schema schema) { + NestedRowToMessage(@Nullable PayloadSerializer serializer, Schema schema) { this.serializer = serializer; if (schema.getField(ATTRIBUTES_FIELD).getType().equals(ATTRIBUTE_MAP_FIELD_TYPE)) { attributesExtractor = NestedRowToMessage::getAttributesFromMap; @@ -81,7 +83,7 @@ private static byte[] getPayloadFromBytes(Row row) { } private byte[] getPayloadFromNested(Row row) { - return serializer.serialize(checkArgumentNotNull(row.getRow(PAYLOAD_FIELD))); + return checkStateNotNull(serializer).serialize(checkArgumentNotNull(row.getRow(PAYLOAD_FIELD))); } @Override diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubSubPayloadTranslation.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubSubPayloadTranslation.java index b77928bd957e..eec590a480f7 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubSubPayloadTranslation.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubSubPayloadTranslation.java @@ -19,6 +19,7 @@ import static org.apache.beam.sdk.io.gcp.pubsub.PubsubIO.ENABLE_CUSTOM_PUBSUB_SINK; import static org.apache.beam.sdk.io.gcp.pubsub.PubsubIO.ENABLE_CUSTOM_PUBSUB_SOURCE; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import com.google.auto.service.AutoService; import java.util.Collections; @@ -37,16 +38,13 @@ import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider; import org.apache.beam.sdk.runners.AppliedPTransform; import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.Preconditions; import org.apache.beam.sdk.util.construction.PTransformTranslation; import org.apache.beam.sdk.util.construction.PTransformTranslation.TransformPayloadTranslator; import org.apache.beam.sdk.util.construction.SdkComponents; import org.apache.beam.sdk.util.construction.TransformPayloadTranslatorRegistrar; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.checkerframework.checker.nullness.qual.Nullable; -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) /** * Utility methods for translating a {@link Unbounded} which reads from {@link * PubsubUnboundedSource} to {@link RunnerApi} representations. @@ -61,7 +59,7 @@ public String getUrn() { } @Override - public RunnerApi.FunctionSpec translate( + public RunnerApi.@Nullable FunctionSpec translate( AppliedPTransform> transform, SdkComponents components) { if (ExperimentalOptions.hasExperiment( transform.getPipeline().getOptions(), ENABLE_CUSTOM_PUBSUB_SOURCE)) { @@ -93,11 +91,13 @@ public RunnerApi.FunctionSpec translate( } } - if (pubsubUnboundedSource.getTimestampAttribute() != null) { - payloadBuilder.setTimestampAttribute(pubsubUnboundedSource.getTimestampAttribute()); + String timestampAttribute = pubsubUnboundedSource.getTimestampAttribute(); + if (timestampAttribute != null) { + payloadBuilder.setTimestampAttribute(timestampAttribute); } - if (pubsubUnboundedSource.getIdAttribute() != null) { - payloadBuilder.setIdAttribute(pubsubUnboundedSource.getIdAttribute()); + String idAttribute = pubsubUnboundedSource.getIdAttribute(); + if (idAttribute != null) { + payloadBuilder.setIdAttribute(idAttribute); } payloadBuilder.setWithAttributes( pubsubUnboundedSource.getNeedsAttributes() || pubsubUnboundedSource.getNeedsMessageId()); @@ -116,7 +116,7 @@ public String getUrn() { } @Override - public RunnerApi.FunctionSpec translate( + public RunnerApi.@Nullable FunctionSpec translate( AppliedPTransform transform, SdkComponents components) { if (ExperimentalOptions.hasExperiment( @@ -125,19 +125,20 @@ public RunnerApi.FunctionSpec translate( } PubSubWritePayload.Builder payloadBuilder = PubSubWritePayload.newBuilder(); ValueProvider topicProvider = - Preconditions.checkStateNotNull(transform.getTransform().outer.getTopicProvider()); + checkStateNotNull(transform.getTransform().outer.getTopicProvider()); if (topicProvider.isAccessible()) { payloadBuilder.setTopic(topicProvider.get().getFullPath()); } else { payloadBuilder.setTopicRuntimeOverridden( ((NestedValueProvider) topicProvider).propertyName()); } - if (transform.getTransform().outer.getTimestampAttribute() != null) { - payloadBuilder.setTimestampAttribute( - transform.getTransform().outer.getTimestampAttribute()); + String timestampAttribute = transform.getTransform().outer.getTimestampAttribute(); + if (timestampAttribute != null) { + payloadBuilder.setTimestampAttribute(timestampAttribute); } - if (transform.getTransform().outer.getIdAttribute() != null) { - payloadBuilder.setIdAttribute(transform.getTransform().outer.getIdAttribute()); + String idAttribute = transform.getTransform().outer.getIdAttribute(); + if (idAttribute != null) { + payloadBuilder.setIdAttribute(idAttribute); } return FunctionSpec.newBuilder() .setUrn(getUrn(transform.getTransform())) @@ -154,7 +155,7 @@ public String getUrn() { } @Override - public RunnerApi.FunctionSpec translate( + public RunnerApi.@Nullable FunctionSpec translate( AppliedPTransform transform, SdkComponents components) { if (ExperimentalOptions.hasExperiment( @@ -162,12 +163,13 @@ public RunnerApi.FunctionSpec translate( return null; } PubSubWritePayload.Builder payloadBuilder = PubSubWritePayload.newBuilder(); - if (transform.getTransform().outer.getTimestampAttribute() != null) { - payloadBuilder.setTimestampAttribute( - transform.getTransform().outer.getTimestampAttribute()); + String timestampAttribute = transform.getTransform().outer.getTimestampAttribute(); + if (timestampAttribute != null) { + payloadBuilder.setTimestampAttribute(timestampAttribute); } - if (transform.getTransform().outer.getIdAttribute() != null) { - payloadBuilder.setIdAttribute(transform.getTransform().outer.getIdAttribute()); + String idAttribute = transform.getTransform().outer.getIdAttribute(); + if (idAttribute != null) { + payloadBuilder.setIdAttribute(idAttribute); } return FunctionSpec.newBuilder() .setUrn(getUrn(transform.getTransform())) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClient.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClient.java index 333497462177..3f4d5765b66c 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClient.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClient.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +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 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; @@ -40,13 +42,11 @@ 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.Nullable; +import org.checkerframework.dataflow.qual.Pure; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** An (abstract) helper class for talking to Pubsub via an underlying transport. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public abstract class PubsubClient implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(PubsubClient.class); private static final Map> @@ -82,7 +82,7 @@ PubsubClient newClient( * Return timestamp as ms-since-unix-epoch corresponding to {@code timestamp}. Throw {@link * IllegalArgumentException} if timestamp cannot be recognized. */ - protected static Long parseTimestampAsMsSinceEpoch(String timestamp) { + protected static long parseTimestampAsMsSinceEpoch(String timestamp) { if (timestamp.isEmpty()) { throw new IllegalArgumentException("Empty timestamp."); } @@ -113,17 +113,12 @@ protected static long extractTimestampAttribute( String timestampAttribute, @Nullable Map attributes) { Preconditions.checkState(!timestampAttribute.isEmpty()); String value = attributes == null ? null : attributes.get(timestampAttribute); - checkArgument( - value != null, - "PubSub message is missing a value for timestamp attribute %s", - timestampAttribute); - Long timestampMsSinceEpoch = parseTimestampAsMsSinceEpoch(value); - checkArgument( - timestampMsSinceEpoch != null, - "Cannot interpret value of attribute %s as timestamp: %s", - timestampAttribute, - value); - return timestampMsSinceEpoch; + String nonNullValue = + checkArgumentNotNull( + value, + "PubSub message is missing a value for timestamp attribute %s", + timestampAttribute); + return parseTimestampAsMsSinceEpoch(nonNullValue); } /** Path representing a cloud project id. */ @@ -386,17 +381,21 @@ public static TopicPath topicPathFromName(String projectId, String topicName) { public abstract static class OutgoingMessage implements Serializable { /** Underlying Message. May not have publish timestamp set. */ + @Pure public abstract PubsubMessage getMessage(); /** Timestamp for element (ms since epoch). */ + @Pure public abstract long getTimestampMsSinceEpoch(); /** * If using an id attribute, the record id to associate with this record's metadata so the * receiver can reject duplicates. Otherwise {@literal null}. */ + @Pure public abstract @Nullable String recordId(); + @Pure public abstract @Nullable String topic(); public static OutgoingMessage of( @@ -415,11 +414,13 @@ public static OutgoingMessage of( @Nullable String topic) { PubsubMessage.Builder builder = PubsubMessage.newBuilder().setData(ByteString.copyFrom(message.getPayload())); - if (message.getAttributeMap() != null) { - builder.putAllAttributes(message.getAttributeMap()); + Map attributeMap = message.getAttributeMap(); + if (attributeMap != null) { + builder.putAllAttributes(attributeMap); } - if (message.getOrderingKey() != null) { - builder.setOrderingKey(message.getOrderingKey()); + String orderingKey = message.getOrderingKey(); + if (orderingKey != null) { + builder.setOrderingKey(orderingKey); } return of(builder.build(), timestampMsSinceEpoch, recordId, topic); } @@ -435,21 +436,23 @@ public static OutgoingMessage of( public abstract static class IncomingMessage implements Serializable { /** Underlying Message. */ + @Pure public abstract PubsubMessage message(); - /** - * Timestamp for element (ms since epoch). Either Pubsub's processing time, or the custom - * timestamp associated with the message. - */ + /** Either Pubsub's processing time, or the custom timestamp associated with the message. */ + @Pure public abstract long timestampMsSinceEpoch(); /** Timestamp (in system time) at which we requested the message (ms since epoch). */ + @Pure public abstract long requestTimeMsSinceEpoch(); /** Id to pass back to Pubsub to acknowledge receipt of this message. */ + @Pure public abstract String ackId(); /** Id to pass to the runner to distinguish this message from all others. */ + @Pure public abstract String recordId(); public static IncomingMessage of( @@ -554,7 +557,7 @@ public abstract void createSchema( public abstract void deleteSchema(SchemaPath schemaPath) throws IOException; /** Return {@link SchemaPath} from {@link TopicPath} if exists. */ - public abstract SchemaPath getSchemaPath(TopicPath topicPath) throws IOException; + public abstract @Nullable SchemaPath getSchemaPath(TopicPath topicPath) throws IOException; /** Return a Beam {@link Schema} from the Pub/Sub schema resource, if exists. */ public abstract Schema getSchema(SchemaPath schemaPath) throws IOException; @@ -567,8 +570,10 @@ static Schema fromPubsubSchema(com.google.api.services.pubsub.model.Schema pubsu "Pub/Sub schema type %s is not supported at this time", pubsubSchema.getType())); } SerializableFunction definitionToSchemaFn = - schemaTypeToConversionFnMap.get(pubsubSchema.getType()); - return definitionToSchemaFn.apply(pubsubSchema.getDefinition()); + checkStateNotNull(schemaTypeToConversionFnMap.get(pubsubSchema.getType())); + String definition = + checkNotNull(pubsubSchema.getDefinition(), "Pub/Sub schema definition is null"); + return definitionToSchemaFn.apply(definition); } /** Convert a {@link com.google.pubsub.v1.Schema} to a Beam {@link Schema}. */ @@ -579,7 +584,7 @@ static Schema fromPubsubSchema(com.google.pubsub.v1.Schema pubsubSchema) { String.format("Pub/Sub schema type %s is not supported at this time", typeName)); } SerializableFunction definitionToSchemaFn = - schemaTypeToConversionFnMap.get(typeName); + checkStateNotNull(schemaTypeToConversionFnMap.get(typeName)); return definitionToSchemaFn.apply(pubsubSchema.getDefinition()); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubGrpcClient.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubGrpcClient.java index 7fb30a1099aa..42fb80b6b42b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubGrpcClient.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubGrpcClient.java @@ -17,7 +17,7 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import com.google.auth.Credentials; import com.google.protobuf.Timestamp; @@ -67,20 +67,17 @@ 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.base.Strings; 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.util.concurrent.ThreadFactoryBuilder; import org.checkerframework.checker.nullness.qual.Nullable; /** A helper class for talking to Pubsub via grpc. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubGrpcClient extends PubsubClient { private static final int LIST_BATCH_SIZE = 1000; private static final int DEFAULT_TIMEOUT_S = 60; + @SuppressWarnings("nullness") // netty requires ciphers(null) but is not annotated private static ManagedChannel channelForRootUrl(String urlString) throws IOException { String format = PubsubOptions.targetForRootUrl(urlString); @@ -104,7 +101,7 @@ public PubsubClient newClient( @Nullable String timestampAttribute, @Nullable String idAttribute, PubsubOptions options, - String rootUrlOverride) + @Nullable String rootUrlOverride) throws IOException { return new PubsubGrpcClient( timestampAttribute, @@ -144,9 +141,9 @@ public String getKind() { /** Cached stubs, or null if not cached. */ private PublisherGrpc.@Nullable PublisherBlockingStub cachedPublisherStub; - private SubscriberGrpc.SubscriberBlockingStub cachedSubscriberStub; + private SubscriberGrpc.@Nullable SubscriberBlockingStub cachedSubscriberStub; - private SchemaServiceGrpc.SchemaServiceBlockingStub cachedSchemaServiceStub; + private SchemaServiceGrpc.@Nullable SchemaServiceBlockingStub cachedSchemaServiceStub; @VisibleForTesting PubsubGrpcClient( @@ -189,7 +186,8 @@ public void close() { /** Return channel with interceptor for returning credentials. */ private Channel newChannel() throws IOException { - checkState(publisherChannel != null, "PubsubGrpcClient has been closed"); + ManagedChannel channel = + checkStateNotNull(publisherChannel, "PubsubGrpcClient has been closed"); ClientAuthInterceptor interceptor = new ClientAuthInterceptor( credentials, @@ -198,7 +196,7 @@ private Channel newChannel() throws IOException { .setDaemon(true) .setNameFormat("PubsubGrpcClient-thread") .build())); - return ClientInterceptors.intercept(publisherChannel, interceptor); + return ClientInterceptors.intercept(channel, interceptor); } /** Return a stub for making a publish request with a timeout. */ @@ -237,8 +235,9 @@ public int publish(TopicPath topic, List outgoingMessages) thro timestampAttribute, String.valueOf(outgoingMessage.getTimestampMsSinceEpoch())); } - if (idAttribute != null && !Strings.isNullOrEmpty(outgoingMessage.recordId())) { - message.putAttributes(idAttribute, outgoingMessage.recordId()); + String recordId = outgoingMessage.recordId(); + if (idAttribute != null && recordId != null && !recordId.isEmpty()) { + message.putAttributes(idAttribute, recordId); } request.addMessages(message); @@ -267,12 +266,18 @@ public List pull( } List incomingMessages = new ArrayList<>(response.getReceivedMessagesCount()); for (ReceivedMessage message : response.getReceivedMessagesList()) { + if (!message.hasMessage()) { + continue; + } PubsubMessage pubsubMessage = message.getMessage(); Map attributes = pubsubMessage.getAttributes(); // Timestamp. long timestampMsSinceEpoch; - if (Strings.isNullOrEmpty(timestampAttribute)) { + if (timestampAttribute == null || timestampAttribute.isEmpty()) { + if (!pubsubMessage.hasPublishTime()) { + throw new IllegalStateException("Received message missing publishTime"); + } Timestamp timestampProto = pubsubMessage.getPublishTime(); timestampMsSinceEpoch = timestampProto.getSeconds() * 1000 + timestampProto.getNanos() / 1000L / 1000L; @@ -282,14 +287,16 @@ public List pull( // Ack id. String ackId = message.getAckId(); - checkState(!Strings.isNullOrEmpty(ackId)); + if (ackId.isEmpty()) { + throw new IllegalStateException("Received message missing ackId"); + } // Record id, if any. - @Nullable String recordId = null; + String recordId = null; if (idAttribute != null) { recordId = attributes.get(idAttribute); } - if (Strings.isNullOrEmpty(recordId)) { + if (recordId == null || recordId.isEmpty()) { // Fall back to the Pubsub provided message id. recordId = pubsubMessage.getMessageId(); } @@ -475,15 +482,15 @@ public void deleteSchema(SchemaPath schemaPath) throws IOException { /** Return {@link SchemaPath} from {@link TopicPath} if exists. */ @Override - public SchemaPath getSchemaPath(TopicPath topicPath) throws IOException { + public @Nullable SchemaPath getSchemaPath(TopicPath topicPath) throws IOException { GetTopicRequest request = GetTopicRequest.newBuilder().setTopic(topicPath.getPath()).build(); Topic topic = publisherStub().getTopic(request); - SchemaSettings schemaSettings = topic.getSchemaSettings(); - if (schemaSettings.getSchema().isEmpty()) { + if (!topic.hasSchemaSettings()) { return null; } + SchemaSettings schemaSettings = topic.getSchemaSettings(); String schemaPath = schemaSettings.getSchema(); - if (schemaPath.equals(SchemaPath.DELETED_SCHEMA_PATH)) { + if (schemaPath.isEmpty() || schemaPath.equals(SchemaPath.DELETED_SCHEMA_PATH)) { return null; } return PubsubClient.schemaPathFromPath(schemaPath); 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..fa2399abd91c 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 @@ -18,6 +18,8 @@ package org.apache.beam.sdk.io.gcp.pubsub; import static org.apache.beam.sdk.transforms.errorhandling.BadRecordRouter.BAD_RECORD_TAG; +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.checkState; import com.google.api.client.util.Clock; @@ -26,9 +28,17 @@ import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; import java.io.IOException; import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -50,6 +60,7 @@ import org.apache.beam.sdk.io.gcp.pubsub.PubsubClient.TopicPath; import org.apache.beam.sdk.metrics.Lineage; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.options.ValueProvider; import org.apache.beam.sdk.options.ValueProvider.NestedValueProvider; import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider; @@ -71,7 +82,6 @@ import org.apache.beam.sdk.transforms.errorhandling.ErrorHandler.DefaultErrorHandler; import org.apache.beam.sdk.transforms.windowing.AfterWatermark; import org.apache.beam.sdk.util.CoderUtils; -import org.apache.beam.sdk.util.Preconditions; import org.apache.beam.sdk.values.EncodableThrowable; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PBegin; @@ -89,6 +99,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.Lists; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; import org.slf4j.Logger; @@ -112,20 +123,21 @@ *

Updates to the I/O connector code

* * For any significant updates to this I/O connector, please consider involving corresponding code - * reviewers mentioned - * here. + * reviewers mentioned here. * *

Example PubsubIO read usage

* *
{@code
- * // Read from a specific topic; a subscription will be created at pipeline start time.
+ * // Read from a specific topic; a subscription will be created at pipeline
+ * // start time.
  * PCollection messages = PubsubIO.readMessages().fromTopic(topic);
  *
  * // Read from a subscription.
  * PCollection messages = PubsubIO.readMessages().fromSubscription(subscription);
  *
- * // Read messages including attributes. All PubSub attributes will be included in the PubsubMessage.
+ * // Read messages including attributes. All PubSub attributes will be included
+ * // in the PubsubMessage.
  * PCollection messages = PubsubIO.readMessagesWithAttributes().fromTopic(topic);
  *
  * // Examples of reading different types from PubSub.
@@ -161,10 +173,11 @@
  * topic and writing using the {@link PubsubIO#writeMessagesDynamic()} method. For example:
  *
  * 
{@code
- * events.apply(MapElements.into(new TypeDescriptor() {})
- *                         .via(e -> new PubsubMessage(
- *                             e.toByteString(), Collections.emptyMap()).withTopic(e.getCountry())))
- * .apply(PubsubIO.writeMessagesDynamic());
+ * events.apply(MapElements.into(new TypeDescriptor() {
+ * })
+ *     .via(e -> new PubsubMessage(
+ *         e.toByteString(), Collections.emptyMap()).withTopic(e.getCountry())))
+ *     .apply(PubsubIO.writeMessagesDynamic());
  * }
* *

Custom timestamps

@@ -175,9 +188,6 @@ * to be used, that timestamp must be published in a PubSub attribute and specified using {@link * PubsubIO.Read#withTimestampAttribute}. See the Javadoc for that method for the timestamp format. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubIO { private static final Logger LOG = LoggerFactory.getLogger(PubsubIO.class); @@ -251,9 +261,9 @@ private static void validatePubsubName(String name) { /** Populate common {@link DisplayData} between Pubsub source and sink. */ private static void populateCommonDisplayData( DisplayData.Builder builder, - String timestampAttribute, - String idAttribute, - ValueProvider topic) { + @Nullable String timestampAttribute, + @Nullable String idAttribute, + @Nullable ValueProvider topic) { builder .addIfNotNull( DisplayData.item("timestampAttribute", timestampAttribute) @@ -324,6 +334,13 @@ public static PubsubSubscription fromPath(String path) { subscriptionName = match.group(2); } + projectName = + checkArgumentNotNull( + projectName, "Invalid path: project name must be non-null: %s", path); + subscriptionName = + checkArgumentNotNull( + subscriptionName, "Invalid path: subscription name must be non-null: %s", path); + validateProjectName(projectName); validatePubsubName(subscriptionName); return new PubsubSubscription(PubsubSubscription.Type.NORMAL, projectName, subscriptionName); @@ -399,7 +416,7 @@ public TopicPath apply(PubsubTopic from) { /** Class representing a Cloud Pub/Sub Topic. */ public static class PubsubTopic implements Serializable { @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -470,6 +487,12 @@ public static PubsubTopic fromPath(String path) { topicName = match.group(2); } + projectName = + checkArgumentNotNull( + projectName, "Invalid path: project name must be non-null: %s", path); + topicName = + checkArgumentNotNull(topicName, "Invalid path: topic name must be non-null: %s", path); + validateProjectName(projectName); validatePubsubName(topicName); return new PubsubTopic(PubsubTopic.Type.NORMAL, projectName, topicName); @@ -604,7 +627,8 @@ public static Read readStrings() { */ public static Read readProtos(Class messageClass) { // TODO: Stop using ProtoCoder and instead parse the payload directly. - // We should not be relying on the fact that ProtoCoder's wire format is identical to + // We should not be relying on the fact that ProtoCoder's wire format is + // identical to // the protobuf wire format, as the wire format is not part of a coder's API. ProtoCoder coder = ProtoCoder.of(messageClass); return Read.newBuilder(parsePayloadUsingCoder(coder)).setCoder(coder).build(); @@ -662,7 +686,8 @@ public static Read readProtoDynamicMessages(Descriptor descripto */ public static Read readAvros(Class clazz) { // TODO: Stop using AvroCoder and instead parse the payload directly. - // We should not be relying on the fact that AvroCoder's wire format is identical to + // We should not be relying on the fact that AvroCoder's wire format is + // identical to // the Avro wire format, as the wire format is not part of a coder's API. AvroCoder coder = AvroCoder.of(clazz); return Read.newBuilder(parsePayloadUsingCoder(coder)).setCoder(coder).build(); @@ -698,6 +723,10 @@ public static Read readMessagesWithAttributesWithCoderAndParseFn( public static Read readAvroGenericRecords(org.apache.avro.Schema avroSchema) { AvroCoder coder = AvroCoder.of(avroSchema); Schema schema = AvroUtils.getSchema(GenericRecord.class, avroSchema); + if (schema == null) { + throw new IllegalArgumentException( + "Could not infer Beam schema from Avro schema: " + avroSchema); + } return Read.newBuilder(parsePayloadUsingCoder(coder)) .setCoder( SchemaCoder.of( @@ -722,6 +751,9 @@ public static Read readAvrosWithBeamSchema(Class clazz) { AvroCoder coder = AvroCoder.of(clazz); org.apache.avro.Schema avroSchema = coder.getSchema(); Schema schema = AvroUtils.getSchema(clazz, avroSchema); + if (schema == null) { + throw new IllegalArgumentException("Could not infer Beam schema for class: " + clazz); + } return Read.newBuilder(parsePayloadUsingCoder(coder)) .setCoder( SchemaCoder.of( @@ -773,7 +805,8 @@ public static Write writeStrings() { * Google Cloud Pub/Sub stream. */ public static Write writeProtos(Class messageClass) { - // TODO: Like in readProtos(), stop using ProtoCoder and instead format the payload directly. + // TODO: Like in readProtos(), stop using ProtoCoder and instead format the + // payload directly. return Write.newBuilder(formatPayloadUsingCoder(ProtoCoder.of(messageClass))) .setDynamicDestinations(false) .build(); @@ -786,7 +819,8 @@ public static Write writeProtos(Class messageClass) { public static Write writeProtos( Class messageClass, SerializableFunction, Map> attributeFn) { - // TODO: Like in readProtos(), stop using ProtoCoder and instead format the payload directly. + // TODO: Like in readProtos(), stop using ProtoCoder and instead format the + // payload directly. return Write.newBuilder(formatPayloadUsingCoder(ProtoCoder.of(messageClass), attributeFn)) .setDynamicDestinations(false) .build(); @@ -797,7 +831,8 @@ public static Write writeProtos( * Google Cloud Pub/Sub stream. */ public static Write writeAvros(Class clazz) { - // TODO: Like in readAvros(), stop using AvroCoder and instead format the payload directly. + // TODO: Like in readAvros(), stop using AvroCoder and instead format the + // payload directly. return Write.newBuilder(formatPayloadUsingCoder(AvroCoder.of(clazz))) .setDynamicDestinations(false) .build(); @@ -810,7 +845,8 @@ public static Write writeAvros(Class clazz) { public static Write writeAvros( Class clazz, SerializableFunction, Map> attributeFn) { - // TODO: Like in readAvros(), stop using AvroCoder and instead format the payload directly. + // TODO: Like in readAvros(), stop using AvroCoder and instead format the + // payload directly. return Write.newBuilder(formatPayloadUsingCoder(AvroCoder.of(clazz), attributeFn)) .setDynamicDestinations(false) .build(); @@ -854,6 +890,8 @@ public abstract static class Read extends PTransform> abstract boolean getNeedsMessageId(); + abstract boolean isEnableOpenTelemetryTracing(); + abstract boolean getNeedsOrderingKey(); abstract BadRecordRouter getBadRecordRouter(); @@ -874,6 +912,7 @@ static Builder newBuilder(SerializableFunction parseFn) builder.setBadRecordRouter(BadRecordRouter.THROWING_ROUTER); builder.setBadRecordErrorHandler(new DefaultErrorHandler<>()); builder.setValidate(false); + builder.setEnableOpenTelemetryTracing(false); return builder; } @@ -896,6 +935,8 @@ abstract static class Builder { abstract Builder setIdAttribute(String idAttribute); + abstract Builder setEnableOpenTelemetryTracing(boolean enableOpenTelemetryTracing); + abstract Builder setCoder(Coder coder); abstract Builder setParseFn(SerializableFunction parseFn); @@ -1079,6 +1120,66 @@ public Read withIdAttribute(String idAttribute) { return toBuilder().setIdAttribute(idAttribute).build(); } + public Read withEnableOpenTelemetryTracing() { + return toBuilder().setEnableOpenTelemetryTracing(true).setNeedsAttributes(true).build(); + } + + static class OpenTelemetryHeaderConsumer extends DoFn { + + Context extractSpanContext(PubsubMessage message) { + TextMapGetter extractMessageAttributes = + new TextMapGetter() { + @Override + public @Nullable String get(@Nullable PubsubMessage carrier, String key) { + if (carrier == null) { + return null; + } + return carrier.getAttribute("googclient_" + key); + } + + @Override + public Iterable keys(PubsubMessage carrier) { + Map attributeMap = carrier.getAttributeMap(); + if (attributeMap == null) { + return ImmutableList.of(); + } + List keys = new java.util.ArrayList<>(); + for (String key : attributeMap.keySet()) { + if (key.startsWith("googclient_")) { + keys.add(key.substring("googclient_".length())); + } + } + return keys; + } + }; + return W3CTraceContextPropagator.getInstance() + .extract(Context.current(), message, extractMessageAttributes); + } + + @Setup + public void setup(PipelineOptions po) { + tracer = po.as(SdkHarnessOptions.class).getOpenTelemetry().getTracer("PubSubIO"); + } + + private transient @MonotonicNonNull Tracer tracer = null; + + @ProcessElement + public void processElement( + @Element PubsubMessage message, OutputReceiver output) { + @Nullable Context context = extractSpanContext(message); + Span span = + checkArgumentNotNull(tracer) + .spanBuilder("PubSubIO.Read") + .setParent(context) + .startSpan(); + try (Scope s = span.makeCurrent()) { + output.output(message); + } finally { + span.end(); + } + } + } + /** * Causes the source to return a PubsubMessage that includes Pubsub attributes, and uses the * given parsing function to transform the PubsubMessage into an output type. A Coder for the @@ -1134,16 +1235,18 @@ public PCollection expand(PBegin input) { "PubSubIO cannot be configured with both a dead letter topic and a bad record router"); } + ValueProvider topicProvider = getTopicProvider(); @Nullable ValueProvider topicPath = - getTopicProvider() == null + topicProvider == null ? null - : NestedValueProvider.of(getTopicProvider(), new TopicPathTranslator()); + : NestedValueProvider.of(topicProvider, new TopicPathTranslator()); + ValueProvider subscriptionProvider = getSubscriptionProvider(); @Nullable ValueProvider subscriptionPath = - getSubscriptionProvider() == null + subscriptionProvider == null ? null - : NestedValueProvider.of(getSubscriptionProvider(), new SubscriptionPathTranslator()); + : NestedValueProvider.of(subscriptionProvider, new SubscriptionPathTranslator()); PubsubUnboundedSource source = new PubsubUnboundedSource( getClock(), @@ -1177,7 +1280,7 @@ private PCollection expandReadContinued( new SerializableFunction() { // flag that reported metrics private final SerializableFunction underlying = - Objects.requireNonNull(getParseFn()); + checkStateNotNull(getParseFn()); private transient boolean reportedMetrics = false; // public @@ -1204,8 +1307,15 @@ public T apply(PubsubMessage input) { return underlying.apply(input); } }; + ValueProvider deadLetterTopicProvider = getDeadLetterTopicProvider(); PCollection read; - if (getDeadLetterTopicProvider() == null + if (isEnableOpenTelemetryTracing()) { + preParse = + preParse.apply( + "Extract OpenTelemetry context from Header", + ParDo.of(new OpenTelemetryHeaderConsumer())); + } + if (deadLetterTopicProvider == null && (getBadRecordRouter() instanceof ThrowingBadRecordRouter)) { read = preParse.apply(MapElements.into(typeDescriptor).via(parseFnWrapped)); } else { @@ -1234,7 +1344,8 @@ public T apply(PubsubMessage input) { // Write out failures to the provided dead-letter topic. result .failures() - // Since the stack trace could easily exceed Pub/Sub limits, we need to remove it from + // Since the stack trace could easily exceed Pub/Sub limits, we need to remove + // it from // the attributes. .apply( "PubsubIO.Read/Map/Remove-Stack-Trace-Attribute", @@ -1254,7 +1365,9 @@ public T apply(PubsubMessage input) { ImmutableMap attributes = ImmutableMap.builder() .put("exceptionClassName", throwable.getClass().getName()) - .put("exceptionMessage", throwable.getMessage()) + .put( + "exceptionMessage", + MoreObjects.firstNonNull(throwable.getMessage(), "")) .put("pubsubMessageId", messageId) .build(); @@ -1266,7 +1379,7 @@ public T apply(PubsubMessage input) { .via(kv -> new PubsubMessage(kv.getKey().getPayload(), kv.getValue()))) .apply( writeMessages() - .to(getDeadLetterTopicProvider().get().asPath()) + .to(checkStateNotNull(deadLetterTopicProvider).get().asPath()) .withClientFactory(getPubsubClientFactory())); } } @@ -1274,16 +1387,20 @@ public T apply(PubsubMessage input) { } @Override - public void validate(PipelineOptions options) { + public void validate(@Nullable PipelineOptions options) { if (!getValidate()) { return; } + if (options == null) { + return; + } PubsubOptions psOptions = options.as(PubsubOptions.class); // Validate the existence of the topic. - if (getTopicProvider() != null) { - PubsubTopic topic = getTopicProvider().get(); + ValueProvider topicProvider = getTopicProvider(); + if (topicProvider != null) { + PubsubTopic topic = topicProvider.get(); boolean topicExists = true; try (PubsubClient pubsubClient = getPubsubClientFactory() @@ -1377,6 +1494,8 @@ public abstract static class Write extends PTransform, PDone> abstract @Nullable String getPubsubRootUrl(); + abstract boolean isEnableOpenTelemetryTracing(); + abstract boolean getPublishWithOrderingKey(); abstract BadRecordRouter getBadRecordRouter(); @@ -1396,6 +1515,7 @@ static Builder newBuilder( builder.setBadRecordErrorHandler(new DefaultErrorHandler<>()); builder.setPublishWithOrderingKey(false); builder.setValidate(false); + builder.setEnableOpenTelemetryTracing(false); return builder; } @@ -1405,10 +1525,10 @@ static Builder newBuilder() { @AutoValue.Builder abstract static class Builder { - abstract Builder setTopicProvider(ValueProvider topicProvider); + abstract Builder setTopicProvider(@Nullable ValueProvider topicProvider); abstract Builder setTopicFunction( - SerializableFunction, PubsubTopic> topicFunction); + @Nullable SerializableFunction, PubsubTopic> topicFunction); abstract Builder setDynamicDestinations(boolean dynamicDestinations); @@ -1420,6 +1540,8 @@ abstract Builder setTopicFunction( abstract Builder setTimestampAttribute(String timestampAttribute); + abstract Builder setEnableOpenTelemetryTracing(boolean enableOpenTelemetryTracing); + abstract Builder setIdAttribute(String idAttribute); abstract Builder setFormatFn( @@ -1439,6 +1561,41 @@ abstract Builder setBadRecordErrorHandler( abstract Write build(); } + static class OpenTelemetryHeaderPropagator extends DoFn { + void injectSpanContext(Map attr) { + TextMapSetter> inject = + new TextMapSetter>() { + @Override + public void set(@Nullable Map attr, String key, String value) { + if (attr != null) { + attr.put("googclient_" + key, value); + } + } + }; + W3CTraceContextPropagator.getInstance().inject(Context.current(), attr, inject); + } + + @ProcessElement + public void processElement( + @Element PubsubMessage message, OutputReceiver output) { + Map attributeMap = message.getAttributeMap(); + Map attr = + attributeMap == null ? new HashMap<>() : new HashMap<>(attributeMap); + injectSpanContext(attr); + + // copy the message, multiple fields + PubsubMessage ps = + new PubsubMessage( + message.getPayload(), attr, message.getMessageId(), message.getOrderingKey()); + + // topic is copied seperately, not via constructor + if (message.getTopic() != null) { + ps = ps.withTopic(message.getTopic()); + } + output.output(ps); + } + } + /** * Publishes to the specified topic. * @@ -1514,6 +1671,10 @@ public Write withMaxBatchBytesSize(int maxBatchBytesSize) { return toBuilder().setMaxBatchBytesSize(maxBatchBytesSize).build(); } + public Write withEnableOpenTelemetryTracing() { + return toBuilder().setEnableOpenTelemetryTracing(true).build(); + } + /** * Writes to Pub/Sub with each record's ordering key. A subscription with message ordering * enabled will receive messages published in the same region with the same ordering key in the @@ -1546,7 +1707,7 @@ public Write withTimestampAttribute(String timestampAttribute) { * attribute with the specified name. The value of the attribute is an opaque string. * *

If the output from this sink is being read by another Beam pipeline, then {@link - * PubsubIO.Read#withIdAttribute(String)} can be used to ensure that* the other source reads + * PubsubIO.Read#withIdAttribute(String)} can be used to ensure that the other source reads * these unique identifiers from the appropriate attribute. */ public Write withIdAttribute(String idAttribute) { @@ -1582,10 +1743,11 @@ public PDone expand(PCollection input) { + "dynamic topic destinations."); } + ValueProvider topicProvider = getTopicProvider(); SerializableFunction, PubsubIO.PubsubTopic> topicFunction = getTopicFunction(); - if (topicFunction == null && getTopicProvider() != null) { - topicFunction = v -> getTopicProvider().get(); + if (topicFunction == null && topicProvider != null) { + topicFunction = v -> topicProvider.get(); } int maxMessageSize = PUBSUB_MESSAGE_MAX_TOTAL_SIZE; if (input.isBounded() == PCollection.IsBounded.BOUNDED) { @@ -1620,6 +1782,11 @@ public PDone expand(PCollection input) { } else { pubsubMessages.setCoder(PubsubMessageWithTopicCoder.of()); } + if (isEnableOpenTelemetryTracing()) { + pubsubMessages = + pubsubMessages.apply( + "Propagate OpenTelemetry Tracing", ParDo.of(new OpenTelemetryHeaderPropagator())); + } switch (input.isBounded()) { case BOUNDED: pubsubMessages.apply( @@ -1633,8 +1800,8 @@ public PDone expand(PCollection input) { return pubsubMessages.apply( new PubsubUnboundedSink( getPubsubClientFactory(), - getTopicProvider() != null - ? NestedValueProvider.of(getTopicProvider(), new TopicPathTranslator()) + topicProvider != null + ? NestedValueProvider.of(topicProvider, new TopicPathTranslator()) : null, getTimestampAttribute(), getIdAttribute(), @@ -1650,16 +1817,20 @@ public PDone expand(PCollection input) { } @Override - public void validate(PipelineOptions options) { + public void validate(@Nullable PipelineOptions options) { if (!getValidate()) { return; } + if (options == null) { + return; + } PubsubOptions psOptions = options.as(PubsubOptions.class); // Validate the existence of the topic. - if (getTopicProvider() != null) { - PubsubTopic topic = getTopicProvider().get(); + ValueProvider topicProvider = getTopicProvider(); + if (topicProvider != null) { + PubsubTopic topic = topicProvider.get(); boolean topicExists = true; try (PubsubClient pubsubClient = getPubsubClientFactory() @@ -1702,10 +1873,11 @@ private class OutgoingData { } // NOTE: A single publish request may only write to one ordering key. - // See https://cloud.google.com/pubsub/docs/publisher#using-ordering-keys for details. - private transient Map, OutgoingData> output; + // See https://cloud.google.com/pubsub/docs/publisher#using-ordering-keys for + // details. + private transient @Nullable Map, OutgoingData> output; - private transient PubsubClient pubsubClient; + private transient @Nullable PubsubClient pubsubClient; private int maxPublishBatchByteSize; private int maxPublishBatchSize; @@ -1727,7 +1899,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 @@ -1747,16 +1922,19 @@ public void processElement(@Element PubsubMessage message, @Timestamp Instant ti final int messageSize = msg.getMessage().getData().size(); final PubsubTopic pubsubTopic; - if (getTopicProvider() != null) { - pubsubTopic = getTopicProvider().get(); + ValueProvider topicProvider = getTopicProvider(); + if (topicProvider != null) { + pubsubTopic = topicProvider.get(); } else { - pubsubTopic = PubsubTopic.fromPath(Preconditions.checkArgumentNotNull(msg.topic())); + pubsubTopic = PubsubTopic.fromPath(checkArgumentNotNull(msg.topic())); } - // Checking before adding the message stops us from violating max batch size or bytes + // Checking before adding the message stops us from violating max batch size or + // bytes String orderingKey = getPublishWithOrderingKey() ? msg.getMessage().getOrderingKey() : ""; final OutgoingData currentTopicAndOrderingKeyOutput = - output.computeIfAbsent(KV.of(pubsubTopic, orderingKey), t -> new OutgoingData()); + checkStateNotNull(output) + .computeIfAbsent(KV.of(pubsubTopic, orderingKey), t -> new OutgoingData()); // TODO(sjvanrossum): https://github.com/apache/beam/issues/31800 if (currentTopicAndOrderingKeyOutput.messages.size() >= maxPublishBatchSize || (!currentTopicAndOrderingKeyOutput.messages.isEmpty() @@ -1773,18 +1951,24 @@ public void processElement(@Element PubsubMessage message, @Timestamp Instant ti @FinishBundle public void finishBundle() throws IOException { - for (Map.Entry, OutgoingData> entry : output.entrySet()) { - publish(entry.getKey().getKey(), entry.getValue().messages); + final Map, OutgoingData> output = this.output; + if (output != null) { + for (Map.Entry, OutgoingData> entry : output.entrySet()) { + publish(entry.getKey().getKey(), entry.getValue().messages); + } + } + this.output = null; + final PubsubClient pubsubClient = this.pubsubClient; + if (pubsubClient != null) { + pubsubClient.close(); } - output = null; - pubsubClient.close(); - pubsubClient = null; + this.pubsubClient = null; } private void publish(PubsubTopic topic, List messages) throws IOException { int n = - pubsubClient.publish( - PubsubClient.topicPathFromName(topic.project, topic.topic), messages); + checkStateNotNull(pubsubClient) + .publish(PubsubClient.topicPathFromName(topic.project, topic.topic), messages); checkState(n == messages.size()); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubJsonClient.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubJsonClient.java index f36e94360996..d0f9635391f9 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubJsonClient.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubJsonClient.java @@ -17,7 +17,7 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.api.client.http.HttpRequestInitializer; @@ -51,14 +51,10 @@ 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.base.Strings; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.checkerframework.checker.nullness.qual.Nullable; /** A Pubsub client using JSON transport. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubJsonClient extends PubsubClient { private static class PubsubJsonClientFactory implements PubsubClientFactory { @@ -85,7 +81,7 @@ public PubsubClient newClient( @Nullable String timestampAttribute, @Nullable String idAttribute, PubsubOptions options, - String rootUrlOverride) + @Nullable String rootUrlOverride) throws IOException { Pubsub pubsub = new Pubsub.Builder( @@ -163,8 +159,9 @@ private Map getMessageAttributes(OutgoingMessage outgoingMessage attributes.put( timestampAttribute, String.valueOf(outgoingMessage.getTimestampMsSinceEpoch())); } - if (idAttribute != null && !Strings.isNullOrEmpty(outgoingMessage.recordId())) { - attributes.put(idAttribute, outgoingMessage.recordId()); + String recordId = outgoingMessage.recordId(); + if (idAttribute != null && recordId != null && !recordId.isEmpty()) { + attributes.put(idAttribute, recordId); } return attributes; } @@ -181,45 +178,51 @@ public List pull( new PullRequest().setReturnImmediately(returnImmediately).setMaxMessages(batchSize); PullResponse response = pubsub.projects().subscriptions().pull(subscription.getPath(), request).execute(); - if (response.getReceivedMessages() == null || response.getReceivedMessages().isEmpty()) { + List receivedMessages = response.getReceivedMessages(); + if (receivedMessages == null || receivedMessages.isEmpty()) { return ImmutableList.of(); } - List incomingMessages = new ArrayList<>(response.getReceivedMessages().size()); - for (ReceivedMessage message : response.getReceivedMessages()) { + List incomingMessages = new ArrayList<>(receivedMessages.size()); + for (ReceivedMessage message : receivedMessages) { PubsubMessage pubsubMessage = message.getMessage(); - Map attributes; - if (pubsubMessage.getAttributes() != null) { - attributes = pubsubMessage.getAttributes(); - } else { + if (pubsubMessage == null) { + continue; + } + Map attributes = pubsubMessage.getAttributes(); + if (attributes == null) { attributes = new HashMap<>(); } // Payload. - byte[] elementBytes = pubsubMessage.getData() == null ? null : pubsubMessage.decodeData(); - if (elementBytes == null) { - elementBytes = new byte[0]; - } + String dataStr = pubsubMessage.getData(); + byte[] elementBytes = (dataStr == null) ? new byte[0] : pubsubMessage.decodeData(); // Timestamp. long timestampMsSinceEpoch; - if (Strings.isNullOrEmpty(timestampAttribute)) { - timestampMsSinceEpoch = parseTimestampAsMsSinceEpoch(message.getMessage().getPublishTime()); + if (timestampAttribute == null || timestampAttribute.isEmpty()) { + String publishTime = pubsubMessage.getPublishTime(); + if (publishTime == null) { + throw new IllegalStateException("Received message missing publishTime"); + } + timestampMsSinceEpoch = parseTimestampAsMsSinceEpoch(publishTime); } else { timestampMsSinceEpoch = extractTimestampAttribute(timestampAttribute, attributes); } // Ack id. String ackId = message.getAckId(); - checkState(!Strings.isNullOrEmpty(ackId)); + if (ackId == null || ackId.isEmpty()) { + throw new IllegalStateException("Received message missing ackId"); + } // Record id, if any. @Nullable String recordId = null; if (idAttribute != null) { recordId = attributes.get(idAttribute); } - if (Strings.isNullOrEmpty(recordId)) { + if (recordId == null || recordId.isEmpty()) { // Fall back to the Pubsub provided message id. - recordId = pubsubMessage.getMessageId(); + recordId = checkStateNotNull(pubsubMessage.getMessageId(), "Message ID is missing"); } com.google.pubsub.v1.PubsubMessage.Builder protoMessage = @@ -229,8 +232,9 @@ public List pull( // {@link PubsubMessage} uses `null` or empty string to represent no ordering key. // {@link com.google.pubsub.v1.PubsubMessage} does not track string field presence and uses // empty string as a default. - if (pubsubMessage.getOrderingKey() != null) { - protoMessage.setOrderingKey(pubsubMessage.getOrderingKey()); + String orderingKey = pubsubMessage.getOrderingKey(); + if (orderingKey != null) { + protoMessage.setOrderingKey(orderingKey); } incomingMessages.add( IncomingMessage.of( @@ -289,19 +293,25 @@ public void deleteTopic(TopicPath topic) throws IOException { public List listTopics(ProjectPath project) throws IOException { Topics.List request = pubsub.projects().topics().list(project.getPath()); ListTopicsResponse response = request.execute(); - if (response.getTopics() == null || response.getTopics().isEmpty()) { + List topicsList = response.getTopics(); + if (topicsList == null || topicsList.isEmpty()) { return ImmutableList.of(); } - List topics = new ArrayList<>(response.getTopics().size()); + List topics = new ArrayList<>(topicsList.size()); while (true) { - for (Topic topic : response.getTopics()) { + for (Topic topic : topicsList) { topics.add(topicPathFromPath(topic.getName())); } - if (Strings.isNullOrEmpty(response.getNextPageToken())) { + String nextPageToken = response.getNextPageToken(); + if (nextPageToken == null || nextPageToken.isEmpty()) { break; } - request.setPageToken(response.getNextPageToken()); + request.setPageToken(nextPageToken); response = request.execute(); + topicsList = response.getTopics(); + if (topicsList == null) { + break; + } } return topics; } @@ -345,21 +355,28 @@ public List listSubscriptions(ProjectPath project, TopicPath t throws IOException { Subscriptions.List request = pubsub.projects().subscriptions().list(project.getPath()); ListSubscriptionsResponse response = request.execute(); - if (response.getSubscriptions() == null || response.getSubscriptions().isEmpty()) { + List subscriptionsList = response.getSubscriptions(); + if (subscriptionsList == null || subscriptionsList.isEmpty()) { return ImmutableList.of(); } - List subscriptions = new ArrayList<>(response.getSubscriptions().size()); + List subscriptions = new ArrayList<>(subscriptionsList.size()); while (true) { - for (Subscription subscription : response.getSubscriptions()) { - if (subscription.getTopic().equals(topic.getPath())) { + for (Subscription subscription : subscriptionsList) { + String subTopic = subscription.getTopic(); + if (subTopic != null && subTopic.equals(topic.getPath())) { subscriptions.add(subscriptionPathFromPath(subscription.getName())); } } - if (Strings.isNullOrEmpty(response.getNextPageToken())) { + String nextPageToken = response.getNextPageToken(); + if (nextPageToken == null || nextPageToken.isEmpty()) { break; } - request.setPageToken(response.getNextPageToken()); + request.setPageToken(nextPageToken); response = request.execute(); + subscriptionsList = response.getSubscriptions(); + if (subscriptionsList == null) { + break; + } } return subscriptions; } @@ -391,13 +408,14 @@ public void deleteSchema(SchemaPath schemaPath) throws IOException { /** Return {@link SchemaPath} from {@link TopicPath} if exists. */ @Override - public SchemaPath getSchemaPath(TopicPath topicPath) throws IOException { + public @Nullable SchemaPath getSchemaPath(TopicPath topicPath) throws IOException { Topic topic = pubsub.projects().topics().get(topicPath.getPath()).execute(); - if (topic.getSchemaSettings() == null) { + com.google.api.services.pubsub.model.SchemaSettings schemaSettings = topic.getSchemaSettings(); + if (schemaSettings == null) { return null; } - String schemaPath = topic.getSchemaSettings().getSchema(); - if (schemaPath.equals(SchemaPath.DELETED_SCHEMA_PATH)) { + String schemaPath = schemaSettings.getSchema(); + if (schemaPath == null || schemaPath.equals(SchemaPath.DELETED_SCHEMA_PATH)) { return null; } return PubsubClient.schemaPathFromPath(schemaPath); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessage.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessage.java index 779a9b847874..7732839181e6 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessage.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessage.java @@ -17,31 +17,32 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; - import com.google.auto.value.AutoValue; import java.util.Map; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; /** * Class representing a Pub/Sub message. Each message contains a single message payload, a map of * attached attributes, a message id and an ordering key. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubMessage { @AutoValue abstract static class Impl { + @Pure abstract @Nullable String getTopic(); + @Pure @SuppressWarnings("mutable") abstract byte[] getPayload(); + @Pure abstract @Nullable Map getAttributeMap(); + @Pure abstract @Nullable String getMessageId(); + @Pure abstract @Nullable String getOrderingKey(); static Impl create( @@ -87,27 +88,32 @@ public PubsubMessage withTopic(String topic) { impl.getOrderingKey())); } + @Pure public @Nullable String getTopic() { return impl.getTopic(); } /** Returns the main PubSub message. */ + @Pure public byte[] getPayload() { return impl.getPayload(); } /** Returns the given attribute value. If not such attribute exists, returns null. */ + @Pure public @Nullable String getAttribute(String attribute) { - checkNotNull(attribute, "attribute"); - return impl.getAttributeMap().get(attribute); + Map attributeMap = impl.getAttributeMap(); + return attributeMap == null ? null : attributeMap.get(attribute); } /** Returns the full map of attributes. This is an unmodifiable map. */ + @Pure public @Nullable Map getAttributeMap() { return impl.getAttributeMap(); } /** Returns the messageId of the message populated by Cloud Pub/Sub. */ + @Pure public @Nullable String getMessageId() { return impl.getMessageId(); } @@ -123,6 +129,7 @@ public PubsubMessage withOrderingKey(@Nullable String orderingKey) { } /** Returns the ordering key of the message. */ + @Pure public @Nullable String getOrderingKey() { return impl.getOrderingKey(); } @@ -133,7 +140,7 @@ public String toString() { } @Override - public boolean equals(Object other) { + public boolean equals(@Nullable Object other) { if (!(other instanceof PubsubMessage)) { return false; } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageToRow.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageToRow.java index d860bfba95ec..c005253110c7 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageToRow.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageToRow.java @@ -19,6 +19,8 @@ import static java.util.stream.Collectors.toList; import static org.apache.beam.sdk.io.gcp.pubsub.PubsubSchemaIOProvider.ATTRIBUTE_ARRAY_ENTRY_SCHEMA; +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 com.google.auto.value.AutoValue; @@ -26,7 +28,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import javax.annotation.Nullable; import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.TypeName; @@ -41,14 +42,12 @@ import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.TupleTagList; 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.Instant; /** Read side converter for {@link PubsubMessage} with JSON/AVRO payload. */ @Internal @AutoValue -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) abstract class PubsubMessageToRow extends PTransform, PCollectionTuple> implements Serializable { interface SerializerProvider extends SerializableFunction {} @@ -94,7 +93,10 @@ public PCollectionTuple expand(PCollection input) { ParDo.of( useFlatSchema() ? new FlatSchemaPubsubMessageToRow( - messageSchema(), useDlq(), serializerProvider()) + messageSchema(), + useDlq(), + checkArgumentNotNull( + serializerProvider(), "Flat schema requires serializerProvider")) : new NestedSchemaPubsubMessageToRow( messageSchema(), useDlq(), serializerProvider())) .withOutputTags( @@ -133,7 +135,8 @@ protected FlatSchemaPubsubMessageToRow( * Get the value for a field from a given payload in the order they're specified in the flat * schema. */ - private Object getValueForFieldFlatSchema(Schema.Field field, Instant timestamp, Row payload) { + private @Nullable Object getValueForFieldFlatSchema( + Schema.Field field, Instant timestamp, Row payload) { String fieldName = field.getName(); if (TIMESTAMP_FIELD.equals(fieldName)) { return timestamp; @@ -147,9 +150,10 @@ public void processElement( @Element PubsubMessage element, @Timestamp Instant timestamp, MultiOutputReceiver o) { try { Row payload = payloadSerializer.deserialize(element.getPayload()); - List values = + List<@Nullable Object> values = messageSchema.getFields().stream() - .map(field -> getValueForFieldFlatSchema(field, timestamp, payload)) + .<@Nullable Object>map( + field -> getValueForFieldFlatSchema(field, timestamp, payload)) .collect(toList()); o.get(MAIN_TAG).output(Row.withSchema(messageSchema).addValues(values).build()); } catch (Exception e) { @@ -186,7 +190,8 @@ private NestedSchemaPubsubMessageToRow( } else { checkArgument( messageSchema.getField(PAYLOAD_FIELD).getType().getTypeName().equals(TypeName.ROW)); - Schema payloadSchema = messageSchema.getField(PAYLOAD_FIELD).getType().getRowSchema(); + Schema payloadSchema = + checkStateNotNull(messageSchema.getField(PAYLOAD_FIELD).getType().getRowSchema()); this.payloadSerializer = serializerProvider.apply(payloadSchema); } } @@ -198,7 +203,13 @@ private Object maybeDeserialize(byte[] payload) { return payloadSerializer.deserialize(payload); } - private Object handleAttributes(Map attributeMap) { + private @Nullable Object handleAttributes(@Nullable Map attributeMap) { + if (attributeMap == null) { + if (messageSchema.getField(ATTRIBUTES_FIELD).getType().getTypeName().isMapType()) { + return null; + } + return ImmutableList.of(); + } if (messageSchema.getField(ATTRIBUTES_FIELD).getType().getTypeName().isMapType()) { return attributeMap; } @@ -209,8 +220,11 @@ private Object handleAttributes(Map attributeMap) { } /** Get the value for a field int the order they're specified in the nested schema. */ - private Object getValueForFieldNestedSchema( - Schema.Field field, Instant timestamp, Map attributeMap, byte[] payload) { + private @Nullable Object getValueForFieldNestedSchema( + Schema.Field field, + Instant timestamp, + @Nullable Map attributeMap, + byte[] payload) { switch (field.getName()) { case TIMESTAMP_FIELD: return timestamp; @@ -232,9 +246,9 @@ private Object getValueForFieldNestedSchema( public void processElement( @Element PubsubMessage element, @Timestamp Instant timestamp, MultiOutputReceiver o) { try { - List values = + List<@Nullable Object> values = messageSchema.getFields().stream() - .map( + .<@Nullable Object>map( field -> getValueForFieldNestedSchema( field, timestamp, element.getAttributeMap(), element.getPayload())) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesAndMessageIdAndOrderingKeyCoder.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesAndMessageIdAndOrderingKeyCoder.java index 968d76584bc3..d2ba0b9446f2 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesAndMessageIdAndOrderingKeyCoder.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesAndMessageIdAndOrderingKeyCoder.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; + import com.google.protobuf.Timestamp; import java.io.IOException; import java.io.InputStream; @@ -30,6 +32,7 @@ import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.extensions.protobuf.ProtoCoder; import org.apache.beam.sdk.values.TypeDescriptor; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A coder for PubsubMessage including all fields of a PubSub message from server. @@ -37,22 +40,20 @@ *

Maintainers should prefer {@link PubsubMessageSchemaCoder} over this coder when adding * features to {@link PubsubIO}. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubMessageWithAttributesAndMessageIdAndOrderingKeyCoder extends CustomCoder { // A message's payload cannot be null private static final Coder PAYLOAD_CODER = ByteArrayCoder.of(); // A message's attributes can be null. - private static final Coder> ATTRIBUTES_CODER = + private static final Coder<@Nullable Map> ATTRIBUTES_CODER = NullableCoder.of(MapCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of())); // A message's messageId cannot be null private static final Coder MESSAGE_ID_CODER = StringUtf8Coder.of(); // A message's publish time, populated by server private static final Coder PUBLISH_TIME_CODER = ProtoCoder.of(Timestamp.class); // A message's ordering key can be null - private static final Coder ORDERING_KEY_CODER = NullableCoder.of(StringUtf8Coder.of()); + private static final Coder<@Nullable String> ORDERING_KEY_CODER = + NullableCoder.of(StringUtf8Coder.of()); public static Coder of(TypeDescriptor ignored) { return of(); @@ -66,7 +67,9 @@ public static PubsubMessageWithAttributesAndMessageIdAndOrderingKeyCoder of() { public void encode(PubsubMessage value, OutputStream outStream) throws IOException { PAYLOAD_CODER.encode(value.getPayload(), outStream); ATTRIBUTES_CODER.encode(value.getAttributeMap(), outStream); - MESSAGE_ID_CODER.encode(value.getMessageId(), outStream); + MESSAGE_ID_CODER.encode( + checkArgumentNotNull(value.getMessageId(), "Cannot encode PubsubMessage without messageId"), + outStream); // TODO(discuss what to do with publish_time field) PUBLISH_TIME_CODER.encode(Timestamp.getDefaultInstance(), outStream); ORDERING_KEY_CODER.encode(value.getOrderingKey(), outStream); @@ -75,10 +78,10 @@ public void encode(PubsubMessage value, OutputStream outStream) throws IOExcepti @Override public PubsubMessage decode(InputStream inStream) throws IOException { byte[] payload = PAYLOAD_CODER.decode(inStream); - Map attributes = ATTRIBUTES_CODER.decode(inStream); + @Nullable Map attributes = ATTRIBUTES_CODER.decode(inStream); String messageId = MESSAGE_ID_CODER.decode(inStream); PUBLISH_TIME_CODER.decode(inStream); - String orderingKey = ORDERING_KEY_CODER.decode(inStream); + @Nullable String orderingKey = ORDERING_KEY_CODER.decode(inStream); return new PubsubMessage(payload, attributes, messageId, orderingKey); } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesAndMessageIdCoder.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesAndMessageIdCoder.java index 803ff83583ce..32449d0425e8 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesAndMessageIdCoder.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesAndMessageIdCoder.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -28,6 +30,7 @@ import org.apache.beam.sdk.coders.NullableCoder; import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.values.TypeDescriptor; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A coder for PubsubMessage including attributes and the message id from the PubSub server. @@ -35,14 +38,11 @@ *

Maintainers should prefer {@link PubsubMessageSchemaCoder} over this coder when adding * features to {@link PubsubIO}. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubMessageWithAttributesAndMessageIdCoder extends CustomCoder { // A message's payload cannot be null private static final Coder PAYLOAD_CODER = ByteArrayCoder.of(); // A message's attributes can be null. - private static final Coder> ATTRIBUTES_CODER = + private static final Coder<@Nullable Map> ATTRIBUTES_CODER = NullableCoder.of(MapCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of())); // A message's messageId cannot be null private static final Coder MESSAGE_ID_CODER = StringUtf8Coder.of(); @@ -59,13 +59,15 @@ public static PubsubMessageWithAttributesAndMessageIdCoder of() { public void encode(PubsubMessage value, OutputStream outStream) throws IOException { PAYLOAD_CODER.encode(value.getPayload(), outStream); ATTRIBUTES_CODER.encode(value.getAttributeMap(), outStream); - MESSAGE_ID_CODER.encode(value.getMessageId(), outStream); + MESSAGE_ID_CODER.encode( + checkArgumentNotNull(value.getMessageId(), "Cannot encode PubsubMessage without messageId"), + outStream); } @Override public PubsubMessage decode(InputStream inStream) throws IOException { byte[] payload = PAYLOAD_CODER.decode(inStream); - Map attributes = ATTRIBUTES_CODER.decode(inStream); + @Nullable Map attributes = ATTRIBUTES_CODER.decode(inStream); String messageId = MESSAGE_ID_CODER.decode(inStream); return new PubsubMessage(payload, attributes, messageId); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesCoder.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesCoder.java index 9dd3ab56a72a..38b7661fba9b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesCoder.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithAttributesCoder.java @@ -28,6 +28,7 @@ import org.apache.beam.sdk.coders.NullableCoder; import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.values.TypeDescriptor; +import org.checkerframework.checker.nullness.qual.Nullable; /** * A coder for PubsubMessage including attributes. @@ -35,14 +36,11 @@ *

Maintainers should prefer {@link PubsubMessageSchemaCoder} over this coder when adding * features to {@link PubsubIO}. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubMessageWithAttributesCoder extends CustomCoder { // A message's payload can not be null private static final Coder PAYLOAD_CODER = ByteArrayCoder.of(); // A message's attributes can be null. - private static final Coder> ATTRIBUTES_CODER = + private static final Coder<@Nullable Map> ATTRIBUTES_CODER = NullableCoder.of(MapCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of())); public static Coder of(TypeDescriptor ignored) { @@ -58,6 +56,7 @@ public void encode(PubsubMessage value, OutputStream outStream) throws IOExcepti encode(value, outStream, Context.NESTED); } + @Override public void encode(PubsubMessage value, OutputStream outStream, Context context) throws IOException { PAYLOAD_CODER.encode(value.getPayload(), outStream); @@ -72,7 +71,7 @@ public PubsubMessage decode(InputStream inStream) throws IOException { @Override public PubsubMessage decode(InputStream inStream, Context context) throws IOException { byte[] payload = PAYLOAD_CODER.decode(inStream); - Map attributes = ATTRIBUTES_CODER.decode(inStream, context); + @Nullable Map attributes = ATTRIBUTES_CODER.decode(inStream, context); return new PubsubMessage(payload, attributes); } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithMessageIdCoder.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithMessageIdCoder.java index 7016146b3671..d150d8ac76cb 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithMessageIdCoder.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubMessageWithMessageIdCoder.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -33,9 +35,6 @@ *

Maintainers should prefer {@link PubsubMessageSchemaCoder} over this coder when adding * features to {@link PubsubIO}. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubMessageWithMessageIdCoder extends CustomCoder { private static final Coder PAYLOAD_CODER = ByteArrayCoder.of(); // A message's messageId cannot be null @@ -48,7 +47,9 @@ public static PubsubMessageWithMessageIdCoder of() { @Override public void encode(PubsubMessage value, OutputStream outStream) throws IOException { PAYLOAD_CODER.encode(value.getPayload(), outStream); - MESSAGE_ID_CODER.encode(value.getMessageId(), outStream); + MESSAGE_ID_CODER.encode( + checkArgumentNotNull(value.getMessageId(), "Cannot encode PubsubMessage without messageId"), + outStream); } @Override diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubReadSchemaTransformProvider.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubReadSchemaTransformProvider.java index c690115045bd..3efe18a08f32 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubReadSchemaTransformProvider.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubReadSchemaTransformProvider.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; + import com.google.api.client.util.Clock; import com.google.auto.service.AutoService; import java.io.Serializable; @@ -247,17 +249,23 @@ void setClock(@Nullable Clock clock) { this.clock = clock; } - @SuppressWarnings("nullness") PubsubIO.Read buildPubsubRead() { PubsubIO.Read pubsubRead = (configuration.getAttributes() == null && configuration.getAttributesMap() == null) ? PubsubIO.readMessages() : PubsubIO.readMessagesWithAttributes(); - if (!Strings.isNullOrEmpty(configuration.getTopic())) { - pubsubRead = pubsubRead.fromTopic(configuration.getTopic()); + String topic = configuration.getTopic(); + if (topic != null && !topic.isEmpty()) { + pubsubRead = pubsubRead.fromTopic(topic); } else { - pubsubRead = pubsubRead.fromSubscription(configuration.getSubscription()); + String subscription = configuration.getSubscription(); + pubsubRead = + pubsubRead.fromSubscription( + checkArgumentNotNull( + subscription, "Either topic or subscription must be specified")); } + final PubsubTestClientFactory clientFactory = this.clientFactory; + final Clock clock = this.clock; if (clientFactory != null && clock != null) { pubsubRead = pubsubRead.withClientFactory(clientFactory); pubsubRead = clientFactory.setClock(pubsubRead, clock); @@ -265,11 +273,13 @@ PubsubIO.Read buildPubsubRead() { throw new IllegalArgumentException( "Both PubsubTestClientFactory and Clock need to be specified for testing, but only one is provided"); } - if (!Strings.isNullOrEmpty(configuration.getIdAttribute())) { - pubsubRead = pubsubRead.withIdAttribute(configuration.getIdAttribute()); + String idAttribute = configuration.getIdAttribute(); + if (idAttribute != null && !idAttribute.isEmpty()) { + pubsubRead = pubsubRead.withIdAttribute(idAttribute); } - if (!Strings.isNullOrEmpty(configuration.getTimestampAttribute())) { - pubsubRead = pubsubRead.withTimestampAttribute(configuration.getTimestampAttribute()); + String timestampAttribute = configuration.getTimestampAttribute(); + if (timestampAttribute != null && !timestampAttribute.isEmpty()) { + pubsubRead = pubsubRead.withTimestampAttribute(timestampAttribute); } return pubsubRead; } @@ -277,11 +287,9 @@ PubsubIO.Read buildPubsubRead() { @Override public PCollectionRowTuple expand(PCollectionRowTuple input) { PubsubIO.Read pubsubRead = buildPubsubRead(); - @SuppressWarnings("nullness") - String errorOutput = - configuration.getErrorHandling() == null - ? null - : configuration.getErrorHandling().getOutput(); + PubsubReadSchemaTransformConfiguration.ErrorHandling errorHandling = + configuration.getErrorHandling(); + String errorOutput = errorHandling == null ? null : errorHandling.getOutput(); PCollectionTuple outputTuple = input diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubRowToMessage.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubRowToMessage.java index da3ee89de0d8..0085662fd3b6 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubRowToMessage.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubRowToMessage.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +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 com.google.auto.value.AutoValue; @@ -24,10 +26,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; -import javax.annotation.Nullable; import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.Field; @@ -43,6 +43,7 @@ import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.TupleTagList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Throwables; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.DateTime; import org.joda.time.Instant; import org.joda.time.ReadableDateTime; @@ -51,9 +52,6 @@ /** Write side {@link Row} to {@link PubsubMessage} converter. */ @Internal @AutoValue -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) abstract class PubsubRowToMessage extends PTransform, PCollectionTuple> { static Builder builder() { @@ -88,18 +86,15 @@ static Builder builder() { * The {@link PayloadSerializer} to {@link PayloadSerializer#serialize(Row)} the payload or user * fields row. */ - @Nullable - abstract PayloadSerializer getPayloadSerializer(); + abstract @Nullable PayloadSerializer getPayloadSerializer(); /** The name of the attribute to apply to the {@link PubsubMessage}. */ - @Nullable - abstract String getTargetTimestampAttributeName(); + abstract @Nullable String getTargetTimestampAttributeName(); /** * Use for testing, simplify assertions of generated timestamp when input lacks a timestamp field. */ - @Nullable - abstract Instant getMockInstant(); + abstract @Nullable Instant getMockInstant(); /** Generates {@link Schema} of the {@link #ERROR} {@link PCollection}. */ static Schema errorSchema(Schema inputSchema) { @@ -345,9 +340,9 @@ static FieldMatcher of(String name, FieldType fieldType) { private final String name; - @Nullable private final TypeName typeName; + private final @Nullable TypeName typeName; - @Nullable private final FieldType fieldType; + private final @Nullable FieldType fieldType; private FieldMatcher(String name, @Nullable TypeName typeName, @Nullable FieldType fieldType) { this.name = name; @@ -379,7 +374,10 @@ boolean match(Schema schema) { if (typeName != null) { return field.getType().getTypeName().equals(typeName); } - return fieldType.equals(field.getType()); + if (fieldType != null) { + return fieldType.equals(field.getType()); + } + return true; } } @@ -404,10 +402,10 @@ static class PubsubRowToMessageDoFn extends DoFn { private final String payloadKeyName; private final Schema errorSchema; - @Nullable private final String targetTimestampKeyName; - @Nullable private final PayloadSerializer payloadSerializer; + private final @Nullable String targetTimestampKeyName; + private final @Nullable PayloadSerializer payloadSerializer; - @Nullable private final Instant mockInstant; + private final @Nullable Instant mockInstant; PubsubRowToMessageDoFn( String attributesKeyName, @@ -449,7 +447,7 @@ public void process(@Element Row row, MultiOutputReceiver receiver) { Row error = Row.withSchema(errorSchema) .withFieldValue(ERROR_DATA_FIELD_NAME, row) - .withFieldValue(ERROR_MESSAGE_FIELD.getName(), message) + .withFieldValue(ERROR_MESSAGE_FIELD.getName(), message != null ? message : "") .withFieldValue(ERROR_STACK_TRACE_FIELD.getName(), stackTrace) .build(); @@ -465,7 +463,8 @@ Map attributesWithoutTimestamp(Row row) { if (!row.getSchema().hasField(attributesKeyName)) { return new HashMap<>(); } - return row.getMap(attributesKeyName); + Map map = row.getMap(attributesKeyName); + return map == null ? new HashMap<>() : map; } /** @@ -482,7 +481,10 @@ String timestampAsString(Row row) { */ ReadableDateTime timestamp(Row row) { if (row.getSchema().hasField(sourceTimestampKeyName)) { - return row.getDateTime(sourceTimestampKeyName); + ReadableDateTime dt = row.getDateTime(sourceTimestampKeyName); + if (dt != null) { + return dt; + } } Instant instant = Instant.now(); if (mockInstant != null) { @@ -503,9 +505,10 @@ ReadableDateTime timestamp(Row row) { byte[] payload(Row row) { if (SchemaReflection.of(row.getSchema()) .matchesAll(FieldMatcher.of(payloadKeyName, PAYLOAD_BYTES_TYPE_NAME))) { - return row.getBytes(payloadKeyName); + return checkArgumentNotNull( + row.getBytes(payloadKeyName), "Payload field '%s' cannot be null", payloadKeyName); } - return Objects.requireNonNull(payloadSerializer).serialize(serializableRow(row)); + return checkStateNotNull(payloadSerializer).serialize(serializableRow(row)); } /** @@ -527,7 +530,8 @@ Row serializableRow(Row row) { } if (schemaReflection.matchesAll(FieldMatcher.of(payloadKeyName, PAYLOAD_ROW_TYPE_NAME))) { - return row.getRow(payloadKeyName); + return checkArgumentNotNull( + row.getRow(payloadKeyName), "Payload field '%s' cannot be null", payloadKeyName); } Schema withUserFieldsOnly = removeFields(row.getSchema(), attributesKeyName, sourceTimestampKeyName); @@ -553,8 +557,7 @@ static Builder builder() { abstract Field getTimestampField(); - @Nullable - abstract Field getPayloadField(); + abstract @Nullable Field getPayloadField(); /** * Builds a {@link Schema} from {@link #getAttributesField()} and {@link #getTimestampField()} @@ -565,8 +568,9 @@ Schema buildSchema(Field... additionalFields) { Schema.Builder builder = Schema.builder().addField(getAttributesField()).addField(getTimestampField()); - if (getPayloadField() != null) { - builder = builder.addField(getPayloadField()); + Field payloadField = getPayloadField(); + if (payloadField != null) { + builder = builder.addField(payloadField); } for (Field field : additionalFields) { diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubSchemaIOProvider.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubSchemaIOProvider.java index d39cee3ddc06..bcecdcc00280 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubSchemaIOProvider.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubSchemaIOProvider.java @@ -24,6 +24,8 @@ import static org.apache.beam.sdk.io.gcp.pubsub.PubsubMessageToRow.PAYLOAD_FIELD; import static org.apache.beam.sdk.io.gcp.pubsub.PubsubMessageToRow.TIMESTAMP_FIELD; import static org.apache.beam.sdk.schemas.Schema.TypeName.ROW; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; @@ -97,9 +99,6 @@ */ @Internal @AutoService(SchemaIOProvider.class) -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubSchemaIOProvider implements SchemaIOProvider { public static final FieldType ATTRIBUTE_MAP_FIELD_TYPE = Schema.FieldType.map(FieldType.STRING.withNullable(false), FieldType.STRING); @@ -137,11 +136,11 @@ public Schema configurationSchema() { * resides there, and some IO-specific configuration object. */ @Override - public PubsubSchemaIO from(String location, Row configuration, Schema dataSchema) { + public PubsubSchemaIO from(String location, Row configuration, @Nullable Schema dataSchema) { validateConfigurationSchema(configuration); - validateDlq(configuration.getValue("deadLetterQueue")); + validateDlq(configuration.getString("deadLetterQueue")); validateDataSchema(dataSchema); - return new PubsubSchemaIO(location, configuration, dataSchema); + return new PubsubSchemaIO(location, configuration, checkArgumentNotNull(dataSchema)); } @Override @@ -154,7 +153,7 @@ public PCollection.IsBounded isBounded() { return PCollection.IsBounded.UNBOUNDED; } - private void validateDataSchema(Schema schema) { + private void validateDataSchema(@Nullable Schema schema) { if (schema == null) { throw new InvalidSchemaException( "Unsupported schema specified for Pubsub source in CREATE TABLE." @@ -168,7 +167,7 @@ private void validateDataSchema(Schema schema) { } } - private void validateDlq(String deadLetterQueue) { + private void validateDlq(@Nullable String deadLetterQueue) { if (deadLetterQueue != null && deadLetterQueue.isEmpty()) { throw new InvalidConfigurationException("Dead letter queue topic name is not specified"); } @@ -252,7 +251,9 @@ public POutput expand(PCollection input) { MapElements.into(TypeDescriptor.of(PubsubMessage.class)) .via( row -> - new PubsubMessage(serializer.serialize(row), ImmutableMap.of()))); + new PubsubMessage( + checkStateNotNull(serializer).serialize(row), + ImmutableMap.of()))); } else { transformed = filtered.apply( @@ -268,33 +269,33 @@ private PubsubIO.Read readMessagesWithAttributes() { PubsubIO.Read read = PubsubIO.readMessagesWithAttributes().fromTopic(location); return config.useTimestampAttribute() - ? read.withTimestampAttribute(config.getTimestampAttributeKey()) + ? read.withTimestampAttribute(checkStateNotNull(config.getTimestampAttributeKey())) : read; } private PubsubIO.Write createPubsubMessageWrite() { PubsubIO.Write write = PubsubIO.writeMessages().to(location); if (config.useTimestampAttribute()) { - write = write.withTimestampAttribute(config.getTimestampAttributeKey()); + write = write.withTimestampAttribute(checkStateNotNull(config.getTimestampAttributeKey())); } return write; } private PubsubIO.Write writeMessagesToDlq() { PubsubIO.Write write = - PubsubIO.writeMessages().to(config.getDeadLetterQueue()); + PubsubIO.writeMessages().to(checkStateNotNull(config.getDeadLetterQueue())); return config.useTimestampAttribute() - ? write.withTimestampAttribute(config.getTimestampAttributeKey()) + ? write.withTimestampAttribute(checkStateNotNull(config.getTimestampAttributeKey())) : write; } - private boolean hasValidAttributesField(Schema schema) { + private static boolean hasValidAttributesField(Schema schema) { return fieldPresent(schema, ATTRIBUTES_FIELD, ATTRIBUTE_MAP_FIELD_TYPE) || fieldPresent(schema, ATTRIBUTES_FIELD, ATTRIBUTE_ARRAY_FIELD_TYPE); } - private boolean hasValidPayloadField(Schema schema) { + private static boolean hasValidPayloadField(Schema schema) { if (!schema.hasField(PAYLOAD_FIELD)) { return false; } @@ -304,7 +305,7 @@ private boolean hasValidPayloadField(Schema schema) { return schema.getField(PAYLOAD_FIELD).getType().getTypeName().equals(ROW); } - private boolean shouldUseNestedSchema(Schema schema) { + private static boolean shouldUseNestedSchema(Schema schema) { return hasValidPayloadField(schema) && hasValidAttributesField(schema); } @@ -350,16 +351,20 @@ boolean useTimestampAttribute() { } PayloadSerializer serializer(Schema schema) { - String format = getFormat() == null ? "json" : getFormat(); + String configFormat = getFormat(); + String format = configFormat == null ? "json" : configFormat; ImmutableMap.Builder params = ImmutableMap.builder(); - if (getThriftClass() != null) { - params.put("thriftClass", getThriftClass()); + String thriftClass = getThriftClass(); + if (thriftClass != null) { + params.put("thriftClass", thriftClass); } - if (getThriftProtocolFactoryClass() != null) { - params.put("thriftProtocolFactoryClass", getThriftProtocolFactoryClass()); + String thriftProtocolFactoryClass = getThriftProtocolFactoryClass(); + if (thriftProtocolFactoryClass != null) { + params.put("thriftProtocolFactoryClass", thriftProtocolFactoryClass); } - if (getProtoClass() != null) { - params.put("protoClass", getProtoClass()); + String protoClass = getProtoClass(); + if (protoClass != null) { + params.put("protoClass", protoClass); } return PayloadSerializers.getSerializer(format, schema, params.build()); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubTestClient.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubTestClient.java index 22fcaae20cad..ee320bb80889 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubTestClient.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubTestClient.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +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.api.client.util.Clock; @@ -40,9 +41,6 @@ * testing {@link #publish}, {@link #pull}, {@link #acknowledge} and {@link #modifyAckDeadline} * methods. Relies on statics to mimic the Pubsub service, though we try to hide that. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubTestClient extends PubsubClient implements Serializable { /** * Mimic the state of the simulated Pubsub 'service'. @@ -362,26 +360,28 @@ private static void setSchemaState( /** Handles verifying {@code STATE} at end of publish test. */ private static void performFinalPublishStateChecks() { checkState(STATE.isActive, "No test still in flight"); + Set remainingExpected = + checkStateNotNull(STATE.remainingExpectedOutgoingMessages); checkState( - STATE.remainingExpectedOutgoingMessages.isEmpty(), + remainingExpected.isEmpty(), "Still waiting for %s messages to be published", - STATE.remainingExpectedOutgoingMessages.size()); + remainingExpected.size()); } /** Handles verifying {@code STATE} at end of pull test. */ private static void performFinalPullStateChecks() { + List remainingPending = + checkStateNotNull(STATE.remainingPendingIncomingMessages); + Map pendingAck = checkStateNotNull(STATE.pendingAckIncomingMessages); + Map ackDeadline = checkStateNotNull(STATE.ackDeadline); checkState( - STATE.remainingPendingIncomingMessages.isEmpty(), + remainingPending.isEmpty(), "Still waiting for %s messages to be pulled", - STATE.remainingPendingIncomingMessages.size()); + remainingPending.size()); checkState( - STATE.pendingAckIncomingMessages.isEmpty(), - "Still waiting for %s messages to be ACKed", - STATE.pendingAckIncomingMessages.size()); + pendingAck.isEmpty(), "Still waiting for %s messages to be ACKed", pendingAck.size()); checkState( - STATE.ackDeadline.isEmpty(), - "Still waiting for %s messages to be ACKed", - STATE.ackDeadline.size()); + ackDeadline.isEmpty(), "Still waiting for %s messages to be ACKed", ackDeadline.size()); } public static PubsubTestClientFactory createFactoryForCreateSubscription() { @@ -445,13 +445,17 @@ private boolean inPublishMode() { public void advance() { synchronized (STATE) { checkState(inPullMode(), "Can only advance in pull mode"); + Clock clock = checkStateNotNull(STATE.clock); + Map ackDeadline = checkStateNotNull(STATE.ackDeadline); + Map pendingAck = checkStateNotNull(STATE.pendingAckIncomingMessages); + List remainingPending = + checkStateNotNull(STATE.remainingPendingIncomingMessages); // Any messages who's ACKs timed out are available for re-pulling. - Iterator> deadlineItr = STATE.ackDeadline.entrySet().iterator(); + Iterator> deadlineItr = ackDeadline.entrySet().iterator(); while (deadlineItr.hasNext()) { Map.Entry entry = deadlineItr.next(); - if (entry.getValue() <= STATE.clock.currentTimeMillis()) { - STATE.remainingPendingIncomingMessages.add( - STATE.pendingAckIncomingMessages.remove(entry.getKey())); + if (entry.getValue() <= clock.currentTimeMillis()) { + remainingPending.add(checkStateNotNull(pendingAck.remove(entry.getKey()))); deadlineItr.remove(); } } @@ -465,14 +469,19 @@ public void close() {} public int publish(TopicPath topic, List outgoingMessages) throws IOException { synchronized (STATE) { checkState(inPublishMode(), "Can only publish in publish mode"); - boolean isDynamic = STATE.expectedTopic == null; + TopicPath expectedTopic = STATE.expectedTopic; + boolean isDynamic = expectedTopic == null; if (!isDynamic) { checkState( - topic.equals(STATE.expectedTopic), + topic.equals(expectedTopic), "Topic %s does not match expected %s", topic, - STATE.expectedTopic); + expectedTopic); } + Set remainingExpected = + checkStateNotNull(STATE.remainingExpectedOutgoingMessages); + Set remainingFailing = + checkStateNotNull(STATE.remainingFailingOutgoingMessages); @MonotonicNonNull String batchOrderingKey = null; for (OutgoingMessage outgoingMessage : outgoingMessages) { if (batchOrderingKey == null) { @@ -480,15 +489,15 @@ public int publish(TopicPath topic, List outgoingMessages) thro } checkState(outgoingMessage.getMessage().getOrderingKey().equals(batchOrderingKey)); if (isDynamic) { - checkState(outgoingMessage.topic().equals(topic.getPath())); + checkState(topic.getPath().equals(outgoingMessage.topic())); } else { checkState(outgoingMessage.topic() == null); } - if (STATE.remainingFailingOutgoingMessages.remove(outgoingMessage)) { + if (remainingFailing.remove(outgoingMessage)) { throw new RuntimeException("Simulating failure for " + outgoingMessage); } checkState( - STATE.remainingExpectedOutgoingMessages.remove(outgoingMessage), + remainingExpected.remove(outgoingMessage), "Unexpected outgoing message %s", outgoingMessage); } @@ -505,21 +514,27 @@ public List pull( throws IOException { synchronized (STATE) { checkState(inPullMode(), "Can only pull in pull mode"); - long now = STATE.clock.currentTimeMillis(); + Clock clock = checkStateNotNull(STATE.clock); + long now = clock.currentTimeMillis(); checkState( requestTimeMsSinceEpoch == now, "Simulated time %s does not match request time %s", now, requestTimeMsSinceEpoch); + SubscriptionPath expectedSubscription = checkStateNotNull(STATE.expectedSubscription); checkState( - subscription.equals(STATE.expectedSubscription), + subscription.equals(expectedSubscription), "Subscription %s does not match expected %s", subscription, - STATE.expectedSubscription); + expectedSubscription); checkState(returnImmediately, "Pull only supported if returning immediately"); List incomingMessages = new ArrayList<>(); - Iterator pendItr = STATE.remainingPendingIncomingMessages.iterator(); + List remainingPending = + checkStateNotNull(STATE.remainingPendingIncomingMessages); + Map pendingAck = checkStateNotNull(STATE.pendingAckIncomingMessages); + Map ackDeadline = checkStateNotNull(STATE.ackDeadline); + Iterator pendItr = remainingPending.iterator(); while (pendItr.hasNext()) { IncomingMessage incomingMessage = pendItr.next(); pendItr.remove(); @@ -531,9 +546,8 @@ public List pull( incomingMessage.ackId(), incomingMessage.recordId()); incomingMessages.add(incomingMessageWithRequestTime); - STATE.pendingAckIncomingMessages.put( - incomingMessageWithRequestTime.ackId(), incomingMessageWithRequestTime); - STATE.ackDeadline.put( + pendingAck.put(incomingMessageWithRequestTime.ackId(), incomingMessageWithRequestTime); + ackDeadline.put( incomingMessageWithRequestTime.ackId(), requestTimeMsSinceEpoch + STATE.ackTimeoutSec * 1000L); if (incomingMessages.size() >= batchSize) { @@ -548,19 +562,22 @@ public List pull( public void acknowledge(SubscriptionPath subscription, List ackIds) throws IOException { synchronized (STATE) { checkState(inPullMode(), "Can only acknowledge in pull mode"); + SubscriptionPath expectedSubscription = checkStateNotNull(STATE.expectedSubscription); checkState( - subscription.equals(STATE.expectedSubscription), + subscription.equals(expectedSubscription), "Subscription %s does not match expected %s", subscription, - STATE.expectedSubscription); + expectedSubscription); + Map ackDeadline = checkStateNotNull(STATE.ackDeadline); + Map pendingAck = checkStateNotNull(STATE.pendingAckIncomingMessages); for (String ackId : ackIds) { checkState( - STATE.ackDeadline.remove(ackId) != null, + ackDeadline.remove(ackId) != null, "No message with ACK id %s is waiting for an ACK", ackId); checkState( - STATE.pendingAckIncomingMessages.remove(ackId) != null, + pendingAck.remove(ackId) != null, "No message with ACK id %s is waiting for an ACK", ackId); } @@ -572,31 +589,40 @@ public void modifyAckDeadline( SubscriptionPath subscription, List ackIds, int deadlineSeconds) throws IOException { synchronized (STATE) { checkState(inPullMode(), "Can only modify ack deadline in pull mode"); + SubscriptionPath expectedSubscription = checkStateNotNull(STATE.expectedSubscription); checkState( - subscription.equals(STATE.expectedSubscription), + subscription.equals(expectedSubscription), "Subscription %s does not match expected %s", subscription, - STATE.expectedSubscription); + expectedSubscription); + Map ackDeadline = checkStateNotNull(STATE.ackDeadline); + Map pendingAck = checkStateNotNull(STATE.pendingAckIncomingMessages); + Clock clock = checkStateNotNull(STATE.clock); + List remainingPending = + checkStateNotNull(STATE.remainingPendingIncomingMessages); for (String ackId : ackIds) { if (deadlineSeconds > 0) { checkState( - STATE.ackDeadline.remove(ackId) != null, + ackDeadline.remove(ackId) != null, "No message with ACK id %s is waiting for an ACK", ackId); checkState( - STATE.pendingAckIncomingMessages.containsKey(ackId), + pendingAck.containsKey(ackId), "No message with ACK id %s is waiting for an ACK", ackId); - STATE.ackDeadline.put(ackId, STATE.clock.currentTimeMillis() + deadlineSeconds * 1000L); + ackDeadline.put(ackId, clock.currentTimeMillis() + deadlineSeconds * 1000L); } else { checkState( - STATE.ackDeadline.remove(ackId) != null, + ackDeadline.remove(ackId) != null, "No message with ACK id %s is waiting for an ACK", ackId); - IncomingMessage message = STATE.pendingAckIncomingMessages.remove(ackId); - checkState(message != null, "No message with ACK id %s is waiting for an ACK", ackId); - STATE.remainingPendingIncomingMessages.add(message); + IncomingMessage message = + checkStateNotNull( + pendingAck.remove(ackId), + "No message with ACK id %s is waiting for an ACK", + ackId); + remainingPending.add(message); } } } @@ -656,7 +682,7 @@ public int ackDeadlineSeconds(SubscriptionPath subscription) throws IOException public boolean isEOF() { synchronized (STATE) { checkState(inPullMode(), "Can only check EOF in pull mode"); - return STATE.remainingPendingIncomingMessages.isEmpty(); + return checkStateNotNull(STATE.remainingPendingIncomingMessages).isEmpty(); } } @@ -674,12 +700,12 @@ public void deleteSchema(SchemaPath schemaPath) throws IOException { } @Override - public SchemaPath getSchemaPath(TopicPath topicPath) throws IOException { + public @Nullable SchemaPath getSchemaPath(TopicPath topicPath) throws IOException { return STATE.expectedSchemaPath; } @Override public Schema getSchema(SchemaPath schemaPath) throws IOException { - return STATE.expectedSchema; + return checkStateNotNull(STATE.expectedSchema, "expectedSchema is not set"); } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubUnboundedSink.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubUnboundedSink.java index 3fe7d51aec1e..45c0b329988d 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubUnboundedSink.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubUnboundedSink.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +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.checkState; import com.google.protobuf.InvalidProtocolBufferException; @@ -96,9 +98,6 @@ * dedup messages. * */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubUnboundedSink extends PTransform, PDone> { /** Default maximum number of messages per publish. */ static final int DEFAULT_PUBLISH_BATCH_SIZE = 1000; @@ -109,7 +108,6 @@ public class PubsubUnboundedSink extends PTransform, /** Default longest delay between receiving a message and pushing it to Pubsub. */ private static final Duration DEFAULT_MAX_LATENCY = Duration.standardSeconds(2); - /** Coder for conveying outgoing messages between internal stages. */ /** Coder for conveying outgoing messages between internal stages. */ private static class OutgoingMessageCoder extends AtomicCoder { private static final NullableCoder RECORD_ID_CODER = @@ -198,7 +196,7 @@ public void processElement( recordId = Hashing.murmur3_128().hashBytes(elementBytes).toString(); break; case RANDOM: - // Since these elements go through a GroupByKey, any failures while sending to + // Since these elements go through a GroupByKey, any failures while sending to // Pubsub will be retried without falling back and generating a new record id. // Thus even though we may send the same message to Pubsub twice, it is guaranteed // to have the same record id. @@ -246,11 +244,11 @@ private static class OutgoingData { private final PubsubClientFactory pubsubFactory; private final @Nullable ValueProvider topic; - private final String timestampAttribute; - private final String idAttribute; + private final @Nullable String timestampAttribute; + private final @Nullable String idAttribute; private final int publishBatchBytes; - private final String pubsubRootUrl; + private final @Nullable String pubsubRootUrl; /** Client on which to talk to Pubsub. Null until created by {@link #startBundle}. */ private transient @Nullable PubsubClient pubsubClient; @@ -262,8 +260,8 @@ private static class OutgoingData { WriterFn( PubsubClientFactory pubsubFactory, @Nullable ValueProvider topic, - String timestampAttribute, - String idAttribute, + @Nullable String timestampAttribute, + @Nullable String idAttribute, int publishBatchSize, int publishBatchBytes) { this.pubsubFactory = pubsubFactory; @@ -277,10 +275,10 @@ private static class OutgoingData { WriterFn( PubsubClientFactory pubsubFactory, @Nullable ValueProvider topic, - String timestampAttribute, - String idAttribute, + @Nullable String timestampAttribute, + @Nullable String idAttribute, int publishBatchBytes, - String pubsubRootUrl) { + @Nullable String pubsubRootUrl) { this.pubsubFactory = pubsubFactory; this.topic = topic; this.timestampAttribute = timestampAttribute; @@ -290,11 +288,13 @@ private static class OutgoingData { } @VisibleForTesting + @Nullable String getIdAttribute() { return idAttribute; } @VisibleForTesting + @Nullable ValueProvider getTopic() { return topic; } @@ -308,11 +308,9 @@ private void publishBatch(List messages, int bytes) throws IOEx } else { // This is the dynamic topic destinations case. Since we first group by topic, we can assume // that all messages in the batch have the same topic. - topicPath = - PubsubClient.topicPathFromPath( - org.apache.beam.sdk.util.Preconditions.checkStateNotNull(messages.get(0).topic())); + topicPath = PubsubClient.topicPathFromPath(checkStateNotNull(messages.get(0).topic())); } - int n = pubsubClient.publish(topicPath, messages); + int n = checkStateNotNull(pubsubClient).publish(topicPath, messages); checkState( n == messages.size(), "Attempted to publish %s messages but %s were successful", @@ -380,8 +378,11 @@ public void processElement(ProcessContext c) throws Exception { @FinishBundle public void finishBundle() throws Exception { - pubsubClient.close(); - pubsubClient = null; + final PubsubClient pubsubClient = this.pubsubClient; + if (pubsubClient != null) { + pubsubClient.close(); + } + this.pubsubClient = null; } @Override @@ -448,21 +449,21 @@ public void populateDisplayData(DisplayData.Builder builder) { */ private final RecordIdMethod recordIdMethod; - private final String pubsubRootUrl; + private final @Nullable String pubsubRootUrl; @VisibleForTesting PubsubUnboundedSink( PubsubClientFactory pubsubFactory, @Nullable ValueProvider topic, - String timestampAttribute, - String idAttribute, + @Nullable String timestampAttribute, + @Nullable String idAttribute, int numShards, boolean publishBatchWithOrderingKey, int publishBatchSize, int publishBatchBytes, Duration maxLatency, RecordIdMethod recordIdMethod, - String pubsubRootUrl) { + @Nullable String pubsubRootUrl) { this.pubsubFactory = pubsubFactory; this.topic = topic; this.timestampAttribute = timestampAttribute; @@ -544,14 +545,14 @@ public PubsubUnboundedSink( public PubsubUnboundedSink( PubsubClientFactory pubsubFactory, - ValueProvider topic, - String timestampAttribute, - String idAttribute, + @Nullable ValueProvider topic, + @Nullable String timestampAttribute, + @Nullable String idAttribute, int numShards, boolean publishBatchWithOrderingKey, int publishBatchSize, int publishBatchBytes, - String pubsubRootUrl) { + @Nullable String pubsubRootUrl) { this( pubsubFactory, topic, @@ -604,7 +605,12 @@ public PDone expand(PCollection input) { return input .apply( "WithDynamicTopics", - WithKeys.of(PubsubMessage::getTopic).withKeyType(TypeDescriptors.strings())) + WithKeys.of( + (PubsubMessage msg) -> + checkArgumentNotNull( + msg.getTopic(), + "Topic must be set on PubsubMessage when writing to dynamic topics.")) + .withKeyType(TypeDescriptors.strings())) .apply( MapValues.into(new TypeDescriptor() {}) .via(new PubsubMessages.ParsePayloadAsPubsubMessageProto())) diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubUnboundedSource.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubUnboundedSource.java index bc91e9f381d4..a6f3917b0d59 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubUnboundedSource.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubUnboundedSource.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +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 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; @@ -114,9 +115,6 @@ * UnboundedSource.UnboundedReader} instances to execute concurrently and thus hide latency. * */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class PubsubUnboundedSource extends PTransform> { private static final Logger LOG = LoggerFactory.getLogger(PubsubUnboundedSource.class); @@ -265,12 +263,18 @@ public PubsubCheckpoint( */ @Override public void finalizeCheckpoint() throws IOException { - checkState(reader != null && safeToAckIds != null, "Cannot finalize a restored checkpoint"); - // Even if the 'true' active reader has changed since the checkpoint was taken we are + final PubsubReader reader = + checkStateNotNull(this.reader, "Cannot finalize a restored checkpoint"); + final List safeToAckIds = + checkStateNotNull(this.safeToAckIds, "Cannot finalize a restored checkpoint"); + // Even if the 'true' active reader has changed since the checkpoint was taken + // we are // fine: - // - The underlying Pubsub topic will not have changed, so the following ACKs will still + // - The underlying Pubsub topic will not have changed, so the following ACKs + // will still // go to the right place. - // - We'll delete the ACK ids from the readers in-flight state, but that only effects + // - We'll delete the ACK ids from the readers in-flight state, but that only + // effects // flow control and stats, neither of which are relevant anymore. try { int n = safeToAckIds.size(); @@ -291,8 +295,8 @@ public void finalizeCheckpoint() throws IOException { int remainingInFlight = reader.numInFlightCheckpoints.decrementAndGet(); checkState(remainingInFlight >= 0, "Miscounted in-flight checkpoints"); reader.maybeCloseClient(); - reader = null; - safeToAckIds = null; + this.reader = null; + this.safeToAckIds = null; } } @@ -330,7 +334,7 @@ public void nackAll(PubsubReader reader) throws IOException { /** The coder for our checkpoints. */ private static class PubsubCheckpointCoder extends AtomicCoder { - private static final Coder SUBSCRIPTION_PATH_CODER = + private static final Coder<@Nullable String> SUBSCRIPTION_PATH_CODER = NullableCoder.of(StringUtf8Coder.of()); private static final Coder> LIST_CODER = ListCoder.of(StringUtf8Coder.of()); @@ -348,7 +352,7 @@ public void encode(PubsubCheckpoint value, OutputStream outStream) throws IOExce @Override public PubsubCheckpoint decode(InputStream inStream) throws IOException { - String path = SUBSCRIPTION_PATH_CODER.decode(inStream); + @Nullable String path = SUBSCRIPTION_PATH_CODER.decode(inStream); List notYetReadIds = LIST_CODER.decode(inStream); return new PubsubCheckpoint(path, null, null, notYetReadIds); } @@ -370,13 +374,13 @@ static class PubsubReader extends UnboundedSource.UnboundedReader { @VisibleForTesting final SubscriptionPath subscription; /** Client on which to talk to Pubsub. Contains a null value if the client has been closed. */ - private AtomicReference pubsubClient; + private final AtomicReference<@Nullable PubsubClient> pubsubClient; /** * The closed state of this {@link PubsubReader}. If true, the reader has not yet been closed, * and it will have a non-null value within {@link #pubsubClient}. */ - private AtomicBoolean active = new AtomicBoolean(true); + private final AtomicBoolean active = new AtomicBoolean(true); /** * Ack timeout, in ms, as set on subscription when we first start reading. Not updated @@ -385,7 +389,7 @@ static class PubsubReader extends UnboundedSource.UnboundedReader { private int ackTimeoutMs; /** ACK ids of messages we have delivered downstream but not yet ACKed. */ - private Set safeToAckIds; + private final Set safeToAckIds; /** * Messages we have received from Pubsub and not yet delivered downstream. We preserve their @@ -427,13 +431,13 @@ public InFlightState(long requestTimeMsSinceEpoch, long ackDeadlineMsSinceEpoch) * Bucketed map from received time (as system time, ms since epoch) to message timestamps * (mssince epoch) of all received but not-yet read messages. Used to estimate watermark. */ - private BucketingFunction minUnreadTimestampMsSinceEpoch; + private final BucketingFunction minUnreadTimestampMsSinceEpoch; /** * Minimum of timestamps (ms since epoch) of all recently read messages. Used to estimate * watermark. */ - private MovingFunction minReadTimestampMsSinceEpoch; + private final MovingFunction minReadTimestampMsSinceEpoch; /** * System time (ms since epoch) we last received a message from Pubsub, or -1 if not yet @@ -454,60 +458,60 @@ public InFlightState(long requestTimeMsSinceEpoch, long ackDeadlineMsSinceEpoch) private long numReceived; /** Stats only: Number of messages which have recently been received. */ - private MovingFunction numReceivedRecently; + private final MovingFunction numReceivedRecently; /** Stats only: Number of messages which have recently had their deadline extended. */ - private MovingFunction numExtendedDeadlines; + private final MovingFunction numExtendedDeadlines; /** * Stats only: Number of messages which have recenttly had their deadline extended even though * it may be too late to do so. */ - private MovingFunction numLateDeadlines; + private final MovingFunction numLateDeadlines; /** Stats only: Number of messages which have recently been ACKed. */ - private MovingFunction numAcked; + private final MovingFunction numAcked; /** * Stats only: Number of messages which have recently expired (ACKs were extended for too long). */ - private MovingFunction numExpired; + private final MovingFunction numExpired; /** Stats only: Number of messages which have recently been NACKed. */ - private MovingFunction numNacked; + private final MovingFunction numNacked; /** Stats only: Number of message bytes which have recently been read by downstream consumer. */ - private MovingFunction numReadBytes; + private final MovingFunction numReadBytes; /** * Stats only: Minimum of timestamp (ms since epoch) of all recently received messages. Used to - * estimate timestamp skew. Does not contribute to watermark estimator. + * estimate timestamp skew. Does not contribute to watermark watermark. */ - private MovingFunction minReceivedTimestampMsSinceEpoch; + private final MovingFunction minReceivedTimestampMsSinceEpoch; /** * Stats only: Maximum of timestamp (ms since epoch) of all recently received messages. Used to * estimate timestamp skew. */ - private MovingFunction maxReceivedTimestampMsSinceEpoch; + private final MovingFunction maxReceivedTimestampMsSinceEpoch; /** Stats only: Minimum of recent estimated watermarks (ms since epoch). */ - private MovingFunction minWatermarkMsSinceEpoch; + private final MovingFunction minWatermarkMsSinceEpoch; /** Stats ony: Maximum of recent estimated watermarks (ms since epoch). */ - private MovingFunction maxWatermarkMsSinceEpoch; + private final MovingFunction maxWatermarkMsSinceEpoch; /** * Stats only: Number of messages with timestamps strictly behind the estimated watermark at the * time they are received. These may be considered 'late' by downstream computations. */ - private MovingFunction numLateMessages; + private final MovingFunction numLateMessages; /** * Stats only: Current number of checkpoints in flight. CAUTION: Accessed by both checkpointing * and reader threads. */ - private AtomicInteger numInFlightCheckpoints; + private final AtomicInteger numInFlightCheckpoints; /** Stats only: Maximum number of checkpoints in flight at any time. */ private int maxInFlightCheckpoints; @@ -563,7 +567,11 @@ public PubsubReader(PubsubOptions options, PubsubSource outer, SubscriptionPath @VisibleForTesting PubsubClient getPubsubClient() { - return pubsubClient.get(); + return getClient(); + } + + private PubsubClient getClient() { + return checkStateNotNull(pubsubClient.get(), "Client has been closed"); } /** @@ -575,7 +583,7 @@ PubsubClient getPubsubClient() { *

CAUTION: Retains {@code ackIds}. */ void ackBatch(List ackIds) throws IOException { - pubsubClient.get().acknowledge(subscription, ackIds); + getClient().acknowledge(subscription, ackIds); ackedIds.add(ackIds); } @@ -584,7 +592,7 @@ void ackBatch(List ackIds) throws IOException { * given {@code ackIds}. Does not retain {@code ackIds}. */ public void nackBatch(long nowMsSinceEpoch, List ackIds) throws IOException { - pubsubClient.get().modifyAckDeadline(subscription, ackIds, 0); + getClient().modifyAckDeadline(subscription, ackIds, 0); numNacked.add(nowMsSinceEpoch, ackIds.size()); } @@ -594,7 +602,7 @@ public void nackBatch(long nowMsSinceEpoch, List ackIds) throws IOExcept */ private void extendBatch(long nowMsSinceEpoch, List ackIds) throws IOException { int extensionSec = (ackTimeoutMs * ACK_EXTENSION_PCT) / (100 * 1000); - pubsubClient.get().modifyAckDeadline(subscription, ackIds, extensionSec); + getClient().modifyAckDeadline(subscription, ackIds, extensionSec); numExtendedDeadlines.add(nowMsSinceEpoch, ackIds.size()); } @@ -648,7 +656,8 @@ private void extend() throws IOException { < nowMsSinceEpoch) { // Pubsub may have already considered this message to have expired. // If so it will (eventually) be made available on a future pull request. - // If this message ends up being committed then it will be considered a duplicate + // If this message ends up being committed then it will be considered a + // duplicate // when re-pulled. assumeExpired.add(entry.getKey()); continue; @@ -693,11 +702,12 @@ private void extend() throws IOException { if (!toBeExtended.isEmpty()) { // Pubsub extends acks from it's notion of current time. - // We'll try to track that on our side, but note the deadlines won't necessarily agree. + // We'll try to track that on our side, but note the deadlines won't necessarily + // agree. long newDeadlineMsSinceEpoch = nowMsSinceEpoch + (ackTimeoutMs * ACK_EXTENSION_PCT) / 100; for (String ackId : toBeExtended) { // Maintain increasing ack deadline order. - InFlightState state = inFlight.remove(ackId); + InFlightState state = checkStateNotNull(inFlight.remove(ackId)); inFlight.put( ackId, new InFlightState(state.requestTimeMsSinceEpoch, newDeadlineMsSinceEpoch)); } @@ -711,8 +721,10 @@ private void extend() throws IOException { private void pull() throws IOException { if (inFlight.size() >= MAX_IN_FLIGHT) { // Wait for checkpoint to be finalized before pulling anymore. - // There may be lag while checkpoints are persisted and the finalizeCheckpoint method - // is invoked. By limiting the in-flight messages we can ensure we don't end up consuming + // There may be lag while checkpoints are persisted and the finalizeCheckpoint + // method + // is invoked. By limiting the in-flight messages we can ensure we don't end up + // consuming // messages faster than we can checkpoint them. return; } @@ -723,7 +735,7 @@ private void pull() throws IOException { // Pull the next batch. // BLOCKs until received. Collection receivedMessages = - pubsubClient.get().pull(requestTimeMsSinceEpoch, subscription, PULL_BATCH_SIZE, true); + getClient().pull(requestTimeMsSinceEpoch, subscription, PULL_BATCH_SIZE, true); if (receivedMessages.isEmpty()) { // Nothing available yet. Try again later. return; @@ -779,7 +791,8 @@ private void stats() { String oldestAckId = Iterables.getFirst(inFlight.keySet(), null); if (oldestAckId != null) { oldestInFlight = - (nowMsSinceEpoch - inFlight.get(oldestAckId).requestTimeMsSinceEpoch) + "ms"; + (nowMsSinceEpoch - checkStateNotNull(inFlight.get(oldestAckId)).requestTimeMsSinceEpoch) + + "ms"; } LOG.debug( @@ -828,7 +841,7 @@ private void stats() { @Override public boolean start() throws IOException { // Determine the ack timeout. - ackTimeoutMs = pubsubClient.get().ackDeadlineSeconds(subscription) * 1000; + ackTimeoutMs = getClient().ackDeadlineSeconds(subscription) * 1000; return advance(); } @@ -843,7 +856,8 @@ public boolean advance() throws IOException { stats(); if (current != null) { - // Current is consumed. It can no longer contribute to holding back the watermark. + // Current is consumed. It can no longer contribute to holding back the + // watermark. minUnreadTimestampMsSinceEpoch.remove(current.requestTimeMsSinceEpoch()); current = null; } @@ -853,24 +867,28 @@ public boolean advance() throws IOException { // Extend all pressing deadlines. // Will BLOCK until done. - // If the system is pulling messages only to let them sit in a downsteam queue then + // If the system is pulling messages only to let them sit in a downsteam queue + // then // this will have the effect of slowing down the pull rate. - // However, if the system is genuinely taking longer to process each message then + // However, if the system is genuinely taking longer to process each message + // then // the work to extend ACKs would be better done in the background. extend(); if (notYetRead.isEmpty()) { // Pull another batch. - // Will BLOCK until fetch returns, but will not block until a message is available. + // Will BLOCK until fetch returns, but will not block until a message is + // available. pull(); } // Take one message from queue. - current = notYetRead.poll(); + final PubsubClient.@Nullable IncomingMessage current = notYetRead.poll(); if (current == null) { // Try again later. return false; } + this.current = current; notYetReadBytes -= current.message().getData().size(); checkState(notYetReadBytes >= 0); long nowMsSinceEpoch = now(); @@ -888,6 +906,7 @@ public boolean advance() throws IOException { @Override public byte[] getCurrent() throws NoSuchElementException { + final PubsubClient.@Nullable IncomingMessage current = this.current; if (current == null) { throw new NoSuchElementException(); } @@ -901,6 +920,7 @@ public byte[] getCurrent() throws NoSuchElementException { @Override public Instant getCurrentTimestamp() throws NoSuchElementException { + final PubsubClient.@Nullable IncomingMessage current = this.current; if (current == null) { throw new NoSuchElementException(); } @@ -909,6 +929,7 @@ public Instant getCurrentTimestamp() throws NoSuchElementException { @Override public byte[] getCurrentRecordId() throws NoSuchElementException { + final PubsubClient.@Nullable IncomingMessage current = this.current; if (current == null) { throw new NoSuchElementException(); } @@ -934,7 +955,8 @@ public void close() throws IOException { */ private void maybeCloseClient() throws IOException { if (!active.get() && numInFlightCheckpoints.get() == 0) { - // The reader has been closed and it has no more outstanding checkpoints. The client + // The reader has been closed and it has no more outstanding checkpoints. The + // client // must be closed so it doesn't leak PubsubClient client = pubsubClient.getAndSet(null); if (client != null) { @@ -950,18 +972,23 @@ public PubsubSource getCurrentSource() { @Override public Instant getWatermark() { - if (pubsubClient.get().isEOF() && notYetRead.isEmpty()) { + if (getClient().isEOF() && notYetRead.isEmpty()) { // For testing only: Advance the watermark to the end of time to signal // the test is complete. return BoundedWindow.TIMESTAMP_MAX_VALUE; } - // NOTE: We'll allow the watermark to go backwards. The underlying runner is responsible - // for aggregating all reported watermarks and ensuring the aggregate is latched. - // If we attempt to latch locally then it is possible a temporary starvation of one reader - // could cause its estimated watermark to fast forward to current system time. Then when + // NOTE: We'll allow the watermark to go backwards. The underlying runner is + // responsible + // for aggregating all reported watermarks and ensuring the aggregate is + // latched. + // If we attempt to latch locally then it is possible a temporary starvation of + // one reader + // could cause its estimated watermark to fast forward to current system time. + // Then when // the reader resumes its watermark would be unable to resume tracking. - // By letting the underlying runner latch we avoid any problems due to localized starvation. + // By letting the underlying runner latch we avoid any problems due to localized + // starvation. long nowMsSinceEpoch = now(); long readMin = minReadTimestampMsSinceEpoch.get(nowMsSinceEpoch); long unreadMin = minUnreadTimestampMsSinceEpoch.get(); @@ -969,17 +996,21 @@ public Instant getWatermark() { && unreadMin == Long.MAX_VALUE && lastReceivedMsSinceEpoch >= 0 && nowMsSinceEpoch > lastReceivedMsSinceEpoch + SAMPLE_PERIOD.getMillis()) { - // We don't currently have any unread messages pending, we have not had any messages - // read for a while, and we have not received any new messages from Pubsub for a while. + // We don't currently have any unread messages pending, we have not had any + // messages + // read for a while, and we have not received any new messages from Pubsub for a + // while. // Advance watermark to current time. // TODO: Estimate a timestamp lag. lastWatermarkMsSinceEpoch = nowMsSinceEpoch; } else if (minReadTimestampMsSinceEpoch.isSignificant() || minUnreadTimestampMsSinceEpoch.isSignificant()) { - // Take minimum of the timestamps in all unread messages and recently read messages. + // Take minimum of the timestamps in all unread messages and recently read + // messages. lastWatermarkMsSinceEpoch = Math.min(readMin, unreadMin); } - // else: We're not confident enough to estimate a new watermark. Stick with the old one. + // else: We're not confident enough to estimate a new watermark. Stick with the + // old one. minWatermarkMsSinceEpoch.add(nowMsSinceEpoch, lastWatermarkMsSinceEpoch); maxWatermarkMsSinceEpoch.add(nowMsSinceEpoch, lastWatermarkMsSinceEpoch); return new Instant(lastWatermarkMsSinceEpoch); @@ -991,15 +1022,18 @@ public PubsubCheckpoint getCheckpointMark() { maxInFlightCheckpoints = Math.max(maxInFlightCheckpoints, cur); // It's possible for a checkpoint to be taken but never finalized. // So we simply copy whatever safeToAckIds we currently have. - // It's possible a later checkpoint will be taken before an earlier one is finalized, - // in which case we'll double ACK messages to Pubsub. However Pubsub is fine with that. + // It's possible a later checkpoint will be taken before an earlier one is + // finalized, + // in which case we'll double ACK messages to Pubsub. However Pubsub is fine + // with that. List snapshotSafeToAckIds = Lists.newArrayList(safeToAckIds); List snapshotNotYetReadIds = new ArrayList<>(notYetRead.size()); for (PubsubClient.IncomingMessage incomingMessage : notYetRead) { snapshotNotYetReadIds.add(incomingMessage.ackId()); } if (outer.subscriptionPath == null) { - // need to include the subscription in case we resume, as it's not stored in the source. + // need to include the subscription in case we resume, as it's not stored in the + // source. return new PubsubCheckpoint( subscription.getPath(), this, snapshotSafeToAckIds, snapshotNotYetReadIds); } @@ -1020,14 +1054,14 @@ public long getSplitBacklogBytes() { static class PubsubSource extends UnboundedSource { public final PubsubUnboundedSource outer; // The subscription to read from. - @VisibleForTesting final ValueProvider subscriptionPath; + @VisibleForTesting final @Nullable ValueProvider subscriptionPath; public PubsubSource(PubsubUnboundedSource outer) { this(outer, outer.getSubscriptionProvider()); } private PubsubSource( - PubsubUnboundedSource outer, ValueProvider subscriptionPath) { + PubsubUnboundedSource outer, @Nullable ValueProvider subscriptionPath) { this.outer = outer; this.subscriptionPath = subscriptionPath; } @@ -1055,16 +1089,22 @@ public PubsubReader createReader( PipelineOptions options, @Nullable PubsubCheckpoint checkpoint) { PubsubReader reader; SubscriptionPath subscription; - if (subscriptionPath == null || subscriptionPath.get() == null) { + final @Nullable ValueProvider subscriptionPath = this.subscriptionPath; + final @Nullable SubscriptionPath subscriptionPathVal = + subscriptionPath == null ? null : subscriptionPath.get(); + if (subscriptionPathVal == null) { if (checkpoint == null) { // This reader has never been started and there was no call to #split; // create a single random subscription, which will be kept in the checkpoint. subscription = outer.createRandomSubscription(options); } else { - subscription = checkpoint.getSubscription(); + subscription = + checkStateNotNull( + checkpoint.getSubscription(), + "Checkpoint must have a subscription path when subscriptionPath is not set."); } } else { - subscription = subscriptionPath.get(); + subscription = subscriptionPathVal; } try { reader = new PubsubReader(options.as(PubsubOptions.class), this, subscription); @@ -1088,7 +1128,7 @@ public PubsubReader createReader( } @Override - public @Nullable Coder getCheckpointMarkCoder() { + public Coder getCheckpointMarkCoder() { return CHECKPOINT_CODER; } @@ -1104,7 +1144,8 @@ public void validate() { @Override public boolean requiresDeduping() { - // We cannot prevent re-offering already read messages after a restore from checkpoint. + // We cannot prevent re-offering already read messages after a restore from + // checkpoint. return true; } } @@ -1205,7 +1246,7 @@ public void populateDisplayData(DisplayData.Builder builder) { @VisibleForTesting PubsubUnboundedSource( - Clock clock, + @Nullable Clock clock, PubsubClientFactory pubsubFactory, @Nullable ValueProvider project, @Nullable ValueProvider topic, @@ -1254,7 +1295,7 @@ public PubsubUnboundedSource( /** Construct an unbounded source to consume from the Pubsub {@code subscription}. */ public PubsubUnboundedSource( - Clock clock, + @Nullable Clock clock, PubsubClientFactory pubsubFactory, @Nullable ValueProvider project, @Nullable ValueProvider topic, @@ -1400,18 +1441,21 @@ private boolean usesStatsFn(PipelineOptions options) { } private SubscriptionPath createRandomSubscription(PipelineOptions options) { - TopicPath topicPath = topic.get(); + TopicPath topicPath = + checkStateNotNull( + checkStateNotNull(topic, "Topic must be set to create a random subscription").get(), + "Topic value must be set to create a random subscription"); ProjectPath projectPath; if (project != null) { - projectPath = project.get(); + projectPath = checkStateNotNull(project.get(), "Project value must be set"); } else { String projectId = options.as(GcpOptions.class).getProject(); checkState( projectId != null, "Cannot create subscription to topic %s because pipeline option 'project' not specified", topicPath); - projectPath = PubsubClient.projectPathFromId(options.as(GcpOptions.class).getProject()); + projectPath = PubsubClient.projectPathFromId(projectId); } try { diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubWriteSchemaTransformProvider.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubWriteSchemaTransformProvider.java index 2abd6f5fa95d..1626cf50b7c7 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubWriteSchemaTransformProvider.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubWriteSchemaTransformProvider.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.io.gcp.pubsub; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; + import com.google.auto.service.AutoService; import java.io.Serializable; import java.nio.charset.StandardCharsets; @@ -41,7 +43,6 @@ import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.TupleTagList; -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.collect.ImmutableSet; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; @@ -106,7 +107,10 @@ public void processElement(@Element Row row, MultiOutputReceiver receiver) throw for (int ix = 0; ix < fields.size(); ix++) { String name = fields.get(ix).getName(); if (attributes != null && attributes.contains(name)) { - messageAttributes.put(name, row.getValue(ix)); + String val = row.getString(ix); + if (val != null) { + messageAttributes.put(name, val); + } } else if (name.equals(attributesMap)) { Map attrs = row.getMap(ix); if (attrs != null) { @@ -135,11 +139,11 @@ public void processElement(@Element Row row, MultiOutputReceiver receiver) throw @Override public SchemaTransform from(PubsubWriteSchemaTransformConfiguration configuration) { - if (!VALID_DATA_FORMATS.contains(configuration.getFormat().toUpperCase())) { + String format = checkArgumentNotNull(configuration.getFormat(), "Format must be specified"); + if (!VALID_DATA_FORMATS.contains(format.toUpperCase())) { throw new IllegalArgumentException( String.format( - "Format %s not supported. Only supported formats are %s", - configuration.getFormat(), VALID_FORMATS_STR)); + "Format %s not supported. Only supported formats are %s", format, VALID_FORMATS_STR)); } return new PubsubWriteSchemaTransform(configuration); } @@ -152,14 +156,10 @@ private static class PubsubWriteSchemaTransform extends SchemaTransform implemen } @Override - @SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) - }) public PCollectionRowTuple expand(PCollectionRowTuple input) { - String errorOutput = - configuration.getErrorHandling() == null - ? null - : configuration.getErrorHandling().getOutput(); + PubsubWriteSchemaTransformConfiguration.ErrorHandling errorHandling = + configuration.getErrorHandling(); + String errorOutput = errorHandling == null ? null : errorHandling.getOutput(); final Schema errorSchema = Schema.builder() @@ -167,16 +167,19 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { .addNullableRowField("row", input.get("input").getSchema()) .build(); - String format = configuration.getFormat(); + String format = checkArgumentNotNull(configuration.getFormat(), "Format must be specified"); Schema beamSchema = input.get("input").getSchema(); Schema payloadSchema; - if (configuration.getAttributes() == null && configuration.getAttributesMap() == null) { + List attributes = configuration.getAttributes(); + String attributesMap = configuration.getAttributesMap(); + if (attributes == null && attributesMap == null) { payloadSchema = beamSchema; } else { Schema.Builder payloadSchemaBuilder = Schema.builder(); for (Schema.Field f : beamSchema.getFields()) { - if (!configuration.getAttributes().contains(f.getName()) - && !f.getName().equals(configuration.getAttributesMap())) { + boolean isAttribute = attributes != null && attributes.contains(f.getName()); + boolean isAttributesMap = f.getName().equals(attributesMap); + if (!isAttribute && !isAttributesMap) { payloadSchemaBuilder.addField(f); } } @@ -190,9 +193,12 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { "Raw output only supported for single-field schemas, got %s", payloadSchema)); } if (payloadSchema.getField(0).getType().equals(Schema.FieldType.BYTES)) { - fn = row -> row.getBytes(0); + fn = row -> checkArgumentNotNull(row.getBytes(0), "Payload bytes value cannot be null"); } else if (payloadSchema.getField(0).getType().equals(Schema.FieldType.STRING)) { - fn = row -> row.getString(0).getBytes(StandardCharsets.UTF_8); + fn = + row -> + checkArgumentNotNull(row.getString(0), "Payload string value cannot be null") + .getBytes(StandardCharsets.UTF_8); } else { throw new IllegalArgumentException( String.format( @@ -217,20 +223,23 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { ParDo.of( new ErrorFn( fn, - configuration.getAttributes(), - configuration.getAttributesMap(), + attributes, + attributesMap, payloadSchema, errorSchema, errorOutput != null)) .withOutputTags(OUTPUT_TAG, TupleTagList.of(ERROR_TAG))); PubsubIO.Write writeTransform = - PubsubIO.writeMessages().to(configuration.getTopic()); - if (!Strings.isNullOrEmpty(configuration.getIdAttribute())) { - writeTransform = writeTransform.withIdAttribute(configuration.getIdAttribute()); + PubsubIO.writeMessages() + .to(checkArgumentNotNull(configuration.getTopic(), "Topic must be specified")); + String idAttribute = configuration.getIdAttribute(); + if (idAttribute != null && !idAttribute.isEmpty()) { + writeTransform = writeTransform.withIdAttribute(idAttribute); } - if (!Strings.isNullOrEmpty(configuration.getTimestampAttribute())) { - writeTransform = writeTransform.withIdAttribute(configuration.getTimestampAttribute()); + String timestampAttribute = configuration.getTimestampAttribute(); + if (timestampAttribute != null && !timestampAttribute.isEmpty()) { + writeTransform = writeTransform.withTimestampAttribute(timestampAttribute); } outputTuple.get(OUTPUT_TAG).apply(writeTransform); outputTuple.get(ERROR_TAG).setRowSchema(errorSchema); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/TestPubsub.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/TestPubsub.java index a55de012d383..902c9ec44459 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/TestPubsub.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/TestPubsub.java @@ -39,6 +39,7 @@ import io.grpc.ManagedChannelBuilder; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingDeque; @@ -71,9 +72,6 @@ * *

Deletes topic and subscription on shutdown. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class TestPubsub implements TestRule { private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd-HH-mm-ss-SSS"); @@ -148,25 +146,29 @@ private void initializePubsub(Description description) throws IOException { } else { channel = ManagedChannelBuilder.forTarget(pubsubEndpoint).useTransportSecurity().build(); } - channelProvider = FixedTransportChannelProvider.create(GrpcTransportChannel.create(channel)); - topicAdmin = + TransportChannelProvider chanProvider = + FixedTransportChannelProvider.create(GrpcTransportChannel.create(channel)); + channelProvider = chanProvider; + TopicAdminClient topAdmin = TopicAdminClient.create( TopicAdminSettings.newBuilder() .setCredentialsProvider(pipelineOptions::getGcpCredential) - .setTransportChannelProvider(channelProvider) + .setTransportChannelProvider(chanProvider) .setEndpoint(pubsubEndpoint) .build()); - subscriptionAdmin = + topicAdmin = topAdmin; + SubscriptionAdminClient subAdmin = SubscriptionAdminClient.create( SubscriptionAdminSettings.newBuilder() .setCredentialsProvider(pipelineOptions::getGcpCredential) - .setTransportChannelProvider(channelProvider) + .setTransportChannelProvider(chanProvider) .setEndpoint(pubsubEndpoint) .build()); + subscriptionAdmin = subAdmin; TopicPath eventsTopicPathTmp = PubsubClient.topicPathFromName( pipelineOptions.getProject(), createTopicName(description, EVENTS_TOPIC_NAME)); - topicAdmin.createTopic(eventsTopicPathTmp.getPath()); + topAdmin.createTopic(eventsTopicPathTmp.getPath()); // Set this after successful creation; it signals that the topic needs teardown eventsTopicPath = eventsTopicPathTmp; @@ -178,7 +180,7 @@ private void initializePubsub(Description description) throws IOException { String.format( "projects/%s/subscriptions/%s", pipelineOptions.getProject(), subscriptionName)); - subscriptionAdmin.createSubscription( + subAdmin.createSubscription( subscriptionPathTmp.getPath(), topicPath().getPath(), PushConfig.getDefaultInstance(), @@ -188,25 +190,30 @@ private void initializePubsub(Description description) throws IOException { } private void tearDown() { - if (subscriptionAdmin == null || topicAdmin == null || channel == null) { + SubscriptionAdminClient subAdmin = subscriptionAdmin; + TopicAdminClient topAdmin = topicAdmin; + ManagedChannel chan = channel; + if (subAdmin == null || topAdmin == null || chan == null) { return; } try { - if (subscriptionPath != null) { - subscriptionAdmin.deleteSubscription(subscriptionPath.getPath()); + SubscriptionPath subPath = subscriptionPath; + if (subPath != null) { + subAdmin.deleteSubscription(subPath.getPath()); } - if (eventsTopicPath != null) { + TopicPath topPath = eventsTopicPath; + if (topPath != null) { for (String subscriptionPath : - topicAdmin.listTopicSubscriptions(eventsTopicPath.getPath()).iterateAll()) { - subscriptionAdmin.deleteSubscription(subscriptionPath); + topAdmin.listTopicSubscriptions(topPath.getPath()).iterateAll()) { + subAdmin.deleteSubscription(subscriptionPath); } - topicAdmin.deleteTopic(eventsTopicPath.getPath()); + topAdmin.deleteTopic(topPath.getPath()); } } finally { - subscriptionAdmin.close(); - topicAdmin.close(); - channel.shutdown(); + subAdmin.close(); + topAdmin.close(); + chan.shutdown(); subscriptionAdmin = null; topicAdmin = null; @@ -251,31 +258,37 @@ static String createTopicName(Description description, String name) throws IOExc /** Topic path where events will be published to. */ public TopicPath topicPath() { - return eventsTopicPath; + return Preconditions.checkNotNull(eventsTopicPath, "eventsTopicPath is not initialized"); } /** Subscription path used to listen for messages on {@link #topicPath()}. */ public SubscriptionPath subscriptionPath() { - return subscriptionPath; + return Preconditions.checkNotNull(subscriptionPath, "subscriptionPath is not initialized"); } private List listSubscriptions(TopicPath topicPath) { - Preconditions.checkNotNull(topicAdmin); + TopicAdminClient topAdmin = + Preconditions.checkNotNull(topicAdmin, "topicAdmin is not initialized"); + SubscriptionPath subPath = + Preconditions.checkNotNull(subscriptionPath, "subscriptionPath is not initialized"); // Exclude subscriptionPath, the subscription that we created - return Streams.stream(topicAdmin.listTopicSubscriptions(topicPath.getPath()).iterateAll()) - .filter((path) -> !path.equals(subscriptionPath.getPath())) + return Streams.stream(topAdmin.listTopicSubscriptions(topicPath.getPath()).iterateAll()) + .filter((path) -> !path.equals(subPath.getPath())) .collect(Collectors.toList()); } /** Publish messages to {@link #topicPath()}. */ public void publish(List messages) { - Preconditions.checkNotNull(eventsTopicPath); + TopicPath topPath = + Preconditions.checkNotNull(eventsTopicPath, "eventsTopicPath is not initialized"); + TransportChannelProvider chanProvider = + Preconditions.checkNotNull(channelProvider, "channelProvider is not initialized"); Publisher eventPublisher; try { eventPublisher = - Publisher.newBuilder(eventsTopicPath.getPath()) + Publisher.newBuilder(topPath.getPath()) .setCredentialsProvider(pipelineOptions::getGcpCredential) - .setChannelProvider(channelProvider) + .setChannelProvider(chanProvider) .setEndpoint(pubsubEndpoint) .build(); } catch (IOException e) { @@ -288,8 +301,11 @@ public void publish(List messages) { (message) -> { com.google.pubsub.v1.PubsubMessage.Builder builder = com.google.pubsub.v1.PubsubMessage.newBuilder() - .setData(ByteString.copyFrom(message.getPayload())) - .putAllAttributes(message.getAttributeMap()); + .setData(ByteString.copyFrom(message.getPayload())); + Map attributeMap = message.getAttributeMap(); + if (attributeMap != null) { + builder.putAllAttributes(attributeMap); + } return eventPublisher.publish(builder.build()); }) .collect(Collectors.toList()); @@ -311,7 +327,10 @@ public void publish(List messages) { */ public List waitForNMessages(int n, Duration timeoutDuration) throws IOException, InterruptedException { - Preconditions.checkNotNull(subscriptionPath); + SubscriptionPath subPath = + Preconditions.checkNotNull(subscriptionPath, "subscriptionPath is not initialized"); + TransportChannelProvider chanProvider = + Preconditions.checkNotNull(channelProvider, "channelProvider is not initialized"); BlockingQueue receivedMessages = new LinkedBlockingDeque<>(n); @@ -326,9 +345,9 @@ public List waitForNMessages(int n, Duration timeoutDuration) }; Subscriber subscriber = - Subscriber.newBuilder(subscriptionPath.getPath(), receiver) + Subscriber.newBuilder(subPath.getPath(), receiver) .setCredentialsProvider(pipelineOptions::getGcpCredential) - .setChannelProvider(channelProvider) + .setChannelProvider(chanProvider) .setEndpoint(pubsubEndpoint) .build(); subscriber.startAsync(); @@ -369,10 +388,13 @@ public List waitForNMessages(int n, Duration timeoutDuration) * hasProperty("payload", equalTo("hello".getBytes(StandardCharsets.US_ASCII))), * hasProperty("payload", equalTo("world".getBytes(StandardCharsets.US_ASCII)))) * .waitForUpTo(Duration.standardSeconds(20)); - * + * } * + * @param matchers Matchers to assert on the received messages. */ - public PollingAssertion assertThatTopicEventuallyReceives(Matcher... matchers) { + @SafeVarargs + public final PollingAssertion assertThatTopicEventuallyReceives( + Matcher... matchers) { return timeoutDuration -> assertThat( waitForNMessages(matchers.length, timeoutDuration), containsInAnyOrder(matchers)); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/TestPubsubSignal.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/TestPubsubSignal.java index f11ba9555a80..02b27886f233 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/TestPubsubSignal.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/TestPubsubSignal.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.gcp.pubsub; import static org.apache.beam.sdk.io.gcp.pubsub.TestPubsub.createTopicName; +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.cloud.pubsub.v1.AckReplyConsumer; @@ -74,7 +75,6 @@ *

Uses a random temporary Pubsub topic for synchronization. */ @SuppressWarnings({ - "nullness", // TODO(https://github.com/apache/beam/issues/20497) // TODO(https://github.com/apache/beam/issues/21230): Remove when new version of // errorprone is released (2.11.0) "unused" @@ -137,18 +137,20 @@ public void evaluate() throws Throwable { } private void initializePubsub(Description description) throws IOException { - topicAdmin = + TopicAdminClient topAdmin = TopicAdminClient.create( TopicAdminSettings.newBuilder() .setCredentialsProvider(pipelineOptions::getGcpCredential) .setEndpoint(pubsubEndpoint) .build()); - subscriptionAdmin = + topicAdmin = topAdmin; + SubscriptionAdminClient subAdmin = SubscriptionAdminClient.create( SubscriptionAdminSettings.newBuilder() .setCredentialsProvider(pipelineOptions::getGcpCredential) .setEndpoint(pubsubEndpoint) .build()); + subscriptionAdmin = subAdmin; // Example topic name: // integ-test-TestClassName-testMethodName-2018-12-11-23-32-333--result @@ -161,26 +163,26 @@ private void initializePubsub(Description description) throws IOException { SubscriptionPath resultSubscriptionPathTmp = PubsubClient.subscriptionPathFromName( pipelineOptions.getProject(), - "result-subscription-" + String.valueOf(ThreadLocalRandom.current().nextLong())); + "result-subscription-" + ThreadLocalRandom.current().nextLong()); SubscriptionPath startSubscriptionPathTmp = PubsubClient.subscriptionPathFromName( pipelineOptions.getProject(), - "start-subscription-" + String.valueOf(ThreadLocalRandom.current().nextLong())); + "start-subscription-" + ThreadLocalRandom.current().nextLong()); // Set variables after successful creation; this signals that they need teardown - topicAdmin.createTopic(resultTopicPathTmp.getPath()); + topAdmin.createTopic(resultTopicPathTmp.getPath()); resultTopicPath = resultTopicPathTmp; - topicAdmin.createTopic(startTopicPathTmp.getPath()); + topAdmin.createTopic(startTopicPathTmp.getPath()); startTopicPath = startTopicPathTmp; - subscriptionAdmin.createSubscription( + subAdmin.createSubscription( resultSubscriptionPathTmp.getPath(), - resultTopicPath.getPath(), + resultTopicPathTmp.getPath(), PushConfig.getDefaultInstance(), 600); resultSubscriptionPath = resultSubscriptionPathTmp; - subscriptionAdmin.createSubscription( + subAdmin.createSubscription( startSubscriptionPathTmp.getPath(), - startTopicPath.getPath(), + startTopicPathTmp.getPath(), PushConfig.getDefaultInstance(), 600); startSubscriptionPath = startSubscriptionPathTmp; @@ -195,25 +197,31 @@ private static void logIfThrows(ThrowingRunnable runnable) { } private void tearDown() { - if (subscriptionAdmin == null || topicAdmin == null) { + SubscriptionAdminClient subAdmin = subscriptionAdmin; + TopicAdminClient topAdmin = topicAdmin; + if (subAdmin == null || topAdmin == null) { return; } - if (resultSubscriptionPath != null) { - logIfThrows(() -> subscriptionAdmin.deleteSubscription(resultSubscriptionPath.getPath())); + SubscriptionPath resSubPath = resultSubscriptionPath; + if (resSubPath != null) { + logIfThrows(() -> subAdmin.deleteSubscription(resSubPath.getPath())); } - if (startSubscriptionPath != null) { - logIfThrows(() -> subscriptionAdmin.deleteSubscription(startSubscriptionPath.getPath())); + SubscriptionPath startSubPath = startSubscriptionPath; + if (startSubPath != null) { + logIfThrows(() -> subAdmin.deleteSubscription(startSubPath.getPath())); } - if (resultTopicPath != null) { - logIfThrows(() -> topicAdmin.deleteTopic(resultTopicPath.getPath())); + TopicPath resTopPath = resultTopicPath; + if (resTopPath != null) { + logIfThrows(() -> topAdmin.deleteTopic(resTopPath.getPath())); } - if (startTopicPath != null) { - logIfThrows(() -> topicAdmin.deleteTopic(startTopicPath.getPath())); + TopicPath startTopPathVal = startTopicPath; + if (startTopPathVal != null) { + logIfThrows(() -> topAdmin.deleteTopic(startTopPathVal.getPath())); } - logIfThrows(subscriptionAdmin::close); - logIfThrows(topicAdmin::close); + logIfThrows(subAdmin::close); + logIfThrows(topAdmin::close); subscriptionAdmin = null; topicAdmin = null; @@ -227,7 +235,7 @@ private void tearDown() { /** Outputs a message that the pipeline has started. */ public PTransform signalStart() { - return new PublishStart(startTopicPath); + return new PublishStart(checkStateNotNull(startTopicPath, "startTopicPath is not initialized")); } /** @@ -250,7 +258,11 @@ public PTransform, POutput> signalSuccessWhen( SerializableFunction formatter, SerializableFunction, Boolean> successPredicate) { - return new PublishSuccessWhen<>(coder, formatter, successPredicate, resultTopicPath); + return new PublishSuccessWhen<>( + coder, + formatter, + successPredicate, + checkStateNotNull(resultTopicPath, "resultTopicPath is not initialized")); } /** @@ -260,7 +272,7 @@ public PTransform, POutput> signalSuccessWhen( public PTransform, POutput> signalSuccessWhen( Coder coder, SerializableFunction, Boolean> successPredicate) { - return signalSuccessWhen(coder, T::toString, successPredicate); + return signalSuccessWhen(coder, String::valueOf, successPredicate); } /** @@ -270,10 +282,12 @@ public PTransform, POutput> signalSuccessWhen( * the start signal being published, which occurs immediately upon pipeline startup. */ public Supplier waitForStart(Duration duration) { + SubscriptionPath subPath = + checkStateNotNull(startSubscriptionPath, "startSubscriptionPath is not initialized"); return Suppliers.memoize( () -> { try { - String result = pollForResultForDuration(startSubscriptionPath, duration); + String result = pollForResultForDuration(subPath, duration); checkState(START_SIGNAL_MESSAGE.equals(result)); return null; } catch (IOException e) { @@ -284,7 +298,9 @@ public Supplier waitForStart(Duration duration) { /** Wait for a success signal for {@code duration}. */ public void waitForSuccess(Duration duration) throws IOException { - String result = pollForResultForDuration(resultSubscriptionPath, duration); + SubscriptionPath subPath = + checkStateNotNull(resultSubscriptionPath, "resultSubscriptionPath is not initialized"); + String result = pollForResultForDuration(subPath, duration); if (!RESULT_SUCCESS_MESSAGE.equals(result)) { throw new AssertionError(result); } @@ -293,7 +309,7 @@ public void waitForSuccess(Duration duration) throws IOException { private String pollForResultForDuration( SubscriptionPath signalSubscriptionPath, Duration timeoutDuration) throws IOException { - AtomicReference result = new AtomicReference<>(null); + AtomicReference<@Nullable String> result = new AtomicReference<>(null); MessageReceiver receiver = (com.google.pubsub.v1.PubsubMessage message, AckReplyConsumer replyConsumer) -> { @@ -330,13 +346,14 @@ private String pollForResultForDuration( subscriber.stopAsync(); subscriber.awaitTerminated(); - if (result.get() == null) { + String resultVal = result.get(); + if (resultVal == null) { throw new AssertionError( String.format( "Did not receive signal on %s in %ss", signalSubscriptionPath, timeoutDuration.getStandardSeconds())); } - return result.get(); + return resultVal; } /** {@link PTransform} that signals once when the pipeline has started. */ @@ -397,7 +414,7 @@ public POutput expand(PCollection input) { * "FAILURE". */ static class StatefulPredicateCheck extends DoFn, String> { - private SerializableFunction, Boolean> successPredicate; + private final SerializableFunction, Boolean> successPredicate; // keep all events seen so far in the state cell private static final String SEEN_EVENTS = "seenEvents"; diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/BatchSpannerRead.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/BatchSpannerRead.java index 9f9cfbdb1fef..dc6a75826d3a 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/BatchSpannerRead.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/BatchSpannerRead.java @@ -27,6 +27,7 @@ import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TimestampBound; +import io.opentelemetry.api.OpenTelemetry; import java.io.Serializable; import java.util.List; import java.util.Objects; @@ -34,6 +35,8 @@ import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO.ReadAll; import org.apache.beam.sdk.metrics.Lineage; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -126,8 +129,9 @@ public GeneratePartitionsFn( } @Setup - public void setup() throws Exception { - spannerAccessor = SpannerAccessor.getOrCreate(config); + public void setup(PipelineOptions options) throws Exception { + OpenTelemetry otel = options.as(SdkHarnessOptions.class).getOpenTelemetry(); + spannerAccessor = SpannerAccessor.getOrCreate(config, otel); } @Teardown @@ -211,8 +215,9 @@ public ReadFromPartitionFn( } @Setup - public void setup() throws Exception { - spannerAccessor = SpannerAccessor.getOrCreate(config); + public void setup(PipelineOptions options) throws Exception { + OpenTelemetry otel = options.as(SdkHarnessOptions.class).getOpenTelemetry(); + spannerAccessor = SpannerAccessor.getOrCreate(config, otel); // Use a LoadingCache for metrics as there can be different read operations which result in // different service call metrics labels. ServiceCallMetric items are created on-demand and diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/CreateTransactionFn.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/CreateTransactionFn.java index d21159fef4d8..9db37f83a1de 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/CreateTransactionFn.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/CreateTransactionFn.java @@ -19,6 +19,9 @@ import com.google.cloud.spanner.BatchReadOnlyTransaction; import com.google.cloud.spanner.TimestampBound; +import io.opentelemetry.api.OpenTelemetry; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.transforms.DoFn; /** Creates a batch transaction. */ @@ -37,8 +40,9 @@ class CreateTransactionFn extends DoFn { private transient SpannerAccessor spannerAccessor; @DoFn.Setup - public void setup() throws Exception { - spannerAccessor = SpannerAccessor.getOrCreate(config); + public void setup(PipelineOptions options) throws Exception { + OpenTelemetry otel = options.as(SdkHarnessOptions.class).getOpenTelemetry(); + spannerAccessor = SpannerAccessor.getOrCreate(config, otel); } @Teardown diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/NaiveSpannerRead.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/NaiveSpannerRead.java index ad5423f942c8..d690e54c73b3 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/NaiveSpannerRead.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/NaiveSpannerRead.java @@ -25,10 +25,13 @@ import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TimestampBound; +import io.opentelemetry.api.OpenTelemetry; import java.util.Objects; import org.apache.beam.runners.core.metrics.ServiceCallMetric; import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.metrics.Lineage; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -97,8 +100,9 @@ private static class NaiveSpannerReadFn extends DoFn { } @Setup - public void setup() throws Exception { - spannerAccessor = SpannerAccessor.getOrCreate(config); + public void setup(PipelineOptions options) throws Exception { + OpenTelemetry otel = options.as(SdkHarnessOptions.class).getOpenTelemetry(); + spannerAccessor = SpannerAccessor.getOrCreate(config, otel); projectId = SpannerIO.resolveSpannerProjectId(config); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/ReadSpannerSchema.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/ReadSpannerSchema.java index 6085431396db..7a628f244408 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/ReadSpannerSchema.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/ReadSpannerSchema.java @@ -22,8 +22,11 @@ import com.google.cloud.spanner.ReadOnlyTransaction; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.Statement; +import io.opentelemetry.api.OpenTelemetry; import java.util.HashSet; import java.util.Set; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.values.PCollectionView; @@ -75,8 +78,9 @@ public ReadSpannerSchema( } @Setup - public void setup() throws Exception { - spannerAccessor = SpannerAccessor.getOrCreate(config); + public void setup(PipelineOptions options) throws Exception { + OpenTelemetry otel = options.as(SdkHarnessOptions.class).getOpenTelemetry(); + spannerAccessor = SpannerAccessor.getOrCreate(config, otel); } @Teardown 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..c0ed8be7384f 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 @@ -36,8 +36,11 @@ import com.google.cloud.spanner.v1.stub.SpannerStubSettings; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.CommitResponse; +import com.google.spanner.v1.DirectedReadOptions; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.PartialResultSet; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -97,13 +100,17 @@ private SpannerAccessor( } public static SpannerAccessor getOrCreate(SpannerConfig spannerConfig) { + return getOrCreate(spannerConfig, GlobalOpenTelemetry.get()); + } + + public static SpannerAccessor getOrCreate(SpannerConfig spannerConfig, OpenTelemetry otel) { synchronized (spannerAccessors) { SpannerAccessor self = spannerAccessors.get(spannerConfig); if (self == null) { // Connect to spanner for this SpannerConfig. LOG.info("Connecting to {}", spannerConfig); - self = SpannerAccessor.createAndConnect(spannerConfig); + self = SpannerAccessor.createAndConnect(spannerConfig, otel); LOG.info("Successfully connected to {}", spannerConfig); spannerAccessors.put(spannerConfig, self); } @@ -116,7 +123,27 @@ public static SpannerAccessor getOrCreate(SpannerConfig spannerConfig) { @VisibleForTesting static SpannerOptions buildSpannerOptions(SpannerConfig spannerConfig) { + return buildSpannerOptions(spannerConfig, GlobalOpenTelemetry.get()); + } + + @VisibleForTesting + static SpannerOptions buildSpannerOptions(SpannerConfig spannerConfig, OpenTelemetry otel) { SpannerOptions.Builder builder = SpannerOptions.newBuilder(); + if (otel != null) { + builder.setOpenTelemetry(otel); + } + ValueProvider enableOpenTelemetryTracing = + spannerConfig.getEnableOpenTelemetryTracing(); + if (enableOpenTelemetryTracing != null + && enableOpenTelemetryTracing.isAccessible() + && enableOpenTelemetryTracing.get()) { + builder.setEnableExtendedTracing(true); + builder.setEnableEndToEndTracing(true); + builder.setEnableApiTracing(true); + SpannerOptions.disableOpenCensusMetrics(); + SpannerOptions.enableOpenTelemetryMetrics(); + SpannerOptions.enableOpenTelemetryTraces(); + } // TODO(https://github.com/apache/beam/issues/37451) Disable gRPC gcp extension which was // causing the application thread to stall. @@ -247,7 +274,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()); } } @@ -270,9 +306,15 @@ static SpannerOptions buildSpannerOptions(SpannerConfig spannerConfig) { if (databaseRole != null && databaseRole.get() != null && !databaseRole.get().isEmpty()) { builder.setDatabaseRole(databaseRole.get()); } + ValueProvider directedReadOptions = spannerConfig.getDirectedReadOptions(); + if (directedReadOptions != null && directedReadOptions.get() != null) { + builder.setDirectedReadOptions(directedReadOptions.get()); + } 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 = @@ -287,8 +329,8 @@ static SpannerOptions buildSpannerOptions(SpannerConfig spannerConfig) { return builder.build(); } - private static SpannerAccessor createAndConnect(SpannerConfig spannerConfig) { - SpannerOptions options = buildSpannerOptions(spannerConfig); + private static SpannerAccessor createAndConnect(SpannerConfig spannerConfig, OpenTelemetry otel) { + SpannerOptions options = buildSpannerOptions(spannerConfig, otel); Spanner spanner = options.getService(); String instanceId = spannerConfig.getInstanceId().get(); String databaseId = spannerConfig.getDatabaseId().get(); 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..36b13f237b47 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 @@ -28,6 +28,9 @@ import com.google.cloud.spanner.Options.RpcPriority; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import com.google.spanner.v1.DirectedReadOptions; import java.io.Serializable; import org.apache.beam.sdk.options.ValueProvider; import org.apache.beam.sdk.transforms.display.DisplayData; @@ -91,12 +94,18 @@ public String getHostValue() { public abstract @Nullable ValueProvider getDatabaseRole(); + public abstract @Nullable ValueProvider getDirectedReadOptions(); + public abstract @Nullable ValueProvider getPartitionQueryTimeout(); public abstract @Nullable ValueProvider getPartitionReadTimeout(); public abstract @Nullable ValueProvider getPlainText(); + public abstract @Nullable ValueProvider getClientCertPath(); + + public abstract @Nullable ValueProvider getClientCertKeyPath(); + @VisibleForTesting abstract @Nullable ServiceFactory getServiceFactory(); @@ -106,6 +115,8 @@ public String getHostValue() { public abstract @Nullable ValueProvider getWaitForSessionCreationDuration(); + public abstract @Nullable ValueProvider getEnableOpenTelemetryTracing(); + abstract Builder toBuilder(); public static SpannerConfig create() { @@ -181,6 +192,8 @@ abstract Builder setExecuteStreamingSqlRetrySettings( abstract Builder setDatabaseRole(ValueProvider databaseRole); + abstract Builder setDirectedReadOptions(ValueProvider directedReadOptions); + abstract Builder setDataBoostEnabled(ValueProvider dataBoostEnabled); abstract Builder setPartitionQueryTimeout(ValueProvider partitionQueryTimeout); @@ -194,6 +207,13 @@ abstract Builder setExecuteStreamingSqlRetrySettings( abstract Builder setWaitForSessionCreationDuration( ValueProvider waitForSessionCreationDuration); + abstract Builder setEnableOpenTelemetryTracing( + ValueProvider enableOpenTelemetryTracing); + + abstract Builder setClientCertPath(ValueProvider clientCertPath); + + abstract Builder setClientCertKeyPath(ValueProvider clientCertKeyPath); + public abstract SpannerConfig build(); } @@ -327,6 +347,40 @@ public SpannerConfig withDatabaseRole(ValueProvider databaseRole) { return toBuilder().setDatabaseRole(databaseRole).build(); } + /** Specifies the Cloud Spanner directed read options. */ + public SpannerConfig withDirectedReadOptions(DirectedReadOptions directedReadOptions) { + return withDirectedReadOptions(ValueProvider.StaticValueProvider.of(directedReadOptions)); + } + + /** Specifies the Cloud Spanner directed read options. */ + public SpannerConfig withDirectedReadOptions( + ValueProvider directedReadOptions) { + return toBuilder().setDirectedReadOptions(directedReadOptions).build(); + } + + /** Specifies the Cloud Spanner directed read options from a string representation. */ + public SpannerConfig withDirectedReadOptions(String directedReadOptions) { + if (directedReadOptions == null || directedReadOptions.isEmpty()) { + return this; + } + return withDirectedReadOptions(parseDirectedReadOptions(directedReadOptions)); + } + + @VisibleForTesting + static DirectedReadOptions parseDirectedReadOptions(String directedReadOptions) { + if (directedReadOptions == null || directedReadOptions.isEmpty()) { + return DirectedReadOptions.getDefaultInstance(); + } + DirectedReadOptions.Builder builder = DirectedReadOptions.newBuilder(); + try { + JsonFormat.parser().merge(directedReadOptions, builder); + return builder.build(); + } catch (InvalidProtocolBufferException e) { + throw new IllegalArgumentException( + "Failed to parse DirectedReadOptions from string: " + directedReadOptions, e); + } + } + /** Specifies if the pipeline has to be run on the independent compute resource. */ public SpannerConfig withDataBoostEnabled(ValueProvider dataBoostEnabled) { return toBuilder().setDataBoostEnabled(dataBoostEnabled).build(); @@ -414,4 +468,43 @@ public SpannerConfig withWaitForSessionCreationDuration( return withWaitForSessionCreationDuration( ValueProvider.StaticValueProvider.of(waitForSessionCreationDuration)); } + + public SpannerConfig withEnableOpenTelemetryTracing( + ValueProvider enableOpenTelemetryTracing) { + return toBuilder().setEnableOpenTelemetryTracing(enableOpenTelemetryTracing).build(); + } + + public SpannerConfig withEnableOpenTelemetryTracing(boolean enableOpenTelemetryTracing) { + return withEnableOpenTelemetryTracing( + ValueProvider.StaticValueProvider.of(enableOpenTelemetryTracing)); + } + + /** + * 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 cccfced08218..7e72d6184e3e 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; @@ -60,6 +61,8 @@ import com.google.cloud.spanner.TimestampBound; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.spanner.v1.DirectedReadOptions; +import io.opentelemetry.api.OpenTelemetry; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -103,6 +106,7 @@ import org.apache.beam.sdk.metrics.Lineage; import org.apache.beam.sdk.metrics.Metrics; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.options.ValueProvider; import org.apache.beam.sdk.schemas.Schema; @@ -146,6 +150,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; @@ -311,10 +316,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 @@ -636,6 +641,24 @@ public ReadAll withExperimentalHost(String experimentalHost) { return withExperimentalHost(ValueProvider.StaticValueProvider.of(experimentalHost)); } + /** Specifies the directed read options for Cloud Spanner. */ + public ReadAll withDirectedReadOptions(DirectedReadOptions directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + + /** Specifies the directed read options for Cloud Spanner. */ + public ReadAll withDirectedReadOptions(ValueProvider directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + + /** Specifies the directed read options for Cloud Spanner from a string representation. */ + public ReadAll withDirectedReadOptions(String directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + /** * Specifies whether to use plaintext channel. * @@ -657,6 +680,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(); @@ -896,6 +948,24 @@ public Read withExperimentalHost(String experimentalHost) { return withExperimentalHost(ValueProvider.StaticValueProvider.of(experimentalHost)); } + /** Specifies the directed read options for Cloud Spanner. */ + public Read withDirectedReadOptions(DirectedReadOptions directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + + /** Specifies the directed read options for Cloud Spanner. */ + public Read withDirectedReadOptions(ValueProvider directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + + /** Specifies the directed read options for Cloud Spanner from a string representation. */ + public Read withDirectedReadOptions(String directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + /** * Specifies whether to use plaintext channel. * @@ -917,6 +987,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 +1343,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(); @@ -1371,6 +1500,18 @@ public Write withHost(String host) { return withHost(ValueProvider.StaticValueProvider.of(host)); } + /** Specifies whether OpenTelemetry tracing is enabled. */ + public Write withEnableOpenTelemetryTracing(boolean enableOpenTelemetryTracing) { + return withEnableOpenTelemetryTracing( + ValueProvider.StaticValueProvider.of(enableOpenTelemetryTracing)); + } + + /** Specifies whether OpenTelemetry tracing is enabled. */ + public Write withEnableOpenTelemetryTracing(ValueProvider enableOpenTelemetryTracing) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withEnableOpenTelemetryTracing(enableOpenTelemetryTracing)); + } + /** Specifies the Cloud Spanner emulator host. */ public Write withEmulatorHost(ValueProvider emulatorHost) { SpannerConfig config = getSpannerConfig(); @@ -1412,6 +1553,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 +1940,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 +1981,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 @@ -1860,6 +2038,19 @@ public ReadChangeStream withDatabaseId(ValueProvider databaseId) { return withSpannerConfig(config.withDatabaseId(databaseId)); } + /** Specifies whether OpenTelemetry tracing is enabled. */ + public ReadChangeStream withEnableOpenTelemetryTracing(boolean enableOpenTelemetryTracing) { + return withEnableOpenTelemetryTracing( + ValueProvider.StaticValueProvider.of(enableOpenTelemetryTracing)); + } + + /** Specifies whether OpenTelemetry tracing is enabled. */ + public ReadChangeStream withEnableOpenTelemetryTracing( + ValueProvider enableOpenTelemetryTracing) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withEnableOpenTelemetryTracing(enableOpenTelemetryTracing)); + } + /** Specifies the change stream name. */ public ReadChangeStream withChangeStreamName(String changeStreamName) { return toBuilder().setChangeStreamName(changeStreamName).build(); @@ -1925,6 +2116,28 @@ public ReadChangeStream withExperimentalHost(String experimentalHost) { return withExperimentalHost(ValueProvider.StaticValueProvider.of(experimentalHost)); } + /** Specifies the directed read options for change stream queries. */ + public ReadChangeStream withDirectedReadOptions(DirectedReadOptions directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + + /** Specifies the directed read options for change stream queries. */ + public ReadChangeStream withDirectedReadOptions( + ValueProvider directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + + /** + * Specifies the directed read options for change stream queries from a string representation + * (e.g., JSON string or "us-central1:READ_ONLY"). + */ + public ReadChangeStream withDirectedReadOptions(String directedReadOptions) { + SpannerConfig config = getSpannerConfig(); + return withSpannerConfig(config.withDirectedReadOptions(directedReadOptions)); + } + /** * Specifies whether to use plaintext channel. * @@ -1946,6 +2159,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: * @@ -2056,7 +2299,9 @@ && getInclusiveStartAt().toSqlTimestamp().after(getInclusiveEndAt().toSqlTimesta final ChangeStreamMetrics metrics = new ChangeStreamMetrics(); final RpcPriority rpcPriority = MoreObjects.firstNonNull(getRpcPriority(), RpcPriority.HIGH); final SpannerAccessor spannerAccessor = - SpannerAccessor.getOrCreate(changeStreamSpannerConfig); + SpannerAccessor.getOrCreate( + changeStreamSpannerConfig, + input.getPipeline().getOptions().as(SdkHarnessOptions.class).getOpenTelemetry()); final boolean isMutableChangeStream = isMutableChangeStream( spannerAccessor.getDatabaseClient(), changeStreamDatabaseDialect, changeStreamName); @@ -2177,9 +2422,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; @@ -2189,7 +2442,11 @@ private static Dialect getDialect(SpannerConfig spannerConfig, PipelineOptions p // Allow passing the credential from pipeline options to the getDialect() call. SpannerConfig spannerConfigWithCredential = buildSpannerConfigWithCredential(spannerConfig, pipelineOptions); - try (SpannerAccessor sa = SpannerAccessor.getOrCreate(spannerConfigWithCredential)) { + OpenTelemetry otel = null; + if (pipelineOptions != null) { + otel = pipelineOptions.as(SdkHarnessOptions.class).getOpenTelemetry(); + } + try (SpannerAccessor sa = SpannerAccessor.getOrCreate(spannerConfigWithCredential, otel)) { DatabaseClient databaseClient = sa.getDatabaseClient(); return databaseClient.getDialect(); } @@ -2554,8 +2811,9 @@ static class WriteToSpannerFn extends DoFn, Void> { } @Setup - public void setup() { - spannerAccessor = SpannerAccessor.getOrCreate(spannerConfig); + public void setup(PipelineOptions options) { + OpenTelemetry otel = options.as(SdkHarnessOptions.class).getOpenTelemetry(); + spannerAccessor = SpannerAccessor.getOrCreate(spannerConfig, otel); bundleWriteBackoff = FluentBackoff.DEFAULT .withMaxCumulativeBackoff(spannerConfig.getMaxCumulativeBackoff().get()) 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/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/ActionFactory.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/ActionFactory.java index 6850d77cbf52..575bcc866303 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/ActionFactory.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/ActionFactory.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.io.gcp.spanner.changestreams.action; +import io.opentelemetry.api.OpenTelemetry; import java.io.Serializable; import org.apache.beam.sdk.io.gcp.spanner.changestreams.ChangeStreamMetrics; import org.apache.beam.sdk.io.gcp.spanner.changestreams.cache.WatermarkCache; @@ -191,7 +192,8 @@ public synchronized QueryChangeStreamAction queryChangeStreamAction( PartitionEventRecordAction partitionEventRecordAction, ChangeStreamMetrics metrics, boolean isMutableChangeStream, - Duration realTimeCheckpointInterval) { + Duration realTimeCheckpointInterval, + OpenTelemetry openTelemetry) { if (queryChangeStreamActionInstance == null) { queryChangeStreamActionInstance = new QueryChangeStreamAction( @@ -207,7 +209,8 @@ public synchronized QueryChangeStreamAction queryChangeStreamAction( partitionEventRecordAction, metrics, isMutableChangeStream, - realTimeCheckpointInterval); + realTimeCheckpointInterval, + openTelemetry); } return queryChangeStreamActionInstance; } 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/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamAction.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamAction.java index 23cd6022610f..ac4acbb6282d 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamAction.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamAction.java @@ -22,6 +22,10 @@ import com.google.cloud.Timestamp; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.SpannerException; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; import java.util.List; import java.util.Optional; import org.apache.beam.sdk.io.gcp.spanner.changestreams.ChangeStreamMetrics; @@ -48,6 +52,7 @@ import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimator; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.joda.time.Duration; import org.joda.time.Instant; import org.slf4j.Logger; @@ -92,6 +97,8 @@ public class QueryChangeStreamAction { private final ChangeStreamMetrics metrics; private final boolean isMutableChangeStream; private final Duration realTimeCheckpointInterval; + private final OpenTelemetry openTelemetry; + private transient volatile @MonotonicNonNull Tracer tracer = null; /** * Constructs an action class for performing a change stream query for a given partition. @@ -111,6 +118,7 @@ public class QueryChangeStreamAction { * @param metrics metrics gathering class * @param isMutableChangeStream whether the change stream is mutable or not * @param realTimeCheckpointInterval duration to add to current time + * @param openTelemetry instance for tracing */ QueryChangeStreamAction( ChangeStreamDao changeStreamDao, @@ -125,7 +133,8 @@ public class QueryChangeStreamAction { PartitionEventRecordAction partitionEventRecordAction, ChangeStreamMetrics metrics, boolean isMutableChangeStream, - Duration realTimeCheckpointInterval) { + Duration realTimeCheckpointInterval, + OpenTelemetry openTelemetry) { this.changeStreamDao = changeStreamDao; this.partitionMetadataDao = partitionMetadataDao; this.changeStreamRecordMapper = changeStreamRecordMapper; @@ -139,6 +148,7 @@ public class QueryChangeStreamAction { this.metrics = metrics; this.isMutableChangeStream = isMutableChangeStream; this.realTimeCheckpointInterval = realTimeCheckpointInterval; + this.openTelemetry = openTelemetry; } /** @@ -240,14 +250,19 @@ public ProcessContinuation run( Optional maybeContinuation; for (final ChangeStreamRecord record : records) { if (record instanceof DataChangeRecord) { - maybeContinuation = - dataChangeRecordAction.run( - updatedPartition, - (DataChangeRecord) record, - tracker, - interrupter, - receiver, - watermarkEstimator); + Span span = getTracer().spanBuilder("DataChangeRecord.run").startSpan(); + try (Scope ignored = span.makeCurrent()) { + maybeContinuation = + dataChangeRecordAction.run( + updatedPartition, + (DataChangeRecord) record, + tracker, + interrupter, + receiver, + watermarkEstimator); + } finally { + span.end(); + } } else if (record instanceof HeartbeatRecord) { maybeContinuation = heartbeatRecordAction.run( @@ -422,4 +437,15 @@ private Timestamp getBoundedQueryEndTimestamp(Timestamp endTimestamp) { } return endTimestamp; } + + private Tracer getTracer() { + if (tracer == null) { + synchronized (this) { + if (tracer == null) { + tracer = openTelemetry.getTracer("SpannerIO.ChangeStreams"); + } + } + } + return tracer; + } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dao/DaoFactory.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dao/DaoFactory.java index 2fe4239ca0fd..2c714436573c 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dao/DaoFactory.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dao/DaoFactory.java @@ -20,11 +20,13 @@ import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.Options.RpcPriority; +import io.opentelemetry.api.OpenTelemetry; import java.io.Serializable; import java.util.List; import org.apache.beam.sdk.io.gcp.spanner.SpannerAccessor; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.changestreams.ChangeStreamsConstants; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Factory class to create data access objects to perform change stream queries and access the @@ -41,6 +43,7 @@ public class DaoFactory implements Serializable { private transient PartitionMetadataAdminDao partitionMetadataAdminDao; private transient PartitionMetadataDao partitionMetadataDaoInstance; private transient ChangeStreamDao changeStreamDaoInstance; + private transient @Nullable OpenTelemetry openTelemetry; private final SpannerConfig changeStreamSpannerConfig; private final SpannerConfig metadataSpannerConfig; @@ -100,6 +103,10 @@ public List getTvfNameList() { return this.tvfNameList; } + public void setOpenTelemetry(@Nullable OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + /** * Creates and returns a singleton DAO instance for admin operations over the partition metadata * table. @@ -111,7 +118,8 @@ public List getTvfNameList() { public synchronized PartitionMetadataAdminDao getPartitionMetadataAdminDao() { if (partitionMetadataAdminDao == null) { DatabaseAdminClient databaseAdminClient = - SpannerAccessor.getOrCreate(metadataSpannerConfig).getDatabaseAdminClient(); + SpannerAccessor.getOrCreate(metadataSpannerConfig, this.openTelemetry) + .getDatabaseAdminClient(); partitionMetadataAdminDao = new PartitionMetadataAdminDao( databaseAdminClient, @@ -131,7 +139,8 @@ public synchronized PartitionMetadataAdminDao getPartitionMetadataAdminDao() { * @return singleton instance of the {@link PartitionMetadataDao} */ public synchronized PartitionMetadataDao getPartitionMetadataDao() { - final SpannerAccessor spannerAccessor = SpannerAccessor.getOrCreate(metadataSpannerConfig); + final SpannerAccessor spannerAccessor = + SpannerAccessor.getOrCreate(metadataSpannerConfig, this.openTelemetry); if (partitionMetadataDaoInstance == null) { partitionMetadataDaoInstance = new PartitionMetadataDao( @@ -150,7 +159,8 @@ public synchronized PartitionMetadataDao getPartitionMetadataDao() { * @return singleton instance of the {@link ChangeStreamDao} */ public synchronized ChangeStreamDao getChangeStreamDao() { - final SpannerAccessor spannerAccessor = SpannerAccessor.getOrCreate(changeStreamSpannerConfig); + final SpannerAccessor spannerAccessor = + SpannerAccessor.getOrCreate(changeStreamSpannerConfig, this.openTelemetry); if (changeStreamDaoInstance == null) { changeStreamDaoInstance = new ChangeStreamDao( diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/CleanUpReadChangeStreamDoFn.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/CleanUpReadChangeStreamDoFn.java index f8aa497292bf..0438e9c9f6cd 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/CleanUpReadChangeStreamDoFn.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/CleanUpReadChangeStreamDoFn.java @@ -20,6 +20,8 @@ import java.io.Serializable; import java.util.List; import org.apache.beam.sdk.io.gcp.spanner.changestreams.dao.DaoFactory; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.transforms.DoFn; public class CleanUpReadChangeStreamDoFn extends DoFn implements Serializable { @@ -32,6 +34,11 @@ public CleanUpReadChangeStreamDoFn(DaoFactory daoFactory) { this.daoFactory = daoFactory; } + @Setup + public void setup(PipelineOptions options) { + daoFactory.setOpenTelemetry(options.as(SdkHarnessOptions.class).getOpenTelemetry()); + } + @ProcessElement public void processElement(OutputReceiver receiver) { List indexes = daoFactory.getPartitionMetadataDao().findAllTableIndexes(); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/DetectNewPartitionsDoFn.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/DetectNewPartitionsDoFn.java index 841ab61e0f5d..4c46307aa661 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/DetectNewPartitionsDoFn.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/DetectNewPartitionsDoFn.java @@ -31,6 +31,8 @@ import org.apache.beam.sdk.io.gcp.spanner.changestreams.restriction.DetectNewPartitionsRangeTracker; import org.apache.beam.sdk.io.gcp.spanner.changestreams.restriction.TimestampRange; import org.apache.beam.sdk.io.gcp.spanner.changestreams.restriction.TimestampUtils; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.DoFn.UnboundedPerElement; import org.apache.beam.sdk.transforms.splittabledofn.ManualWatermarkEstimator; @@ -145,7 +147,8 @@ public DetectNewPartitionsRangeTracker newTracker(@Restriction TimestampRange re /** Obtains the instance of {@link DetectNewPartitionsAction}. */ @Setup - public void setup() { + public void setup(PipelineOptions options) { + daoFactory.setOpenTelemetry(options.as(SdkHarnessOptions.class).getOpenTelemetry()); final PartitionMetadataDao partitionMetadataDao = daoFactory.getPartitionMetadataDao(); final PartitionMetadataMapper partitionMetadataMapper = mapperFactory.partitionMetadataMapper(); final WatermarkCache watermarkCache = cacheFactory.getWatermarkCache(); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/InitializeDoFn.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/InitializeDoFn.java index d643707e9688..fb0348dac9ac 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/InitializeDoFn.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/InitializeDoFn.java @@ -28,6 +28,8 @@ import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.InitialPartition; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.PartitionMetadata; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.PartitionMetadata.State; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.transforms.DoFn; /** @@ -61,6 +63,11 @@ public InitializeDoFn( this.heartbeatMillis = heartbeatMillis; } + @Setup + public void setup(PipelineOptions options) { + daoFactory.setOpenTelemetry(options.as(SdkHarnessOptions.class).getOpenTelemetry()); + } + @ProcessElement public void processElement(OutputReceiver receiver) { PartitionMetadataDao partitionMetadataDao = daoFactory.getPartitionMetadataDao(); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/ReadChangeStreamPartitionDoFn.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/ReadChangeStreamPartitionDoFn.java index de509257ad8f..5901f60c9d18 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/ReadChangeStreamPartitionDoFn.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/ReadChangeStreamPartitionDoFn.java @@ -42,6 +42,8 @@ import org.apache.beam.sdk.io.gcp.spanner.changestreams.restriction.ReadChangeStreamPartitionRangeTracker; import org.apache.beam.sdk.io.gcp.spanner.changestreams.restriction.TimestampRange; import org.apache.beam.sdk.io.gcp.spanner.changestreams.restriction.TimestampUtils; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.DoFn.UnboundedPerElement; import org.apache.beam.sdk.transforms.splittabledofn.ManualWatermarkEstimator; @@ -75,6 +77,7 @@ public class ReadChangeStreamPartitionDoFn extends DoFn shouldFailRow = (Function & Serializable) tr -> false; + + private volatile String appendRowsErrorCode = null; + private volatile String appendRowsErrorDescription = null; + + public void setAppendRowsError(Throwable t) { + io.grpc.Status status = io.grpc.Status.fromThrowable(t); + this.appendRowsErrorCode = status.getCode().name(); + this.appendRowsErrorDescription = status.getDescription(); + } + Map> insertErrors = Maps.newHashMap(); // The counter for the number of insertions performed. @@ -801,6 +811,14 @@ Exceptions.StorageException tryInitialize() throws Exception { @Override public ApiFuture appendRows(long offset, ProtoRows rows) throws Exception { + if (appendRowsErrorCode != null) { + io.grpc.Status.Code code = io.grpc.Status.Code.valueOf(appendRowsErrorCode); + io.grpc.Status status = + appendRowsErrorDescription != null + ? code.toStatus().withDescription(appendRowsErrorDescription) + : code.toStatus(); + return ApiFutures.immediateFailedFuture(new io.grpc.StatusRuntimeException(status)); + } // The BigQuery client returns stream-open errors when the first append is called, so we // duplicate that here. Exceptions.StorageException storageException = tryInitialize(); 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 c3067ba0934f..56cd296a6cb0 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 @@ -123,6 +123,7 @@ public void testGcpApiSurface() throws Exception { classesInPackage("com.fasterxml.jackson.databind"), classesInPackage("io.grpc"), classesInPackage("io.opentelemetry.context"), + classesInPackage("io.opentelemetry.api"), classesInPackage("java"), classesInPackage("javax"), classesInPackage("org.apache.avro"), diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BeamRowToStorageApiProtoTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BeamRowToStorageApiProtoTest.java index 88bf3ef3f14d..50fb2c073621 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BeamRowToStorageApiProtoTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BeamRowToStorageApiProtoTest.java @@ -62,6 +62,11 @@ /** Unit tests form {@link BeamRowToStorageApiProto}. */ @RunWith(JUnit4.class) public class BeamRowToStorageApiProtoTest { + private static final LocalTime TEST_TIME_MICROS = LocalTime.of(12, 30, 45, 123_456_000); + private static final LocalDateTime TEST_DATETIME_MICROS = + LocalDateTime.of(2024, 1, 15, 12, 30, 45, 123_456_000); + private static final java.time.Instant TEST_INSTANT_MICROS = + java.time.Instant.parse("2024-01-15T12:30:45.123456Z"); private static final java.time.Instant TEST_INSTANT_NANOS = java.time.Instant.parse("2024-01-15T12:30:45.123456789Z"); @@ -254,9 +259,9 @@ public class BeamRowToStorageApiProtoTest { .withFieldValue("arrayNullValue", null) .withFieldValue("iterableValue", ImmutableList.of("blue", "red", "two", "one")) .withFieldValue("sqlDateValue", LocalDate.now(ZoneOffset.UTC)) - .withFieldValue("sqlTimeValue", LocalTime.now(ZoneOffset.UTC)) - .withFieldValue("sqlDatetimeValue", LocalDateTime.now(ZoneOffset.UTC)) - .withFieldValue("sqlTimestampValue", java.time.Instant.now().plus(123, ChronoUnit.MICROS)) + .withFieldValue("sqlTimeValue", TEST_TIME_MICROS) + .withFieldValue("sqlDatetimeValue", TEST_DATETIME_MICROS) + .withFieldValue("sqlTimestampValue", TEST_INSTANT_MICROS) .withFieldValue("enumValue", TEST_ENUM.valueOf("RED")) .build(); private static final Map BASE_PROTO_EXPECTED_FIELDS = diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOWriteTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOWriteTest.java index 601ed71473ed..7310e2f850e5 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOWriteTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOWriteTest.java @@ -1659,6 +1659,69 @@ public void testStreamingStorageApiWriteWithErrorHandling() throws Exception { storageWriteWithErrorHandling(false); } + @Test + public void testStorageApiWriteFailureExhaustedRetries() throws Exception { + assumeTrue(useStorageApi); + // Skip streaming parameterizations: the streaming write path uses triggering + // frequency which requires an unbounded PCollection. With our bounded test + // data, the pipeline hangs indefinitely. The error handling code is identical + // in both paths, so testing the non-streaming path is sufficient. + assumeFalse(useStreaming); + + // Create the table in the fake dataset service so write stream creation succeeds. + // The error will be injected at the appendRows level. + Table table = + new Table() + .setTableReference( + new TableReference() + .setProjectId("project-id") + .setDatasetId("dataset-id") + .setTableId("table-id")) + .setSchema( + new TableSchema() + .setFields( + ImmutableList.of( + new TableFieldSchema().setName("number").setType("INTEGER")))); + fakeDatasetService.createTable(table); + + // Set up fake dataset service to return PERMISSION_DENIED for appendRows + fakeDatasetService.setAppendRowsError( + new io.grpc.StatusRuntimeException( + io.grpc.Status.PERMISSION_DENIED.withDescription("Missing permissions"))); + + List elements = Lists.newArrayList(1, 2, 3); + + BigQueryIO.Write write = + BigQueryIO.write() + .to("project-id:dataset-id.table-id") + .withCreateDisposition(BigQueryIO.Write.CreateDisposition.CREATE_NEVER) + .withFormatFunction( + (SerializableFunction) + input -> new TableRow().set("number", input)) + .withSchema( + new TableSchema() + .setFields( + ImmutableList.of( + new TableFieldSchema().setName("number").setType("INTEGER")))) + .withTestServices(fakeBqServices) + .withoutValidation(); + + // Note: This test uses a bounded PCollection, so we don't set withTriggeringFrequency + // (which requires an unbounded PCollection). The Storage Write API is still exercised + // because useStorageApi=true. + + PCollection input = p.apply(Create.of(elements).withCoder(BigEndianIntegerCoder.of())); + input.apply("WriteToBQ", write); + + thrown.expect(RuntimeException.class); + thrown.expectMessage("More than"); + thrown.expectMessage("attempts to call AppendRows failed"); + thrown.expectMessage("PERMISSION_DENIED"); + thrown.expectMessage("bigquery.tables.updateData"); + + p.run().waitUntilFinish(); + } + @Test public void testStreamingStorageApiWriteWithAutoShardingWithErrorHandling() throws Exception { assumeTrue(useStreaming); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryUtilsTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryUtilsTest.java index b50e8448698a..52dbef55286f 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryUtilsTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryUtilsTest.java @@ -41,6 +41,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.util.Arrays; import java.util.Base64; import java.util.Collections; @@ -1444,22 +1445,43 @@ public void testToBeamRow_timestampNanos_utcSuffix() { @Test @SuppressWarnings("JavaInstantGetSecondsGetNano") - public void testToBeamRow_timestampMicros_utcSuffix() { + public void testToBeamRow_timestampMicros() { Schema schema = Schema.builder().addLogicalTypeField("ts", Timestamp.MICROS).build(); // BigQuery format with " UTC" suffix String timestamp = "2024-08-10 16:52:07.123456 UTC"; + String parsableTimestamp = "2024-08-10T16:52:07.123456Z"; + String negativeTimestamp = "1960-08-10T16:52:07.000123Z"; - Row beamRow = BigQueryUtils.toBeamRow(schema, new TableRow().set("ts", timestamp)); + java.time.Instant instant = OffsetDateTime.parse(parsableTimestamp).toInstant(); + String value = instant.getEpochSecond() + "." + instant.getNano() / 1000; + java.time.Instant negInstant = OffsetDateTime.parse(negativeTimestamp).toInstant(); + String negValue = + BigDecimal.valueOf(negInstant.getEpochSecond()) + .add(BigDecimal.valueOf(negInstant.getNano(), 9)) + .toPlainString(); - java.time.Instant actual = (java.time.Instant) beamRow.getValue("ts"); - assertEquals(2024, actual.atZone(java.time.ZoneOffset.UTC).getYear()); - assertEquals(8, actual.atZone(java.time.ZoneOffset.UTC).getMonthValue()); - assertEquals(10, actual.atZone(java.time.ZoneOffset.UTC).getDayOfMonth()); - assertEquals(16, actual.atZone(java.time.ZoneOffset.UTC).getHour()); - assertEquals(52, actual.atZone(java.time.ZoneOffset.UTC).getMinute()); - assertEquals(7, actual.atZone(java.time.ZoneOffset.UTC).getSecond()); - assertEquals(123456000, actual.getNano()); + List testRows = + Arrays.asList( + new TableRow().set("ts", timestamp), + new TableRow().set("ts", value), + new TableRow().set("negative", true).set("ts", negValue)); + + for (TableRow row : testRows) { + Row beamRow = BigQueryUtils.toBeamRow(schema, row); + + java.time.Instant actual = (java.time.Instant) beamRow.getValue("ts"); + + assertEquals( + row.get("negative") == null ? 2024 : 1960, + actual.atZone(java.time.ZoneOffset.UTC).getYear()); + assertEquals(8, actual.atZone(java.time.ZoneOffset.UTC).getMonthValue()); + assertEquals(10, actual.atZone(java.time.ZoneOffset.UTC).getDayOfMonth()); + assertEquals(16, actual.atZone(java.time.ZoneOffset.UTC).getHour()); + assertEquals(52, actual.atZone(java.time.ZoneOffset.UTC).getMinute()); + assertEquals(7, actual.atZone(java.time.ZoneOffset.UTC).getSecond()); + assertEquals(row.get("negative") == null ? 123456000 : 123000, actual.getNano()); + } } @Test diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateITBase.java similarity index 94% rename from sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateIT.java rename to sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateITBase.java index 3cb9897ada52..c4aeffd25aae 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateITBase.java @@ -70,44 +70,32 @@ 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.AfterClass; -import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@RunWith(Parameterized.class) -public class StorageApiSinkSchemaUpdateIT { - @Parameterized.Parameters - public static Iterable data() { - return ImmutableList.of( - new Object[] {false, false}, - new Object[] {false, true}, - new Object[] {true, false}, - new Object[] {true, true}); - } - - @Parameterized.Parameter(0) - public boolean useInputSchema; +abstract class StorageApiSinkSchemaUpdateITBase { + private final boolean useInputSchema; + private final boolean changeTableSchema; + private final String bigQueryDatasetId; - @Parameterized.Parameter(1) - public boolean changeTableSchema; + StorageApiSinkSchemaUpdateITBase( + boolean useInputSchema, boolean changeTableSchema, String bigQueryDatasetId) { + this.useInputSchema = useInputSchema; + this.changeTableSchema = changeTableSchema; + this.bigQueryDatasetId = bigQueryDatasetId; + } @Rule public TestName testName = new TestName(); - private static final Logger LOG = LoggerFactory.getLogger(StorageApiSinkSchemaUpdateIT.class); + private static final Logger LOG = LoggerFactory.getLogger(StorageApiSinkSchemaUpdateITBase.class); private static final BigqueryClient BQ_CLIENT = new BigqueryClient("StorageApiSinkSchemaChangeIT"); private static final String PROJECT = TestPipeline.testingPipelineOptions().as(GcpOptions.class).getProject(); - private static final String BIG_QUERY_DATASET_ID = - "storage_api_sink_schema_change_" + System.nanoTime(); - private static final String[] FIELDS = { "BOOL", "BOOLEAN", @@ -149,18 +137,17 @@ public static Iterable data() { // used when test suite specifies a particular GCP location for BigQuery operations private static String bigQueryLocation; - @BeforeClass - public static void setUpTestEnvironment() throws IOException, InterruptedException { + static void setUpTestEnvironment(String bigQueryDatasetId) + throws IOException, InterruptedException { // Create one BQ dataset for all test cases. bigQueryLocation = TestPipeline.testingPipelineOptions().as(TestBigQueryOptions.class).getBigQueryLocation(); - BQ_CLIENT.createNewDataset(PROJECT, BIG_QUERY_DATASET_ID, null, bigQueryLocation); + BQ_CLIENT.createNewDataset(PROJECT, bigQueryDatasetId, null, bigQueryLocation); } - @AfterClass - public static void cleanUp() { - LOG.info("Cleaning up dataset {} and tables.", BIG_QUERY_DATASET_ID); - BQ_CLIENT.deleteDataset(PROJECT, BIG_QUERY_DATASET_ID); + static void cleanUp(String bigQueryDatasetId) { + LOG.info("Cleaning up dataset {} and tables.", bigQueryDatasetId); + BQ_CLIENT.deleteDataset(PROJECT, bigQueryDatasetId); } private String createTable(TableSchema tableSchema) throws IOException, InterruptedException { @@ -178,16 +165,16 @@ private String createTable(TableSchema tableSchema, String suffix) } tableId += suffix; - BQ_CLIENT.deleteTable(PROJECT, BIG_QUERY_DATASET_ID, tableId); + BQ_CLIENT.deleteTable(PROJECT, bigQueryDatasetId, tableId); BQ_CLIENT.createNewTable( PROJECT, - BIG_QUERY_DATASET_ID, + bigQueryDatasetId, new Table() .setSchema(tableSchema) .setTableReference( new TableReference() .setTableId(tableId) - .setDatasetId(BIG_QUERY_DATASET_ID) + .setDatasetId(bigQueryDatasetId) .setProjectId(PROJECT))); return tableId; } @@ -395,7 +382,7 @@ private void runStreamingPipelineWithSchemaChange( makeTableSchemaFromTypes(fieldNamesWithExtra, ImmutableSet.of(extraField)); String tableId = createTable(bqTableSchema); - String tableSpec = PROJECT + ":" + BIG_QUERY_DATASET_ID + "." + tableId; + String tableSpec = PROJECT + ":" + bigQueryDatasetId + "." + tableId; // build write transform Write write = @@ -462,7 +449,7 @@ private void runStreamingPipelineWithSchemaChange( "Update Schema", ParDo.of( new UpdateSchemaDoFn( - PROJECT, BIG_QUERY_DATASET_ID, ImmutableMap.of(tableId, updatedSchema)))); + PROJECT, bigQueryDatasetId, ImmutableMap.of(tableId, updatedSchema)))); } WriteResult result = rows.apply("Stream to BigQuery", write); if (useIgnoreUnknownValues) { @@ -648,7 +635,7 @@ public void runDynamicDestinationsWithAutoSchemaUpdate(boolean useAtLeastOnce) t GenerateRowFunc generateRowFunc = new GenerateRowFunc(fieldNamesOrigin, fieldNamesWithExtra); String tableId = createTable(bqTableSchema, "_dynamic_" + i); - String tableSpec = PROJECT + ":" + BIG_QUERY_DATASET_ID + "." + tableId; + String tableSpec = PROJECT + ":" + bigQueryDatasetId + "." + tableId; rowFuncs.put((long) i, generateRowFunc); destinations.put((long) i, tableSpec); @@ -727,7 +714,7 @@ public void runDynamicDestinationsWithAutoSchemaUpdate(boolean useAtLeastOnce) t .apply("Add a dummy key", WithKeys.of(1)) .apply( "Update Schema", - ParDo.of(new UpdateSchemaDoFn(PROJECT, BIG_QUERY_DATASET_ID, updatedSchemas))); + ParDo.of(new UpdateSchemaDoFn(PROJECT, bigQueryDatasetId, updatedSchemas))); } WriteResult result = rows.apply("Stream to BigQuery", write); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateWithInputSchemaIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateWithInputSchemaIT.java new file mode 100644 index 000000000000..cd29d9fb65a1 --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateWithInputSchemaIT.java @@ -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. + */ +package org.apache.beam.sdk.io.gcp.bigquery; + +import java.io.IOException; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class StorageApiSinkSchemaUpdateWithInputSchemaIT extends StorageApiSinkSchemaUpdateITBase { + private static final String BIG_QUERY_DATASET_ID = + "storage_api_sink_schema_change_with_input_" + System.nanoTime(); + + @Parameterized.Parameters(name = "changeTableSchema={0}") + public static Iterable data() { + return ImmutableList.of(new Object[] {false}, new Object[] {true}); + } + + public StorageApiSinkSchemaUpdateWithInputSchemaIT(boolean changeTableSchema) { + super(true, changeTableSchema, BIG_QUERY_DATASET_ID); + } + + @BeforeClass + public static void setUpTestEnvironment() throws IOException, InterruptedException { + StorageApiSinkSchemaUpdateITBase.setUpTestEnvironment(BIG_QUERY_DATASET_ID); + } + + @AfterClass + public static void cleanUp() { + StorageApiSinkSchemaUpdateITBase.cleanUp(BIG_QUERY_DATASET_ID); + } +} diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateWithoutInputSchemaIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateWithoutInputSchemaIT.java new file mode 100644 index 000000000000..9b02e0be396b --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkSchemaUpdateWithoutInputSchemaIT.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.sdk.io.gcp.bigquery; + +import java.io.IOException; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class StorageApiSinkSchemaUpdateWithoutInputSchemaIT + extends StorageApiSinkSchemaUpdateITBase { + private static final String BIG_QUERY_DATASET_ID = + "storage_api_sink_schema_change_without_input_" + System.nanoTime(); + + @Parameterized.Parameters(name = "changeTableSchema={0}") + public static Iterable data() { + return ImmutableList.of(new Object[] {false}, new Object[] {true}); + } + + public StorageApiSinkSchemaUpdateWithoutInputSchemaIT(boolean changeTableSchema) { + super(false, changeTableSchema, BIG_QUERY_DATASET_ID); + } + + @BeforeClass + public static void setUpTestEnvironment() throws IOException, InterruptedException { + StorageApiSinkSchemaUpdateITBase.setUpTestEnvironment(BIG_QUERY_DATASET_ID); + } + + @AfterClass + public static void cleanUp() { + StorageApiSinkSchemaUpdateITBase.cleanUp(BIG_QUERY_DATASET_ID); + } +} 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/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/datastore/AdaptiveThrottlerTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/datastore/AdaptiveThrottlerTest.java deleted file mode 100644 index 6db8af3906c3..000000000000 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/datastore/AdaptiveThrottlerTest.java +++ /dev/null @@ -1,114 +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.sdk.io.gcp.datastore; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.closeTo; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; -import static org.junit.Assert.assertFalse; - -import java.util.Random; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.Mockito; - -/** Tests for {@link AdaptiveThrottler}. */ -@RunWith(JUnit4.class) -public class AdaptiveThrottlerTest { - - static final long START_TIME_MS = 0; - static final long SAMPLE_PERIOD_MS = 60000; - static final long SAMPLE_BUCKET_MS = 1000; - static final double OVERLOAD_RATIO = 2; - - /** Returns a throttler configured with the standard parameters above. */ - AdaptiveThrottler getThrottler() { - return new AdaptiveThrottler(SAMPLE_PERIOD_MS, SAMPLE_BUCKET_MS, OVERLOAD_RATIO); - } - - @Test - public void testNoInitialThrottling() throws Exception { - AdaptiveThrottler throttler = getThrottler(); - assertThat(throttler.throttlingProbability(START_TIME_MS), equalTo(0.0)); - assertThat( - "first request is not throttled", throttler.throttleRequest(START_TIME_MS), equalTo(false)); - } - - @Test - public void testNoThrottlingIfNoErrors() throws Exception { - AdaptiveThrottler throttler = getThrottler(); - long t = START_TIME_MS; - for (; t < START_TIME_MS + 20; t++) { - assertFalse(throttler.throttleRequest(t)); - throttler.successfulRequest(t); - } - assertThat(throttler.throttlingProbability(t), equalTo(0.0)); - } - - @Test - public void testNoThrottlingAfterErrorsExpire() throws Exception { - AdaptiveThrottler throttler = getThrottler(); - long t = START_TIME_MS; - for (; t < START_TIME_MS + SAMPLE_PERIOD_MS; t++) { - throttler.throttleRequest(t); - // and no successfulRequest. - } - assertThat( - "check that we set up a non-zero probability of throttling", - throttler.throttlingProbability(t), - greaterThan(0.0)); - for (; t < START_TIME_MS + 2 * SAMPLE_PERIOD_MS; t++) { - throttler.throttleRequest(t); - throttler.successfulRequest(t); - } - assertThat(throttler.throttlingProbability(t), equalTo(0.0)); - } - - @Test - public void testThrottlingAfterErrors() throws Exception { - Random mockRandom = Mockito.mock(Random.class); - Mockito.when(mockRandom.nextDouble()) - .thenReturn( - 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, - 0.7, 0.8, 0.9); - AdaptiveThrottler throttler = - new AdaptiveThrottler(SAMPLE_PERIOD_MS, SAMPLE_BUCKET_MS, OVERLOAD_RATIO, mockRandom); - for (int i = 0; i < 20; i++) { - boolean throttled = throttler.throttleRequest(START_TIME_MS + i); - // 1/3rd of requests succeeding. - if (i % 3 == 1) { - throttler.successfulRequest(START_TIME_MS + i); - } - - // Once we have some history in place, check what throttling happens. - if (i >= 10) { - // Expect 1/3rd of requests to be throttled. (So 1/3rd throttled, 1/3rd succeeding, 1/3rd - // tried and failing). - assertThat( - String.format("for i=%d", i), - throttler.throttlingProbability(START_TIME_MS + i), - closeTo(0.33, /*error=*/ 0.1)); - // Requests 10..13 should be throttled, 14..19 not throttled given the mocked random numbers - // that we fed to throttler. - assertThat(String.format("for i=%d", i), throttled, equalTo(i < 14)); - } - } - } -} diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessorTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessorTest.java index aad44879ce90..9ab49131b822 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessorTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessorTest.java @@ -24,6 +24,7 @@ import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.SpannerOptions; +import com.google.spanner.v1.DirectedReadOptions; import org.apache.beam.sdk.extensions.gcp.auth.TestCredential; import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider; import org.junit.Before; @@ -251,4 +252,54 @@ public void testBuildSpannerOptionsWithCustomHost() { SpannerOptions options = SpannerAccessor.buildSpannerOptions(config1); assertEquals(host, options.getHost()); } + + @Test + public void testBuildSpannerOptionsWithDirectedReadOptions() { + DirectedReadOptions directedReadOptions = + DirectedReadOptions.newBuilder() + .setIncludeReplicas( + DirectedReadOptions.IncludeReplicas.newBuilder() + .addReplicaSelections( + DirectedReadOptions.ReplicaSelection.newBuilder() + .setLocation("us-central1") + .setType(DirectedReadOptions.ReplicaSelection.Type.READ_ONLY))) + .build(); + SpannerConfig config1 = + SpannerConfig.create() + .toBuilder() + .setServiceFactory(serviceFactory) + .setDirectedReadOptions(StaticValueProvider.of(directedReadOptions)) + .setProjectId(StaticValueProvider.of("project")) + .setInstanceId(StaticValueProvider.of("test1")) + .setDatabaseId(StaticValueProvider.of("test1")) + .build(); + + SpannerOptions options = SpannerAccessor.buildSpannerOptions(config1); + assertEquals(directedReadOptions, options.getDirectedReadOptions()); + } + + @Test + public void testBuildSpannerOptionsWithDirectedReadOptionsJson() { + String jsonString = + "{\"includeReplicas\":{\"replicaSelections\":[{\"location\":\"us-east1\",\"type\":\"READ_WRITE\"}]}}"; + SpannerConfig config1 = + SpannerConfig.create() + .withServiceFactory(serviceFactory) + .withProjectId("project") + .withInstanceId("test1") + .withDatabaseId("test1") + .withDirectedReadOptions(jsonString); + + SpannerOptions options = SpannerAccessor.buildSpannerOptions(config1); + assertEquals( + DirectedReadOptions.ReplicaSelection.Type.READ_WRITE, + options.getDirectedReadOptions().getIncludeReplicas().getReplicaSelections(0).getType()); + assertEquals( + "us-east1", + options + .getDirectedReadOptions() + .getIncludeReplicas() + .getReplicaSelections(0) + .getLocation()); + } } diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIOReadChangeStreamTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIOReadChangeStreamTest.java index fd9fdf835f59..13fd343f5c83 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIOReadChangeStreamTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIOReadChangeStreamTest.java @@ -159,6 +159,22 @@ public void testWithDefaultCredential() { defaultCredential, changeStreamSpannerConfigWithCredential.getCredentials().get()); assertEquals(defaultCredential, metadataSpannerConfigWithCredential.getCredentials().get()); } + + @Test + public void testSetDirectedReadOptions() { + String directedReadString = + "{\"includeReplicas\":{\"replicaSelections\":[{\"location\":\"us-central1\",\"type\":\"READ_ONLY\"}]}}"; + readChangeStream = readChangeStream.withDirectedReadOptions(directedReadString); + SpannerConfig changeStreamSpannerConfig = readChangeStream.buildChangeStreamSpannerConfig(); + assertEquals( + "us-central1", + changeStreamSpannerConfig + .getDirectedReadOptions() + .get() + .getIncludeReplicas() + .getReplicaSelections(0) + .getLocation()); + } } /** Parameterized tests for Dialect and Partition Mode combinations. */ diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIOWriteTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIOWriteTest.java index dbe017531b28..2df83665c9a5 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIOWriteTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/SpannerIOWriteTest.java @@ -1569,9 +1569,9 @@ public void testRefCountedSpannerAccessorOnlyOnce() { SpannerIO.WriteToSpannerFn test3Fn = new SpannerIO.WriteToSpannerFn(config1, FailureMode.REPORT_FAILURES, null /* failedTag */); - test1Fn.setup(); - test2Fn.setup(); - test3Fn.setup(); + test1Fn.setup(pipeline.getOptions()); + test2Fn.setup(pipeline.getOptions()); + test3Fn.setup(pipeline.getOptions()); test2Fn.teardown(); test3Fn.teardown(); @@ -1611,10 +1611,10 @@ public void testRefCountedSpannerAccessorDifferentDbsOnlyOnce() { SpannerIO.WriteToSpannerFn test4Fn = new SpannerIO.WriteToSpannerFn(config2, FailureMode.REPORT_FAILURES, null /* failedTag */); - test1Fn.setup(); - test2Fn.setup(); - test3Fn.setup(); - test4Fn.setup(); + test1Fn.setup(pipeline.getOptions()); + test2Fn.setup(pipeline.getOptions()); + test3Fn.setup(pipeline.getOptions()); + test4Fn.setup(pipeline.getOptions()); test2Fn.teardown(); test3Fn.teardown(); 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/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"); 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())); } } diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamActionTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamActionTest.java index 17ece3da2877..13648e1e6763 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamActionTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamActionTest.java @@ -34,6 +34,7 @@ import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.Struct; +import io.opentelemetry.api.OpenTelemetry; import java.util.Arrays; import java.util.Optional; import org.apache.beam.sdk.io.gcp.spanner.changestreams.ChangeStreamMetrics; @@ -122,7 +123,8 @@ public void setUp() throws Exception { partitionEventRecordAction, metrics, false, - Duration.standardMinutes(2)); + Duration.standardMinutes(2), + OpenTelemetry.noop()); final Struct row = mock(Struct.class); partition = PartitionMetadata.newBuilder() @@ -1046,7 +1048,8 @@ public void testQueryChangeStreamWithMutableChangeStreamCappedEndTimestamp() { partitionEventRecordAction, metrics, true, - Duration.standardMinutes(2)); + Duration.standardMinutes(2), + OpenTelemetry.noop()); // Set endTimestamp to 60 minutes in the future Timestamp now = Timestamp.now(); @@ -1098,7 +1101,8 @@ public void testQueryChangeStreamWithMutableChangeStreamUncappedEndTimestamp() { partitionEventRecordAction, metrics, true, - Duration.standardMinutes(2)); + Duration.standardMinutes(2), + OpenTelemetry.noop()); // Set endTimestamp to only 10 seconds in the future Timestamp now = Timestamp.now(); diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/ReadChangeStreamPartitionDoFnTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/ReadChangeStreamPartitionDoFnTest.java index 9a783f5de31e..73da42ba48df 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/ReadChangeStreamPartitionDoFnTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/dofn/ReadChangeStreamPartitionDoFnTest.java @@ -47,6 +47,7 @@ import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.PartitionMetadata; import org.apache.beam.sdk.io.gcp.spanner.changestreams.restriction.TimestampRange; +import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.DoFn.BundleFinalizer; import org.apache.beam.sdk.transforms.DoFn.OutputReceiver; import org.apache.beam.sdk.transforms.DoFn.ProcessContinuation; @@ -163,10 +164,11 @@ public void setUp() { eq(partitionEventRecordAction), eq(metrics), anyBoolean(), - eq(Duration.standardMinutes(2)))) + eq(Duration.standardMinutes(2)), + any())) .thenReturn(queryChangeStreamAction); - doFn.setup(); + doFn.setup(PipelineOptionsFactory.create()); } @Test 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 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/build.gradle b/sdks/java/io/hadoop-format/build.gradle index 73fc44a0f311..6bb581568d74 100644 --- a/sdks/java/io/hadoop-format/build.gradle +++ b/sdks/java/io/hadoop-format/build.gradle @@ -49,6 +49,7 @@ configurations.testRuntimeClasspath { resolutionStrategy.force 'com.github.jbellis:jamm:0.3.0' // Pin snakeyaml version due to cassandra-all does not support 2.x resolutionStrategy.force 'org.yaml:snakeyaml:1.33' + exclude group: "at.yawk.lz4", module: "lz4-java" } dependencies { @@ -202,3 +203,24 @@ static def createTaskNames(Map hadoopVersions, String suffix) { .map{num -> "runHadoopFormatIO$num$suffix"} .collect(Collectors.toList()) } + +def testJavaVer = project.findProperty('testJavaVersion') ?: JavaVersion.current().getMajorVersion() +def isJava17OrHigher = testJavaVer.toInteger() >= 17 + +tasks.withType(Test).configureEach { + if (isJava17OrHigher) { + systemProperty 'java.security.manager', 'allow' + } + // Open and export modules for embedded Cassandra and Elasticsearch under Java 17+ + jvmArgs '--add-exports=java.base/sun.nio.ch=ALL-UNNAMED', + '--add-exports=java.rmi/sun.rmi.registry=ALL-UNNAMED', + '--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED', + '--add-opens=java.base/sun.nio.ch=ALL-UNNAMED', + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', + '--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/java.net=ALL-UNNAMED', + '--add-opens=java.base/java.nio=ALL-UNNAMED' +} 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) { diff --git a/sdks/java/io/hcatalog/build.gradle b/sdks/java/io/hcatalog/build.gradle index d3bdd8f10765..b44487ab6ec4 100644 --- a/sdks/java/io/hcatalog/build.gradle +++ b/sdks/java/io/hcatalog/build.gradle @@ -40,6 +40,10 @@ hadoopVersions.each {kv -> configurations.create("hadoopVersion$kv.key")} def hive_version = "4.0.1" +tasks.withType(Test).configureEach { + jvmArgs '--add-opens=java.base/java.net=ALL-UNNAMED' +} + dependencies { implementation library.java.vendored_guava_32_1_2_jre implementation project(path: ":sdks:java:core", configuration: "shadow") diff --git a/sdks/java/io/iceberg/build.gradle b/sdks/java/io/iceberg/build.gradle index 8142c5f5b90b..983ebd07fefc 100644 --- a/sdks/java/io/iceberg/build.gradle +++ b/sdks/java/io/iceberg/build.gradle @@ -23,8 +23,8 @@ import java.util.stream.Collectors plugins { id 'org.apache.beam.module' } applyJavaNature( automaticModuleName: 'org.apache.beam.sdk.io.iceberg', - // iceberg ended support for Java 8 in 1.7.0 - requireJavaVersion: JavaVersion.VERSION_11, + // iceberg ended support for Java 11 in 1.11.0 + requireJavaVersion: JavaVersion.VERSION_17, ) description = "Apache Beam :: SDKs :: Java :: IO :: Iceberg" @@ -37,7 +37,7 @@ def hadoopVersions = [ hadoopVersions.each {kv -> configurations.create("hadoopVersion$kv.key")} -def iceberg_version = "1.10.0" +def iceberg_version = "1.11.0" def parquet_version = "1.16.0" def orc_version = "1.9.6" def hive_version = "3.1.3" @@ -118,6 +118,10 @@ dependencies { configurations.all { // iceberg-core needs avro:1.12.0 resolutionStrategy.force 'org.apache.avro:avro:1.12.0' + // Iceberg 1.11.0 pulls parquet 1.17.1 transitively; hold at 1.16.0 to keep + // this PR zero-behavior-change. Bump parquet_version in a separate PR. + resolutionStrategy.force 'org.apache.parquet:parquet-avro:1.16.0' + resolutionStrategy.force 'org.apache.parquet:parquet-hadoop:1.16.0' // 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' 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..18b95ca50b1f 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; @@ -65,7 +66,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; @@ -97,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; @@ -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( @@ -515,7 +514,7 @@ private Callable createProcessTask( .withPartitionPath(partitionPath) .build(); return new ProcessResult( - SerializableDataFile.from(df, partitionPath), null, timestamp, window, paneInfo); + SerializableDataFile.from(df, table.spec()), null, timestamp, window, paneInfo); }; } @@ -524,7 +523,7 @@ static T transformValue(Transform transform, Type type, ByteBuffer } private Table getOrCreateTable(String filePath, FileFormat format) throws IOException { - TableIdentifier tableId = TableIdentifier.parse(identifier); + TableIdentifier tableId = IcebergUtils.parseTableIdentifier(identifier); try { return catalogConfig.catalog().loadTable(tableId); } catch (NoSuchTableException e) { @@ -532,19 +531,25 @@ 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)); + return catalogConfig.catalog().loadTable(IcebergUtils.parseTableIdentifier(identifier)); } } } @@ -660,7 +665,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,17 +677,17 @@ 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()) { return; } if (table == null) { - table = catalogConfig.catalog().loadTable(TableIdentifier.parse(identifier)); + table = catalogConfig.catalog().loadTable(IcebergUtils.parseTableIdentifier(identifier)); } - PartitionSpec spec = checkStateNotNull(table.specs().get(batch.getKey().getKey())); + PartitionSpec spec = checkStateNotNull(table.specs().get(batch.getKey())); String manifestPath = String.format( @@ -764,7 +769,7 @@ public void process( } String commitId = commitHash(manifests); if (table == null) { - table = catalogConfig.catalog().loadTable(TableIdentifier.parse(identifier)); + table = catalogConfig.catalog().loadTable(IcebergUtils.parseTableIdentifier(identifier)); } table.refresh(); ensureNameMappingPresent(table); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AppendFilesToTables.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AppendFilesToTables.java index c7981d697de4..917b087e39e0 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AppendFilesToTables.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AppendFilesToTables.java @@ -47,7 +47,6 @@ import org.apache.iceberg.Snapshot; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Catalog; -import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.io.FileIO; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.slf4j.Logger; @@ -75,7 +74,7 @@ public PCollection> expand(PCollection new SerializableFunction() { @Override public String apply(FileWriteResult input) { - return input.getTableIdentifier().toString(); + return IcebergUtils.tableIdentifierToString(input.getTableIdentifier()); } })) .apply("Group metadata updates by table", GroupByKey.create()) @@ -128,7 +127,7 @@ public void processElement( BoundedWindow window) throws IOException { String tableStringIdentifier = element.getKey(); - Table table = getCatalog().loadTable(TableIdentifier.parse(element.getKey())); + Table table = getCatalog().loadTable(IcebergUtils.parseTableIdentifier(element.getKey())); Iterable fileWriteResults = element.getValue(); if (shouldSkip(table, fileWriteResults)) { return; 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..364b44c37ee4 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 @@ -35,10 +35,10 @@ 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.Duration; import org.joda.time.Instant; /** @@ -51,8 +51,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 +77,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 +96,7 @@ static class AssignDoFn extends DoFn> { public void setup() { this.wrappers = new HashMap<>(); this.partitionKeys = new HashMap<>(); + this.lastRefreshTimes = new HashMap<>(); } @ProcessElement @@ -98,42 +106,77 @@ 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(); + spec = + TableCache.getAndRefreshIfStale( + catalogConfig, IcebergUtils.parseTableIdentifier(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)); } } 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/DynamicDestinations.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/DynamicDestinations.java index 0185758c8aeb..ba9ec771c11d 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/DynamicDestinations.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/DynamicDestinations.java @@ -18,10 +18,12 @@ package org.apache.beam.sdk.io.iceberg; import java.io.Serializable; +import java.util.List; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.ValueInSingleWindow; import org.apache.iceberg.catalog.TableIdentifier; +import org.checkerframework.checker.nullness.qual.Nullable; public interface DynamicDestinations extends Serializable { @@ -34,6 +36,14 @@ public interface DynamicDestinations extends Serializable { String getTableStringIdentifier(ValueInSingleWindow element); static DynamicDestinations singleTable(TableIdentifier tableId, Schema inputSchema) { - return new OneTableDynamicDestinations(tableId, inputSchema); + return new OneTableDynamicDestinations(tableId, inputSchema, null, null); + } + + static DynamicDestinations singleTable( + TableIdentifier tableId, + Schema inputSchema, + @Nullable List partitionFields, + @Nullable List sortFields) { + return new OneTableDynamicDestinations(tableId, inputSchema, partitionFields, sortFields); } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/FileWriteResult.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/FileWriteResult.java index b96b1d42c949..713b8029c15c 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/FileWriteResult.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/FileWriteResult.java @@ -44,7 +44,7 @@ abstract class FileWriteResult { @SchemaIgnore public TableIdentifier getTableIdentifier() { if (cachedTableIdentifier == null) { - cachedTableIdentifier = TableIdentifier.parse(getTableIdentifierString()); + cachedTableIdentifier = IcebergUtils.parseTableIdentifier(getTableIdentifierString()); } return cachedTableIdentifier; } @@ -70,7 +70,7 @@ abstract static class Builder { @SchemaIgnore public Builder setTableIdentifier(TableIdentifier tableId) { - return setTableIdentifierString(tableId.toString()); + return setTableIdentifierString(IcebergUtils.tableIdentifierToString(tableId)); } public abstract FileWriteResult 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 e01b174decb0..8fe05cb5b21f 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 @@ -154,8 +154,8 @@ public void createTable( String tableIdentifier, Schema tableSchema, @Nullable List partitionFields, - Map properties) { - TableIdentifier icebergIdentifier = TableIdentifier.parse(tableIdentifier); + @Nullable Map properties) { + TableIdentifier icebergIdentifier = IcebergUtils.parseTableIdentifier(tableIdentifier); org.apache.iceberg.Schema icebergSchema = IcebergUtils.beamSchemaToIcebergSchema(tableSchema); PartitionSpec icebergSpec = PartitionUtils.toPartitionSpec(partitionFields, tableSchema); try { @@ -164,7 +164,13 @@ public void createTable( icebergIdentifier, icebergSchema, icebergSpec); - catalog().createTable(icebergIdentifier, icebergSchema, icebergSpec, properties); + Catalog.TableBuilder builder = + catalog().buildTable(icebergIdentifier, icebergSchema).withPartitionSpec(icebergSpec); + if (properties != null) { + builder = builder.withProperties(properties); + } + builder.create(); + LOG.info("Successfully created table '{}'.", icebergIdentifier); } catch (AlreadyExistsException e) { throw new TableAlreadyExistsException(e); @@ -172,7 +178,7 @@ public void createTable( } public @Nullable IcebergTableInfo loadTable(String tableIdentifier) { - TableIdentifier icebergIdentifier = TableIdentifier.parse(tableIdentifier); + TableIdentifier icebergIdentifier = IcebergUtils.parseTableIdentifier(tableIdentifier); try { Table table = catalog().loadTable(icebergIdentifier); return new IcebergTableInfo(tableIdentifier, table); @@ -264,13 +270,13 @@ public void updatePartitionSpec( } public boolean dropTable(String tableIdentifier) { - TableIdentifier icebergIdentifier = TableIdentifier.parse(tableIdentifier); + TableIdentifier icebergIdentifier = IcebergUtils.parseTableIdentifier(tableIdentifier); return catalog().dropTable(icebergIdentifier); } public Set listTables(String namespace) { return catalog().listTables(Namespace.of(namespace)).stream() - .map(TableIdentifier::name) + .map(TableIdentifier::toString) .collect(Collectors.toSet()); } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProvider.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProvider.java index 31ff57a668bb..30930e880607 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProvider.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProvider.java @@ -41,7 +41,6 @@ import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Enums; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Optional; -import org.apache.iceberg.catalog.TableIdentifier; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; @@ -109,7 +108,7 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { IcebergIO.ReadRows readRows = IcebergIO.readRows(configuration.getIcebergCatalog()) .withCdc() - .from(TableIdentifier.parse(configuration.getTable())) + .from(IcebergUtils.parseTableIdentifier(configuration.getTable())) .fromSnapshot(configuration.getFromSnapshot()) .toSnapshot(configuration.getToSnapshot()) .fromTimestamp(configuration.getFromTimestamp()) @@ -118,7 +117,10 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { .streaming(configuration.getStreaming()) .keeping(configuration.getKeep()) .dropping(configuration.getDrop()) - .withFilter(configuration.getFilter()); + .withFilter(configuration.getFilter()) + .withWatermarkColumn(configuration.getWatermarkColumn()) + .withWatermarkColumnTimeUnit(configuration.getWatermarkColumnTimeUnit()) + .withMetadataColumns(configuration.getIncludeMetadataColumns()); @Nullable Integer pollIntervalSeconds = configuration.getPollIntervalSeconds(); if (pollIntervalSeconds != null) { @@ -194,6 +196,26 @@ static Builder builder() { "A subset of column names to exclude from reading. If null or empty, all columns will be read.") abstract @Nullable List getDrop(); + @SchemaFieldDescription( + "Column used to derive the source's output watermark. " + + "Must be an existing, required, top-level column of type 'long' or 'timestamp'. " + + "If not set, the watermark advances according to snapshot commit timestamp.") + abstract @Nullable String getWatermarkColumn(); + + @SchemaFieldDescription( + "Time unit used to interpret watermark column of type LONG. One of NANOSECONDS, MICROSECONDS, " + + "MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS. Defaults to MICROSECONDS.") + abstract @Nullable String getWatermarkColumnTimeUnit(); + + @SchemaFieldDescription( + "List of top-level metadata columns to include with CDC output rows. Supported columns: \n" + + "- `_change_type`\n" + + "- `_row_id`\n" + + "- `_last_updated_sequence_number`\n" + + "- `_commit_snapshot_id`\n" + + "- `_commit_snapshot_sequence_number`\n") + abstract @Nullable List getIncludeMetadataColumns(); + @AutoValue.Builder abstract static class Builder { abstract Builder setTable(String table); @@ -224,6 +246,12 @@ abstract static class Builder { abstract Builder setFilter(String filter); + abstract Builder setWatermarkColumn(String watermarkColumn); + + abstract Builder setWatermarkColumnTimeUnit(String timeUnit); + + abstract Builder setIncludeMetadataColumns(List metadataColumns); + abstract Configuration build(); } 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..e92504d41e67 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 @@ -22,8 +22,11 @@ import com.google.auto.value.AutoValue; import java.util.Arrays; import java.util.List; +import java.util.Map; import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.io.Read; +import org.apache.beam.sdk.io.iceberg.cdc.IncrementalChangelogSource; +import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.values.PBegin; @@ -31,6 +34,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.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.iceberg.DistributionMode; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Catalog; @@ -406,6 +410,12 @@ public abstract static class WriteRows extends PTransform, Iceb abstract boolean getAutoSharding(); + abstract @Nullable Map getWriteProperties(); + + abstract @Nullable List getPartitionFields(); + + abstract @Nullable List getSortFields(); + abstract Builder toBuilder(); @AutoValue.Builder @@ -424,6 +434,12 @@ abstract static class Builder { abstract Builder setAutoSharding(boolean autoSharding); + abstract Builder setWriteProperties(Map writeProperties); + + abstract Builder setPartitionFields(List partitionFields); + + abstract Builder setSortFields(List sortFields); + abstract WriteRows build(); } @@ -474,6 +490,40 @@ public WriteRows withAutosharding() { return toBuilder().setAutoSharding(true).build(); } + /** + * Defines properties to be passed to the Iceberg writer itself. Note that these properties are + * execution-scoped, meaning that they are applied to a preexisting table and will not mutate + * any table-level properties. + * + *

To set table-level properties that will be applied to dynamically created tables, use the + * managed Iceberg transform instead, setting the `table_properties` config property. + * + *

See: https://iceberg.apache.org/docs/latest/configuration/#write-properties + */ + public WriteRows withWriteProperties(Map writeProperties) { + return toBuilder().setWriteProperties(writeProperties).build(); + } + + /** + * Defines the desired Partition Spec to be applied when the Iceberg table must be dynamically + * created, e.g. `bucket(id_field, 32)` or `day(timestamp_field)` + * + *

See: https://iceberg.apache.org/spec/#partitioning + */ + public WriteRows withPartitionFields(List partitionFields) { + return toBuilder().setPartitionFields(partitionFields).build(); + } + + /** + * Defines the desired Sort Order to be applied when the Iceberg table must be dynamically + * created, e.g. `int_field desc` or `bucket(modulo_5, 4) asc nulls last` + * + *

See: https://iceberg.apache.org/spec/#sorting + */ + public WriteRows withSortOrder(List sortFields) { + return toBuilder().setSortFields(sortFields).build(); + } + @Override public IcebergWriteResult expand(PCollection input) { List allToArgs = Arrays.asList(getTableIdentifier(), getDynamicDestinations()); @@ -485,7 +535,10 @@ public IcebergWriteResult expand(PCollection input) { if (destinations == null) { destinations = DynamicDestinations.singleTable( - Preconditions.checkNotNull(getTableIdentifier()), input.getSchema()); + Preconditions.checkNotNull(getTableIdentifier()), + input.getSchema(), + getPartitionFields(), + getSortFields()); } // Assign destinations before re-windowing to global in WriteToDestinations because @@ -509,7 +562,8 @@ public IcebergWriteResult expand(PCollection input) { getCatalogConfig(), destinations, getTriggeringFrequency(), - getDirectWriteByteLimit())); + getDirectWriteByteLimit(), + getWriteProperties())); case HASH: return input .apply( @@ -521,7 +575,8 @@ public IcebergWriteResult expand(PCollection input) { getCatalogConfig(), destinations, getTriggeringFrequency(), - getAutoSharding())); + getAutoSharding(), + getWriteProperties())); default: throw new UnsupportedOperationException( "Unsupported distribution mode: " + getDistributionMode()); @@ -533,6 +588,7 @@ public static ReadRows readRows(IcebergCatalogConfig catalogConfig) { return new AutoValue_IcebergIO_ReadRows.Builder() .setCatalogConfig(catalogConfig) .setUseCdc(false) + .setMetadataColumns(ImmutableList.of()) .build(); } @@ -569,6 +625,12 @@ public enum StartingStrategy { abstract @Nullable String getFilter(); + abstract @Nullable String getWatermarkColumn(); + + abstract @Nullable String getWatermarkColumnTimeUnit(); + + abstract List getMetadataColumns(); + abstract Builder toBuilder(); @AutoValue.Builder @@ -599,6 +661,12 @@ abstract static class Builder { abstract Builder setFilter(@Nullable String filter); + abstract Builder setWatermarkColumn(@Nullable String watermarkColumn); + + abstract Builder setWatermarkColumnTimeUnit(@Nullable String timeUnit); + + abstract Builder setMetadataColumns(List metadataColumns); + abstract ReadRows build(); } @@ -650,19 +718,55 @@ public ReadRows withFilter(@Nullable String filter) { return toBuilder().setFilter(filter).build(); } + public ReadRows withWatermarkColumn(@Nullable String watermarkColumn) { + return toBuilder().setWatermarkColumn(watermarkColumn).build(); + } + + public ReadRows withWatermarkColumnTimeUnit(@Nullable String timeUnit) { + return toBuilder().setWatermarkColumnTimeUnit(timeUnit).build(); + } + + /** + * Appends top-level metadata columns to CDC output rows. + * + *

Supported values are {@code _change_type}, {@code _commit_snapshot_id}, {@code + * _commit_snapshot_sequence_number}, {@code _row_id}, and {@code + * _last_updated_sequence_number}. The row metadata columns are read from Iceberg data files and + * require a row-lineage table. The changelog metadata columns come from the emitted change kind + * and snapshot context and are appended when final Beam rows are emitted. + * + *

This option is only valid {@link #withCdc()}. + */ + public ReadRows withMetadataColumns(@Nullable List metadataColumns) { + return toBuilder() + .setMetadataColumns(metadataColumns == null ? ImmutableList.of() : metadataColumns) + .build(); + } + @Override 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); + + @Nullable + String updateCompatibilityVersion = + input + .getPipeline() + .getOptions() + .as(StreamingOptions.class) + .getUpdateCompatibilityVersion(); IcebergScanConfig scanConfig = IcebergScanConfig.builder() .setCatalogConfig(getCatalogConfig()) .setScanType(IcebergScanConfig.ScanType.TABLE) .setTableIdentifier(tableId) - .setSchema(IcebergUtils.icebergSchemaToBeamSchema(table.schema())) + .setSchema( + IcebergUtils.icebergSchemaToBeamSchema( + table.schema(), updateCompatibilityVersion)) + .setUpdateCompatibilityVersion(updateCompatibilityVersion) .setFromSnapshotInclusive(getFromSnapshot()) .setToSnapshot(getToSnapshot()) .setFromTimestamp(getFromTimestamp()) @@ -674,12 +778,15 @@ public PCollection expand(PBegin input) { .setKeepFields(getKeep()) .setDropFields(getDrop()) .setFilterString(getFilter()) + .setWatermarkColumn(getWatermarkColumn()) + .setWatermarkColumnTimeUnit(getWatermarkColumnTimeUnit()) + .setMetadataColumns(getMetadataColumns()) .build(); scanConfig.validate(table); PTransform> source = getUseCdc() - ? new IncrementalScanSource(scanConfig) + ? new IncrementalChangelogSource(scanConfig) : Read.from(new ScanSource(scanConfig)); return input.apply(source); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergReadSchemaTransformProvider.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergReadSchemaTransformProvider.java index 63d6f792e56a..67ea60505072 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergReadSchemaTransformProvider.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergReadSchemaTransformProvider.java @@ -37,7 +37,6 @@ import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PCollectionRowTuple; import org.apache.beam.sdk.values.Row; -import org.apache.iceberg.catalog.TableIdentifier; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -93,7 +92,7 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { .getPipeline() .apply( IcebergIO.readRows(configuration.getIcebergCatalog()) - .from(TableIdentifier.parse(configuration.getTable())) + .from(IcebergUtils.parseTableIdentifier(configuration.getTable())) .keeping(configuration.getKeep()) .dropping(configuration.getDrop()) .withFilter(configuration.getFilter())); 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..bcd574afdaba 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 @@ -17,27 +17,45 @@ */ package org.apache.beam.sdk.io.iceberg; +import static org.apache.beam.sdk.io.iceberg.IcebergUtils.icebergSchemaToBeamSchema; +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 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 static org.apache.iceberg.types.Type.TypeID.LONG; +import static org.apache.iceberg.types.Type.TypeID.TIMESTAMP; import com.google.auto.value.AutoValue; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Set; +import java.util.concurrent.TimeUnit; 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.MetadataColumns; +import org.apache.iceberg.StructLike; 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; +import org.apache.iceberg.types.Comparators; import org.apache.iceberg.types.TypeUtil; +import org.apache.iceberg.types.Types.NestedField; +import org.apache.iceberg.util.SnapshotUtil; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; @@ -48,8 +66,9 @@ 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; + private transient org.apache.iceberg.@MonotonicNonNull Schema cachedRecordIdSchema; + private transient @MonotonicNonNull Schema cachedRowIdBeamSchema; public enum ScanType { TABLE, @@ -69,7 +88,8 @@ public enum ScanType { public Table getTable() { if (cachedTable == null) { cachedTable = - getCatalogConfig().catalog().loadTable(TableIdentifier.parse(getTableIdentifier())); + TableCache.get( + getCatalogConfig(), IcebergUtils.parseTableIdentifier(getTableIdentifier())); } return cachedTable; } @@ -89,9 +109,9 @@ static org.apache.iceberg.Schema resolveSchema( @Nullable List keep, @Nullable List drop, @Nullable Set fieldsInFilter) { - ImmutableList.Builder selectedFieldsBuilder = ImmutableList.builder(); + Set selectedFields = new LinkedHashSet<>(); if (keep != null && !keep.isEmpty()) { - selectedFieldsBuilder.addAll(keep); + selectedFields.addAll(keep); } else if (drop != null && !drop.isEmpty()) { List paths = new ArrayList<>(TypeUtil.indexNameById(schema.asStruct()).values()); Collections.sort(paths); @@ -100,7 +120,7 @@ static org.apache.iceberg.Schema resolveSchema( boolean isParent = i + 1 < paths.size() && paths.get(i + 1).startsWith(path + "."); boolean isDrop = drop.stream().anyMatch(d -> path.equals(d) || path.startsWith(d + ".")); if (!isParent && !isDrop) { - selectedFieldsBuilder.add(path); + selectedFields.add(path); } } } else { @@ -111,9 +131,8 @@ static org.apache.iceberg.Schema resolveSchema( if (fieldsInFilter != null && !fieldsInFilter.isEmpty()) { fieldsInFilter.stream() .map(f -> schema.caseInsensitiveFindField(f).name()) - .forEach(selectedFieldsBuilder::add); + .forEach(selectedFields::add); } - ImmutableList selectedFields = selectedFieldsBuilder.build(); return selectedFields.isEmpty() ? schema : schema.select(selectedFields); } @@ -141,17 +160,34 @@ public org.apache.iceberg.Schema getRequiredSchema() { return cachedRequiredSchema; } + public org.apache.iceberg.Schema recordIdSchema() { + if (cachedRecordIdSchema == null) { + org.apache.iceberg.Schema fullSchema = getTable().schema(); + cachedRecordIdSchema = TypeUtil.select(fullSchema, fullSchema.identifierFieldIds()); + } + return cachedRecordIdSchema; + } + + public Schema rowIdBeamSchema() { + if (cachedRowIdBeamSchema == null) { + cachedRowIdBeamSchema = + icebergSchemaToBeamSchema(recordIdSchema(), getUpdateCompatibilityVersion()); + } + return cachedRowIdBeamSchema; + } + + public Comparator recordIdComparator() { + return Comparators.forType(recordIdSchema().asStruct()); + } + @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 @@ -208,6 +244,9 @@ public Expression getFilter() { @Pure public abstract boolean getUseCdc(); + @Pure + public abstract @Nullable String getUpdateCompatibilityVersion(); + @Pure public abstract @Nullable Boolean getStreaming(); @@ -226,6 +265,15 @@ public Expression getFilter() { @Pure public abstract @Nullable List getDropFields(); + @Pure + public abstract @Nullable String getWatermarkColumn(); + + @Pure + public abstract @Nullable String getWatermarkColumnTimeUnit(); + + @Pure + public abstract List getMetadataColumns(); + @Pure public static Builder builder() { return new AutoValue_IcebergScanConfig.Builder() @@ -248,7 +296,9 @@ public static Builder builder() { .setPollInterval(null) .setStartingStrategy(null) .setTag(null) - .setBranch(null); + .setBranch(null) + .setWatermarkColumn(null) + .setMetadataColumns(ImmutableList.of()); } @AutoValue.Builder @@ -260,7 +310,7 @@ public abstract static class Builder { public abstract Builder setTableIdentifier(String tableIdentifier); public Builder setTableIdentifier(TableIdentifier tableIdentifier) { - return this.setTableIdentifier(tableIdentifier.toString()); + return this.setTableIdentifier(IcebergUtils.tableIdentifierToString(tableIdentifier)); } public Builder setTableIdentifier(String... names) { @@ -299,6 +349,9 @@ public Builder setTableIdentifier(String... names) { public abstract Builder setUseCdc(boolean useCdc); + public abstract Builder setUpdateCompatibilityVersion( + @Nullable String updateCompatibilityVersion); + public abstract Builder setStreaming(@Nullable Boolean streaming); public abstract Builder setPollInterval(@Nullable Duration pollInterval); @@ -311,12 +364,19 @@ public Builder setTableIdentifier(String... names) { public abstract Builder setDropFields(@Nullable List fields); + public abstract Builder setWatermarkColumn(@Nullable String watermarkColumn); + + public abstract Builder setWatermarkColumnTimeUnit(@Nullable String timeUnit); + + public abstract Builder setMetadataColumns(List metadataColumns); + public abstract IcebergScanConfig build(); } @VisibleForTesting abstract Builder toBuilder(); + @SuppressWarnings("ReturnValueIgnored") void validate(Table table) { @Nullable List keep = getKeepFields(); @Nullable List drop = getDropFields(); @@ -328,16 +388,19 @@ void validate(Table table) { String param; if (keep != null) { param = "keep"; - fieldsSpecified = newHashSet(checkNotNull(keep)); + fieldsSpecified = newHashSet(checkArgumentNotNull(keep)); } else { // drop != null param = "drop"; - fieldsSpecified = newHashSet(checkNotNull(drop)); + fieldsSpecified = newHashSet(checkArgumentNotNull(drop)); } fieldsSpecified.removeIf(name -> table.schema().findField(name) != null); checkArgument( - fieldsSpecified.isEmpty(), - error(String.format("'%s' specifies unknown field(s): %s", param, fieldsSpecified))); + fieldsSpecified.isEmpty() + || fieldsSpecified.stream().allMatch(MetadataColumns::isMetadataColumn), + error("'%s' specifies unknown field(s): %s"), + param, + fieldsSpecified); } // TODO(#34168, ahmedabu98): fill these gaps for the existing batch source @@ -364,6 +427,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( @@ -371,6 +437,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) { @@ -385,12 +464,139 @@ void validate(Table table) { checkArgument( getToTimestamp() == null || getToSnapshot() == null, error("only one of 'to_timestamp' or 'to_snapshot' can be set")); + @Nullable Long fromSnapshotId = ReadUtils.getFromSnapshotInclusive(table, this); + @Nullable Long toSnapshotId = ReadUtils.getToSnapshot(table, this); + if (fromSnapshotId != null) { + checkArgumentNotNull( + table.snapshot(fromSnapshotId), + error("configured starting snapshot does not exist: '%s'"), + fromSnapshotId); + } + if (toSnapshotId != null) { + checkArgumentNotNull( + table.snapshot(toSnapshotId), + error("configured end snapshot does not exist: '%s'"), + toSnapshotId); + } + if (fromSnapshotId != null && toSnapshotId != null) { + checkArgument( + SnapshotUtil.isAncestorOf(table, toSnapshotId, fromSnapshotId), + error("fromSnapshot '%s' is not an ancestor of toSnapshot '%s'"), + fromSnapshotId, + toSnapshotId); + } + + if (fromSnapshotId != null) { + checkArgumentNotNull( + table.snapshot(fromSnapshotId), + error("configured starting snapshot does not exist: '%s'"), + fromSnapshotId); + } + if (toSnapshotId != null) { + checkArgumentNotNull( + table.snapshot(toSnapshotId), + error("configured end snapshot does not exist: '%s'"), + toSnapshotId); + } + if (fromSnapshotId != null && toSnapshotId != null) { + checkArgument( + SnapshotUtil.isAncestorOf(table, toSnapshotId, fromSnapshotId), + error("fromSnapshot '%s' is not an ancestor of toSnapshot '%s'"), + fromSnapshotId, + toSnapshotId); + } if (getPollInterval() != null) { checkArgument( Boolean.TRUE.equals(getStreaming()), error("'poll_interval_seconds' can only be set when streaming is true")); } + + @Nullable String watermarkColumn = getWatermarkColumn(); + if (watermarkColumn != null) { + checkArgument(getUseCdc(), error("'watermark_column' is only supported in CDC mode")); + NestedField field = table.schema().findField(watermarkColumn); + checkArgument( + field != null, error("'watermark_column' refers to unknown column: %s"), watermarkColumn); + checkArgument( + field.isRequired(), + error("'watermark_column' needs to be a non-nullable column: %s"), + watermarkColumn); + checkArgument( + field.type().typeId() == TIMESTAMP || field.type().typeId() == LONG, + error("'watermark_column' must be a timestamp-typed column, but '%s' has type %s"), + watermarkColumn, + field.type().typeId()); + checkArgumentNotNull( + getProjectedSchema().findField(watermarkColumn), + "'watermark_column' column should not be dropped."); + } + + @Nullable String watermarkColumnTimeUnit = getWatermarkColumnTimeUnit(); + if (watermarkColumnTimeUnit != null) { + checkArgument( + table + .schema() + .findField( + checkStateNotNull( + watermarkColumn, + "watermark_column_time_unit is configured without a specified watermark_column")) + .type() + .typeId() + == LONG, + error("watermark_column_time_unit is only applicable for LONG columns.")); + try { + TimeUnit.valueOf(watermarkColumnTimeUnit.toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + error( + String.format( + "watermark_column_time_unit '%s' is invalid. Please choose one of: %s", + watermarkColumnTimeUnit, Arrays.toString(TimeUnit.values()))), + e); + } + } + } + + 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) { diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergUtils.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergUtils.java index d0d24532ff39..fa8d17f3c47c 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergUtils.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergUtils.java @@ -40,12 +40,17 @@ import org.apache.beam.sdk.schemas.logicaltypes.MicrosInstant; import org.apache.beam.sdk.schemas.logicaltypes.PassThroughLogicalType; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; import org.apache.beam.sdk.util.Preconditions; +import org.apache.beam.sdk.util.construction.TransformUpgrader; 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.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.ImmutableMap; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.catalog.TableIdentifierParser; import org.apache.iceberg.data.GenericRecord; import org.apache.iceberg.data.Record; import org.apache.iceberg.types.Type; @@ -81,7 +86,8 @@ private IcebergUtils() {} .put(MicrosInstant.IDENTIFIER, Types.TimestampType.withZone()) .build(); - private static Schema.FieldType icebergTypeToBeamFieldType(final Type type) { + private static Schema.FieldType icebergTypeToBeamFieldType( + final Type type, @Nullable String updateCompatibilityVersion) { switch (type.typeId()) { case BOOLEAN: return Schema.FieldType.BOOLEAN; @@ -100,48 +106,70 @@ private static Schema.FieldType icebergTypeToBeamFieldType(final Type type) { case TIMESTAMP: Types.TimestampType ts = (Types.TimestampType) type.asPrimitiveType(); if (ts.shouldAdjustToUTC()) { - return Schema.FieldType.DATETIME; + // timestamptz. The micros-precision Timestamp logical type preserves microseconds, while + // the legacy DATETIME (joda) mapping truncates to millis. Gated for update compatibility. + if (updateCompatibilityVersion != null + && !updateCompatibilityVersion.isEmpty() + && TransformUpgrader.compareVersions(updateCompatibilityVersion, "2.76.0") < 0) { + return Schema.FieldType.DATETIME; + } + return Schema.FieldType.logicalType(Timestamp.MICROS); } return Schema.FieldType.logicalType(SqlTypes.DATETIME); case STRING: return Schema.FieldType.STRING; case UUID: case BINARY: - return Schema.FieldType.BYTES; case FIXED: + return Schema.FieldType.BYTES; case DECIMAL: return Schema.FieldType.DECIMAL; case STRUCT: - return Schema.FieldType.row(icebergStructTypeToBeamSchema(type.asStructType())); + return Schema.FieldType.row( + icebergStructTypeToBeamSchema(type.asStructType(), updateCompatibilityVersion)); case LIST: - return Schema.FieldType.array(icebergTypeToBeamFieldType(type.asListType().elementType())); + return Schema.FieldType.array( + icebergTypeToBeamFieldType( + type.asListType().elementType(), updateCompatibilityVersion)); case MAP: return Schema.FieldType.map( - icebergTypeToBeamFieldType(type.asMapType().keyType()), - icebergTypeToBeamFieldType(type.asMapType().valueType())); + icebergTypeToBeamFieldType(type.asMapType().keyType(), updateCompatibilityVersion), + icebergTypeToBeamFieldType(type.asMapType().valueType(), updateCompatibilityVersion)); default: throw new RuntimeException("Unrecognized Iceberg Type: " + type.typeId()); } } - private static Schema.Field icebergFieldToBeamField(final Types.NestedField field) { - return Schema.Field.of(field.name(), icebergTypeToBeamFieldType(field.type())) + private static Schema.Field icebergFieldToBeamField( + final Types.NestedField field, @Nullable String updateCompatibilityVersion) { + return Schema.Field.of( + field.name(), icebergTypeToBeamFieldType(field.type(), updateCompatibilityVersion)) .withNullable(field.isOptional()); } /** Converts an Iceberg {@link org.apache.iceberg.Schema} to a Beam {@link Schema}. */ public static Schema icebergSchemaToBeamSchema(final org.apache.iceberg.Schema schema) { + return icebergSchemaToBeamSchema(schema, null); + } + + /** + * Converts an Iceberg {@link org.apache.iceberg.Schema} to a Beam {@link Schema}, accounting for + * update compatibility. + */ + public static Schema icebergSchemaToBeamSchema( + final org.apache.iceberg.Schema schema, @Nullable String updateCompatibilityVersion) { Schema.Builder builder = Schema.builder(); for (Types.NestedField f : schema.columns()) { - builder.addField(icebergFieldToBeamField(f)); + builder.addField(icebergFieldToBeamField(f, updateCompatibilityVersion)); } return builder.build(); } - private static Schema icebergStructTypeToBeamSchema(final Types.StructType struct) { + private static Schema icebergStructTypeToBeamSchema( + final Types.StructType struct, @Nullable String updateCompatibilityVersion) { Schema.Builder builder = Schema.builder(); for (Types.NestedField f : struct.fields()) { - builder.addField(icebergFieldToBeamField(f)); + builder.addField(icebergFieldToBeamField(f, updateCompatibilityVersion)); } return builder.build(); } @@ -196,7 +224,17 @@ static TypeAndMaxId beamFieldTypeToIcebergFieldType( String logicalTypeIdentifier = logicalType.getIdentifier(); @Nullable Type type = BEAM_LOGICAL_TYPES_TO_ICEBERG_TYPES.get(logicalTypeIdentifier); if (type == null) { - throw new RuntimeException("Unsupported Beam logical type " + logicalTypeIdentifier); + if (beamType.isLogicalType(Timestamp.IDENTIFIER)) { + int precision = checkStateNotNull(logicalType.getArgument()); + if (precision == Timestamp.MICROS.getArgument()) { + type = Types.TimestampType.withZone(); + } else { + throw new UnsupportedOperationException( + "Unsupported Timestamp precision: " + precision); + } + } else { + throw new RuntimeException("Unsupported Beam logical type " + logicalTypeIdentifier); + } } return new TypeAndMaxId(--nestedFieldId, type); } else if (beamType.getTypeName().isCollectionType()) { // ARRAY or ITERABLE @@ -358,7 +396,8 @@ private static void copyFieldIntoRecord(Record rec, Types.NestedField field, Row .ifPresent(v -> rec.setField(name, UUID.nameUUIDFromBytes(v))); break; case FIXED: - throw new UnsupportedOperationException("Fixed-precision fields are not yet supported."); + Optional.ofNullable(value.getBytes(name)).ifPresent(v -> rec.setField(name, v)); + break; case BINARY: Optional.ofNullable(value.getBytes(name)) .ifPresent(v -> rec.setField(name, ByteBuffer.wrap(v))); @@ -469,120 +508,150 @@ private static Object getIcebergTimestampValue(Object beamValue, boolean shouldA } } + /** Converts a {@link StructLike} to a Beam {@link Row}. */ + public static Row structToRow(Schema schema, StructLike struct) { + checkState( + schema.getFieldCount() == struct.size(), + "Struct of size %s does not match expected schema size %s", + struct.size(), + schema.getFieldCount()); + Row.Builder rowBuilder = Row.withSchema(schema); + for (int i = 0; i < schema.getFieldCount(); i++) { + Schema.Field field = schema.getField(i); + @Nullable Object icebergValue = struct.get(i, Object.class); + addIcebergValue(rowBuilder, field, icebergValue); + } + return rowBuilder.build(); + } + /** Converts an Iceberg {@link Record} to a Beam {@link Row}. */ public static Row icebergRecordToBeamRow(Schema schema, Record record) { Row.Builder rowBuilder = Row.withSchema(schema); for (Schema.Field field : schema.getFields()) { - boolean isNullable = field.getType().getNullable(); @Nullable Object icebergValue = record.getField(field.getName()); - if (icebergValue == null) { - if (isNullable) { - rowBuilder.addValue(null); - continue; - } - throw new RuntimeException( - String.format("Received null value for required field '%s'.", field.getName())); + addIcebergValue(rowBuilder, field, icebergValue); + } + return rowBuilder.build(); + } + + private static void addIcebergValue( + Row.Builder rowBuilder, Schema.Field field, @Nullable Object icebergValue) { + boolean isNullable = field.getType().getNullable(); + if (icebergValue == null) { + if (isNullable) { + rowBuilder.addValue(null); + return; } - switch (field.getType().getTypeName()) { - case BYTE: - case INT16: - case INT32: - case INT64: - case DECIMAL: // Iceberg and Beam both use BigDecimal - case FLOAT: // Iceberg and Beam both use float - case DOUBLE: // Iceberg and Beam both use double - case STRING: // Iceberg and Beam both use String - case BOOLEAN: // Iceberg and Beam both use boolean - rowBuilder.addValue(icebergValue); - break; - case ARRAY: - checkState( - icebergValue instanceof List, - "Expected List type for field '%s' but received %s", - field.getName(), - icebergValue.getClass()); - List<@NonNull ?> beamList = (List<@NonNull ?>) icebergValue; - Schema.FieldType collectionType = - checkStateNotNull(field.getType().getCollectionElementType()); - // recurse on struct types - if (collectionType.getTypeName().isCompositeType()) { - Schema innerSchema = checkStateNotNull(collectionType.getRowSchema()); - beamList = - beamList.stream() - .map(v -> icebergRecordToBeamRow(innerSchema, (Record) v)) - .collect(Collectors.toList()); - } - rowBuilder.addValue(beamList); - break; - case ITERABLE: - checkState( - icebergValue instanceof Iterable, - "Expected Iterable type for field '%s' but received %s", - field.getName(), - icebergValue.getClass()); - Iterable<@NonNull ?> beamIterable = (Iterable<@NonNull ?>) icebergValue; - Schema.FieldType iterableCollectionType = - checkStateNotNull(field.getType().getCollectionElementType()); - // recurse on struct types - if (iterableCollectionType.getTypeName().isCompositeType()) { - Schema innerSchema = checkStateNotNull(iterableCollectionType.getRowSchema()); - ImmutableList.Builder builder = ImmutableList.builder(); - for (Record v : (Iterable<@NonNull Record>) icebergValue) { - builder.add(icebergRecordToBeamRow(innerSchema, v)); - } - beamIterable = builder.build(); + throw new RuntimeException( + String.format("Received null value for required field '%s'.", field.getName())); + } + switch (field.getType().getTypeName()) { + case BYTE: + case INT16: + case INT32: + case INT64: + case DECIMAL: // Iceberg and Beam both use BigDecimal + case FLOAT: // Iceberg and Beam both use float + case DOUBLE: // Iceberg and Beam both use double + case STRING: // Iceberg and Beam both use String + case BOOLEAN: // Iceberg and Beam both use boolean + rowBuilder.addValue(icebergValue); + break; + case ARRAY: + checkState( + icebergValue instanceof List, + "Expected List type for field '%s' but received %s", + field.getName(), + icebergValue.getClass()); + List<@NonNull ?> beamList = (List<@NonNull ?>) icebergValue; + Schema.FieldType collectionType = + checkStateNotNull(field.getType().getCollectionElementType()); + // recurse on struct types + if (collectionType.getTypeName().isCompositeType()) { + Schema innerSchema = checkStateNotNull(collectionType.getRowSchema()); + beamList = + beamList.stream() + .map(v -> icebergRecordToBeamRow(innerSchema, (Record) v)) + .collect(Collectors.toList()); + } + rowBuilder.addValue(beamList); + break; + case ITERABLE: + checkState( + icebergValue instanceof Iterable, + "Expected Iterable type for field '%s' but received %s", + field.getName(), + icebergValue.getClass()); + Iterable<@NonNull ?> beamIterable = (Iterable<@NonNull ?>) icebergValue; + Schema.FieldType iterableCollectionType = + checkStateNotNull(field.getType().getCollectionElementType()); + // recurse on struct types + if (iterableCollectionType.getTypeName().isCompositeType()) { + Schema innerSchema = checkStateNotNull(iterableCollectionType.getRowSchema()); + ImmutableList.Builder builder = ImmutableList.builder(); + for (Record v : (Iterable<@NonNull Record>) icebergValue) { + builder.add(icebergRecordToBeamRow(innerSchema, v)); } - rowBuilder.addValue(beamIterable); - break; - case MAP: - checkState( - icebergValue instanceof Map, - "Expected Map type for field '%s' but received %s", - field.getName(), - icebergValue.getClass()); - Map beamMap = (Map) icebergValue; - Schema.FieldType valueType = checkStateNotNull(field.getType().getMapValueType()); - // recurse on struct types - if (valueType.getTypeName().isCompositeType()) { - Schema innerSchema = checkStateNotNull(valueType.getRowSchema()); - ImmutableMap.Builder newMap = ImmutableMap.builder(); - for (Map.Entry entry : ((Map) icebergValue).entrySet()) { - Record rec = ((Record) entry.getValue()); - newMap.put( - checkStateNotNull(entry.getKey()), - icebergRecordToBeamRow(innerSchema, checkStateNotNull(rec))); - } - beamMap = newMap.build(); + beamIterable = builder.build(); + } + rowBuilder.addValue(beamIterable); + break; + case MAP: + checkState( + icebergValue instanceof Map, + "Expected Map type for field '%s' but received %s", + field.getName(), + icebergValue.getClass()); + Map beamMap = (Map) icebergValue; + Schema.FieldType valueType = checkStateNotNull(field.getType().getMapValueType()); + // recurse on struct types + if (valueType.getTypeName().isCompositeType()) { + Schema innerSchema = checkStateNotNull(valueType.getRowSchema()); + ImmutableMap.Builder newMap = ImmutableMap.builder(); + for (Map.Entry entry : ((Map) icebergValue).entrySet()) { + Record rec = ((Record) entry.getValue()); + newMap.put( + checkStateNotNull(entry.getKey()), + icebergRecordToBeamRow(innerSchema, checkStateNotNull(rec))); } - rowBuilder.addValue(beamMap); - break; - case DATETIME: - // Iceberg uses a long for micros. - // Beam DATETIME uses joda's DateTime, which only supports millis, - // so we do lose some precision here - rowBuilder.addValue(getBeamDateTimeValue(icebergValue)); - break; - case BYTES: - // Iceberg uses ByteBuffer; Beam uses byte[] - rowBuilder.addValue(((ByteBuffer) icebergValue).array()); - break; - case ROW: - Record nestedRecord = (Record) icebergValue; - Schema nestedSchema = - checkArgumentNotNull( - field.getType().getRowSchema(), - "Corrupted schema: Row type did not have associated nested schema."); - rowBuilder.addValue(icebergRecordToBeamRow(nestedSchema, nestedRecord)); - break; - case LOGICAL_TYPE: - rowBuilder.addValue(getLogicalTypeValue(icebergValue, field.getType())); - break; - default: + beamMap = newMap.build(); + } + rowBuilder.addValue(beamMap); + break; + case DATETIME: + // Iceberg uses a long for micros. + // Beam DATETIME uses joda's DateTime, which only supports millis, + // so we do lose some precision here + rowBuilder.addValue(getBeamDateTimeValue(icebergValue)); + break; + case BYTES: + // Beam uses byte[]. Iceberg represents `binary` as a ByteBuffer but `fixed` as a byte[]. + rowBuilder.addValue( + icebergValue instanceof byte[] + ? (byte[]) icebergValue + : ((ByteBuffer) icebergValue).array()); + break; + case ROW: + Schema nestedSchema = + checkArgumentNotNull( + field.getType().getRowSchema(), + "Corrupted schema: Row type did not have associated nested schema."); + if (icebergValue instanceof Record) { + rowBuilder.addValue(icebergRecordToBeamRow(nestedSchema, (Record) icebergValue)); + } else if (icebergValue instanceof StructLike) { + rowBuilder.addValue(structToRow(nestedSchema, (StructLike) icebergValue)); + } else { throw new UnsupportedOperationException( - "Unsupported Beam type: " + field.getType().getTypeName()); - } + "Unsupported row type: " + icebergValue.getClass()); + } + break; + case LOGICAL_TYPE: + rowBuilder.addValue(getLogicalTypeValue(icebergValue, field.getType())); + break; + default: + throw new UnsupportedOperationException( + "Unsupported Beam type: " + field.getType().getTypeName()); } - return rowBuilder.build(); } private static DateTime getBeamDateTimeValue(Object icebergValue) { @@ -611,26 +680,85 @@ private static Object getLogicalTypeValue(Object icebergValue, Schema.FieldType return LocalTime.parse(strValue); } else if (type.isLogicalType(SqlTypes.DATETIME.getIdentifier())) { return LocalDateTime.parse(strValue); + } else if (type.isLogicalType(Timestamp.IDENTIFIER)) { + return OffsetDateTime.parse(strValue).toInstant(); } } else if (icebergValue instanceof Long) { if (type.isLogicalType(SqlTypes.TIME.getIdentifier())) { return DateTimeUtil.timeFromMicros((Long) icebergValue); } else if (type.isLogicalType(SqlTypes.DATETIME.getIdentifier())) { return DateTimeUtil.timestampFromMicros((Long) icebergValue); + } else if (type.isLogicalType(Timestamp.IDENTIFIER)) { + // timestamptz stored as micros since epoch -> java.time.Instant (micros preserved). + return DateTimeUtil.timestamptzFromMicros((Long) icebergValue).toInstant(); } } else if (icebergValue instanceof Integer && type.isLogicalType(SqlTypes.DATE.getIdentifier())) { return DateTimeUtil.dateFromDays((Integer) icebergValue); - } else if (icebergValue instanceof OffsetDateTime - && type.isLogicalType(SqlTypes.DATETIME.getIdentifier())) { - return ((OffsetDateTime) icebergValue) - .withOffsetSameInstant(ZoneOffset.UTC) - .toLocalDateTime(); + } else if (icebergValue instanceof OffsetDateTime) { + OffsetDateTime odt = (OffsetDateTime) icebergValue; + if (type.isLogicalType(SqlTypes.DATETIME.getIdentifier())) { + return odt.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); + } else if (type.isLogicalType(Timestamp.IDENTIFIER)) { + return odt.toInstant(); + } } // LocalDateTime, LocalDate, LocalTime return icebergValue; } + /** Serializes a table identifier without losing dots or other special characters in each part. */ + public static String tableIdentifierToString(TableIdentifier tableIdentifier) { + TableIdentifier identifier = checkArgumentNotNull(tableIdentifier); + return requiresJsonTableIdentifier(identifier) + ? TableIdentifierParser.toJson(identifier) + : identifier.toString(); + } + + /** Parses either Iceberg's JSON table identifier representation or the legacy dotted form. */ + public static TableIdentifier parseTableIdentifier(String table) { + if (looksLikeJsonObject(table)) { + return TableIdentifierParser.fromJson(table); + } + + return TableIdentifier.parse(table); + } + + private static boolean looksLikeJsonObject(@Nullable String value) { + if (value == null) { + return false; + } + + int start = 0; + while (start < value.length() && Character.isWhitespace(value.charAt(start))) { + start++; + } + if (start == value.length() || value.charAt(start) != '{') { + return false; + } + + int end = value.length() - 1; + while (end >= 0 && Character.isWhitespace(value.charAt(end))) { + end--; + } + + return end >= 0 && value.charAt(end) == '}'; + } + + private static boolean requiresJsonTableIdentifier(TableIdentifier identifier) { + if (looksLikeJsonObject(identifier.toString())) { + return true; + } + + for (String level : identifier.namespace().levels()) { + if (level.contains(".")) { + return true; + } + } + + return identifier.name().contains("."); + } + static boolean isUnbounded(PCollection input) { return input.isBounded().equals(PCollection.IsBounded.UNBOUNDED); } 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 8db4fb77a8e8..0ae9d5fb0ecb 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 @@ -158,6 +158,11 @@ public static Builder builder() { + "during high-throughput writes. Only available with 'hash' distribution mode.") public abstract @Nullable Boolean getAutosharding(); + @SchemaFieldDescription( + "Properties applied to the underlying file writer (e.g. Parquet write properties like " + + "'write.parquet.bloom-filter-enabled.column.').") + public abstract @Nullable Map getWriteProperties(); + @AutoValue.Builder public abstract static class Builder { public abstract Builder setTable(String table); @@ -188,6 +193,8 @@ public abstract static class Builder { public abstract Builder setAutosharding(Boolean autosharding); + public abstract Builder setWriteProperties(Map writeProperties); + public abstract Configuration build(); } @@ -279,6 +286,11 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { writeTransform = writeTransform.withAutosharding(); } + @Nullable Map writeProperties = configuration.getWriteProperties(); + if (writeProperties != null && !writeProperties.isEmpty()) { + writeTransform = writeTransform.withWriteProperties(writeProperties); + } + // 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/IncrementalScanSource.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java deleted file mode 100644 index 4df3eecb18e5..000000000000 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java +++ /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. - */ -package org.apache.beam.sdk.io.iceberg; - -import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; - -import java.util.List; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.coders.ListCoder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -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.transforms.Redistribute; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PBegin; -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.base.MoreObjects; -import org.apache.iceberg.Table; -import org.apache.iceberg.catalog.TableIdentifier; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Duration; - -/** - * An Iceberg source that reads a table incrementally using range(s) of table snapshots. The bounded - * source creates a single range, while the unbounded implementation continuously polls for new - * snapshots at the specified interval. - */ -class IncrementalScanSource extends PTransform> { - private static final Duration DEFAULT_POLL_INTERVAL = Duration.standardSeconds(60); - private final IcebergScanConfig scanConfig; - - IncrementalScanSource(IcebergScanConfig scanConfig) { - this.scanConfig = scanConfig; - } - - @Override - public PCollection expand(PBegin input) { - Table table = - scanConfig - .getCatalogConfig() - .catalog() - .loadTable(TableIdentifier.parse(scanConfig.getTableIdentifier())); - - PCollection>> snapshots = - MoreObjects.firstNonNull(scanConfig.getStreaming(), false) - ? unboundedSnapshots(input) - : boundedSnapshots(input, table); - - return snapshots - .setCoder(KvCoder.of(StringUtf8Coder.of(), ListCoder.of(SnapshotInfo.getCoder()))) - .apply(Redistribute.byKey()) - .apply("Create Read Tasks", ParDo.of(new CreateReadTasksDoFn(scanConfig))) - .setCoder(KvCoder.of(ReadTaskDescriptor.getCoder(), ReadTask.getCoder())) - .apply(Redistribute.arbitrarily()) - .apply("Read Rows From Tasks", ParDo.of(new ReadFromTasks(scanConfig))) - .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(scanConfig.getProjectedSchema())); - } - - /** Continuously watches for new snapshots. */ - private PCollection>> unboundedSnapshots(PBegin input) { - Duration pollInterval = - MoreObjects.firstNonNull(scanConfig.getPollInterval(), DEFAULT_POLL_INTERVAL); - return input.apply("Watch for Snapshots", new WatchForSnapshots(scanConfig, pollInterval)); - } - - /** Creates a fixed snapshot range. */ - private PCollection>> boundedSnapshots(PBegin input, Table table) { - checkStateNotNull( - table.currentSnapshot().snapshotId(), - "Table %s does not have any snapshots to read from.", - scanConfig.getTableIdentifier()); - - @Nullable Long from = ReadUtils.getFromSnapshotExclusive(table, scanConfig); - // if no end snapshot is provided, we read up to the current snapshot. - long to = - MoreObjects.firstNonNull( - ReadUtils.getToSnapshot(table, scanConfig), table.currentSnapshot().snapshotId()); - return input.apply( - "Create Snapshot Range", - Create.of( - KV.of( - scanConfig.getTableIdentifier(), - ReadUtils.snapshotsBetween(table, scanConfig.getTableIdentifier(), from, to)))); - } -} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/OneTableDynamicDestinations.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/OneTableDynamicDestinations.java index 861a8ad198a8..a93995589483 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/OneTableDynamicDestinations.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/OneTableDynamicDestinations.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; +import java.util.List; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.ValueInSingleWindow; @@ -30,6 +31,7 @@ import org.apache.iceberg.FileFormat; import org.apache.iceberg.catalog.TableIdentifier; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; class OneTableDynamicDestinations implements DynamicDestinations, Externalizable { // TableId represented as String for serializability @@ -37,18 +39,26 @@ class OneTableDynamicDestinations implements DynamicDestinations, Externalizable private transient @MonotonicNonNull TableIdentifier tableId; private transient @MonotonicNonNull Schema rowSchema; + private transient @Nullable List partitionFields; + private transient @Nullable List sortFields; @VisibleForTesting TableIdentifier getTableIdentifier() { if (tableId == null) { - tableId = TableIdentifier.parse(checkStateNotNull(tableIdString)); + tableId = IcebergUtils.parseTableIdentifier(checkStateNotNull(tableIdString)); } return tableId; } - OneTableDynamicDestinations(TableIdentifier tableId, Schema rowSchema) { - this.tableIdString = tableId.toString(); + OneTableDynamicDestinations( + TableIdentifier tableId, + Schema rowSchema, + @Nullable List partitionFields, + @Nullable List sortFields) { + this.tableIdString = IcebergUtils.tableIdentifierToString(tableId); this.rowSchema = rowSchema; + this.partitionFields = partitionFields; + this.sortFields = sortFields; } @Override @@ -68,9 +78,18 @@ public String getTableStringIdentifier(ValueInSingleWindow element) { @Override public IcebergDestination instantiateDestination(String unused) { + @Nullable IcebergTableCreateConfig createConfig = null; + if (partitionFields != null || sortFields != null) { + createConfig = + IcebergTableCreateConfig.builder() + .setSchema(checkStateNotNull(rowSchema)) + .setPartitionFields(partitionFields) + .setSortFields(sortFields) + .build(); + } return IcebergDestination.builder() .setTableIdentifier(getTableIdentifier()) - .setTableCreateConfig(null) + .setTableCreateConfig(createConfig) .setFileFormat(FileFormat.PARQUET) .build(); } @@ -78,14 +97,22 @@ public IcebergDestination instantiateDestination(String unused) { // Need a public default constructor for custom serialization public OneTableDynamicDestinations() {} + @SuppressWarnings("nullness") @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeUTF(checkStateNotNull(tableIdString)); + out.writeObject(rowSchema); + out.writeObject(partitionFields); + out.writeObject(sortFields); } + @SuppressWarnings("nullness") @Override - public void readExternal(ObjectInput in) throws IOException { + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { tableIdString = in.readUTF(); - tableId = TableIdentifier.parse(tableIdString); + tableId = IcebergUtils.parseTableIdentifier(tableIdString); + rowSchema = (Schema) in.readObject(); + partitionFields = (List) in.readObject(); + sortFields = (List) in.readObject(); } } 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..6c8e79c4b8c6 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 dataSequenceNumber) { + 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 dataSequenceNumber, + // so we pipe it along the util methods to include it here. + dataSequenceNumber = + dataSequenceNumber != null ? dataSequenceNumber : file.dataSequenceNumber(); + idToConstant.put( + MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.fieldId(), + convertConstant(Types.LongType.get(), dataSequenceNumber)); + + // 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/PortableIcebergDestinations.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PortableIcebergDestinations.java index f2cec9bdfec7..775020879f67 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PortableIcebergDestinations.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PortableIcebergDestinations.java @@ -25,7 +25,6 @@ import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.ValueInSingleWindow; import org.apache.iceberg.FileFormat; -import org.apache.iceberg.catalog.TableIdentifier; import org.checkerframework.checker.nullness.qual.Nullable; class PortableIcebergDestinations implements DynamicDestinations { @@ -84,7 +83,7 @@ public String getTableStringIdentifier(ValueInSingleWindow element) { @Override public IcebergDestination instantiateDestination(String dest) { return IcebergDestination.builder() - .setTableIdentifier(TableIdentifier.parse(dest)) + .setTableIdentifier(IcebergUtils.parseTableIdentifier(dest)) .setTableCreateConfig( IcebergTableCreateConfig.builder() .setSchema(getDataSchema()) 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 deleted file mode 100644 index 528b89c203bf..000000000000 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java +++ /dev/null @@ -1,103 +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.sdk.io.iceberg; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.ExecutionException; -import org.apache.beam.sdk.io.range.OffsetRange; -import org.apache.beam.sdk.metrics.Counter; -import org.apache.beam.sdk.metrics.Metrics; -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.KV; -import org.apache.beam.sdk.values.Row; -import org.apache.iceberg.FileScanTask; -import org.apache.iceberg.Table; -import org.apache.iceberg.data.Record; -import org.apache.iceberg.io.CloseableIterable; - -/** - * Bounded read implementation. - * - *

For each {@link ReadTask}, reads Iceberg {@link Record}s, and converts to Beam {@link Row}s. - * - *

Implemented as an SDF to leverage communicating bundle size (i.e. {@link DoFn.GetSize}) to the - * runner, to help with scaling decisions. - */ -@DoFn.BoundedPerElement -class ReadFromTasks extends DoFn, Row> { - private final IcebergScanConfig scanConfig; - private final Counter scanTasksCompleted = - Metrics.counter(ReadFromTasks.class, "scanTasksCompleted"); - - ReadFromTasks(IcebergScanConfig scanConfig) { - this.scanConfig = scanConfig; - } - - @Setup - public void setup() { - TableCache.setup(scanConfig); - } - - @ProcessElement - public void process( - @Element KV element, - RestrictionTracker tracker, - OutputReceiver out) - throws IOException, ExecutionException, InterruptedException { - ReadTask readTask = element.getValue(); - Table table = TableCache.get(scanConfig.getTableIdentifier()); - - List fileScanTasks = readTask.getFileScanTasks(); - - for (long l = tracker.currentRestriction().getFrom(); - l < tracker.currentRestriction().getTo(); - l++) { - if (!tracker.tryClaim(l)) { - return; - } - 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); - - for (Record record : reader) { - Row row = IcebergUtils.icebergRecordToBeamRow(beamSchema, record); - out.output(row); - } - } - scanTasksCompleted.inc(); - } - } - - @GetSize - public double getSize( - @Element KV element, @Restriction OffsetRange restriction) { - // TODO(ahmedabu98): this is actually the file byte size, likely compressed. - // find a way to output the actual Beam Row byte size. - return element.getValue().getSize(restriction.getFrom(), restriction.getTo()); - } - - @GetInitialRestriction - public OffsetRange getInitialRange(@Element KV element) { - return new OffsetRange(0, element.getValue().getFileScanTaskJsons().size()); - } -} 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..3e70ae126877 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,17 +151,25 @@ 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) { - fromSnapshot = table.snapshot(fromSnapshot).parentId(); + Snapshot snapshot = table.snapshot(fromSnapshot); + fromSnapshot = snapshot != null ? snapshot.parentId() : null; } - // 4. if snapshot is still null, the scan will default to the oldest snapshot, i.e. EARLIEST + // if snapshot is still null, the scan will default to the oldest snapshot, i.e. EARLIEST 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 +185,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 +205,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/RecordWriter.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java index fd3d5d63327c..c3b63b2a336f 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 @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.iceberg; import java.io.IOException; +import java.util.Map; import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Metrics; import org.apache.iceberg.DataFile; @@ -34,6 +35,7 @@ import org.apache.iceberg.io.DataWriter; import org.apache.iceberg.io.OutputFile; import org.apache.iceberg.parquet.Parquet; +import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,11 +56,22 @@ class RecordWriter { catalog.loadTable(destination.getTableIdentifier()), destination.getFileFormat(), filename, - partitionKey); + partitionKey, + null); } RecordWriter(Table table, FileFormat fileFormat, String filename, StructLike partitionKey) throws IOException { + this(table, fileFormat, filename, partitionKey, null); + } + + RecordWriter( + Table table, + FileFormat fileFormat, + String filename, + StructLike partitionKey, + @Nullable Map writeProperties) + throws IOException { this.table = table; this.fileFormat = fileFormat; @@ -91,14 +104,17 @@ class RecordWriter { .build(); break; case PARQUET: - icebergDataWriter = + Parquet.DataWriteBuilder parquetBuilder = Parquet.writeData(outputFile) .forTable(table) .createWriterFunc(GenericParquetWriter::create) .withPartition(partitionKey) .withKeyMetadata(keyMetadata) - .overwrite() - .build(); + .overwrite(); + if (writeProperties != null && !writeProperties.isEmpty()) { + parquetBuilder.setAll(writeProperties); + } + icebergDataWriter = parquetBuilder.build(); break; case ORC: throw new UnsupportedOperationException("ORC file format not currently supported."); 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..64f0ab6232a4 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; @@ -106,7 +104,6 @@ class DestinationState { final Cache writers; private final List dataFiles = Lists.newArrayList(); @VisibleForTesting final Map writerCounts = Maps.newHashMap(); - private final Map partitionFieldMap = Maps.newHashMap(); private final List exceptions = Lists.newArrayList(); private final InternalRecordWrapper wrapper; // wrapper that facilitates partitioning @@ -117,9 +114,6 @@ class DestinationState { this.routingPartitionKey = new PartitionKey(spec, schema); this.wrapper = new InternalRecordWrapper(schema.asStruct()); this.table = table; - for (PartitionField partitionField : spec.fields()) { - partitionFieldMap.put(partitionField.name(), partitionField); - } // build a cache of RecordWriters. // writers will expire after 1 min of idle time. @@ -129,7 +123,6 @@ class DestinationState { .expireAfterAccess(1, TimeUnit.MINUTES) .removalListener( (RemovalNotification removal) -> { - final PartitionKey pk = Preconditions.checkStateNotNull(removal.getKey()); final RecordWriter recordWriter = Preconditions.checkStateNotNull(removal.getValue()); try { @@ -146,9 +139,9 @@ class DestinationState { throw rethrow; } openWriters--; - String partitionPath = getPartitionDataPath(pk.toPath(), partitionFieldMap); + // Serialize against the file's own spec (looked up by its spec id) dataFiles.add( - SerializableDataFile.from(recordWriter.getDataFile(), partitionPath)); + SerializableDataFile.from(recordWriter.getDataFile(), table.specs())); }) .build(); } @@ -165,7 +158,10 @@ boolean write(Record record) { @Nullable RecordWriter writer = writers.getIfPresent(routingPartitionKey); if (writer == null && openWriters >= maxNumWriters) { - return false; + writers.cleanUp(); + if (openWriters >= maxNumWriters) { + return false; + } } writer = fetchWriterForPartition(routingPartitionKey, writer); writer.write(record); @@ -201,7 +197,8 @@ private RecordWriter createWriter(PartitionKey partitionKey) { table, icebergDestination.getFileFormat(), filePrefix + "_" + stateToken + "_" + recordIndex, - partitionKey); + partitionKey, + writeProperties); openWriters++; return writer; } catch (IOException e) { @@ -248,10 +245,11 @@ 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; + private final @Nullable Map writeProperties; @VisibleForTesting int openWriters = 0; @VisibleForTesting @@ -260,57 +258,32 @@ 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; - } + private boolean isClosed = false; - /** - * 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(); - } - } - } + RecordWriterManager( + IcebergCatalogConfig catalogConfig, String filePrefix, long maxFileSize, int maxNumWriters) { + this(catalogConfig, filePrefix, maxFileSize, maxNumWriters, null); } - @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, + @Nullable Map writeProperties) { + this.catalogConfig = catalogConfig; this.filePrefix = filePrefix; this.maxFileSize = maxFileSize; this.maxNumWriters = maxNumWriters; + this.writeProperties = writeProperties; } /** * 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 +292,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 +309,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..d8c1780c5db3 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) { @@ -115,7 +116,9 @@ public void populateDisplayData(DisplayData.Builder builder) { @Override public Coder getOutputCoder() { - return RowCoder.of(IcebergUtils.icebergSchemaToBeamSchema(scanConfig.getProjectedSchema())); + return RowCoder.of( + IcebergUtils.icebergSchemaToBeamSchema( + scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion())); } @Override 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..c6ddd0a7e250 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; @@ -67,7 +66,10 @@ class ScanTaskReader extends BoundedSource.BoundedReader { public ScanTaskReader(ScanTaskSource source) { this.source = source; - this.beamSchema = icebergSchemaToBeamSchema(source.getScanConfig().getProjectedSchema()); + this.beamSchema = + icebergSchemaToBeamSchema( + source.getScanConfig().getProjectedSchema(), + source.getScanConfig().getUpdateCompatibilityVersion()); } @Override @@ -121,8 +123,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 1f717b82c21e..01d50010606d 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 @@ -26,9 +26,11 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import org.apache.beam.sdk.annotations.Internal; 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.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.Equivalence; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; import org.apache.iceberg.DataFile; @@ -37,6 +39,8 @@ import org.apache.iceberg.Metrics; import org.apache.iceberg.PartitionKey; import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.SingleValueParser; +import org.apache.iceberg.StructLike; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -49,18 +53,19 @@ *

NOTE: If you add any new fields here, you need to also update the {@link #equals} and {@link * #hashCode()} methods. * - *

Use {@link #from(DataFile, String)} to create a {@link SerializableDataFile} and {@link + *

Use {@link #from(DataFile, PartitionSpec)} to create a {@link SerializableDataFile} and {@link * #createDataFile(Map)} to reconstruct the original {@link DataFile}. */ @DefaultSchema(AutoValueSchema.class) @AutoValue -abstract class SerializableDataFile { +@Internal +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 +74,12 @@ public static Builder builder() { abstract long getRecordCount(); @SchemaFieldNumber("3") - abstract long getFileSizeInBytes(); + public abstract long getFileSizeInBytes(); + /** @deprecated Use {@link #getJsonPartition()} instead. */ @SchemaFieldNumber("4") - abstract String getPartitionPath(); + @Deprecated + public abstract String getPartitionPath(); @SchemaFieldNumber("5") abstract int getPartitionSpecId(); @@ -96,13 +103,25 @@ 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(); + + @SchemaFieldNumber("17") + abstract @Nullable String getJsonPartition(); @AutoValue.Builder - abstract static class Builder { + public abstract static class Builder { abstract Builder setPath(String path); abstract Builder setFileFormat(String fileFormat); @@ -113,6 +132,8 @@ abstract static class Builder { abstract Builder setPartitionPath(String partitionPath); + abstract Builder setJsonPartition(String jsonPartition); + abstract Builder setPartitionSpecId(int partitionSpec); abstract Builder setKeyMetadata(ByteBuffer keyMetadata); @@ -131,31 +152,72 @@ 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, Map specs) { + return from( + f, + checkStateNotNull( + specs.get(f.specId()), + "Could not create a SerializableDataFile because DataFile is written using a partition spec id '%s' that is not found in the provided specs: %s", + f.specId(), + specs.keySet()), + true); + } + + public static SerializableDataFile from(DataFile f, PartitionSpec spec) { + return from(f, spec, 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, PartitionSpec spec, boolean includeMetrics) { + if (spec.specId() != f.specId()) { + throw new IllegalArgumentException( + String.format( + "Cannot serialize DataFile: its partition spec id %s does not match the provided " + + "spec id %s. Serialize the file with the exact spec it was written with.", + f.specId(), spec.specId())); + } + // jsonPartition is the primary (handles evolved specs, special characters). + // partitionPath is the fallback for values that don't round-trip through JSON. + String jsonPartition = SingleValueParser.toJson(spec.partitionType(), f.partition()); + String partitionPath = spec.partitionToPath(f.partition()); + + SerializableDataFile.Builder builder = + SerializableDataFile.builder() + .setPath(f.location()) + .setFileFormat(f.format().toString()) + .setRecordCount(f.recordCount()) + .setFileSizeInBytes(f.fileSizeInBytes()) + .setPartitionPath(partitionPath) + .setJsonPartition(jsonPartition) + .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 +227,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()), @@ -184,34 +246,65 @@ DataFile createDataFile(Map partitionSpecs) { toByteBufferMap(getLowerBounds()), toByteBufferMap(getUpperBounds())); - return DataFiles.builder(partitionSpec) - .withFormat(FileFormat.fromString(getFileFormat())) - .withPath(getPath()) - .withPartitionPath(getPartitionPath()) - .withEncryptionKeyMetadata(getKeyMetadata()) - .withFileSizeInBytes(getFileSizeInBytes()) - .withMetrics(dataFileMetrics) - .withSplitOffsets(getSplitOffsets()) - .build(); + DataFiles.Builder builder = + DataFiles.builder(partitionSpec) + .withFormat(FileFormat.fromString(getFileFormat())) + .withPath(getPath()) + .withEncryptionKeyMetadata(getKeyMetadata()) + .withFileSizeInBytes(getFileSizeInBytes()) + .withMetrics(dataFileMetrics) + .withSplitOffsets(getSplitOffsets()) + .withFirstRowId(getFirstRowId()); + + @Nullable String jsonPartition = getJsonPartition(); + if (jsonPartition != null) { + try { + builder = builder.withPartition(partition(partitionSpec)); + } catch (RuntimeException e) { + // Some partition values (e.g. NaN / Infinity floating-point) don't round-trip through the + // JSON representation; fall back to the partition-path string, which handles them. + builder = builder.withPartitionPath(getPartitionPath()); + } + } else { + // Elements decoded from a pre-jsonPartition release carry only the partition path. + builder = builder.withPartitionPath(getPartitionPath()); + } + return builder.build(); + } + + @VisibleForTesting + StructLike partition(PartitionSpec spec) { + return (StructLike) + SingleValueParser.fromJson(spec.partitionType(), checkStateNotNull(getJsonPartition())); } // 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; } 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; } - private static @Nullable Map toByteBufferMap( - @Nullable Map input) { + // 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; + } + + static @Nullable Map toByteBufferMap(@Nullable Map input) { if (input == null) { return null; } @@ -237,6 +330,8 @@ && getRecordCount() == that.getRecordCount() && getFileSizeInBytes() == that.getFileSizeInBytes() && getPartitionPath().equals(that.getPartitionPath()) && getPartitionSpecId() == that.getPartitionSpecId() + && Objects.equals(getPartitionPath(), that.getPartitionPath()) + && Objects.equals(getJsonPartition(), that.getJsonPartition()) && Objects.equals(getKeyMetadata(), that.getKeyMetadata()) && Objects.equals(getSplitOffsets(), that.getSplitOffsets()) && Objects.equals(getColumnSizes(), that.getColumnSizes()) @@ -244,10 +339,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; @@ -279,19 +377,23 @@ public final int hashCode() { getRecordCount(), getFileSizeInBytes(), getPartitionPath(), + getJsonPartition(), getPartitionSpecId(), getKeyMetadata(), getSplitOffsets(), 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/SnapshotInfo.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SnapshotInfo.java index bab5405cd4a5..de8c400c08c8 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SnapshotInfo.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SnapshotInfo.java @@ -94,7 +94,8 @@ public static Builder builder() { @SchemaIgnore public TableIdentifier getTableIdentifier() { if (cachedTableIdentifier == null) { - cachedTableIdentifier = TableIdentifier.parse(checkStateNotNull(getTableIdentifierString())); + cachedTableIdentifier = + IcebergUtils.parseTableIdentifier(checkStateNotNull(getTableIdentifierString())); } return cachedTableIdentifier; } 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..bc2fc25de30f 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,176 @@ */ 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.sdk.annotations.Internal; +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; + +/** + * 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. + */ +@Internal +public class TableCache { + 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. */ + public 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. */ + public static Table get(IcebergCatalogConfig catalogConfig, String identifier) { + return get(catalogConfig, IcebergUtils.parseTableIdentifier(identifier)); + } + + /** Returns the cached table, using the given loader only on a cache miss. */ + public 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. */ + public 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. */ + public static Table getRefreshed(IcebergCatalogConfig catalogConfig, String identifier) { + return getRefreshed(catalogConfig, IcebergUtils.parseTableIdentifier(identifier)); + } -/** Utility to fetch and cache Iceberg {@link Table}s. */ -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) { + public static Table getAndRefreshIfStale(IcebergCatalogConfig catalogConfig, String identifier) { + return getAndRefreshIfStale(catalogConfig, IcebergUtils.parseTableIdentifier(identifier)); + } + + /** + * Returns the cached table, refreshing it only if it is older than {@link + * #DEFAULT_REFRESH_INTERVAL}. + */ + public 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. */ + public 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(); + } + + @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)); } - 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 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 deleted file mode 100644 index 1af5588c2519..000000000000 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java +++ /dev/null @@ -1,195 +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.sdk.io.iceberg; - -import static org.apache.beam.sdk.transforms.Watch.Growth.PollResult; - -import java.util.List; -import java.util.stream.Collectors; -import org.apache.beam.sdk.coders.ListCoder; -import org.apache.beam.sdk.metrics.Counter; -import org.apache.beam.sdk.metrics.Gauge; -import org.apache.beam.sdk.metrics.Metrics; -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.PTransform; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.Watch; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PBegin; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.TimestampedValue; -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.Objects; -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.iceberg.Snapshot; -import org.apache.iceberg.Table; -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; - -/** - * Keeps watch over an Iceberg table and continuously outputs a range of snapshots, at the specified - * interval. - * - *

A downstream transform will create a list of read tasks for each range. - */ -class WatchForSnapshots extends PTransform>>> { - private static final Logger LOG = LoggerFactory.getLogger(WatchForSnapshots.class); - private final Duration pollInterval; - private final IcebergScanConfig scanConfig; - - WatchForSnapshots(IcebergScanConfig scanConfig, Duration pollInterval) { - this.pollInterval = pollInterval; - this.scanConfig = scanConfig; - } - - @Override - public PCollection>> expand(PBegin input) { - return input - .apply(Create.of(scanConfig.getTableIdentifier())) - .apply( - "Scan Table Snapshots", - Watch.growthOf(new SnapshotPollFn(scanConfig)) - .withPollInterval(pollInterval) - .withOutputCoder(ListCoder.of(SnapshotInfo.getCoder()))) - .apply("Persist Snapshot Progress", ParDo.of(new PersistSnapshotProgress())); - } - - /** - * Periodically scans the table for new snapshots, emitting a list for each new snapshot range. - * - *

This tracks progress locally but is not resilient to retries -- upon worker failure, it will - * restart from the initial starting strategy. Resilience is handled downstream by {@link - * PersistSnapshotProgress}. - */ - 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; - } - - @Override - public PollResult> apply(String tableIdentifier, Context c) { - if (!isCacheSetup) { - TableCache.setup(scanConfig); - isCacheSetup = true; - } - Table table = TableCache.getRefreshed(tableIdentifier); - - @Nullable Long userSpecifiedToSnapshot = ReadUtils.getToSnapshot(table, scanConfig); - boolean isComplete = userSpecifiedToSnapshot != null; - if (fromSnapshotId == null) { - // first scan, initialize starting point with user config - fromSnapshotId = ReadUtils.getFromSnapshotExclusive(table, scanConfig); - } - - Snapshot currentSnapshot = table.currentSnapshot(); - if (currentSnapshot == null || Objects.equal(currentSnapshot.snapshotId(), fromSnapshotId)) { - // no new snapshots since last poll. return empty result. - return getPollResult(null, isComplete); - } - - Long currentSnapshotId = currentSnapshot.snapshotId(); - // if no upper bound is specified, we poll up to the current snapshot - long toSnapshotId = MoreObjects.firstNonNull(userSpecifiedToSnapshot, currentSnapshotId); - - List snapshots = - ReadUtils.snapshotsBetween(table, tableIdentifier, fromSnapshotId, toSnapshotId); - - fromSnapshotId = currentSnapshotId; - return getPollResult(snapshots, isComplete); - } - - private PollResult> getPollResult( - @Nullable List snapshots, boolean isComplete) { - ImmutableList.Builder>> timestampedSnapshots = - ImmutableList.builder(); - if (snapshots != null) { - // watermark based on the oldest observed snapshot in this poll interval - Instant watermark = Instant.ofEpochMilli(snapshots.get(0).getTimestampMillis()); - timestampedSnapshots.add(TimestampedValue.of(snapshots, watermark)); - } - - return isComplete - ? PollResult.complete(timestampedSnapshots.build()) // stop at specified snapshot - : PollResult.incomplete(timestampedSnapshots.build()); // continue forever - } - } - - /** - * Stateful DoFn that persists the latest observed snapshot ID to state, making sure we pick up - * where we left off in case of a worker crash. - */ - // Ideally, Watch.Growth would support state out of the box, but that is a bigger change. - static class PersistSnapshotProgress - extends DoFn>, KV>> { - private final Gauge latestSnapshot = Metrics.gauge(SnapshotPollFn.class, "latestSnapshot"); - private final Counter snapshotsObserved = - Metrics.counter(SnapshotPollFn.class, "snapshotsObserved"); - - @StateId("latestObservedSnapshotId") - @SuppressWarnings("UnusedVariable") - private final StateSpec> latestObservedSnapshotId = StateSpecs.value(); - - @ProcessElement - public void process( - @Element KV> element, - final @AlwaysFetched @StateId("latestObservedSnapshotId") ValueState - latestObservedSnapshotId, - OutputReceiver>> out) { - List snapshots = element.getValue(); - - @Nullable Long latest = latestObservedSnapshotId.read(); - if (latest != null) { - int newSnapshotIndex = 0; - for (int i = 0; i < snapshots.size(); i++) { - if (snapshots.get(i).getSnapshotId() == latest) { - newSnapshotIndex = i + 1; - break; - } - } - if (newSnapshotIndex > 0) { - snapshots = snapshots.subList(newSnapshotIndex, snapshots.size()); - } - } - - SnapshotInfo checkpoint = Iterables.getLast(snapshots); - out.output(KV.of(element.getKey(), snapshots)); - LOG.info( - "New poll fetched {} snapshots: {}. Checkpointing at snapshot {} of timestamp {}.", - snapshots.size(), - snapshots.stream().map(SnapshotInfo::getSnapshotId).collect(Collectors.toList()), - checkpoint.getSnapshotId(), - checkpoint.getTimestampMillis()); - - latestObservedSnapshotId.write(checkpoint.getSnapshotId()); - latestSnapshot.set(checkpoint.getSnapshotId()); - snapshotsObserved.inc(snapshots.size()); - } - } -} 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..e03085e6be78 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 @@ -42,16 +40,19 @@ class WriteDirectRowsToFiles private final IcebergCatalogConfig catalogConfig; private final String filePrefix; private final long maxBytesPerFile; + private final @Nullable Map writeProperties; WriteDirectRowsToFiles( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, String filePrefix, - long maxBytesPerFile) { + long maxBytesPerFile, + @Nullable Map writeProperties) { this.catalogConfig = catalogConfig; this.dynamicDestinations = dynamicDestinations; this.filePrefix = filePrefix; this.maxBytesPerFile = maxBytesPerFile; + this.writeProperties = writeProperties; } @Override @@ -59,41 +60,37 @@ public PCollection expand(PCollection> input) { return input.apply( ParDo.of( new WriteDirectRowsToFilesDoFn( - catalogConfig, dynamicDestinations, maxBytesPerFile, filePrefix))); + catalogConfig, dynamicDestinations, maxBytesPerFile, filePrefix, writeProperties))); } private static class WriteDirectRowsToFilesDoFn extends DoFn, FileWriteResult> { private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; - private transient @MonotonicNonNull Catalog catalog; private final String filePrefix; private final long maxFileSize; + private final @Nullable Map writeProperties; private transient @Nullable RecordWriterManager recordWriterManager; WriteDirectRowsToFilesDoFn( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, long maxFileSize, - String filePrefix) { + String filePrefix, + @Nullable Map writeProperties) { this.catalogConfig = catalogConfig; this.dynamicDestinations = dynamicDestinations; this.filePrefix = filePrefix; this.maxFileSize = maxFileSize; + this.writeProperties = writeProperties; 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, writeProperties); } @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..e74715a7eebc 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 @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.iceberg; import java.util.List; +import java.util.Map; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -30,8 +31,7 @@ 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; +import org.checkerframework.checker.nullness.qual.Nullable; class WriteGroupedRowsToFiles extends PTransform< @@ -41,16 +41,19 @@ class WriteGroupedRowsToFiles private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; private final String filePrefix; + private final @Nullable Map writeProperties; WriteGroupedRowsToFiles( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, String filePrefix, - long maxBytesPerFile) { + long maxBytesPerFile, + @Nullable Map writeProperties) { this.catalogConfig = catalogConfig; this.dynamicDestinations = dynamicDestinations; this.filePrefix = filePrefix; this.maxBytesPerFile = maxBytesPerFile; + this.writeProperties = writeProperties; } @Override @@ -59,7 +62,7 @@ public PCollection expand( return input.apply( ParDo.of( new WriteGroupedRowsToFilesDoFn( - catalogConfig, dynamicDestinations, maxBytesPerFile, filePrefix))); + catalogConfig, dynamicDestinations, maxBytesPerFile, filePrefix, writeProperties))); } private static class WriteGroupedRowsToFilesDoFn @@ -67,26 +70,21 @@ 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; + private final @Nullable Map writeProperties; WriteGroupedRowsToFilesDoFn( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, long maxFileSize, - String filePrefix) { + String filePrefix, + @Nullable Map writeProperties) { this.catalogConfig = catalogConfig; this.dynamicDestinations = dynamicDestinations; this.filePrefix = filePrefix; this.maxFileSize = maxFileSize; - } - - private org.apache.iceberg.catalog.Catalog getCatalog() { - if (catalog == null) { - this.catalog = catalogConfig.catalog(); - } - return catalog; + this.writeProperties = writeProperties; } @ProcessElement @@ -103,7 +101,8 @@ 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, writeProperties)) { 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..338a2162080b 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,13 +34,12 @@ 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.SortOrder; import org.apache.iceberg.StructLike; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Catalog; @@ -53,6 +49,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; @@ -63,14 +60,17 @@ class WritePartitionedRowsToFiles private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; private final String filePrefix; + private final @Nullable Map writeProperties; WritePartitionedRowsToFiles( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, - String filePrefix) { + String filePrefix, + @Nullable Map writeProperties) { this.catalogConfig = catalogConfig; this.dynamicDestinations = dynamicDestinations; this.filePrefix = filePrefix; + this.writeProperties = writeProperties; } @Override @@ -82,7 +82,9 @@ public PCollection expand(PCollection>> i .getElemCoder()) .getSchema(); return input.apply( - ParDo.of(new WriteDoFn(catalogConfig, dynamicDestinations, filePrefix, dataSchema))); + ParDo.of( + new WriteDoFn( + catalogConfig, dynamicDestinations, filePrefix, dataSchema, writeProperties))); } private static class WriteDoFn extends DoFn>, FileWriteResult> { @@ -91,18 +93,28 @@ 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 final @Nullable Map writeProperties; + private transient @MonotonicNonNull Map specIds; + private transient @MonotonicNonNull Map> + partitionFieldMaps; WriteDoFn( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, String filePrefix, - Schema dataSchema) { + Schema dataSchema, + @Nullable Map writeProperties) { this.catalogConfig = catalogConfig; this.dynamicDestinations = dynamicDestinations; this.filePrefix = filePrefix; this.dataSchema = dataSchema; + this.writeProperties = writeProperties; + } + + @Setup + public void setup() { + partitionFieldMaps = Maps.newHashMap(); + specIds = Maps.newHashMap(); } @ProcessElement @@ -113,9 +125,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() @@ -128,7 +141,8 @@ public void processElement( .addExtension(String.format("%s-%s", filePrefix, UUID.randomUUID())); RecordWriter writer = - new RecordWriter(table, destination.getFileFormat(), fileName, partitionData); + new RecordWriter( + table, destination.getFileFormat(), fileName, partitionData, writeProperties); try { for (Row row : element.getValue()) { Record record = IcebergUtils.beamRowToIcebergRecord(table.schema(), row); @@ -138,7 +152,8 @@ public void processElement( writer.close(); } - SerializableDataFile sdf = SerializableDataFile.from(writer.getDataFile(), partitionPath); + // Serialize against the file's own spec + SerializableDataFile sdf = SerializableDataFile.from(writer.getDataFile(), table.specs()); out.output( FileWriteResult.builder() .setTableIdentifier(destination.getTableIdentifier()) @@ -146,118 +161,86 @@ 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 = createConfig != null ? createConfig.getPartitionSpec() : PartitionSpec.unpartitioned(); + SortOrder sortOrder = + createConfig != null ? createConfig.getSortOrder() : SortOrder.unsorted(); 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); - } + // 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 + .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 lastRefreshedTable; } } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToDestinations.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToDestinations.java index bea84fc826b7..684ef350a20f 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToDestinations.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToDestinations.java @@ -59,16 +59,19 @@ class WriteToDestinations extends PTransform>, Icebe private final @Nullable Duration triggeringFrequency; private final String filePrefix; private final @Nullable Integer directWriteByteLimit; + private final @Nullable Map writeProperties; WriteToDestinations( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, @Nullable Duration triggeringFrequency, - @Nullable Integer directWriteByteLimit) { + @Nullable Integer directWriteByteLimit, + @Nullable Map writeProperties) { this.dynamicDestinations = dynamicDestinations; this.catalogConfig = catalogConfig; this.triggeringFrequency = triggeringFrequency; this.directWriteByteLimit = directWriteByteLimit; + this.writeProperties = writeProperties; // single unique prefix per write transform this.filePrefix = UUID.randomUUID().toString(); } @@ -112,7 +115,11 @@ private PCollection groupAndWriteRecords(PCollection applyUserTriggering(PCollection input) { @@ -157,7 +164,11 @@ private PCollection writeTriggeredWithBundleLifting( largeBatches.apply( "WriteDirectRowsToFiles", new WriteDirectRowsToFiles( - catalogConfig, dynamicDestinations, filePrefix, DEFAULT_MAX_BYTES_PER_FILE)); + catalogConfig, + dynamicDestinations, + filePrefix, + DEFAULT_MAX_BYTES_PER_FILE, + writeProperties)); PCollection groupedFileWrites = groupAndWriteRecords(smallBatches); @@ -189,7 +200,11 @@ private PCollection writeUntriggered(PCollection writeGroupedResult = @@ -199,7 +214,11 @@ private PCollection writeUntriggered(PCollection>, IcebergWri private final @Nullable Duration triggeringFrequency; private final String filePrefix; private final boolean autoSharding; + private final @Nullable Map writeProperties; WriteToPartitions( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, @Nullable Duration triggeringFrequency, - boolean autoSharding) { + boolean autoSharding, + @Nullable Map writeProperties) { this.dynamicDestinations = dynamicDestinations; this.catalogConfig = catalogConfig; this.triggeringFrequency = triggeringFrequency; // single unique prefix per write transform this.filePrefix = UUID.randomUUID().toString(); this.autoSharding = autoSharding; + this.writeProperties = writeProperties; } private PCollection>> groupByPartition(PCollection> input) { @@ -95,7 +99,8 @@ public IcebergWriteResult expand(PCollection> input) { PCollection writtenFiles = groupedRows.apply( - new WritePartitionedRowsToFiles(catalogConfig, dynamicDestinations, filePrefix)); + new WritePartitionedRowsToFiles( + catalogConfig, dynamicDestinations, filePrefix, writeProperties)); if (IcebergUtils.isUnbounded(input) && triggeringFrequency != null) { writtenFiles = 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..7c780e6395df 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; /** @@ -74,16 +72,19 @@ class WriteUngroupedRowsToFiles private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; private final long maxBytesPerFile; + private final @Nullable Map writeProperties; WriteUngroupedRowsToFiles( IcebergCatalogConfig catalogConfig, DynamicDestinations dynamicDestinations, String filePrefix, - long maxBytesPerFile) { + long maxBytesPerFile, + @Nullable Map writeProperties) { this.catalogConfig = catalogConfig; this.dynamicDestinations = dynamicDestinations; this.filePrefix = filePrefix; this.maxBytesPerFile = maxBytesPerFile; + this.writeProperties = writeProperties; } @Override @@ -97,7 +98,8 @@ public Result expand(PCollection> input) { dynamicDestinations, filePrefix, DEFAULT_MAX_WRITERS_PER_BUNDLE, - maxBytesPerFile)) + maxBytesPerFile, + writeProperties)) .withOutputTags( WRITTEN_FILES_TAG, TupleTagList.of(ImmutableList.of(WRITTEN_ROWS_TAG, SPILLED_ROWS_TAG)))); @@ -193,7 +195,7 @@ private static class WriteUngroupedRowsToFilesDoFn private final long maxFileSize; private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; - private transient @MonotonicNonNull Catalog catalog; + private final @Nullable Map writeProperties; private transient @Nullable RecordWriterManager recordWriterManager; private int spilledShardNumber; @@ -202,25 +204,21 @@ public WriteUngroupedRowsToFilesDoFn( DynamicDestinations dynamicDestinations, String filename, int maximumWritersPerBundle, - long maxFileSize) { + long maxFileSize, + @Nullable Map writeProperties) { this.catalogConfig = catalogConfig; this.dynamicDestinations = dynamicDestinations; this.filename = filename; this.maxWritersPerBundle = maximumWritersPerBundle; this.maxFileSize = maxFileSize; - } - - private org.apache.iceberg.catalog.Catalog getCatalog() { - if (catalog == null) { - this.catalog = catalogConfig.catalog(); - } - return catalog; + this.writeProperties = writeProperties; } @StartBundle public void startBundle() { recordWriterManager = - new RecordWriterManager(getCatalog(), filename, maxFileSize, maxWritersPerBundle); + new RecordWriterManager( + catalogConfig, filename, maxFileSize, maxWritersPerBundle, writeProperties); this.spilledShardNumber = ThreadLocalRandom.current().nextInt(SPILLED_RECORD_SHARDING_FACTOR); } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ApplyWatermarkColumn.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ApplyWatermarkColumn.java new file mode 100644 index 000000000000..0b312904fe26 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ApplyWatermarkColumn.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 java.util.concurrent.TimeUnit.MICROSECONDS; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.util.Preconditions; +import org.apache.beam.sdk.values.Row; +import org.apache.iceberg.util.DateTimeUtil; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.joda.time.Instant; + +/** + * Re-stamps each output row using the configured {@code watermarkColumn}'s value, so the source's + * output watermark advances per record rather than per snapshot. + * + *

If the configured column's value on a record is null or missing, this DoFn is a pass-through, + * preserving the snapshot commit timestamp. + * + *

The {@link #getAllowedTimestampSkew()} return is intentionally generous — the user's watermark + * column may produce values well before the snapshot commit time (event-time data can lag + * wall-clock by hours or days). Restricting the skew here would force the source to drop legitimate + * output. + */ +class ApplyWatermarkColumn extends DoFn { + private final String watermarkColumn; + private final TimeUnit timeUnit; + + ApplyWatermarkColumn(String watermarkColumn, @Nullable String timeUnit) { + this.watermarkColumn = watermarkColumn; + this.timeUnit = timeUnit != null ? TimeUnit.valueOf(timeUnit.toUpperCase()) : MICROSECONDS; + } + + @ProcessElement + public void process(@Element Row row, OutputReceiver out) { + @Nullable + Instant instant = + getInstant(row.getValue(watermarkColumn), row.getSchema().getField(watermarkColumn)); + if (instant != null) { + out.outputWithTimestamp(row, instant); + } else { + out.output(row); + } + } + + private @Nullable Instant getInstant(@Nullable Object value, Schema.Field field) { + if (value == null) { + return null; + } + switch (field.getType().getTypeName()) { + case INT64: + return Instant.ofEpochMilli(timeUnit.toMillis((Long) value)); + case DATETIME: + return (Instant) value; + case LOGICAL_TYPE: + String logicalType = + Preconditions.checkStateNotNull(field.getType().getLogicalType()).getIdentifier(); + if (logicalType.equals(SqlTypes.DATETIME.getIdentifier())) { + return Instant.ofEpochMilli( + MICROSECONDS.toMillis(DateTimeUtil.microsFromTimestamp((LocalDateTime) value))); + } else if (logicalType.equals(SqlTypes.TIMESTAMP.getIdentifier()) + || logicalType.equals(org.apache.beam.sdk.schemas.logicaltypes.Timestamp.IDENTIFIER)) { + return Instant.ofEpochMilli( + MICROSECONDS.toMillis(DateTimeUtil.microsFromInstant((java.time.Instant) value))); + } else { + throw new UnsupportedOperationException("Unexpected logical type: " + logicalType); + } + default: + throw new UnsupportedOperationException("Unexpected Beam type: " + field.getType()); + } + } + + @Override + public Duration getAllowedTimestampSkew() { + // Generous skew to cover backfill of historical data and late-arriving CDC patterns. + return Duration.standardDays(365); + } +} 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..a421b3af276c --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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(); + } + + static Row outputRow( + List metadataColumns, + Schema outputSchema, + ChangelogDescriptor descriptor, + ValueKind valueKind, + Row dataAndRowMetadata) { + return outputRow( + metadataColumns, + outputSchema, + descriptor.getCommitSnapshotId(), + descriptor.getSnapshotSequenceNumber(), + valueKind, + dataAndRowMetadata); + } + + /** + * 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 snapshotSequenceNumber, + 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, + snapshotSequenceNumber, + valueKind, + dataAndRowMetadata)); + } + return Row.withSchema(outputSchema).addValues(values).build(); + } + + static Schema readBeamSchemaWithRowMetadata( + List metadataColumns, + org.apache.iceberg.Schema dataSchema, + @Nullable String updateCompatibilityVersion) { + return IcebergUtils.icebergSchemaToBeamSchema( + readSchemaWithRowMetadata(metadataColumns, dataSchema), updateCompatibilityVersion); + } + + 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..b8c18d1a4e53 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtils.java @@ -0,0 +1,700 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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); + // Heuristic for estimating the decoded byte size of a compressed file + static final int COMPRESSED_TO_DECODED_BYTES_ESTIMATE = 4; + + /** + * 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().getDataSequenceNumber(), + 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/CdcResolver.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcResolver.java new file mode 100644 index 000000000000..284c10abe44b --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcResolver.java @@ -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. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.ValueKind; +import org.apache.iceberg.data.Record; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Helper class to reconcile CDC rows. Used by {@link ResolveChanges} (with Beam {@link Row}s) and + * {@link LocalResolveDoFn} (with Iceberg {@link Record}s). + * + *

For rows that share a given Primary Key, we determine the output ValueKind as follows: + * + *

    + *
  • (delete, insert) pairs become {@code UPDATE_BEFORE} + {@code UPDATE_AFTER} + *
  • singletons remain {@code DELETE} or {@code INSERT} + *
  • matching delete+insert with identical non-PK fields are considered Copy-on-Write side + * effects and are dropped + *
+ * + *

General implementation: + * + *

    + *
  1. Hash-index inserts by their non-PK field hash, for efficient Copy-on-Write detection. + *
  2. Skip matching (delete, insert) pairs with identical non-PK columns. A CoW operation deletes + * and rewrites the whole file (minus some records that are actually marked for deletion). + * Unchanged records are no-ops and should not be mistaken for updates. + *
  3. Walk the remaining deletes and inserts, emitting matched pairs as {@link + * ValueKind#UPDATE_BEFORE} / {@link ValueKind#UPDATE_AFTER}. + *
  4. Emit any unmatched extras as {@link ValueKind#DELETE} / {@link ValueKind#INSERT}. + *
+ * + *

Duplicate identifier values

+ * + *

Iceberg does not enforce PK uniqueness, so a single PK group may contain more than one delete + * and/or insert (although it would be unusual). In the normal case, identifier values are unique + * and a snapshot contributes at most one delete and one insert per PK. + * + *

If duplicates are encountered, we do not fail. Instead, we pair off the deletes and inserts, + * and any leftovers are emitted as plain {@code DELETE} / {@code INSERT}. The pairing is + * necessarily arbitrary as Iceberg only keeps track of commit-level sequencing. We do not have + * further insight within a commit to determine record ordering. To produce deterministic outputs in + * the duplicate case, both sides are ordered by {@link #nonPkHash} before pairing. + */ +abstract class CdcResolver { + /** Hashes the non-PK fields of an element. Used as the index for O(n+m) CoW deduplication. */ + protected abstract int nonPkHash(T element); + + /** + * Returns true if two records (already known to share a PK) share identical non-PK fields. Called + * only when the two elements collide in the {@link #nonPkHash} index, so the implementation can + * stay simple (linear scan of non-PK fields). + */ + protected abstract boolean nonPkEquals(T delete, T insert); + + /** + * Resolves a Primary Key group of deletes and inserts. Caller provides {@code emit} which decides + * how to materialize each output. + * + *

The dominant case (unique identifier values) is exactly one delete and one insert, decided + * directly by a single {@link #nonPkEquals} with no hashing. In the rare case of duplicate PKs + * within a snapshot, one side may hold more than one record. When this happens, we re-order the + * lists by {@link #nonPkHash} so the result is deterministic. + */ + final void resolve(List deletes, List inserts, BiConsumer emit) { + if (deletes.size() == 1 && inserts.size() == 1) { + // No-op if non-PK fields are equal, otherwise we emit an update pair + T delete = deletes.get(0); + T insert = inserts.get(0); + if (!nonPkEquals(delete, insert)) { + emit.accept(ValueKind.UPDATE_BEFORE, delete); + emit.accept(ValueKind.UPDATE_AFTER, insert); + } + return; + } + + if (deletes.size() > 1 || inserts.size() > 1) { + resolveOrdered(sortedByNonPkHash(deletes), sortedByNonPkHash(inserts), emit); + } else { + resolveOrdered(deletes, inserts, emit); + } + } + + private List sortedByNonPkHash(List records) { + List sorted = new ArrayList<>(records); + sorted.sort(Comparator.comparingInt(this::nonPkHash)); + return sorted; + } + + private void resolveOrdered(List deletes, List inserts, BiConsumer emit) { + boolean hasDeletes = !deletes.isEmpty(); + boolean hasInserts = !inserts.isEmpty(); + + if (hasInserts && hasDeletes) { + // First, check if any (delete, insert) pairs are duplicates that should not be + // included in the output + boolean[] dupDeletes = new boolean[deletes.size()]; + boolean[] dupInserts = new boolean[inserts.size()]; + + // Map hash to insert-indices + Map> insertHashToIdx = new HashMap<>(); + for (int insertIdx = 0; insertIdx < inserts.size(); insertIdx++) { + int insertHash = nonPkHash(inserts.get(insertIdx)); + insertHashToIdx.computeIfAbsent(insertHash, k -> new ArrayList<>()).add(insertIdx); + } + for (int deleteIdx = 0; deleteIdx < deletes.size(); deleteIdx++) { + int deleteHash = nonPkHash(deletes.get(deleteIdx)); + @Nullable List candidates = insertHashToIdx.get(deleteHash); + if (candidates != null) { + // check if candidates are just duplicates (e.g. from CoW) + for (int idx = 0; idx < candidates.size(); idx++) { + int insertIdx = candidates.get(idx); + if (!dupInserts[insertIdx] + && nonPkEquals(deletes.get(deleteIdx), inserts.get(insertIdx))) { + // this (delete, insert) pair is a duplicate --> should be skipped + dupDeletes[deleteIdx] = true; + dupInserts[insertIdx] = true; + candidates.remove(idx); + break; + } + } + } + } + + // Emit matched pairs as UPDATE_BEFORE / UPDATE_AFTER. + int d = 0; + int i = 0; + while (d < deletes.size() && i < inserts.size()) { + // skip duplicates + while (d < deletes.size() && dupDeletes[d]) { + d++; + } + while (i < inserts.size() && dupInserts[i]) { + i++; + } + + if (d < deletes.size() && i < inserts.size()) { + emit.accept(ValueKind.UPDATE_BEFORE, deletes.get(d)); + emit.accept(ValueKind.UPDATE_AFTER, inserts.get(i)); + d++; + i++; + } + } + + // emit unmatched extras as DELETE / INSERT. + while (d < deletes.size()) { + if (!dupDeletes[d]) { + emit.accept(ValueKind.DELETE, deletes.get(d)); + } + d++; + } + while (i < inserts.size()) { + if (!dupInserts[i]) { + emit.accept(ValueKind.INSERT, inserts.get(i)); + } + i++; + } + } else if (hasInserts) { + for (T r : inserts) { + emit.accept(ValueKind.INSERT, r); + } + } else if (hasDeletes) { + for (T r : deletes) { + emit.accept(ValueKind.DELETE, r); + } + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcRowDescriptor.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcRowDescriptor.java new file mode 100644 index 000000000000..3bf3e8a619fd --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcRowDescriptor.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.auto.value.AutoValue; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaCoder; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptor; + +/** + * Shuffle key for bidirectional CDC rows. + * + *

The primary key isolates rows for update resolution. The snapshot sequence number and commit + * snapshot id carry commit-sourced metadata through {@link ResolveChanges}, where they can be + * appended to final output rows if requested. + */ +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class CdcRowDescriptor { + @SuppressWarnings("nullness") + public static SchemaCoder coder(Schema identifierSchema) { + Schema descriptorSchema = + Schema.builder() + .addInt64Field("snapshotSequenceNumber") + .addInt64Field("commitSnapshotId") + .addRowField("primaryKey", identifierSchema) + .build(); + + return SchemaCoder.of( + descriptorSchema, + TypeDescriptor.of(CdcRowDescriptor.class), + descriptor -> + Row.withSchema(descriptorSchema) + .addValues( + descriptor.getSnapshotSequenceNumber(), + descriptor.getCommitSnapshotId(), + descriptor.getPrimaryKey()) + .build(), + row -> + CdcRowDescriptor.builder() + .setSnapshotSequenceNumber(row.getInt64("snapshotSequenceNumber")) + .setCommitSnapshotId(row.getInt64("commitSnapshotId")) + .setPrimaryKey(row.getRow("primaryKey")) + .build()); + } + + public static Builder builder() { + return new AutoValue_CdcRowDescriptor.Builder(); + } + + @SchemaFieldNumber("0") + public abstract long getSnapshotSequenceNumber(); + + @SchemaFieldNumber("1") + public abstract long getCommitSnapshotId(); + + @SchemaFieldNumber("2") + public abstract Row getPrimaryKey(); + + @AutoValue.Builder + public abstract static class Builder { + abstract Builder setSnapshotSequenceNumber(long sequenceNumber); + + abstract Builder setCommitSnapshotId(long snapshotId); + + abstract Builder setPrimaryKey(Row primaryKey); + + abstract CdcRowDescriptor build(); + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogDescriptor.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogDescriptor.java new file mode 100644 index 000000000000..10291fe63681 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogDescriptor.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 com.google.auto.value.AutoValue; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaCoder; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptor; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Descriptor for a set of {@link SerializableChangelogTask}s. + * + *

This carries commit-sourced metadata for all rows produced from the task group. These values + * are not read from data files; they are appended to final CDC output rows by {@link + * CdcOutputUtils#outputRow} when requested. + */ +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class ChangelogDescriptor { + public static Builder builder() { + return new AutoValue_ChangelogDescriptor.Builder(); + } + + @SuppressWarnings("nullness") + public static SchemaCoder coder(Schema overlapSchema) { + Schema descriptorSchema = + Schema.builder() + .addStringField("tableIdentifierString") + .addInt64Field("snapshotSequenceNumber") + .addInt64Field("commitSnapshotId") + .addNullableField("overlapLower", Schema.FieldType.row(overlapSchema)) + .addNullableField("overlapUpper", Schema.FieldType.row(overlapSchema)) + .build(); + + return SchemaCoder.of( + descriptorSchema, + TypeDescriptor.of(ChangelogDescriptor.class), + descriptor -> + Row.withSchema(descriptorSchema) + .addValues( + descriptor.getTableIdentifierString(), + descriptor.getSnapshotSequenceNumber(), + descriptor.getCommitSnapshotId(), + descriptor.getOverlapLower(), + descriptor.getOverlapUpper()) + .build(), + row -> + ChangelogDescriptor.builder() + .setTableIdentifierString(row.getString("tableIdentifierString")) + .setSnapshotSequenceNumber(row.getInt64("snapshotSequenceNumber")) + .setCommitSnapshotId(row.getInt64("commitSnapshotId")) + .setOverlapLower(row.getRow("overlapLower")) + .setOverlapUpper(row.getRow("overlapUpper")) + .build()); + } + + @SchemaFieldNumber("0") + public abstract String getTableIdentifierString(); + + @SchemaFieldNumber("1") + public abstract long getSnapshotSequenceNumber(); + + @SchemaFieldNumber("2") + public abstract long getCommitSnapshotId(); + + @SchemaFieldNumber("3") + public abstract @Nullable Row getOverlapLower(); + + @SchemaFieldNumber("4") + public abstract @Nullable Row getOverlapUpper(); + + @AutoValue.Builder + public abstract static class Builder { + abstract Builder setTableIdentifierString(String table); + + abstract Builder setSnapshotSequenceNumber(long sequenceNumber); + + abstract Builder setCommitSnapshotId(long snapshotId); + + abstract Builder setOverlapLower(@Nullable Row overlapLower); + + abstract Builder setOverlapUpper(@Nullable Row overlapUpper); + + abstract ChangelogDescriptor build(); + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogScanner.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogScanner.java new file mode 100644 index 000000000000..979d2f973081 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogScanner.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.beam.sdk.io.iceberg.cdc; + +import static java.lang.String.format; +import static org.apache.beam.sdk.io.iceberg.cdc.CdcReadUtils.COMPRESSED_TO_DECODED_BYTES_ESTIMATE; +import static org.apache.beam.sdk.io.iceberg.cdc.SerializableChangelogTask.Type.ADDED_ROWS; +import static org.apache.beam.sdk.io.iceberg.cdc.SerializableChangelogTask.getDataFile; +import static org.apache.beam.sdk.io.iceberg.cdc.SerializableChangelogTask.getLength; +import static org.apache.beam.sdk.io.iceberg.cdc.SerializableChangelogTask.getPartition; +import static org.apache.beam.sdk.io.iceberg.cdc.SerializableChangelogTask.getSpec; +import static org.apache.beam.sdk.io.iceberg.cdc.SerializableChangelogTask.getType; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +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.coders.KvCoder; +import org.apache.beam.sdk.coders.ListCoder; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.TableCache; +import org.apache.beam.sdk.metrics.Counter; +import org.apache.beam.sdk.metrics.Metrics; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.Row; +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.iceberg.AddedRowsScanTask; +import org.apache.iceberg.BeamBaseIncrementalChangelogScan; +import org.apache.iceberg.ChangelogScanTask; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataOperations; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.DeletedDataFileScanTask; +import org.apache.iceberg.DeletedRowsScanTask; +import org.apache.iceberg.IncrementalChangelogScan; +import org.apache.iceberg.MetricsConfig; +import org.apache.iceberg.MetricsModes; +import org.apache.iceberg.PartitionField; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.ScanTaskGroup; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.TableProperties; +import org.apache.iceberg.data.GenericRecord; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.types.Conversions; +import org.apache.iceberg.types.Type; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.PropertyUtil; +import org.apache.iceberg.util.StructLikeMap; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * DoFn that takes incoming Iceberg snapshots and scans them for changelogs using Iceberg's {@link + * IncrementalChangelogScan}. Changelog tasks are organized into batches and routed to different + * downstream PCollections based on complexity. + * + *

The Iceberg scan generates batches of changelog scan tasks, each of size {@link + * TableProperties#SPLIT_SIZE}. This can be configured with the table's read.split.target-size + * property. + * + *

This DoFn analyzes the nature of changes within the snapshot, partition, and file level, then + * routes the changes accordingly: + * + *

    + *
  1. Unidirectional (Fast Path): If an isolated level contains only inserts OR only + * deletes, its tasks are emitted to {@link #UNIDIRECTIONAL_TASKS}. These records bypass + * the CoGBK shuffle and are output immediately. + *
  2. Small Bidirectional (Medium Path): If an isolated level contains a mix of inserts + * and deletes, and is small enough, its tasks are emitted to {@link + * #SMALL_BIDIRECTIONAL_TASKS}. These records are resolved in memory to identify potential + * updates. Task groups are considered small enough if the estimated overlap region is within + * {@link TableProperties#SPLIT_SIZE}. + *
  3. Bidirectional (Slow Path): If an isolated level contains a mix of inserts and + * deletes, and is too large, its tasks are emitted to {@link #LARGE_BIDIRECTIONAL_TASKS}. + * These records are grouped by Primary Key and processed by {@link ResolveChanges} to + * identify potential updates. + *
+ * + *

Optimizing by Shuffling Less Data

+ * + *

We take a three-layered approach to identify data that can bypass the expensive downstream + * CoGroupByKey shuffle: + * + *

Snapshots

+ * + * We start by analyzing the nature of changes at the snapshot level. If a snapshot's operation is + * not of type {@link DataOperations#OVERWRITE}, then it's a uni-directional change. + * + *

Pinned Partitions

+ * + *

If the table's partition fields are derived entirely from Primary Key fields, we know that a + * record will not migrate between partitions. This narrows down the isolated level and allows us to + * only check for bi-directional changes within a partition. Doing this will allow partitions + * with uni-directional changes to bypass the expensive CoGBK shuffle. It also gives partitions with + * small bi-directional changes a chance to be processed in-memory instead of needing to pass + * through the CoGBK. + * + *

Optimization for Individual Files

+ * + * When we have narrowed down our group of tasks with bi-directional changes, we start analyzing the + * metadata of their underlying files. We compare the upper and lower bounds of Partition Keys + * relevant to each file, and consider any overlaps as potentially containing an update. If a given + * task's Primary Key bounds has no overlap with any opposing task's Primary Key bounds, then we + * know it's not possible to create an (insert, delete) pair with it. Such a task can safely bypass + * the shuffle. + * + *

Note: "opposing" refers to a change that happens in the opposite direction (e.g. insert is + * "positive", delete is "negative") + * + *

For example, say we have a group of tasks: + * + *

    + *
  1. Task A (adds rows): bounds [3, 8] + *
  2. Task B (adds rows): bounds [2, 4] + *
  3. Task C (deletes rows): bounds [1, 5] + *
  4. Task D (adds rows): bounds [6, 12] + *
+ * + *

Tasks A and B add rows, and overlap with Task C which deletes row. We need to resolve the rows + * in these 3 tasks because they might all contain (insert, delete) pairs that lead to an update. + * + *

Task D however, does not overlap with any delete rows. It will never produce an (insert, + * delete) pair, so we can directly emit it without resolving its output rows. + */ +class ChangelogScanner + extends DoFn>> { + private static final Logger LOG = LoggerFactory.getLogger(ChangelogScanner.class); + private static final Counter totalChangelogScanTasks = + Metrics.counter(ChangelogScanner.class, "totalChangelogScanTasks"); + private static final Counter numAddedRowsScanTasks = + Metrics.counter(ChangelogScanner.class, "numAddedRowsScanTasks"); + private static final Counter numDeletedRowsScanTasks = + Metrics.counter(ChangelogScanner.class, "numDeletedRowsScanTasks"); + private static final Counter numDeletedDataFileScanTasks = + Metrics.counter(ChangelogScanner.class, "numDeletedDataFileScanTasks"); + private static final Counter numUniDirectionalTasks = + Metrics.counter(ChangelogScanner.class, "numUniDirectionalTasks"); + private static final Counter numLargeBiDirectionalTasks = + Metrics.counter(ChangelogScanner.class, "numLargeBiDirectionalTasks"); + private static final Counter numSmallBiDirectionalTasks = + Metrics.counter(ChangelogScanner.class, "numSmallBiDirectionalTasks"); + static final TupleTag>> + UNIDIRECTIONAL_TASKS = new TupleTag<>(); + static final TupleTag>> + SMALL_BIDIRECTIONAL_TASKS = new TupleTag<>(); + static final TupleTag>> + LARGE_BIDIRECTIONAL_TASKS = new TupleTag<>(); + + private final IcebergScanConfig scanConfig; + private boolean canDoPartitionOptimization = false; + // for metrics + private int numAddedRowsTasks = 0; + private int numDeletedRowsTasks = 0; + private int numDeletedFileTasks = 0; + private int numUniDirTasks = 0; + private int numSmallBiDirTasks = 0; + private int numLargeBiDirTasks = 0; + private int numUniDirSplits = 0; + private int numSmallBiDirSplits = 0; + private int numLargeBiDirSplits = 0; + + ChangelogScanner(IcebergScanConfig scanConfig) { + this.scanConfig = scanConfig; + } + + static KvCoder> coder( + org.apache.beam.sdk.schemas.Schema rowIdBeamSchema) { + return KvCoder.of( + ChangelogDescriptor.coder(rowIdBeamSchema), + ListCoder.of(SerializableChangelogTask.coder())); + } + + @ProcessElement + public void process(@Element Long snapshotId, MultiOutputReceiver out) throws IOException { + resetLocalMetrics(); + // upstream Watch should have already refreshed the table + Table table = + TableCache.getAndRefreshIfStale( + scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); + @Nullable Snapshot snapshot = table.snapshot(snapshotId); + + // refresh anyway on miss + if (snapshot == null) { + table = + TableCache.getRefreshed(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); + snapshot = + checkStateNotNull( + table.snapshot(snapshotId), "Could not retrieve table snapshot: %s", snapshotId); + } + + @Nullable Long fromSnapshotId = snapshot.parentId(); + @Nullable Expression filter = scanConfig.getFilter(); + + // TODO(ahmedabu98): replace this with table.newIncrementalChangelogScan() when + // https://github.com/apache/iceberg/pull/14264/ gets merged and released. + IncrementalChangelogScan scan = + new BeamBaseIncrementalChangelogScan(table) + .toSnapshot(snapshotId) + .project(scanConfig.getProjectedSchema()); + if (fromSnapshotId != null) { + scan = scan.fromSnapshotExclusive(fromSnapshotId); + } + if (filter != null) { + scan = scan.filter(filter); + } + + // configure the scan to store upper/lower bound metrics only + // if it's available for primary key fields + if (metricsAvailableForIdentifierFields(table)) { + scan = scan.includeColumnStats(table.schema().identifierFieldNames()); + } + + createAndOutputReadTasks(table, snapshot, scan, out); + } + + private boolean metricsAvailableForIdentifierFields(Table table) { + MetricsConfig metricsConfig = MetricsConfig.forTable(table); + Collection pkFields = table.schema().identifierFieldNames(); + for (String field : pkFields) { + MetricsModes.MetricsMode mode = metricsConfig.columnMode(field); + if (!(mode instanceof MetricsModes.Full) && !(mode instanceof MetricsModes.Truncate)) { + return false; + } + } + return true; + } + + @SuppressWarnings("Slf4jFormatShouldBeConst") + private void createAndOutputReadTasks( + Table table, + Snapshot snapshot, + IncrementalChangelogScan scan, + MultiOutputReceiver multiOutputReceiver) + throws IOException { + + // ******** Partition Optimization ******** + // Determine which partition specs "pin" records to their partition + // (i.e. partition fields are sourced entirely from a record's PK). + // If records are pinned, we can optimize by only shuffling bi-directional changes + // *within* a partition, since no cross-partition changes will occur. + Set pinnedSpecs = + table.specs().entrySet().stream() + .filter(e -> doesSpecPinRecordsToPartition(e.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + boolean tableHasPinnedSpecs = !pinnedSpecs.isEmpty(); + + // The optimization cannot apply if any file in this snapshot uses an unpinned spec + boolean snapshotHasUnpinnedSpec = false; + Set specsInSnapshot = new HashSet<>(); + ChangeTypesInPartition changeTypesInPartition = new ChangeTypesInPartition(); + + // Buffer tasks from OVERWRITE snapshots, because they are potentially bi-directional + OverwriteTasks overwriteTasks = new OverwriteTasks(); + + // batcher for uni-directional tasks, which can be directly emitted when splitSize is reached + TaskBatcher uniBatcher = + new TaskBatcher( + scanConfig.getTableIdentifier(), + snapshot.timestampMillis(), + splitSize(table), + multiOutputReceiver.get(UNIDIRECTIONAL_TASKS)); + + // === collect partition metadata and route/buffer tasks === + LOG.info( + "Planning to scan snapshot {} (seq: {})", snapshot.snapshotId(), snapshot.sequenceNumber()); + try (CloseableIterable> scanTaskGroups = scan.planTasks()) { + for (ScanTaskGroup scanTaskGroup : scanTaskGroups) { + for (ChangelogScanTask task : scanTaskGroup.tasks()) { + SerializableChangelogTask.Type type = getType(task); + StructLike partition = getPartition(task); + PartitionSpec spec = getSpec(task); + gatherTaskTypeMetrics(type); + + // Collect partition metadata for pinned-spec optimization + if (tableHasPinnedSpecs) { + if (!pinnedSpecs.contains(spec.specId())) { + snapshotHasUnpinnedSpec = true; + } else { + changeTypesInPartition.add(spec, partition, type); + specsInSnapshot.add(spec.specId()); + } + } + + // non-overwrite tasks are always unidirectional (the scan planner + // skips REPLACE ops). + if (!DataOperations.OVERWRITE.equals(snapshot.operation())) { + uniBatcher.add(makeTask(task, table), snapshot.sequenceNumber(), getLength(task)); + numUniDirTasks++; + continue; + } + + // Overwrite tasks need further analysis — buffer for post-loop processing + overwriteTasks.add(spec, partition, task); + } + } + } + // a snapshot using multiple specs is also not safe for the partition optimization, + // unless we account for the spec ID in the file-to-file comparison, which complicates things + canDoPartitionOptimization = + tableHasPinnedSpecs && !snapshotHasUnpinnedSpec && specsInSnapshot.size() <= 1; + + // === analyze buffered overwrite tasks using the partition metadata === + processOverwriteTasks( + table, snapshot, overwriteTasks, changeTypesInPartition, uniBatcher, multiOutputReceiver); + uniBatcher.flush(); + + numUniDirSplits = uniBatcher.totalSplits; + int totalTasks = numAddedRowsTasks + numDeletedRowsTasks + numDeletedFileTasks; + updateTaskCounters(); + + LOG.info(scanResultMessage(snapshot, totalTasks)); + } + + private void processOverwriteTasks( + Table table, + Snapshot snapshot, + OverwriteTasks overwriteTasks, + ChangeTypesInPartition changeTypesInPartition, + TaskBatcher uniBatcher, + MultiOutputReceiver multiOutputReceiver) { + if (overwriteTasks.isEmpty()) { + return; + } + boolean metricsAreAvailable = metricsAvailableForIdentifierFields(table); + TaskBatcher largeBiBatcher = + new TaskBatcher( + scanConfig.getTableIdentifier(), + snapshot.timestampMillis(), + splitSize(table), + multiOutputReceiver.get(LARGE_BIDIRECTIONAL_TASKS)); + + if (!canDoPartitionOptimization) { + // Records are not pinned to partition (or no pinned specs at all). + // We need to compare underlying files across the whole snapshot. + List tasks = overwriteTasks.allTasks(); + + AnalysisResult result = + analyzeFiles( + metricsAreAvailable, + tasks, + scanConfig.recordIdSchema(), + scanConfig.recordIdComparator()); + + uniBatcher.add(result.unidirectional, snapshot.sequenceNumber(), table); + numUniDirTasks += result.unidirectional.size(); + + routeBidirectional(table, snapshot, result, largeBiBatcher, multiOutputReceiver); + } else { + // Records are pinned to partition. + // Narrow down by comparing the files within each partition independently. + for (Map.Entry>> tasksPerSpec : + overwriteTasks.tasks.entrySet()) { + int specId = tasksPerSpec.getKey(); + for (Map.Entry> tasksInPartition : + tasksPerSpec.getValue().entrySet()) { + StructLike partition = tasksInPartition.getKey(); + @Nullable + Set partitionChangeTypes = + changeTypesInPartition.typesFor(specId, partition); + + // If this partition has only uni-directional changes, output to UNIDIRECTIONAL and bypass + // file analysis + if (partitionChangeTypes != null && !containsBiDirectionalChanges(partitionChangeTypes)) { + uniBatcher.add(tasksInPartition.getValue(), snapshot.sequenceNumber(), table); + numUniDirTasks += tasksInPartition.getValue().size(); + continue; + } + + // Partition has bi-directional changes — analyze file-level overlaps + AnalysisResult result = + analyzeFiles( + metricsAreAvailable, + tasksInPartition.getValue(), + scanConfig.recordIdSchema(), + scanConfig.recordIdComparator()); + + uniBatcher.add(result.unidirectional, snapshot.sequenceNumber(), table); + routeBidirectional(table, snapshot, result, largeBiBatcher, multiOutputReceiver); + + // metrics + numUniDirTasks += result.unidirectional.size(); + numLargeBiDirTasks += result.bidirectional.size(); + } + } + } + largeBiBatcher.flush(); + numLargeBiDirSplits = largeBiBatcher.totalSplits; + } + + /** + * Helper class for storing + processing {@link ChangelogScanTask}s organized by partition and + * spec ID. + */ + static class OverwriteTasks { + Map>> tasks = new HashMap<>(); + + void add(PartitionSpec spec, StructLike partition, ChangelogScanTask task) { + tasks + .computeIfAbsent(spec.specId(), id -> StructLikeMap.create(spec.partitionType())) + .computeIfAbsent(partition, p -> new ArrayList<>()) + .add(task); + } + + boolean isEmpty() { + return tasks.isEmpty(); + } + + List allTasks() { + return tasks.values().stream() + .flatMap(taskMap -> taskMap.values().stream()) + .flatMap(List::stream) + .collect(Collectors.toList()); + } + } + + /** + * Helper class for identifying types of {@link ChangelogScanTask} per spec ID and partition. This + * is used to determine whether this snapshot is eligible for partition optimization. + */ + static class ChangeTypesInPartition { + Map>> changeTypesPerPartition = + new HashMap<>(); + + void add(PartitionSpec spec, StructLike partition, SerializableChangelogTask.Type type) { + changeTypesPerPartition + .computeIfAbsent(spec.specId(), id -> StructLikeMap.create(spec.partitionType())) + .computeIfAbsent(partition, p -> new HashSet<>()) + .add(type); + } + + @Nullable + Set typesFor(Integer specId, StructLike partition) { + if (!changeTypesPerPartition.containsKey(specId)) { + return null; + } + return checkStateNotNull(changeTypesPerPartition.get(specId)).get(partition); + } + } + + /** Checks if a set of change types include both inserts and deletes. */ + private static boolean containsBiDirectionalChanges( + Set changeTypes) { + return changeTypes.contains(ADDED_ROWS) && changeTypes.size() > 1; + } + + /** Helper class for analyzing overlaps between opposing tasks. */ + static class AnalysisResult { + final List unidirectional; + final List bidirectional; + final @Nullable StructLike overlapLower; + final @Nullable StructLike overlapUpper; + + AnalysisResult( + List unidirectional, + List bidirectional, + @Nullable StructLike overlapLower, + @Nullable StructLike overlapUpper) { + this.unidirectional = unidirectional; + this.bidirectional = bidirectional; + this.overlapLower = overlapLower; + this.overlapUpper = overlapUpper; + } + + @Nullable + Row overlapLowerRow(org.apache.beam.sdk.schemas.Schema idSchema) { + return this.overlapLower == null + ? null + : IcebergUtils.icebergRecordToBeamRow(idSchema, (Record) this.overlapLower); + } + + @Nullable + Row overlapUpperRow(org.apache.beam.sdk.schemas.Schema idSchema) { + return this.overlapUpper == null + ? null + : IcebergUtils.icebergRecordToBeamRow(idSchema, (Record) this.overlapUpper); + } + + static AnalysisResult allBidirectional(List tasks) { + return new AnalysisResult(Collections.emptyList(), tasks, null, null); + } + } + + /** + * Analyzes all tasks in the given list by comparing the bounds of each task's underlying files. + * If a task's partition key bounds overlap with an opposing task's partition key bounds, they are + * both considered bi-directional changes. If a task's bounds do not overlap with any opposing + * task's bounds, it is considered a uni-directional change. + */ + static AnalysisResult analyzeFiles( + boolean metricsAreAvailable, + List tasks, + Schema recIdSchema, + Comparator idComp) { + // if table doesn't keep track of metrics, we need to play it safe and consider all tasks may + // overlap. + if (!metricsAreAvailable) { + return AnalysisResult.allBidirectional(tasks); + } + + List insertTasks = new ArrayList<>(); + List deleteTasks = new ArrayList<>(); + + try { + for (ChangelogScanTask task : tasks) { + if (task instanceof AddedRowsScanTask) { + insertTasks.add(TaskAndBounds.of(task, recIdSchema, idComp)); + } else if (task instanceof DeletedDataFileScanTask || task instanceof DeletedRowsScanTask) { + deleteTasks.add(TaskAndBounds.of(task, recIdSchema, idComp)); + } else { + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + } + } catch (TaskAndBounds.NoBoundMetricsException e) { + // if metrics are not available for some files, we should also play it safe. + return AnalysisResult.allBidirectional(tasks); + } + + if (!insertTasks.isEmpty() && !deleteTasks.isEmpty()) { + Comparator lowerBoundComp = (t1, t2) -> idComp.compare(t1.lowerId, t2.lowerId); + Comparator upperBoundComp = (t1, t2) -> idComp.compare(t1.upperId, t2.upperId); + + insertTasks.sort(lowerBoundComp); + deleteTasks.sort(lowerBoundComp); + + TaskAndBounds firstInsert = insertTasks.get(0); + TaskAndBounds firstDelete = deleteTasks.get(0); + TaskAndBounds lastInsert = insertTasks.stream().max(upperBoundComp).orElseThrow(); + TaskAndBounds lastDelete = deleteTasks.stream().max(upperBoundComp).orElseThrow(); + + boolean overlapExists = + idComp.compare(firstDelete.lowerId, lastInsert.upperId) <= 0 + && idComp.compare(firstInsert.lowerId, lastDelete.upperId) <= 0; + + if (overlapExists) { + // Iterate through inserts and only check relevant deletes + for (TaskAndBounds insert : insertTasks) { + // First check if the insert task overlaps with the global delete window. + // If not, we can just skip it. + if (idComp.compare(insert.upperId, firstDelete.lowerId) < 0 + || idComp.compare(insert.lowerId, lastDelete.upperId) > 0) { + continue; + } + + for (TaskAndBounds del : deleteTasks) { + // if the delete task's lower bound is already past the insert task's upper bound, + // no subsequent delete can overlap this insert (because we sorted above). + // We can break inner loop. + if (idComp.compare(del.lowerId, insert.upperId) > 0) { + break; + } + + del.checkOverlapWith(insert, idComp); + } + } + } + } + + // collect results and return. + // overlapping tasks are bidirectional. + // otherwise they are unidirectional. + List unidirectional = new ArrayList<>(); + List bidirectional = new ArrayList<>(); + + for (TaskAndBounds taskAndBounds : Iterables.concat(deleteTasks, insertTasks)) { + if (taskAndBounds.overlaps) { + bidirectional.add(taskAndBounds.task); + } else { + unidirectional.add(taskAndBounds.task); + } + } + + StructLike overlapLower = null; + StructLike overlapUpper = null; + if (!bidirectional.isEmpty()) { + StructLike globalInsertLower = null; + StructLike globalInsertUpper = null; + for (TaskAndBounds t : insertTasks) { + if (t.overlaps) { + if (globalInsertLower == null || idComp.compare(t.lowerId, globalInsertLower) < 0) { + globalInsertLower = t.lowerId; + } + if (globalInsertUpper == null || idComp.compare(t.upperId, globalInsertUpper) > 0) { + globalInsertUpper = t.upperId; + } + } + } + + StructLike globalDeleteLower = null; + StructLike globalDeleteUpper = null; + for (TaskAndBounds t : deleteTasks) { + if (t.overlaps) { + if (globalDeleteLower == null || idComp.compare(t.lowerId, globalDeleteLower) < 0) { + globalDeleteLower = t.lowerId; + } + if (globalDeleteUpper == null || idComp.compare(t.upperId, globalDeleteUpper) > 0) { + globalDeleteUpper = t.upperId; + } + } + } + + if (globalInsertLower == null + || globalDeleteLower == null + || globalInsertUpper == null + || globalDeleteUpper == null) { + throw new IllegalStateException( + "Expected at least one overlapping task in bidirectional list"); + } + + overlapLower = + idComp.compare(globalInsertLower, globalDeleteLower) > 0 + ? globalInsertLower + : globalDeleteLower; + overlapUpper = + idComp.compare(globalInsertUpper, globalDeleteUpper) < 0 + ? globalInsertUpper + : globalDeleteUpper; + } + + return new AnalysisResult(unidirectional, bidirectional, overlapLower, overlapUpper); + } + + /** + * Routes bi-directional tasks from an {@link AnalysisResult} to either the in-memory local + * resolve path (when the estimated overlap region fits in one split) or the CoGroupByKey shuffle + * path otherwise. + * + *

For LOCAL routing, all bi-directional tasks for this snapshot/partition group are emitted as + * a batch so that the downstream {@link LocalResolveDoFn} can resolve them together in-memory. We + * only take this path when the group's estimated decoded size fits within {@code splitSize}. This + * bounds a single thread to roughly that footprint in the worst case (when metrics are missing or + * there is a very large overlap). The footprint is typically much smaller though: when PK bounds + * are available the resolver further prunes records outside the overlap range via per-record PK + * routing. + * + *

Returns the number of tasks routed to LOCAL so the caller can update counters. + */ + private void routeBidirectional( + Table table, + Snapshot snapshot, + AnalysisResult result, + TaskBatcher largeBiBatcher, + MultiOutputReceiver multiOutputReceiver) { + + if (result.bidirectional.isEmpty()) { + return; + } + + long totalBytes = + result.bidirectional.stream().mapToLong(SerializableChangelogTask::getLength).sum(); + + @Nullable Row overlapLowerRow = result.overlapLowerRow(scanConfig.rowIdBeamSchema()); + @Nullable Row overlapUpperRow = result.overlapUpperRow(scanConfig.rowIdBeamSchema()); + ChangelogDescriptor descriptor = + ChangelogDescriptor.builder() + .setTableIdentifierString(scanConfig.getTableIdentifier()) + .setSnapshotSequenceNumber(snapshot.sequenceNumber()) + .setCommitSnapshotId(snapshot.snapshotId()) + .setOverlapLower(overlapLowerRow) + .setOverlapUpper(overlapUpperRow) + .build(); + + List serializedTasks = + result.bidirectional.stream().map(t -> makeTask(t, table)).collect(Collectors.toList()); + + // If the batch is small enough, we can route to LOCAL (in-memory) resolver + if (totalBytes * COMPRESSED_TO_DECODED_BYTES_ESTIMATE <= splitSize(table)) { + Instant ts = Instant.ofEpochMilli(snapshot.timestampMillis()); + multiOutputReceiver + .get(SMALL_BIDIRECTIONAL_TASKS) + .outputWithTimestamp(KV.of(descriptor, serializedTasks), ts); + numSmallBiDirTasks += result.bidirectional.size(); + numSmallBiDirSplits++; + return; + } + + // If the batch is too big, we need to route to the CoGBK for distributed resolution + for (SerializableChangelogTask t : serializedTasks) { + largeBiBatcher.add(descriptor, t, t.getLength()); + } + } + + private static SerializableChangelogTask makeTask(ChangelogScanTask task, Table table) { + return SerializableChangelogTask.from(task, table.specs()); + } + + /** + * Wraps the {@link ChangelogScanTask}, and stores its lower and upper Primary Keys. Identifies + * overlaps with other tasks by comparing lower and upper keys using Iceberg libraries. + */ + static class TaskAndBounds { + ChangelogScanTask task; + StructLike lowerId; + StructLike upperId; + boolean overlaps = false; + + private TaskAndBounds(ChangelogScanTask task, StructLike lowerId, StructLike upperId) { + this.task = task; + this.lowerId = lowerId; + this.upperId = upperId; + } + + static TaskAndBounds of( + ChangelogScanTask task, Schema recIdSchema, Comparator idComp) + throws NoBoundMetricsException { + @MonotonicNonNull GenericRecord lowerId = null; + @MonotonicNonNull GenericRecord upperId = null; + + if (task instanceof AddedRowsScanTask || task instanceof DeletedDataFileScanTask) { + // just store the bounds of the DataFile + DataFile df = getDataFile(task); + @Nullable Map lowerBounds = df.lowerBounds(); + @Nullable Map upperBounds = df.upperBounds(); + if (lowerBounds == null || upperBounds == null) { + throw new NoBoundMetricsException( + format( + "Upper and/or lower bounds are missing for %s with DataFile: %s.", + task.getClass().getSimpleName(), df.location())); + } + + lowerId = createRecId(recIdSchema, lowerBounds); + upperId = createRecId(recIdSchema, upperBounds); + } else if (task instanceof DeletedRowsScanTask) { + // iterate over all added DeleteFiles and keep track of only the + // minimum and maximum bounds over the list + for (DeleteFile deleteFile : ((DeletedRowsScanTask) task).addedDeletes()) { + @Nullable Map lowerDelBounds = deleteFile.lowerBounds(); + @Nullable Map upperDelBounds = deleteFile.upperBounds(); + if (lowerDelBounds == null || upperDelBounds == null) { + throw new NoBoundMetricsException( + format( + "Upper and/or lower bounds are missing for %s with " + + "DataFile '%s' and DeleteFile '%s'", + task.getClass().getSimpleName(), + getDataFile(task).location(), + deleteFile.location())); + } + + GenericRecord delFileLower = createRecId(recIdSchema, lowerDelBounds); + GenericRecord delFileUpper = createRecId(recIdSchema, upperDelBounds); + + if (lowerId == null || idComp.compare(delFileLower, lowerId) < 0) { + lowerId = delFileLower; + } + if (upperId == null || idComp.compare(delFileUpper, upperId) > 0) { + upperId = delFileUpper; + } + } + } else { + throw new UnsupportedOperationException( + "Unsupported task type: " + task.getClass().getSimpleName()); + } + + if (lowerId == null || upperId == null) { + throw new NoBoundMetricsException( + format( + "Could not compute min and/or max bounds for %s with DataFile: %s", + task.getClass().getSimpleName(), getDataFile(task).location())); + } + return new TaskAndBounds(task, lowerId, upperId); + } + + /** + * Compares itself with another task. If the bounds overlap, sets {@link #overlaps} to true for + * both tasks. + */ + private void checkOverlapWith(TaskAndBounds other, Comparator idComp) { + if (overlaps && other.overlaps) { + return; + } + + int left = idComp.compare(lowerId, other.upperId); + int right = idComp.compare(other.lowerId, upperId); + + if (left <= 0 && right <= 0) { + overlaps = true; + other.overlaps = true; + } + } + + private static GenericRecord createRecId(Schema recIdSchema, Map bounds) + throws NoBoundMetricsException { + GenericRecord recId = GenericRecord.create(recIdSchema); + + for (Types.NestedField field : recIdSchema.columns()) { + int fieldId = field.fieldId(); + Type type = field.type(); + String name = field.name(); + @Nullable ByteBuffer value = bounds.get(fieldId); + if (value == null) { + throw new NoBoundMetricsException("Could not fetch metric value for column: " + name); + } + Object data = checkStateNotNull(Conversions.fromByteBuffer(type, value)); + recId.setField(name, data); + } + return recId; + } + + static class NoBoundMetricsException extends Exception { + public NoBoundMetricsException(String msg) { + super(msg); + } + } + } + + /** Checks if all partition fields are derived from record identifier fields. */ + private static boolean doesSpecPinRecordsToPartition(PartitionSpec spec) { + Set identifierFieldsIds = spec.schema().identifierFieldIds(); + if (spec.isUnpartitioned() || identifierFieldsIds.isEmpty()) { + return false; + } + + for (PartitionField field : spec.fields()) { + if (!identifierFieldsIds.contains(field.sourceId())) { + return false; + } + } + + return true; + } + + /** + * Helper class to batch tasks going to the same tagged PCollection. + * + *

Used to create batches of uni-directional tasks to send to {@link #UNIDIRECTIONAL_TASKS} + * tag. + * + *

Also used to create batches of large bi-directional tasks to send to {@link + * #LARGE_BIDIRECTIONAL_TASKS} tag. + * + *

A batch is emitted once it reaches {@link #splitSize(Table)}. + * + *

Note: This is not used by small bi-directional tasks. Instead, they are emitted immediately + * to {@link #SMALL_BIDIRECTIONAL_TASKS}. + */ + static class TaskBatcher { + Map> tasks = new HashMap<>(); + long byteSize = 0L; + final long maxSplitSize; + final String tableIdentifier; + final Instant timestamp; + final OutputReceiver>> output; + int totalSplits = 0; + + TaskBatcher( + String tableIdentifier, + Long timestampMillis, + long maxSplitSize, + OutputReceiver>> output) { + this.tableIdentifier = tableIdentifier; + this.timestamp = Instant.ofEpochMilli(timestampMillis); + this.maxSplitSize = maxSplitSize; + this.output = output; + } + + boolean canTake(long sizeBytes) { + return byteSize + sizeBytes <= maxSplitSize; + } + + void add(List tasks, long sequenceNumber, Table table) { + tasks.forEach(t -> add(makeTask(t, table), sequenceNumber, getLength(t))); + } + + void add(SerializableChangelogTask task, long snapshotSequenceNumber, long sizeBytes) { + add( + ChangelogDescriptor.builder() + .setTableIdentifierString(tableIdentifier) + .setSnapshotSequenceNumber(snapshotSequenceNumber) + .setCommitSnapshotId(task.getCommitSnapshotId()) + .build(), + task, + sizeBytes); + } + + void add(ChangelogDescriptor descriptor, SerializableChangelogTask task, long sizeBytes) { + if (!canTake(sizeBytes)) { + flush(); + } + byteSize += sizeBytes; + tasks.computeIfAbsent(descriptor, d -> new ArrayList<>()).add(task); + } + + void flush() { + if (tasks.isEmpty()) { + return; + } + + for (Map.Entry> entry : + tasks.entrySet()) { + ChangelogDescriptor descriptor = entry.getKey(); + List taskList = entry.getValue(); + output.outputWithTimestamp(KV.of(descriptor, taskList), timestamp); + } + + byteSize = 0; + tasks = new HashMap<>(); + totalSplits++; + } + } + + /** + * Fetch the desired split size for downstream read DoFn. We do our best to put tasks into groups + * of that size. This allows the user to control load per worker by tuning `read.split.target-size` + */ + long splitSize(Table table) { + return PropertyUtil.propertyAsLong( + table.properties(), TableProperties.SPLIT_SIZE, TableProperties.SPLIT_SIZE_DEFAULT); + } + + private void resetLocalMetrics() { + numAddedRowsTasks = 0; + numDeletedRowsTasks = 0; + numDeletedFileTasks = 0; + numUniDirTasks = 0; + numLargeBiDirTasks = 0; + numSmallBiDirTasks = 0; + numUniDirSplits = 0; + numSmallBiDirSplits = 0; + numLargeBiDirSplits = 0; + } + + private void gatherTaskTypeMetrics(SerializableChangelogTask.Type type) { + switch (type) { + case ADDED_ROWS: + numAddedRowsTasks++; + break; + case DELETED_ROWS: + numDeletedRowsTasks++; + break; + case DELETED_FILE: + numDeletedFileTasks++; + break; + } + } + + private void updateTaskCounters() { + int totalTasks = numAddedRowsTasks + numDeletedRowsTasks + numDeletedFileTasks; + totalChangelogScanTasks.inc(totalTasks); + numAddedRowsScanTasks.inc(numAddedRowsTasks); + numDeletedRowsScanTasks.inc(numDeletedRowsTasks); + numDeletedDataFileScanTasks.inc(numDeletedFileTasks); + numUniDirectionalTasks.inc(numUniDirTasks); + numSmallBiDirectionalTasks.inc(numSmallBiDirTasks); + numLargeBiDirectionalTasks.inc(numLargeBiDirTasks - numSmallBiDirTasks); + } + + private String scanResultMessage(Snapshot snapshot, int totalTasks) { + StringBuilder message = new StringBuilder(); + message.append( + format( + "Snapshot %s (seq: %s) produced %s changelog tasks.", + snapshot.snapshotId(), snapshot.sequenceNumber(), totalTasks)); + if (totalTasks > 0) { + message.append("Emitted:"); + if (numUniDirTasks > 0) { + message.append( + format( + "%n\t%s splits containing %s uni-directional tasks", + numUniDirSplits, numUniDirTasks)); + } + if (numSmallBiDirTasks > 0) { + message.append( + format( + "%n\t%s splits containing %s small bi-directional tasks (for local resolution)", + numSmallBiDirSplits, numSmallBiDirTasks)); + } + if (numLargeBiDirTasks > 0) { + message.append( + format( + "%n\t%s splits containing %s large bi-directional tasks (to be shuffled)", + numLargeBiDirSplits, numLargeBiDirTasks)); + } + } + return message.toString(); + } +} 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/IncrementalChangelogSource.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSource.java new file mode 100644 index 000000000000..fe6240d5dc53 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSource.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.io.iceberg.cdc.ChangelogScanner.LARGE_BIDIRECTIONAL_TASKS; +import static org.apache.beam.sdk.io.iceberg.cdc.ChangelogScanner.SMALL_BIDIRECTIONAL_TASKS; +import static org.apache.beam.sdk.io.iceberg.cdc.ChangelogScanner.UNIDIRECTIONAL_TASKS; +import static org.apache.beam.sdk.io.iceberg.cdc.ResolveChanges.DELETES; +import static org.apache.beam.sdk.io.iceberg.cdc.ResolveChanges.INSERTS; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.util.List; +import java.util.stream.Collectors; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.ReadUtils; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.Flatten; +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.join.CoGroupByKey; +import org.apache.beam.sdk.transforms.join.KeyedPCollectionTuple; +import org.apache.beam.sdk.transforms.windowing.AfterWatermark; +import org.apache.beam.sdk.transforms.windowing.DefaultTrigger; +import org.apache.beam.sdk.transforms.windowing.GlobalWindows; +import org.apache.beam.sdk.transforms.windowing.Window; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PBegin; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionList; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TimestampedValue; +import org.apache.beam.sdk.values.TupleTagList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; +import org.apache.iceberg.Table; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.joda.time.Instant; + +/** + * An Iceberg source that incrementally reads a table's changelogs, processing one snapshot at a + * time. + * + *

Each snapshot is resolved independently. For a given primary key, this source emits the net + * change the snapshot produces, and not its intermediate states. + * + *

Implications: if a writer batches several transitions for the same PK into one snapshot (for + * example {@code A → B, then B → C}), only the endpoints survive. The intermediate {@code B} is + * dropped. A round-trip within a single snapshot (for example {@code A → B → A}) is also dropped. + * + *

The streaming path uses {@link WatchForSnapshotsSdf} for proper per-snapshot watermarks. The + * bounded path creates the snapshot range up front. + */ +public class IncrementalChangelogSource extends PTransform> { + private final IcebergScanConfig scanConfig; + + public IncrementalChangelogSource(IcebergScanConfig scanConfig) { + this.scanConfig = scanConfig; + } + + @Override + public PCollection expand(PBegin input) { + // emit one SnapshotInfo per element, with element timestamp -> snapshot commit time. + PCollection snapshots = + MoreObjects.firstNonNull(scanConfig.getStreaming(), false) + ? unboundedSnapshots(input) + : boundedSnapshots(input); + + // process one snapshot at a time and produce batches of changelog scan tasks. + // tasks are emitted to three outputs: + // 1. unidirectional tasks: we know these won't have any updates + // 2. small bidirectional tasks: these may contain an update, but the batch is small enough to + // resolve in-memory + // 2. large bidirectional tasks: may contain an update, but are too large for in-memory + // resolution. will + // need to run these output rows through a CoGBK + PCollectionTuple changelogTasks = + snapshots.apply( + "Create Changelog Tasks", + ParDo.of(new ChangelogScanner(scanConfig)) + .withOutputTags( + UNIDIRECTIONAL_TASKS, + TupleTagList.of(LARGE_BIDIRECTIONAL_TASKS).and(SMALL_BIDIRECTIONAL_TASKS))); + KvCoder> tasksCoder = + ChangelogScanner.coder(scanConfig.rowIdBeamSchema()); + changelogTasks.get(UNIDIRECTIONAL_TASKS).setCoder(tasksCoder); + changelogTasks.get(SMALL_BIDIRECTIONAL_TASKS).setCoder(tasksCoder); + changelogTasks.get(LARGE_BIDIRECTIONAL_TASKS).setCoder(tasksCoder); + + Schema projectedRowSchema = + IcebergUtils.icebergSchemaToBeamSchema( + scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion()); + Schema outputRowSchema = CdcOutputUtils.outputSchema(scanConfig, projectedRowSchema); + + // reads UNIDIRECTIONAL and BIDIRECTIONAL tags and produces rows. + ReadFromChangelogs.Output outputRows = changelogTasks.apply(new ReadFromChangelogs(scanConfig)); + + // Small overlapping groups get resolved entirely in memory with no shuffle. + PCollection smallBidirectionalCdcRows = + changelogTasks + .get(SMALL_BIDIRECTIONAL_TASKS) + .apply("Redistribute Small Bidirectional Changes", Redistribute.arbitrarily()) + .apply("Resolve Locally", ParDo.of(new LocalResolveDoFn(scanConfig))) + .setRowSchema(outputRowSchema); + + // BIDIRECTIONAL records go through a CoGBK and ResolveChanges + // We window locally using a custom WindowFn based on the snapsot's commit time. Each snapshot + // exists in its own window. + // We re-window the resolved output back to GlobalWindows before the final Flatten + // to align with the other branches. + Window> keyedWindowing = + Window.>into(new SnapshotWindowFn()) + .triggering(AfterWatermark.pastEndOfWindow()) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes(); + PCollection> keyedInserts = + outputRows.biDirectionalInserts().apply("Window Inserts", keyedWindowing); + PCollection> keyedDeletes = + outputRows.biDirectionalDeletes().apply("Window Deletes", keyedWindowing); + PCollection biDirectionalCdcRows = + KeyedPCollectionTuple.of(INSERTS, keyedInserts) + .and(DELETES, keyedDeletes) + .apply("CoGroupBy Primary Key", CoGroupByKey.create()) + .apply("Resolve Delete-Insert Pairs", ParDo.of(new ResolveChanges(scanConfig))) + .setRowSchema(outputRowSchema) + .apply( + "Re-window to Global", + Window.into(new GlobalWindows()) + .triggering(DefaultTrigger.of()) + .discardingFiredPanes()); + + // Merge all three paths into a single output. All three are in GlobalWindows. + PCollection merged = + PCollectionList.of(outputRows.uniDirectionalRows()) + .and(smallBidirectionalCdcRows) + .and(biDirectionalCdcRows) + .apply(Flatten.pCollections()); + + // If the user configures a watermark column, restamp each record by + // that column's value. Output watermark then advances per-record rather than per-snapshot. + @Nullable String watermarkColumn = scanConfig.getWatermarkColumn(); + if (watermarkColumn != null) { + merged = + merged.apply( + "Apply Watermark Column", + ParDo.of( + new ApplyWatermarkColumn( + watermarkColumn, scanConfig.getWatermarkColumnTimeUnit()))); + } + + return merged.setRowSchema(outputRowSchema); + } + + /** + * Continuously watches the Iceberg table for new snapshots via {@link WatchForSnapshotsSdf} and + * emits per snapshot. + */ + private PCollection unboundedSnapshots(PBegin input) { + return input + .apply("Impulse", Create.of("")) + .apply("Watch for Snapshots", ParDo.of(new WatchForSnapshotsSdf(scanConfig))); + } + + /** + * Reads the full snapshot range up front and emits each snapshot individually, each carrying its + * own commit time as the element timestamp. + */ + private PCollection boundedSnapshots(PBegin input) { + Table table = + scanConfig + .getCatalogConfig() + .catalog() + .loadTable(IcebergUtils.parseTableIdentifier(scanConfig.getTableIdentifier())); + checkStateNotNull( + table.currentSnapshot(), + "Table %s does not have any snapshots to read from.", + scanConfig.getTableIdentifier()); + + @Nullable Long from = ReadUtils.getFromSnapshotExclusive(table, scanConfig); + long to = + MoreObjects.firstNonNull( + ReadUtils.getToSnapshot(table, scanConfig), table.currentSnapshot().snapshotId()); + List> timestamped = + ReadUtils.snapshotsBetween(table, scanConfig.getTableIdentifier(), from, to).stream() + .map( + s -> + TimestampedValue.of( + s.getSnapshotId(), Instant.ofEpochMilli(s.getTimestampMillis()))) + .collect(Collectors.toList()); + return input.apply("Create Snapshot Range", Create.timestamped(timestamped)); + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/LocalResolveDoFn.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/LocalResolveDoFn.java new file mode 100644 index 000000000000..683a7ed86132 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/LocalResolveDoFn.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.io.iceberg.IcebergUtils.icebergSchemaToBeamSchema; +import static org.apache.beam.sdk.io.iceberg.cdc.SerializableChangelogTask.Type.ADDED_ROWS; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.TableCache; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.join.CoGroupByKey; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.ValueKind; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.TableProperties; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.types.Type; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.StructLikeMap; +import org.apache.iceberg.util.StructLikeUtil; +import org.apache.iceberg.util.StructProjection; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Resolves a small bi-directional changelog group entirely in memory. This is the equivalent of + * {@link ReadFromChangelogs} + {@link CoGroupByKey} + {@link ResolveChanges}. + * + *

All tasks in a changelog group belong to the same Iceberg {@link Snapshot}. The upstream + * {@link ChangelogScanner} routes here only when the total size of the bi-directional group fits + * within {@link TableProperties#SPLIT_SIZE}. + * + *

The incoming batch's overlap region has already been computed in the scanning phase by {@link + * ChangelogScanner}. In this DoFn, we just process each task and route records: + * + *

    + *
  • Records whose PK falls outside the overlap range cannot have an opposing-side match, + * so they are emitted directly with {@code INSERT} or {@code DELETE} kind. + *
  • Records whose PK falls inside the overlap range are stashed in a {@link + * StructLikeMap} keyed by PK, then resolved by {@link CdcResolver}. + *
+ */ +class LocalResolveDoFn extends DoFn>, Row> { + private final IcebergScanConfig scanConfig; + private final org.apache.beam.sdk.schemas.Schema projectedBeamSchema; + private final org.apache.beam.sdk.schemas.Schema outputBeamSchema; + + private transient @MonotonicNonNull OverlapRange overlap; + private transient @MonotonicNonNull List nonPkFields; + private transient @MonotonicNonNull StructProjection projector; + + LocalResolveDoFn(IcebergScanConfig scanConfig) { + this.scanConfig = scanConfig; + this.projectedBeamSchema = + CdcOutputUtils.readBeamSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), + scanConfig.getProjectedSchema(), + scanConfig.getUpdateCompatibilityVersion()); + this.outputBeamSchema = + CdcOutputUtils.outputSchema( + scanConfig, + icebergSchemaToBeamSchema( + scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion())); + } + + @Setup + public void setup() { + Schema tableSchema = + TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()).schema(); + Schema fullReadSchema = + CdcOutputUtils.readSchemaWithRowMetadata(scanConfig.getMetadataColumns(), tableSchema); + this.overlap = OverlapRange.forScanConfig(scanConfig); + Set pkFieldNames = new HashSet<>(overlap.recordIdSchema().identifierFieldNames()); + // The dedup logic only inspects non-PK fields, so precompute them once. + List nonPk = new ArrayList<>(); + for (Types.NestedField f : tableSchema.columns()) { + if (!pkFieldNames.contains(f.name())) { + nonPk.add(f); + } + } + this.nonPkFields = nonPk; + this.projector = + StructProjection.create( + fullReadSchema, + CdcOutputUtils.readSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), scanConfig.getProjectedSchema())); + } + + @ProcessElement + public void process( + @Element KV> element, + OutputReceiver out) + throws IOException { + ChangelogDescriptor descriptor = element.getKey(); + Table table = TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); + OverlapRange ovl = checkStateNotNull(overlap); + + // {PK: (inserts | deletes)} for in-overlap records that need resolution. + // Records outside the overlap are emitted directly + StructLikeMap pkGroups = StructLikeMap.create(ovl.recordIdSchema().asStruct()); + + @Nullable StructLike overlapLower = ovl.toStructLike(descriptor.getOverlapLower()); + @Nullable StructLike overlapUpper = ovl.toStructLike(descriptor.getOverlapUpper()); + for (SerializableChangelogTask task : element.getValue()) { + readAndRoute(descriptor, task, table, overlapLower, overlapUpper, pkGroups, out); + } + + resolveAndEmit(descriptor, pkGroups, out); + } + + /** + * Processes a {@link SerializableChangelogTask} and routes each record. + * + *
    + *
  • Out of overlap: emit directly + *
  • Inside overlap: stash in {@code pkGroups} to resolve in {@link #resolveAndEmit} + *
+ */ + private void readAndRoute( + ChangelogDescriptor descriptor, + SerializableChangelogTask task, + Table table, + @Nullable StructLike overlapLower, + @Nullable StructLike overlapUpper, + StructLikeMap pkGroups, + OutputReceiver out) + throws IOException { + OverlapRange ovl = checkStateNotNull(overlap); + boolean isInsert = task.getType() == ADDED_ROWS; + try (CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig, false)) { + for (Record rec : records) { + if (ovl.contains(rec, overlapLower, overlapUpper)) { // needs resolution + StructLike pk = StructLikeUtil.copy(ovl.recordIdProjection()); + PkGroup group = pkGroups.computeIfAbsent(pk, k -> new PkGroup()); + if (isInsert) { + group.inserts.add(rec); + } else { + group.deletes.add(rec); + } + } else { // safe to emit directly + emit(descriptor, rec, isInsert ? ValueKind.INSERT : ValueKind.DELETE, out); + } + } + } + } + + /** Resolves each PK group using {@link CdcResolver}. */ + private void resolveAndEmit( + ChangelogDescriptor descriptor, StructLikeMap pkGroups, OutputReceiver out) { + CdcResolver resolver = new RecordResolver(checkStateNotNull(nonPkFields)); + for (PkGroup group : pkGroups.values()) { + resolver.resolve( + group.deletes, + group.inserts, + (kind, rec) -> { + emit(descriptor, rec, kind, out); + }); + } + } + + /** Resolver specialization that hashes Iceberg Record non-PK fields. */ + private static final class RecordResolver extends CdcResolver { + private final List nonPkFields; + + RecordResolver(List nonPkFields) { + this.nonPkFields = nonPkFields; + } + + @Override + protected int nonPkHash(Record rec) { + int hash = 1; + for (Types.NestedField field : nonPkFields) { + hash = 31 * hash + deepHash(rec.getField(field.name())); + } + return hash; + } + + @Override + protected boolean nonPkEquals(Record delete, Record insert) { + for (Types.NestedField field : nonPkFields) { + // consistent with deepHash + if (!Objects.deepEquals(delete.getField(field.name()), insert.getField(field.name()))) { + return false; + } + } + return true; + } + + /** + * Content hash consistent with {@link Objects#deepEquals}. Iceberg's generic model only ever + * produces a {@code byte[]} for {@link Type.TypeID#FIXED} columns, but we use {@link + * Class#isArray} to handle any array type. + */ + private static int deepHash(@Nullable Object value) { + if (value != null && value.getClass().isArray()) { + return Arrays.deepHashCode(new Object[] {value}); + } + return Objects.hashCode(value); + } + } + + /** Prune to get the final projected record then output as a Beam Row. */ + private void emit( + ChangelogDescriptor descriptor, Record rec, ValueKind kind, OutputReceiver out) { + StructLike projected = checkStateNotNull(projector).wrap(rec); + Row record = IcebergUtils.structToRow(projectedBeamSchema, projected); + out.builder( + CdcOutputUtils.outputRow( + scanConfig.getMetadataColumns(), outputBeamSchema, descriptor, kind, record)) + .setValueKind(kind) + .output(); + } + + /** Two parallel lists of inserts/deletes that share a primary key. */ + private static final class PkGroup { + final List inserts = new ArrayList<>(); + final List deletes = new ArrayList<>(); + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/OverlapRange.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/OverlapRange.java new file mode 100644 index 000000000000..04e0429030a9 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/OverlapRange.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.Comparator; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.TableCache; +import org.apache.beam.sdk.values.Row; +import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.util.StructProjection; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Primary-key-projection and overlap-range comparison helper. + * + *

Used by {@link LocalResolveDoFn} and {@link ReadFromChangelogs} to decide whether a record's + * PK falls within an overlap of two opposing tasks. If so, the record needs to be compared with + * others to determine if it is part of an update pair. + */ +final class OverlapRange { + private final Schema recordIdSchema; + private final StructProjection recordIdProjection; + private final Comparator idComp; + + private OverlapRange( + Schema recordIdSchema, StructProjection recordIdProjection, Comparator idComp) { + this.recordIdSchema = recordIdSchema; + this.recordIdProjection = recordIdProjection; + this.idComp = idComp; + } + + static OverlapRange forScanConfig(IcebergScanConfig scanConfig) { + Schema tableSchema = + TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()).schema(); + Schema fullSchema = + CdcOutputUtils.readSchemaWithRowMetadata(scanConfig.getMetadataColumns(), tableSchema); + StructProjection projection = StructProjection.create(fullSchema, scanConfig.recordIdSchema()); + return new OverlapRange( + scanConfig.recordIdSchema(), projection, scanConfig.recordIdComparator()); + } + + StructProjection recordIdProjection() { + return recordIdProjection; + } + + Schema recordIdSchema() { + return recordIdSchema; + } + + /** Converts a Beam Row (overlap bound) back to an Iceberg {@link StructLike}. */ + @Nullable + StructLike toStructLike(@Nullable Row beamBound) { + if (beamBound == null) { + return null; + } + return IcebergUtils.beamRowToIcebergRecord(recordIdSchema, beamBound); + } + + /** + * Wraps the record to project its Primary Key, then checks if the PK is within the overlap {@code + * [lower, upper]} (inclusive). Can be paired with a subsequent {@link #recordIdProjection()} call + * to fetch the PK value. + * + *

Both ends are inclusive because the bounds are Iceberg file statistics (actual min/max PK + * values), making the overlap an intersection of two closed ranges. Note the error directions are + * not symmetric: being over-inclusive only costs extra buffering, since an unmatched record + * resolves to the same {@code INSERT} / {@code DELETE} it would have been emitted as, whereas + * excluding a boundary PK would split a genuine update into a spurious {@code INSERT} + {@code + * DELETE}. + * + *

If either bound is null, we conservatively assume it falls within the overlap. + */ + boolean contains(Record rec, @Nullable StructLike lower, @Nullable StructLike upper) { + checkStateNotNull(recordIdProjection).wrap(rec); + + if (lower == null || upper == null) { + return true; + } + return idComp.compare(recordIdProjection, lower) >= 0 + && idComp.compare(recordIdProjection, upper) <= 0; + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ReadFromChangelogs.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ReadFromChangelogs.java new file mode 100644 index 000000000000..da1796f949e1 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ReadFromChangelogs.java @@ -0,0 +1,499 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.io.iceberg.IcebergUtils.icebergRecordToBeamRow; +import static org.apache.beam.sdk.io.iceberg.IcebergUtils.icebergSchemaToBeamSchema; +import static org.apache.beam.sdk.io.iceberg.IcebergUtils.structToRow; +import static org.apache.beam.sdk.io.iceberg.cdc.CdcReadUtils.COMPRESSED_TO_DECODED_BYTES_ESTIMATE; +import static org.apache.beam.sdk.io.iceberg.cdc.ChangelogScanner.LARGE_BIDIRECTIONAL_TASKS; +import static org.apache.beam.sdk.io.iceberg.cdc.ChangelogScanner.UNIDIRECTIONAL_TASKS; +import static org.apache.beam.sdk.io.iceberg.cdc.SerializableChangelogTask.Type.ADDED_ROWS; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.Pipeline; +import org.apache.beam.sdk.coders.KvCoder; +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.TableCache; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.metrics.Counter; +import org.apache.beam.sdk.metrics.Metrics; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaCoder; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.Flatten; +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.join.CoGroupByKey; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +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.PCollectionTuple; +import org.apache.beam.sdk.values.PInput; +import org.apache.beam.sdk.values.POutput; +import org.apache.beam.sdk.values.PValue; +import org.apache.beam.sdk.values.Row; +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.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.iceberg.ChangelogScanTask; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.util.StructProjection; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A {@link PTransform} that processes batches of {@link ChangelogScanTask}s and routes them + * accordingly: + * + *

    + *
  • Records from Uni-directional batches are directly emitted, as INSERT or DELETE kind + *
  • Records from Bi-directional batches are compared against the Primary Key overlap range: + *
      + *
    • if outside the overlap, emit directly as INSERT or DELETE kind + *
    • if inside the overlap, key by (snapshot seq#, pk) and route to downstream {@link + * CoGroupByKey} and final resolution by {@link ResolveChanges} + *
    + *
+ * + *

We first key bi-directional rows by (snapshot sequence number, primary key) before sending to + * {@link CoGroupByKey} to ensure they stay isolated from other PKs or snapshots. Inserts are routed + * to + * + *

A {@link ChangelogScanTask} comes in three types: + * + *

    + *
  1. AddedRowsScanTask: Indicates records have been inserted by a new DataFile. + *
  2. DeletedRowsScanTask: Indicates records have been deleted using a DeleteFile. + *
  3. DeletedDataFileScanTask: Indicates a whole DataFile has been deleted. + *
+ * + *

Each of these types need to be processed differently. More details in {@link + * CdcReadUtils#changelogRecordsForTask}. + * + *

CDC metadata has two entry points in this transform. Row metadata columns are requested from + * the Iceberg reader by {@link CdcReadUtils} and travel inside intermediate rows until final output + * assembly. Snapshot metadata columns come from the {@link ChangelogDescriptor} / {@link + * CdcRowDescriptor} carried with each task or shuffled row, and {@code _change_type} comes from the + * emitted change kind. Final user-visible rows are assembled by {@link CdcOutputUtils#outputRow}, + * which appends all requested metadata as top-level columns in the configured order. + */ +public class ReadFromChangelogs extends PTransform { + private static final Counter numAddedRowsScanTasksCompleted = + Metrics.counter(ReadFromChangelogs.class, "numAddedRowsScanTasksCompleted"); + private static final Counter numDeletedRowsScanTasksCompleted = + Metrics.counter(ReadFromChangelogs.class, "numDeletedRowsScanTasksCompleted"); + private static final Counter numDeletedDataFileScanTasksCompleted = + Metrics.counter(ReadFromChangelogs.class, "numDeletedDataFileScanTasksCompleted"); + + private static final TupleTag UNIDIRECTIONAL_ROWS = new TupleTag<>(); + private static final TupleTag> BIDIRECTIONAL_INSERTS = new TupleTag<>(); + private static final TupleTag> BIDIRECTIONAL_DELETES = new TupleTag<>(); + + private final IcebergScanConfig scanConfig; + + ReadFromChangelogs(IcebergScanConfig scanConfig) { + this.scanConfig = scanConfig; + } + + @Override + public org.apache.beam.sdk.io.iceberg.cdc.ReadFromChangelogs.Output expand( + PCollectionTuple input) { + Schema fullRowSchema = + CdcOutputUtils.readBeamSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), scanConfig.getSchema()); + Schema projectedRowSchema = + IcebergUtils.icebergSchemaToBeamSchema( + scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion()); + Schema outputRowSchema = CdcOutputUtils.outputSchema(scanConfig, projectedRowSchema); + + // === UNIDIRECTIONAL tasks === + // (i.e. only deletes, or only inserts) + // take the fast approach of just reading and emitting CDC records + PCollection uniDirectionalRows = + input + .get(UNIDIRECTIONAL_TASKS) + .apply("Redistribute Uni-Directional Changes", Redistribute.arbitrarily()) + .apply( + "Read Uni-Directional Changes", + ParDo.of(ReadDoFn.unidirectional(scanConfig)) + .withOutputTags(UNIDIRECTIONAL_ROWS, TupleTagList.empty())) + .get(UNIDIRECTIONAL_ROWS) + .setRowSchema(outputRowSchema); + + // === BIDIRECTIONAL tasks === + // (i.e. a task group containing a mix of deletes and inserts) + // read and route records according to their PK (see class java doc) + PCollectionTuple biDirectionalRows = + input + .get(LARGE_BIDIRECTIONAL_TASKS) + .apply("Redistribute Large Bi-Directional Changes", Redistribute.arbitrarily()) + .apply( + "Read Bi-Directional Changes", + ParDo.of(ReadDoFn.bidirectional(scanConfig)) + .withOutputTags( + BIDIRECTIONAL_INSERTS, + TupleTagList.of(BIDIRECTIONAL_DELETES).and(UNIDIRECTIONAL_ROWS))); + // Collect pruned (non-overlapping) rows from bi-directional reader + PCollection nonOverlappingRowsFromBiDirTasks = + biDirectionalRows.get(UNIDIRECTIONAL_ROWS).setRowSchema(outputRowSchema); + + // Flatten uni-directional rows from both sources + PCollection allUniDirectionalRows = + PCollectionList.of(uniDirectionalRows) + .and(nonOverlappingRowsFromBiDirTasks) + .apply("Flatten Uni-Directional Rows", Flatten.pCollections()); + + // Reify to preserve each record's timestamp (CoGBK overwrites timestamps with the window's + // end-of-window) + // Note: element timestamps are snapshot commit timestamp + KvCoder keyedOutputCoder = + KvCoder.of( + CdcRowDescriptor.coder(scanConfig.rowIdBeamSchema()), SchemaCoder.of(fullRowSchema)); + PCollection> keyedInsertsWithTimestamps = + biDirectionalRows.get(BIDIRECTIONAL_INSERTS).setCoder(keyedOutputCoder); + PCollection> keyedDeletesWithTimestamps = + biDirectionalRows.get(BIDIRECTIONAL_DELETES).setCoder(keyedOutputCoder); + + return new org.apache.beam.sdk.io.iceberg.cdc.ReadFromChangelogs.Output( + input.getPipeline(), + allUniDirectionalRows, + keyedInsertsWithTimestamps, + keyedDeletesWithTimestamps); + } + + public static class Output implements POutput { + private final Pipeline pipeline; + private final PCollection uniDirectionalRows; + private final PCollection> biDirectionalInserts; + private final PCollection> biDirectionalDeletes; + + Output( + Pipeline p, + PCollection uniDirectionalRows, + PCollection> biDirectionalInserts, + PCollection> biDirectionalDeletes) { + this.pipeline = p; + this.uniDirectionalRows = uniDirectionalRows; + this.biDirectionalInserts = biDirectionalInserts; + this.biDirectionalDeletes = biDirectionalDeletes; + } + + PCollection uniDirectionalRows() { + return uniDirectionalRows; + } + + PCollection> biDirectionalInserts() { + return biDirectionalInserts; + } + + PCollection> biDirectionalDeletes() { + return biDirectionalDeletes; + } + + @Override + public Pipeline getPipeline() { + return pipeline; + } + + @Override + public Map, PValue> expand() { + return ImmutableMap.of( + UNIDIRECTIONAL_ROWS, + uniDirectionalRows, + BIDIRECTIONAL_INSERTS, + biDirectionalInserts, + BIDIRECTIONAL_DELETES, + biDirectionalDeletes); + } + + @Override + public void finishSpecifyingOutput( + String transformName, PInput input, PTransform transform) {} + } + + @DoFn.BoundedPerElement + private static class ReadDoFn + extends DoFn>, OutT> { + private final IcebergScanConfig scanConfig; + private final boolean keyedOutput; + private final Schema projectedBeamRowSchema; + private final Schema outputBeamRowSchema; + private final Schema fullBeamRowSchema; + private transient @MonotonicNonNull OverlapRange overlap; + private transient @MonotonicNonNull StructProjection outputProjector; + private transient @MonotonicNonNull StructProjection pkProjector; + + /** Used for uni-directional changes. Records are output immediately as-is. */ + static ReadDoFn unidirectional(IcebergScanConfig scanConfig) { + return new ReadDoFn<>(scanConfig, false); + } + + /** + * Used for bi-directional changes. Records are keyed by (snapshot sequence number, primary key) + * and sent to a CoGBK. + */ + static ReadDoFn> bidirectional(IcebergScanConfig scanConfig) { + return new ReadDoFn<>(scanConfig, true); + } + + private ReadDoFn(IcebergScanConfig scanConfig, boolean keyedOutput) { + this.scanConfig = scanConfig; + this.keyedOutput = keyedOutput; + + this.projectedBeamRowSchema = + CdcOutputUtils.readBeamSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), + scanConfig.getProjectedSchema(), + scanConfig.getUpdateCompatibilityVersion()); + this.outputBeamRowSchema = + CdcOutputUtils.outputSchema( + scanConfig, + icebergSchemaToBeamSchema( + scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion())); + this.fullBeamRowSchema = + CdcOutputUtils.readBeamSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), scanConfig.getSchema()); + } + + @Setup + public void setup() { + this.overlap = OverlapRange.forScanConfig(scanConfig); + } + + @ProcessElement + public void process( + @Element KV> element, + RestrictionTracker tracker, + MultiOutputReceiver out) + throws IOException { + Table table = TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); + + List tasks = element.getValue(); + ChangelogDescriptor descriptor = element.getKey(); + @Nullable Row overlapLower = descriptor.getOverlapLower(); + @Nullable Row overlapUpper = descriptor.getOverlapUpper(); + + for (long l = tracker.currentRestriction().getFrom(); + l < tracker.currentRestriction().getTo(); + l++) { + if (!tracker.tryClaim(l)) { + return; + } + + SerializableChangelogTask task = tasks.get((int) l); + processTaskRecords(descriptor, task, overlapLower, overlapUpper, table, out); + } + } + + /** + * Processes a ChangelogScanTask and routes records accordingly: + * + *

If this DoFn is configured with {@link #unidirectional}, we simply read records and output + * directly to {@link #UNIDIRECTIONAL_ROWS}. + * + *

If this DoFn is configured with {@link #bidirectional}, we compare against the Primary Key + * overlap range. If within the overlap, we key by (snapshotId, PK) and out to either {@link + * #BIDIRECTIONAL_INSERTS} or {@link #BIDIRECTIONAL_DELETES}. Otherwise (not in overlap), we + * output the record directly to {@link #UNIDIRECTIONAL_ROWS}. + */ + private void processTaskRecords( + ChangelogDescriptor descriptor, + SerializableChangelogTask task, + @Nullable Row overlapLowerRow, + @Nullable Row overlapUpperRow, + Table table, + MultiOutputReceiver outputReceiver) + throws IOException { + OverlapRange ovl = checkStateNotNull(overlap); + @Nullable StructLike overlapLower = ovl.toStructLike(overlapLowerRow); + @Nullable StructLike overlapUpper = ovl.toStructLike(overlapUpperRow); + + boolean isInsert = task.getType() == ADDED_ROWS; + TupleTag> taggedOutput = + isInsert ? BIDIRECTIONAL_INSERTS : BIDIRECTIONAL_DELETES; + ValueKind kind = isInsert ? ValueKind.INSERT : ValueKind.DELETE; + long commitSnapshotId = descriptor.getCommitSnapshotId(); + long commitSnapshotSequenceNumber = descriptor.getSnapshotSequenceNumber(); + + Schema readSchema = keyedOutput ? fullBeamRowSchema : projectedBeamRowSchema; + try (CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig, !keyedOutput)) { + for (Record rec : records) { + // uni-directional -- just output records (they are already projected by read pushdown) + if (!keyedOutput) { + Row row = icebergRecordToBeamRow(projectedBeamRowSchema, rec); + outputReceiver + .get(UNIDIRECTIONAL_ROWS) + .builder( + CdcOutputUtils.outputRow( + scanConfig.getMetadataColumns(), + outputBeamRowSchema, + descriptor, + kind, + row)) + .setValueKind(kind) + .output(); + continue; + } + + // bi-directional -- compare overlap + if (ovl.contains(rec, overlapLower, overlapUpper)) { + // inside overlap -- read full row and output KV + Row row = icebergRecordToBeamRow(readSchema, rec); + Row pk = structToRow(scanConfig.rowIdBeamSchema(), pkProjector().wrap(rec)); + outputReceiver + .get(taggedOutput) + .builder( + KV.of( + CdcRowDescriptor.builder() + .setCommitSnapshotId(commitSnapshotId) + .setSnapshotSequenceNumber(commitSnapshotSequenceNumber) + .setPrimaryKey(pk) + .build(), + row)) + .setValueKind(kind) + .output(); + + } else { + // outside overlap -- get projected record and output + StructLike projected = outputProjector().wrap(rec); + Row row = structToRow(projectedBeamRowSchema, projected); + outputReceiver + .get(UNIDIRECTIONAL_ROWS) + .builder( + CdcOutputUtils.outputRow( + scanConfig.getMetadataColumns(), + outputBeamRowSchema, + descriptor, + kind, + row)) + .setValueKind(kind) + .output(); + } + } + } + + trackMetrics(task.getType()); + } + + private StructProjection outputProjector() { + if (outputProjector == null) { + outputProjector = + StructProjection.create( + CdcOutputUtils.readSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), + TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()) + .schema()), + CdcOutputUtils.readSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), scanConfig.getProjectedSchema())); + } + return outputProjector; + } + + private StructProjection pkProjector() { + if (pkProjector == null) { + pkProjector = + StructProjection.create( + CdcOutputUtils.readSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), + TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()) + .schema()), + scanConfig.recordIdSchema()); + } + return pkProjector; + } + + private void trackMetrics(SerializableChangelogTask.Type type) { + switch (type) { + case ADDED_ROWS: + numAddedRowsScanTasksCompleted.inc(); + break; + case DELETED_ROWS: + numDeletedRowsScanTasksCompleted.inc(); + break; + case DELETED_FILE: + numDeletedDataFileScanTasksCompleted.inc(); + break; + } + } + + private String getKind(SerializableChangelogTask.Type taskType) { + switch (taskType) { + case ADDED_ROWS: + return "INSERT"; + case DELETED_ROWS: + return "DELETE"; + case DELETED_FILE: + default: + return "DELETE-DF"; + } + } + + @GetSize + public double getSize( + @Element KV> element, + @Restriction OffsetRange restriction) { + // TODO(ahmedabu98): can we make this estimate more accurate? + long size = 0; + + for (long l = restriction.getFrom(); l < restriction.getTo(); l++) { + SerializableChangelogTask task = element.getValue().get((int) l); + size += task.getLength() * COMPRESSED_TO_DECODED_BYTES_ESTIMATE; + size += + task.getAddedDeletes().stream() + .mapToLong(SerializableDeleteFile::getFileSizeInBytes) + .sum() + * COMPRESSED_TO_DECODED_BYTES_ESTIMATE; + size += + task.getExistingDeletes().stream() + .mapToLong(SerializableDeleteFile::getFileSizeInBytes) + .sum() + * COMPRESSED_TO_DECODED_BYTES_ESTIMATE; + } + + return size; + } + + @GetInitialRestriction + public OffsetRange getInitialRange( + @Element KV> element) { + return new OffsetRange(0, element.getValue().size()); + } + + @SplitRestriction + public void splitRestriction( + @Restriction OffsetRange restriction, OutputReceiver out) { + // Split into individual tasks for maximum initial parallelism + for (long i = restriction.getFrom(); i < restriction.getTo(); i++) { + out.output(new OffsetRange(i, i + 1)); + } + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ResolveChanges.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ResolveChanges.java new file mode 100644 index 000000000000..f2a4c6ebe832 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/ResolveChanges.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.iceberg.cdc; + +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +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.transforms.DoFn; +import org.apache.beam.sdk.transforms.join.CoGbkResult; +import org.apache.beam.sdk.util.RowFilter; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.apache.iceberg.types.Types; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.joda.time.Instant; + +/** + * Receives a {@link CoGbkResult} containing inserts and deletes sharing the same snapshot sequence + * number and Primary Key, and uses {@link CdcResolver} to identify logical updates. + */ +class ResolveChanges extends DoFn, Row> { + static final TupleTag DELETES = new TupleTag<>() {}; + static final TupleTag INSERTS = new TupleTag<>() {}; + private final IcebergScanConfig scanConfig; + private final RowFilter rowFilter; + private final Schema outputSchema; + // Positions and types of the non-PK data fields in the input row schema, precomputed once so + // the per-record hash/equals loops need no name lookups. The input schema is fixed by the + // CoGroupByKey's coder, so positions are stable across elements. + private final int[] nonPkIndices; + private final Schema.FieldType[] nonPkTypes; + private transient @MonotonicNonNull RowResolver resolver; + + ResolveChanges(IcebergScanConfig scanConfig) { + this.scanConfig = scanConfig; + Schema inputSchema = + CdcOutputUtils.readBeamSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), scanConfig.getSchema()); + this.rowFilter = + new RowFilter(inputSchema) + .keep( + CdcOutputUtils.readSchemaWithRowMetadata( + scanConfig.getMetadataColumns(), scanConfig.getProjectedSchema()) + .columns().stream() + .map(Types.NestedField::name) + .collect(Collectors.toList())); + this.outputSchema = + CdcOutputUtils.outputSchema( + scanConfig, + IcebergUtils.icebergSchemaToBeamSchema( + scanConfig.getProjectedSchema(), scanConfig.getUpdateCompatibilityVersion())); + + Set pkFields = new HashSet<>(scanConfig.rowIdBeamSchema().getFieldNames()); + List metadataColumns = scanConfig.getMetadataColumns(); + List indices = new ArrayList<>(); + List types = new ArrayList<>(); + List fields = inputSchema.getFields(); + for (int i = 0; i < fields.size(); i++) { + Schema.Field field = fields.get(i); + String name = field.getName(); + if (pkFields.contains(name) + || (IcebergCdcMetadataColumns.isSupportedColumn(name) + && metadataColumns.contains(name))) { + continue; + } + indices.add(i); + types.add(field.getType()); + } + this.nonPkIndices = indices.stream().mapToInt(Integer::intValue).toArray(); + this.nonPkTypes = types.toArray(new Schema.FieldType[0]); + } + + @Setup + public void setup() { + this.resolver = new RowResolver(nonPkIndices, nonPkTypes); + } + + @ProcessElement + public void processElement( + @Element KV element, + @Timestamp Instant timestamp, + OutputReceiver out) { + CdcRowDescriptor descriptor = element.getKey(); + CoGbkResult result = element.getValue(); + + // should be okay to materialize these lists. a PK collision will likely be a handful of records + // at most + List deletes = Lists.newArrayList(result.getAll(DELETES)); + List inserts = Lists.newArrayList(result.getAll(INSERTS)); + + checkStateNotNull(resolver) + .resolve( + deletes, + inserts, + (kind, row) -> { + Row projectedRow = rowFilter.filter(row); + out.builder( + CdcOutputUtils.outputRow( + scanConfig.getMetadataColumns(), + outputSchema, + descriptor.getCommitSnapshotId(), + descriptor.getSnapshotSequenceNumber(), + kind, + projectedRow)) + .setValueKind(kind) + .setTimestamp(timestamp) + .output(); + }); + } + + /** Resolver specialization over Beam Rows, using precomputed non-PK field positions. */ + private static final class RowResolver extends CdcResolver { + private final int[] nonPkIndices; + private final Schema.FieldType[] nonPkTypes; + + RowResolver(int[] nonPkIndices, Schema.FieldType[] nonPkTypes) { + this.nonPkIndices = nonPkIndices; + this.nonPkTypes = nonPkTypes; + } + + @Override + protected int nonPkHash(Row element) { + int hash = 1; + for (int i = 0; i < nonPkIndices.length; i++) { + hash = + 31 * hash + Row.Equals.deepHashCode(element.getValue(nonPkIndices[i]), nonPkTypes[i]); + } + return hash; + } + + @Override + protected boolean nonPkEquals(Row delete, Row insert) { + // compare non-PK, we already know PK values are equal + for (int i = 0; i < nonPkIndices.length; i++) { + int idx = nonPkIndices[i]; + // return early if two values are not equal + if (!Row.Equals.deepEquals(insert.getValue(idx), delete.getValue(idx), nonPkTypes[i])) { + return false; + } + } + return true; + } + } +} 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..3410c0a9d7ee --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTask.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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, PartitionSpec spec, boolean includeMetrics) { + return setDataFile(SerializableDataFile.from(df, spec, 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, 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/SnapshotWindowFn.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SnapshotWindowFn.java new file mode 100644 index 000000000000..06dbc1740bdb --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SnapshotWindowFn.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.sdk.io.iceberg.cdc; + +import java.util.Collection; +import java.util.Collections; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.NonMergingWindowFn; +import org.apache.beam.sdk.transforms.windowing.WindowFn; +import org.apache.beam.sdk.transforms.windowing.WindowMappingFn; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.joda.time.Instant; + +/** + * A {@link WindowFn} that assigns each element to a 1-millisecond {@link IntervalWindow} anchored + * at the element's event timestamp. + * + *

We set the element's timestamp as its snapshot commit timestamp. All tasks/records from the + * same snapshot land in the same window. + * + *

With the per-snapshot watermark from {@link WatchForSnapshotsSdf}, the CoGroupByKey fires when + * a snapshot is fully drained. The watermark advances past the snapshot's commit time only after + * every downstream stage has finished processing that snapshot's records. + * + *

Two snapshots committed within the same millisecond may collapse into the same window. But + * that's okay because {@link ReadFromChangelogs} includes snapshot sequence number in the key + * before routing to the CoGBK, so it won't produce incorrect joins. + */ +public class SnapshotWindowFn extends NonMergingWindowFn { + private static final Duration WINDOW_LENGTH = Duration.millis(1); + + @Override + public Collection assignWindows(AssignContext c) { + Instant ts = c.timestamp(); + return Collections.singletonList(new IntervalWindow(ts, ts.plus(WINDOW_LENGTH))); + } + + @Override + public boolean isCompatible(WindowFn other) { + return other instanceof SnapshotWindowFn; + } + + @Override + public Coder windowCoder() { + return IntervalWindow.getCoder(); + } + + @Override + public WindowMappingFn getDefaultWindowMappingFn() { + // Just return a window covering the main-input window's end timestamp. + return new WindowMappingFn<>() { + @Override + public IntervalWindow getSideInputWindow(BoundedWindow mainWindow) { + Instant end = mainWindow.maxTimestamp(); + return new IntervalWindow(end, end.plus(WINDOW_LENGTH)); + } + }; + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof SnapshotWindowFn; + } + + @Override + public int hashCode() { + return SnapshotWindowFn.class.hashCode(); + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/WatchForSnapshotsSdf.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/WatchForSnapshotsSdf.java new file mode 100644 index 000000000000..658c019b30e7 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/WatchForSnapshotsSdf.java @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.ReadUtils; +import org.apache.beam.sdk.io.iceberg.SnapshotInfo; +import org.apache.beam.sdk.io.iceberg.TableCache; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.metrics.Counter; +import org.apache.beam.sdk.metrics.Gauge; +import org.apache.beam.sdk.metrics.Metrics; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.splittabledofn.GrowableOffsetRangeTracker; +import org.apache.beam.sdk.transforms.splittabledofn.ManualWatermarkEstimator; +import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimators.Manual; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.util.Preconditions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.Table; +import org.apache.iceberg.TableUtil; +import org.apache.iceberg.util.SnapshotUtil; +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; + +/** + * SplittableDoFn that watches an Iceberg table for new {@link Snapshot}s and emits them one at a + * time, advancing the watermark per snapshot. Each snapshot is later processed by {@link + * ChangelogScanner}. + * + *

The restriction tracks Snapshots via their sequence numbers, which are monotonic, + * unlike snapshot IDs. The initial range starts at the sequence number of the user-configured + * starting snapshot (or one if none configured) and runs to {@link Long#MAX_VALUE}. Each call to + * {@code @ProcessElement} claims the sequence numbers of newly discovered snapshots in + * chronological order. + * + *

Uses a {@link Manual} watermark estimator. After emitting a snapshot, the watermark is set + * just past that snapshot's timestamp, so its {@link SnapshotWindowFn} window can fire as + * soon as its records drain. On empty polls, the watermark is bumped to {@code now() - + * pollInterval} to prevent downstream windows from stalling indefinitely during quiet periods. + * + *

If a snapshot is somehow discovered after the watermark has already moved past its commit time + * (e.g. idle bump ran ahead of a slow/downed catalog), it will be emitted with its timestamp + * clamped to the current watermark instead. This guarantees that no snapshot is ever late, and no + * records are silently dropped. + */ +@DoFn.UnboundedPerElement +class WatchForSnapshotsSdf extends DoFn { + private static final Logger LOG = LoggerFactory.getLogger(WatchForSnapshotsSdf.class); + private static final Duration DEFAULT_POLL_INTERVAL = Duration.standardSeconds(60); + private static final long DEFAULT_STARTING_SEQ = 1L; + + private static final Counter snapshotsEmitted = + Metrics.counter(WatchForSnapshotsSdf.class, "snapshotsEmitted"); + private static final Counter lateDiscoveredSnapshots = + Metrics.counter(WatchForSnapshotsSdf.class, "lateDiscoveredSnapshots"); + private static final Gauge latestEmittedSnapshotId = + Metrics.gauge(WatchForSnapshotsSdf.class, "latestEmittedSnapshotId"); + private static final Long POLL_FOREVER = Long.MAX_VALUE; + + private final IcebergScanConfig scanConfig; + private final Duration pollInterval; + + WatchForSnapshotsSdf(IcebergScanConfig scanConfig) { + this.scanConfig = scanConfig; + this.pollInterval = + MoreObjects.firstNonNull(scanConfig.getPollInterval(), DEFAULT_POLL_INTERVAL); + } + + @GetInitialRestriction + public OffsetRange initialRestriction() { + Table table = + TableCache.getRefreshed(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); + checkArgument( + TableUtil.formatVersion(table) >= 2, + "Reading CDC records from an Iceberg table requires a table version > 2, " + + "but table %s has version %s", + scanConfig.getTableIdentifier(), + TableUtil.formatVersion(table)); + + long toSnapshotExclusiveSeq = POLL_FOREVER; + @Nullable Long toSnapshotId = ReadUtils.getToSnapshot(table, scanConfig); + if (toSnapshotId != null) { + toSnapshotExclusiveSeq = + Preconditions.checkStateNotNull( + table.snapshot(toSnapshotId), + "Configured end snapshot %s does not exist", + toSnapshotId) + .sequenceNumber() + + 1; + } + + @Nullable Long fromSnapshotInclusiveId = ReadUtils.getFromSnapshotInclusive(table, scanConfig); + long fromSnapshotInclusiveSeq; + if (fromSnapshotInclusiveId == null) { + fromSnapshotInclusiveSeq = DEFAULT_STARTING_SEQ; // sequence numbers start at 1 + } else { + Snapshot fromSnapshotInclusive = + Preconditions.checkArgumentNotNull( + table.snapshot(fromSnapshotInclusiveId), + "The specified starting snapshot %s does not exist", + fromSnapshotInclusiveId); + fromSnapshotInclusiveSeq = fromSnapshotInclusive.sequenceNumber(); + + boolean sameLineage = + toSnapshotId == null + ? SnapshotUtil.isAncestorOf(table, fromSnapshotInclusiveId) + : SnapshotUtil.isAncestorOf(table, toSnapshotId, fromSnapshotInclusiveId); + checkArgument( + sameLineage, + "Configured starting snapshot %s is not an ancestor of %s", + fromSnapshotInclusiveId, + toSnapshotId == null ? "the current table" : "end snapshot " + toSnapshotId); + } + + return new OffsetRange( + fromSnapshotInclusiveSeq, Math.max(fromSnapshotInclusiveSeq, toSnapshotExclusiveSeq)); + } + + @NewTracker + public RestrictionTracker newTracker(@Restriction OffsetRange restriction) { + if (restriction.getTo() == POLL_FOREVER) { + return new GrowableOffsetRangeTracker( + restriction.getFrom(), this::estimateCurrentRangeEndExclusive); + } + + return new OffsetRangeTracker(restriction); + } + + private long estimateCurrentRangeEndExclusive() { + Table table = + TableCache.getAndRefreshIfStale( + scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); + + @Nullable Long toSnapshotId = ReadUtils.getToSnapshot(table, scanConfig); + if (toSnapshotId != null) { + @Nullable Snapshot toSnapshot = table.snapshot(toSnapshotId); + return toSnapshot == null ? DEFAULT_STARTING_SEQ : toSnapshot.sequenceNumber() + 1; + } + + @Nullable Snapshot current = table.currentSnapshot(); + return current == null ? DEFAULT_STARTING_SEQ : current.sequenceNumber() + 1; + } + + @GetRestrictionCoder + public Coder restrictionCoder() { + return new OffsetRange.Coder(); + } + + @GetInitialWatermarkEstimatorState + public Instant initialWatermarkState() { + return BoundedWindow.TIMESTAMP_MIN_VALUE; + } + + @NewWatermarkEstimator + public ManualWatermarkEstimator newWatermarkEstimator( + @WatermarkEstimatorState Instant state) { + return new Manual(state); + } + + @ProcessElement + public ProcessContinuation process( + RestrictionTracker tracker, + ManualWatermarkEstimator watermark, + OutputReceiver out) { + Table table = + TableCache.getRefreshed(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); + + @Nullable Long userToSnapshotId = ReadUtils.getToSnapshot(table, scanConfig); + boolean bounded = userToSnapshotId != null; + + @Nullable Snapshot current = table.currentSnapshot(); + if (current == null) { + // no snapshots yet. + LOG.info("Skipping scan: table is empty with no snapshots yet"); + return pauseOrStop(watermark, bounded); + } + + // Resolve the upper bound: user-specified bounded mode, or "current" for unbounded. + long toSnapshotId; + long toSnapshotSeq; + if (userToSnapshotId != null) { + toSnapshotId = userToSnapshotId; + toSnapshotSeq = + Preconditions.checkStateNotNull( + table.snapshot(userToSnapshotId), + "Configured toSnapshotId %s does not exist", + userToSnapshotId) + .sequenceNumber(); + } else { + toSnapshotId = current.snapshotId(); + toSnapshotSeq = current.sequenceNumber(); + } + + long nextSeqInclusive = tracker.currentRestriction().getFrom(); + if (toSnapshotSeq < nextSeqInclusive) { + // Nothing new since last poll. + LOG.info("Skipping scan: nothing new since last poll."); + return pauseOrStop(watermark, bounded); + } + + // Collect snapshots in [nextSeqInclusive, toSnapshotSeq] chronologically + String tableId = scanConfig.getTableIdentifier(); + List fresh = snapshotsAfter(table, tableId, nextSeqInclusive, toSnapshotId); + LOG.info("Collected snapshots: {}", fresh); + + for (SnapshotInfo snap : fresh) { + if (!tracker.tryClaim(snap.getSequenceNumber())) { + return ProcessContinuation.stop(); + } + Instant commitTs = Instant.ofEpochMilli(snap.getTimestampMillis()); + Instant ts = commitTs; + if (ts.isBefore(watermark.currentWatermark())) { + // The watermark already moved past this snapshot's commit time (e.g. the idle bump ran + // ahead of a slow discovery). Use the current watermark so the snapshot is not dropped + ts = watermark.currentWatermark(); + lateDiscoveredSnapshots.inc(); + LOG.warn( + "Snapshot {} (commit ts: {}) was discovered after the watermark already advanced " + + "to {}. Emitting it with the current watermark.", + snap.getSnapshotId(), + commitTs, + ts); + } + out.outputWithTimestamp(snap.getSnapshotId(), ts); + + // Advance just past `ts` so this snapshot's window can fire as + // soon as its records drain + watermark.setWatermark(ts.plus(Duration.millis(1))); + snapshotsEmitted.inc(); + latestEmittedSnapshotId.set(snap.getSnapshotId()); + LOG.info( + "Emitted snapshot {} (sequence id: {}, timestamp: {})", + snap.getSnapshotId(), + snap.getSequenceNumber(), + ts); + } + + return continueOrStop(bounded); + } + + /** + * On an empty poll, bump the watermark to {@code now() - pollInterval} so downstream windows and + * timers can make progress while the table is quiet. Returns {@code stop()} when end snapshot has + * been reached, otherwise {@code resume()} after the poll interval. + */ + private ProcessContinuation pauseOrStop( + ManualWatermarkEstimator watermark, boolean bounded) { + Instant idleWatermark = Instant.now().minus(pollInterval); + if (watermark.currentWatermark().isBefore(idleWatermark)) { + LOG.info( + "Sitting idle for {} seconds. Bumping watermark to {}", + TimeUnit.MILLISECONDS.toSeconds( + Instant.now().getMillis() - watermark.currentWatermark().getMillis()), + idleWatermark); + watermark.setWatermark(idleWatermark); + } + return continueOrStop(bounded); + } + + private ProcessContinuation continueOrStop(boolean bounded) { + return bounded + ? ProcessContinuation.stop() + : ProcessContinuation.resume().withResumeDelay(pollInterval); + } + + /** + * Returns snapshots with sequence number in {@code [nextSeqInclusive, toSnapshotSeq]}, keyed off + * the lineage ending at {@code toSnapshotId}. + */ + @SuppressWarnings("return") // ancestorsOf accepts null returns as a "stop" signal + static List snapshotsAfter( + Table table, String tableIdentifier, long nextSeqInclusive, long toSnapshotId) { + + List snapshots = new ArrayList<>(); + // ancestorsOf returns an iterable of snapshots looking backwards. + // we'll need to reverse it to process snapshots chronologically. + for (Snapshot snapshot : + SnapshotUtil.ancestorsOf( + toSnapshotId, snapshotId -> snapshotId != null ? table.snapshot(snapshotId) : null)) { + if (snapshot.sequenceNumber() < nextSeqInclusive) { + break; + } + snapshots.add(SnapshotInfo.fromSnapshot(snapshot, tableIdentifier)); + } + Collections.reverse(snapshots); + return snapshots; + } +} diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/datagenerator/package-info.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java similarity index 90% rename from it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/datagenerator/package-info.java rename to sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java index d563025ebeb4..8285d91689be 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/datagenerator/package-info.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java @@ -16,5 +16,5 @@ * limitations under the License. */ -/** Data generator for load tests. */ -package org.apache.beam.it.gcp.datagenerator; +/** Iceberg CDC connectors. */ +package org.apache.beam.sdk.io.iceberg.cdc; diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BeamBaseIncrementalChangelogScan.java b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BeamBaseIncrementalChangelogScan.java new file mode 100644 index 000000000000..a7fe19218c1e --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BeamBaseIncrementalChangelogScan.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 BeamBaseIncrementalChangelogScan + 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(BeamBaseIncrementalChangelogScan.class); + + public BeamBaseIncrementalChangelogScan(Table table) { + this(table, table.schema(), TableScanContext.empty()); + } + + private BeamBaseIncrementalChangelogScan(Table table, Schema schema, TableScanContext context) { + super(table, schema, context); + } + + @Override + protected IncrementalChangelogScan newRefinedScan( + Table newTable, Schema newSchema, TableScanContext newContext) { + return new BeamBaseIncrementalChangelogScan(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/AddFilesIT.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java index 5ecf14556476..528d6453eac2 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java @@ -68,6 +68,7 @@ import org.apache.beam.sdk.transforms.Filter; import org.apache.beam.sdk.transforms.JsonToRow; import org.apache.beam.sdk.transforms.MapElements; +import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PCollectionRowTuple; import org.apache.beam.sdk.values.Row; @@ -125,8 +126,7 @@ public class AddFilesIT { "warehouse", WAREHOUSE, "header.x-goog-user-project", PROJECT, "rest.auth.type", "google", - "io-impl", "org.apache.iceberg.gcp.gcs.GCSFileIO", - "rest-metrics-reporting-enabled", "false"); + "io-impl", "org.apache.iceberg.gcp.gcs.GCSFileIO"); private Storage storage; private PubsubClient pubsub; private Notification notification; @@ -246,10 +246,13 @@ public void testStreamingImportFromExistingIcebergTable() // first create a source iceberg table catalog.createTable(srcTableId, beamSchemaToIcebergSchema(ROW_SCHEMA), SPEC); - String filter = format("%s/%s/data/", namespace, srcTableName); + // BigLake may write under {namespace}/{table}/{id}/data/... rather than the Hive-style + // {namespace}/{table}/data/... layout, so match the table prefix and a /data/ segment. + String tablePrefix = format("%s/%s/", namespace, srcTableName); // build AddFiles pipeline and let it run in the background - PipelineResult addFilesPipeline = startAddFilesListener(filter); + PipelineResult addFilesPipeline = + startAddFilesListener(name -> name.contains(tablePrefix) && name.contains("/data/")); // before writing, confirm the destination table still does not exist assertFalse(catalog.tableExists(destTableId)); @@ -503,6 +506,11 @@ private boolean checkTableHasRegisteredParquetFiles(List parquetFiles) { } private PipelineResult startAddFilesListener(String filter) throws InterruptedException { + return startAddFilesListener(name -> name.contains(filter)); + } + + private PipelineResult startAddFilesListener( + SerializableFunction objectNameFilter) throws InterruptedException { DirectOptions options = TestPipeline.testingPipelineOptions().as(DirectOptions.class); options.setBlockOnRun(false); Pipeline p = Pipeline.create(options); @@ -510,7 +518,7 @@ private PipelineResult startAddFilesListener(String filter) throws InterruptedEx PCollectionRowTuple tuple = p.apply(PubsubIO.readStrings().fromTopic(notificationsTopic)) .apply(JsonToRow.withSchema(NOTIFICATION_SCHEMA)) - .apply(Filter.by(row -> row.getString("name").contains(filter))) + .apply(Filter.by(row -> objectNameFilter.apply(row.getString("name")))) .apply( MapElements.into(strings()) .via( 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 index bd8cead72987..f361bc94c15b 100644 --- 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 @@ -85,6 +85,8 @@ public class BeamRowWrapperTest { 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 Instant TEST_MICROS_INSTANT = + Instant.ofEpochSecond(1_609_459_200L, 123_456_000); private static final Row NESTED_ROW = Row.withSchema(NESTED_BEAM_SCHEMA).addValue(999).build(); private static final Row ROW = Row.withSchema(BEAM_SCHEMA) @@ -99,7 +101,7 @@ public class BeamRowWrapperTest { .array(), new BigDecimal("123.45"), org.joda.time.Instant.now(), - Instant.now(), + TEST_MICROS_INSTANT, LocalDateTime.now(ZoneId.systemDefault()), LocalDate.now(ZoneId.systemDefault()), LocalTime.now(ZoneId.systemDefault()), @@ -180,10 +182,7 @@ public void testDateTimeConversion() { @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)); + assertEquals(1_609_459_200_123_456L, (long) WRAPPER.get(7, Long.class)); } @Test 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..9d12389184a7 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 @@ -28,6 +28,8 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.beam.sdk.io.iceberg.cdc.IcebergCdcMetadataColumns; import org.apache.beam.sdk.managed.Managed; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.testing.PAssert; @@ -35,12 +37,17 @@ 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.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.iceberg.CatalogUtil; import org.apache.iceberg.Snapshot; import org.apache.iceberg.Table; +import org.apache.iceberg.TableProperties; 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; @@ -52,6 +59,18 @@ 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)); + + private static final org.apache.iceberg.Schema CDC_CONFIG_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.optional(3, "category", Types.StringType.get()), + Types.NestedField.required(4, "event_micros", Types.LongType.get())), + ImmutableSet.of(1)); + @Rule public TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); @Rule public TestPipeline testPipeline = TestPipeline.create(); @@ -73,6 +92,14 @@ public void testBuildTransformWithRow() { .withFieldValue("to_timestamp", 456L) .withFieldValue("starting_strategy", "earliest") .withFieldValue("poll_interval_seconds", 789) + .withFieldValue("keep", ImmutableList.of("id", "data", "event_micros")) + .withFieldValue("filter", "\"category\" = 'include'") + .withFieldValue("watermark_column", "event_micros") + .withFieldValue( + "include_metadata_columns", + ImmutableList.of( + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID, + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_SEQUENCE_NUMBER)) .build(); new IcebergCdcReadSchemaTransformProvider().from(config); @@ -83,8 +110,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 +149,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()); @@ -159,4 +186,95 @@ public void testStreamingReadUsingManagedTransform() throws Exception { testPipeline.run(); } + + @Test + public void testManagedReadWithProjectionFilterWatermarkAndSnapshotRange() throws Exception { + String identifier = "default.table_" + Long.toString(UUID.randomUUID().hashCode(), 16); + TableIdentifier tableId = TableIdentifier.parse(identifier); + + Table table = + warehouse.createTable( + tableId, CDC_CONFIG_SCHEMA, null, ImmutableMap.of(TableProperties.FORMAT_VERSION, "3")); + long eventMicros = (System.currentTimeMillis() - 1_000L) * 1_000L; + List records = + ImmutableList.of( + record(1L, "keep-a", "include", eventMicros), + record(2L, "drop", "exclude", eventMicros + 1_000L), + record(3L, "keep-b", "include", eventMicros + 2_000L)); + table + .newFastAppend() + .appendFile(warehouse.writeRecords("cdc-managed-config.parquet", table.schema(), records)) + .commit(); + + Map properties = new HashMap<>(); + properties.put("type", CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP); + properties.put("warehouse", warehouse.location); + + Map configMap = new HashMap<>(); + configMap.put("table", identifier); + configMap.put("catalog_name", "test-name"); + configMap.put("catalog_properties", properties); + configMap.put("from_snapshot", table.currentSnapshot().snapshotId()); + configMap.put("to_snapshot", table.currentSnapshot().snapshotId()); + configMap.put("keep", ImmutableList.of("id", "data", "event_micros")); + configMap.put("filter", "\"category\" = 'include'"); + configMap.put("watermark_column", "event_micros"); + configMap.put( + "include_metadata_columns", + ImmutableList.of( + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID, + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_SEQUENCE_NUMBER, + IcebergCdcMetadataColumns.ROW_ID, + IcebergCdcMetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER)); + + org.apache.iceberg.Schema projectedSchema = table.schema().select("id", "data", "event_micros"); + Schema recordSchema = IcebergUtils.icebergSchemaToBeamSchema(projectedSchema); + Schema outputSchema = + Schema.builder() + .addFields(recordSchema.getFields()) + .addInt64Field(IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID) + .addInt64Field(IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_SEQUENCE_NUMBER) + .addNullableField(IcebergCdcMetadataColumns.ROW_ID, Schema.FieldType.INT64) + .addNullableField( + IcebergCdcMetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER, Schema.FieldType.INT64) + .build(); + long snapshotId = table.currentSnapshot().snapshotId(); + long sequenceNumber = table.currentSnapshot().sequenceNumber(); + long firstRowId = table.currentSnapshot().firstRowId(); + List expectedRows = + IntStream.range(0, records.size()) + .filter(i -> "include".equals(records.get(i).getField("category"))) + .mapToObj( + i -> { + Row record = IcebergUtils.icebergRecordToBeamRow(recordSchema, records.get(i)); + return Row.withSchema(outputSchema) + .addValues( + record.getInt64("id"), + record.getString("data"), + record.getInt64("event_micros"), + snapshotId, + sequenceNumber, + firstRowId + i, + sequenceNumber) + .build(); + }) + .collect(Collectors.toList()); + + PCollection output = + testPipeline + .apply(Managed.read(Managed.ICEBERG_CDC).withConfig(configMap)) + .getSinglePCollection(); + + assertThat(output.isBounded(), equalTo(BOUNDED)); + assertThat(output.getSchema(), equalTo(outputSchema)); + PAssert.that(output).containsInAnyOrder(expectedRows); + + testPipeline.run(); + } + + private static Record record(long id, String data, String category, long eventMicros) { + return TestFixtures.createRecord( + CDC_CONFIG_SCHEMA, + ImmutableMap.of("id", id, "data", data, "category", category, "event_micros", eventMicros)); + } } 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..7920fc37c84b 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 @@ -32,7 +32,9 @@ import java.io.File; import java.io.IOException; +import java.time.OffsetDateTime; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -41,8 +43,11 @@ import java.util.stream.Stream; import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericRecord; +import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.io.iceberg.IcebergIO.ReadRows.StartingStrategy; +import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.DoFn; @@ -54,6 +59,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; @@ -82,6 +88,7 @@ import org.apache.parquet.avro.AvroParquetWriter; import org.apache.parquet.hadoop.ParquetWriter; import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.DateTime; import org.joda.time.Duration; import org.junit.ClassRule; import org.junit.Rule; @@ -119,6 +126,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 +158,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 +176,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 +194,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 +212,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 +225,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()); @@ -306,9 +347,10 @@ public void testProjectedSchemaWithNestedFields() { @Test 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); + TableIdentifier.of( + "default", "table.with.dots" + Long.toString(UUID.randomUUID().hashCode(), 16)); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + final Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); @@ -339,8 +381,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 +410,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 +434,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 +487,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 +532,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 +548,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 +654,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 +671,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 +683,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); @@ -691,12 +737,54 @@ public void testBatchReadBetweenTimestamps() throws IOException { runReadWithBoundary(false, false); } + @Test + public void testTimestampUpdateCompat() throws IOException { + String val = "2026-07-15T13:18:20.053123+03:27"; + OffsetDateTime ts = OffsetDateTime.parse(val); + + TableIdentifier tableId = + TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); + org.apache.iceberg.Schema schema = + new org.apache.iceberg.Schema( + Collections.singletonList(required(1, "ts", Types.TimestampType.withZone())), + ImmutableSet.of(1)); + Table table = warehouse.createTable(tableId, schema); + DataFile file = + warehouse.writeData( + "date.parquet", schema, Collections.singletonList(ImmutableMap.of("ts", ts))); + table.newFastAppend().appendFile(file).commit(); + + IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()).from(tableId); + if (useIncrementalScan) { + read = read.withCdc().toSnapshot(table.currentSnapshot().snapshotId()); + } + + Schema expectedBeamSchema = + Schema.builder().addLogicalTypeField("ts", Timestamp.MICROS).build(); + Row expectedRow = Row.withSchema(expectedBeamSchema).addValue(ts.toInstant()).build(); + + PCollection output = testPipeline.apply(read).apply(new PrintRow()); + PAssert.that(output).containsInAnyOrder(expectedRow); + testPipeline.run().waitUntilFinish(); + + // test again but with older versions that require primitive DATETIME type + Schema expectedLegacyBeamSchema = Schema.builder().addDateTimeField("ts").build(); + Row expectedLegacyRow = + Row.withSchema(expectedLegacyBeamSchema).addValue(DateTime.parse(val)).build(); + + Pipeline testPipeline2 = Pipeline.create(); + testPipeline2.getOptions().as(StreamingOptions.class).setUpdateCompatibilityVersion("2.75.0"); + PCollection outputLegacy = testPipeline2.apply(read).apply(new PrintRow()); + PAssert.that(outputLegacy).containsInAnyOrder(expectedLegacyRow); + testPipeline2.run().waitUntilFinish(); + } + public void runWithStartingStrategy(@Nullable StartingStrategy strategy, 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()); List> expectedRecords = warehouse.commitData(simpleTable); if ((strategy == StartingStrategy.LATEST) || (streaming && strategy == null)) { @@ -731,8 +819,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/IcebergIOWriteTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java index 52d92911f4e4..384e11b761bb 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 @@ -76,7 +76,10 @@ import org.apache.iceberg.DataFile; import org.apache.iceberg.DistributionMode; import org.apache.iceberg.FileFormat; +import org.apache.iceberg.NullOrder; import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.SortDirection; +import org.apache.iceberg.SortOrder; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.SupportsNamespaces; @@ -148,7 +151,8 @@ private IcebergIO.WriteRows applyDistribution(IcebergIO.WriteRows write) { @Test public void testSimpleAppend() throws Exception { TableIdentifier tableId = - TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); + TableIdentifier.of( + "default", "table.with.dots" + Long.toString(UUID.randomUUID().hashCode(), 16)); Map catalogProps = ImmutableMap.builder() @@ -774,4 +778,55 @@ public void process(@Element KV>> sums) { .getCommitted(); assertEquals(5L, numWaves); } + + @Test + public void testCreateTableWithPartitionSpecAndSortOrder() { + TableIdentifier tableId = + TableIdentifier.of( + "default", "sorted_partitioned_" + Long.toString(UUID.randomUUID().hashCode(), 16)); + + Schema beamSchema = IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + + 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(); + + testPipeline + .apply("Records To Add", Create.of(TestFixtures.asRows(TestFixtures.FILE1SNAPSHOT1))) + .setRowSchema(beamSchema) + .apply( + "Append To Table", + writeTransform(catalog, tableId) + .withPartitionFields(ImmutableList.of("bucket(id, 2)")) + .withSortOrder(ImmutableList.of("data asc nulls first"))); + + testPipeline.run().waitUntilFinish(); + + Table table = warehouse.loadTable(tableId); + + // verify partition spec + PartitionSpec spec = table.spec(); + assertTrue(spec.isPartitioned()); + assertEquals(1, spec.fields().size()); + assertEquals("id_bucket", spec.fields().get(0).name()); + + // verify sort order + SortOrder sortOrder = table.sortOrder(); + assertTrue(sortOrder.isSorted()); + assertEquals(1, sortOrder.fields().size()); + assertEquals(SortDirection.ASC, sortOrder.fields().get(0).direction()); + assertEquals(NullOrder.NULLS_FIRST, sortOrder.fields().get(0).nullOrder()); + + // verify data was written correctly + List writtenRecords = ImmutableList.copyOf(IcebergGenerics.read(table).build()); + assertThat(writtenRecords, Matchers.containsInAnyOrder(TestFixtures.FILE1SNAPSHOT1.toArray())); + } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfigTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfigTest.java new file mode 100644 index 000000000000..e928d3cf8f01 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfigTest.java @@ -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. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.apache.beam.sdk.io.iceberg.cdc.IcebergCdcMetadataColumns; +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.types.Types; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link IcebergScanConfig}. */ +public class IcebergScanConfigTest { + @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); + + 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()), + Types.NestedField.optional(3, "category", Types.StringType.get()), + Types.NestedField.required(4, "event_time", Types.TimestampType.withoutZone()), + Types.NestedField.required(5, "event_micros", Types.LongType.get()), + Types.NestedField.optional(6, "optional_time", Types.TimestampType.withoutZone()), + Types.NestedField.required(7, "required_text", Types.StringType.get()), + Types.NestedField.required( + 8, + "nested", + Types.StructType.of( + Types.NestedField.required( + 9, "nested_time", Types.TimestampType.withoutZone())))), + ImmutableSet.of(1)); + + @Rule public TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); + + @Test + public void cdcValidationRequiresIdentifierFields() { + TableIdentifier tableId = uniqueTableId(); + Table table = warehouse.createTable(tableId, TestFixtures.SCHEMA); + IcebergScanConfig scanConfig = + scanConfigBuilder(tableId, TestFixtures.SCHEMA).setUseCdc(true).build(); + + IllegalStateException thrown = + assertThrows(IllegalStateException.class, () -> scanConfig.validate(table)); + assertThat(thrown.getMessage(), containsString("Cannot read CDC records")); + assertThat(thrown.getMessage(), containsString("primary key fields")); + } + + @Test + public void cdcValidationRejectsProjectionDroppingIdentifierFields() { + TableIdentifier tableId = uniqueTableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + + IcebergScanConfig keepWithoutPk = + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(true) + .setKeepFields(ImmutableList.of("data")) + .build(); + IllegalArgumentException keepException = + assertThrows(IllegalArgumentException.class, () -> keepWithoutPk.validate(table)); + assertThat( + keepException.getMessage(), + containsString("projected schema must not drop primary key fields")); + + IcebergScanConfig dropPk = + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(true) + .setDropFields(ImmutableList.of("id")) + .build(); + IllegalArgumentException dropException = + assertThrows(IllegalArgumentException.class, () -> dropPk.validate(table)); + assertThat( + dropException.getMessage(), + containsString("projected schema must not drop primary key fields")); + } + + @Test + public void requiredSchemaIncludesFilterOnlyFieldsWithoutChangingProjection() { + TableIdentifier tableId = uniqueTableId(); + warehouse.createTable(tableId, CDC_SCHEMA); + IcebergScanConfig scanConfig = + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(true) + .setKeepFields(ImmutableList.of("id")) + .setFilterString("\"data\" = 'keep' AND \"category\" = 'include'") + .build(); + + assertEquals(ImmutableList.of("id"), fieldNames(scanConfig.getProjectedSchema())); + assertEquals( + ImmutableSet.of("id", "data", "category"), + ImmutableSet.copyOf(fieldNames(scanConfig.getRequiredSchema()))); + } + + @Test + public void metadataColumnsRequireCdcMode() { + TableIdentifier tableId = uniqueTableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + IcebergScanConfig scanConfig = + scanConfigBuilder(tableId, CDC_SCHEMA) + .setMetadataColumns(ImmutableList.of(IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID)) + .build(); + + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> scanConfig.validate(table)); + assertThat(thrown.getMessage(), containsString("metadata_columns")); + } + + @Test + public void metadataColumnsRejectUnsupportedAndDuplicateNames() { + TableIdentifier tableId = uniqueTableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + + IcebergScanConfig unsupported = + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(true) + .setMetadataColumns(ImmutableList.of("_missing_metadata")) + .build(); + IllegalArgumentException unsupportedThrown = + assertThrows(IllegalArgumentException.class, () -> unsupported.validate(table)); + assertThat(unsupportedThrown.getMessage(), containsString("unsupported metadata_columns")); + + IcebergScanConfig duplicate = + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(true) + .setMetadataColumns( + ImmutableList.of( + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID, + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID)) + .build(); + IllegalArgumentException duplicateThrown = + assertThrows(IllegalArgumentException.class, () -> duplicate.validate(table)); + assertThat(duplicateThrown.getMessage(), containsString("duplicate")); + } + + @Test + public void rowLineageMetadataRequiresFormatV3Table() { + TableIdentifier tableId = uniqueTableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + IcebergScanConfig scanConfig = + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(true) + .setMetadataColumns(ImmutableList.of(IcebergCdcMetadataColumns.ROW_ID)) + .build(); + + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> scanConfig.validate(table)); + assertThat(thrown.getMessage(), containsString("format v3+")); + } + + @Test + public void watermarkColumnAcceptsRequiredTimestampAndLongColumns() { + TableIdentifier tableId = uniqueTableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(true) + .setKeepFields(ImmutableList.of("id", "event_time")) + .setWatermarkColumn("event_time") + .build() + .validate(table); + + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(true) + .setKeepFields(ImmutableList.of("id", "event_micros")) + .setWatermarkColumn("event_micros") + .build() + .validate(table); + } + + @Test + public void watermarkColumnRejectsInvalidConfigurations() { + TableIdentifier tableId = uniqueTableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + + assertInvalidWatermark( + tableId, table, "event_time", false, ImmutableList.of("id", "event_time"), "CDC mode"); + assertInvalidWatermark( + tableId, table, "missing", true, ImmutableList.of("id"), "unknown column"); + assertInvalidWatermark( + tableId, + table, + "optional_time", + true, + ImmutableList.of("id", "optional_time"), + "non-nullable"); + assertInvalidWatermark( + tableId, + table, + "required_text", + true, + ImmutableList.of("id", "required_text"), + "must be a timestamp-typed column"); + assertInvalidWatermark( + tableId, table, "event_time", true, ImmutableList.of("id"), "should not be dropped"); + } + + private void assertInvalidWatermark( + TableIdentifier tableId, + Table table, + String watermarkColumn, + boolean useCdc, + List keepFields, + String expectedMessage) { + IcebergScanConfig scanConfig = + scanConfigBuilder(tableId, CDC_SCHEMA) + .setUseCdc(useCdc) + .setKeepFields(keepFields) + .setWatermarkColumn(watermarkColumn) + .build(); + + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> scanConfig.validate(table)); + assertThat(thrown.getMessage(), containsString(expectedMessage)); + } + + private IcebergScanConfig.Builder scanConfigBuilder( + TableIdentifier tableId, org.apache.iceberg.Schema schema) { + return IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties( + ImmutableMap.of( + "type", + CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP, + "warehouse", + warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(schema)); + } + + private static List fieldNames(org.apache.iceberg.Schema schema) { + return schema.columns().stream().map(Types.NestedField::name).collect(Collectors.toList()); + } + + private static TableIdentifier uniqueTableId() { + return TableIdentifier.of( + "default", "table_" + Long.toString(UUID.randomUUID().hashCode(), 16)); + } +} 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..675b4aafe76a 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,9 @@ 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'") + .withFieldValue("watermark_column", "event_micros") .build(); @Test @@ -269,7 +284,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 +301,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 +346,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/IcebergUtilsTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergUtilsTest.java index c9026522dba3..80e5e2195f98 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergUtilsTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergUtilsTest.java @@ -19,11 +19,15 @@ import static org.apache.beam.sdk.io.iceberg.IcebergUtils.TypeAndMaxId; import static org.apache.beam.sdk.io.iceberg.IcebergUtils.beamFieldTypeToIcebergFieldType; +import static org.apache.beam.sdk.io.iceberg.IcebergUtils.parseTableIdentifier; +import static org.apache.beam.sdk.io.iceberg.IcebergUtils.tableIdentifierToString; import static org.apache.iceberg.types.Types.NestedField.optional; import static org.apache.iceberg.types.Types.NestedField.required; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.math.BigDecimal; @@ -38,12 +42,14 @@ import org.apache.beam.sdk.schemas.logicaltypes.FixedPrecisionNumeric; import org.apache.beam.sdk.schemas.logicaltypes.FixedString; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; import org.apache.beam.sdk.schemas.logicaltypes.UuidLogicalType; import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; import org.apache.beam.sdk.schemas.logicaltypes.VariableString; import org.apache.beam.sdk.values.Row; 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.TableIdentifier; import org.apache.iceberg.data.GenericRecord; import org.apache.iceberg.data.Record; import org.apache.iceberg.types.Type; @@ -60,6 +66,65 @@ @RunWith(Enclosed.class) public class IcebergUtilsTest { + @RunWith(JUnit4.class) + public static class TableIdentifierTests { + + @Test + public void parseTableIdentifierParsesJson() { + TableIdentifier identifier = + parseTableIdentifier( + " {\"namespace\": [\"dogs\", \"owners.and.handlers\"]," + + " \"name\": \"food.with.dots\"}"); + + assertArrayEquals( + new String[] {"dogs", "owners.and.handlers"}, identifier.namespace().levels()); + assertEquals("food.with.dots", identifier.name()); + } + + @Test + public void parseTableIdentifierParsesLegacyDottedString() { + TableIdentifier identifier = parseTableIdentifier("dogs.owners.and.handlers.food"); + + assertArrayEquals( + new String[] {"dogs", "owners", "and", "handlers"}, identifier.namespace().levels()); + assertEquals("food", identifier.name()); + } + + @Test + public void tableIdentifierToStringRoundTripsSpecialCharacters() { + TableIdentifier expected = + TableIdentifier.of("dogs", "owners.and.handlers", "food.with.dots"); + + assertEquals(expected, parseTableIdentifier(tableIdentifierToString(expected))); + } + + @Test + public void tableIdentifierToStringUsesLegacyFormWhenUnambiguous() { + assertEquals("dogs.food", tableIdentifierToString(TableIdentifier.of("dogs", "food"))); + } + + @Test + public void tableIdentifierToStringUsesJsonForLegacyStringsThatLookLikeJson() { + TableIdentifier expected = TableIdentifier.of("{dogs}", "{food}"); + + assertEquals(expected, parseTableIdentifier(tableIdentifierToString(expected))); + } + + @Test + public void tableIdentifierToStringDoesNotUseJsonForPartialJsonLikeStrings() { + TableIdentifier expected = TableIdentifier.of("{dogs}", "food"); + + assertEquals("{dogs}.food", tableIdentifierToString(expected)); + assertEquals(expected, parseTableIdentifier(tableIdentifierToString(expected))); + } + + @Test + public void parseTableIdentifierRejectsInvalidJsonIdentifier() { + assertThrows( + IllegalArgumentException.class, () -> parseTableIdentifier("{\"table_name\":\"food\"}")); + } + } + @RunWith(JUnit4.class) public static class RowToRecordTests { /** @@ -168,6 +233,13 @@ public void testTimestampWithZone() { OffsetDateTime offsetDateTime = OffsetDateTime.parse(val); LocalDateTime localDateTime = offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); + // Timestamp.MICROS + checkRowValueToRecordValue( + Schema.FieldType.logicalType(Timestamp.MICROS), + offsetDateTime.toInstant(), + Types.TimestampType.withZone(), + offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC)); + // SqlTypes.DATETIME checkRowValueToRecordValue( Schema.FieldType.logicalType(SqlTypes.DATETIME), @@ -198,7 +270,10 @@ public void testTimestampWithZone() { } @Test - public void testFixed() {} + public void testFixed() { + byte[] bytes = new byte[] {1, 2, 3, 4}; + checkRowValueToRecordValue(Schema.FieldType.BYTES, bytes, Types.FixedType.ofLength(4), bytes); + } @Test public void testBinary() { @@ -362,6 +437,24 @@ public void testTimestampWithZone() { OffsetDateTime offsetDateTime = OffsetDateTime.parse(timestamp); LocalDateTime localDateTime = offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime(); + + // Timestamp.MICROS + checkRecordValueToRowValue( + Types.TimestampType.withZone(), + offsetDateTime, + Schema.FieldType.logicalType(Timestamp.MICROS), + offsetDateTime.toInstant()); + checkRecordValueToRowValue( + Types.TimestampType.withZone(), + DateTimeUtil.microsFromTimestamptz(offsetDateTime), + Schema.FieldType.logicalType(Timestamp.MICROS), + offsetDateTime.toInstant()); + checkRecordValueToRowValue( + Types.TimestampType.withZone(), + timestamp, + Schema.FieldType.logicalType(Timestamp.MICROS), + offsetDateTime.toInstant()); + // SqlTypes.DATETIME checkRecordValueToRowValue( Types.TimestampType.withZone(), @@ -395,7 +488,25 @@ public void testTimestampWithZone() { } @Test - public void testFixed() {} + public void testUpdateCompatibilityVersionGatesTimestamptzMapping() { + org.apache.iceberg.Schema icebergSchema = + new org.apache.iceberg.Schema(required(0, "ts", Types.TimestampType.withZone())); + + // A pinned, older update-compatibility version keeps the legacy DATETIME mapping on read. + Schema pinnedOld = IcebergUtils.icebergSchemaToBeamSchema(icebergSchema, "2.50.0"); + assertEquals(Schema.FieldType.DATETIME, pinnedOld.getField("ts").getType()); + + // An unset (null) or future update-compatibility version uses the new micros mapping. + Schema unpinned = IcebergUtils.icebergSchemaToBeamSchema(icebergSchema, null); + assertEquals( + Schema.FieldType.logicalType(Timestamp.MICROS), unpinned.getField("ts").getType()); + } + + @Test + public void testFixed() { + byte[] bytes = new byte[] {1, 2, 3, 4}; + checkRecordValueToRowValue(Types.FixedType.ofLength(4), bytes, Schema.FieldType.BYTES, bytes); + } @Test public void testBinary() { @@ -804,7 +915,7 @@ public void testMapBeamFieldTypeToIcebergFieldType() { .addNullableStringField("str") .addNullableBooleanField("bool") .addByteArrayField("bytes") - .addDateTimeField("datetime_tz") + .addLogicalTypeField("datetime_tz", Timestamp.MICROS) .addLogicalTypeField("datetime", SqlTypes.DATETIME) .addLogicalTypeField("time", SqlTypes.TIME) .addLogicalTypeField("date", SqlTypes.DATE) 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 c5fc5a6b6fe7..bfb762f8e89d 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 @@ -25,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.Assert.assertTrue; import static org.junit.Assume.assumeTrue; import java.time.LocalDate; @@ -38,6 +39,8 @@ import java.util.UUID; import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.managed.Managed; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.testing.PAssert; @@ -60,6 +63,8 @@ import org.apache.iceberg.CatalogUtil; import org.apache.iceberg.DistributionMode; import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.SortDirection; +import org.apache.iceberg.SortOrder; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.data.IcebergGenerics; @@ -484,7 +489,12 @@ public void writePartitionedData(boolean autosharding) { .satisfies(new VerifyOutputs(Collections.singletonList(identifier), "append")); testPipeline.run().waitUntilFinish(); - Pipeline p = Pipeline.create(TestPipeline.testingPipelineOptions()); + // This table has timestamptz columns written as joda DateTime. Pin an older update + // compatibility version so the read keeps the legacy DATETIME mapping and matches the written + // rows + PipelineOptions readOptions = TestPipeline.testingPipelineOptions(); + readOptions.as(StreamingOptions.class).setUpdateCompatibilityVersion("2.75.0"); + Pipeline p = Pipeline.create(readOptions); PCollection readRows = p.apply(Managed.read(Managed.ICEBERG).withConfig(config)).getSinglePCollection(); PAssert.that(readRows).containsInAnyOrder(rows); @@ -554,7 +564,9 @@ public void testWriteCreateTableWithPartitionSpec() { .satisfies(new VerifyOutputs(Collections.singletonList(identifier), "append")); testPipeline.run().waitUntilFinish(); - Pipeline p = Pipeline.create(TestPipeline.testingPipelineOptions()); + PipelineOptions readOptions = TestPipeline.testingPipelineOptions(); + readOptions.as(StreamingOptions.class).setUpdateCompatibilityVersion("2.75.0"); + Pipeline p = Pipeline.create(readOptions); PCollection readRows = p.apply(Managed.read(Managed.ICEBERG).withConfig(config)).getSinglePCollection(); PAssert.that(readRows).containsInAnyOrder(rows); @@ -684,4 +696,64 @@ public void testWriteCreateTableWithTableProperties() { assertEquals("5", table.properties().get("commit.retry.num-retries")); assertEquals("134217728", table.properties().get("read.split.target-size")); } + + @Test + public void testDynamicWriteCreateTableWithTableProperties() { + String identifier = "default.table_" + Long.toString(UUID.randomUUID().hashCode(), 16); + Schema schema = Schema.builder().addStringField("str").addInt32Field("int").build(); + + String customDataPath = warehouse.location + "/custom_data_path"; + + Map config = + ImmutableMap.of( + "table", + identifier, + "catalog_properties", + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location), + "table_properties", + ImmutableMap.of( + "write.data.path", + customDataPath, + "write.parquet.bloom-filter-enabled.column.int", + "true"), + "sort_fields", + Collections.singletonList("str desc"), + "partition_fields", + Collections.singletonList("int")); + + List rows = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Row row = Row.withSchema(schema).addValues("str_" + i, i).build(); + rows.add(row); + } + + PCollection result = + testPipeline + .apply("Records To Add", Create.of(rows)) + .setRowSchema(schema) + .apply(Managed.write(Managed.ICEBERG).withConfig(config)) + .get(SNAPSHOTS_TAG); + + PAssert.that(result) + .satisfies(new VerifyOutputs(Collections.singletonList(identifier), "append")); + testPipeline.run().waitUntilFinish(); + + Table table = warehouse.loadTable(TableIdentifier.parse(identifier)); + + PartitionSpec spec = table.spec(); + assertTrue(spec.isPartitioned()); + assertEquals(1, spec.fields().size()); + assertEquals("int", spec.fields().get(0).name()); + + SortOrder sortOrder = table.sortOrder(); + assertTrue(sortOrder.isSorted()); + assertEquals(1, sortOrder.fields().size()); + assertEquals(SortDirection.DESC, sortOrder.fields().get(0).direction()); + + assertEquals(customDataPath, table.properties().get("write.data.path")); + assertEquals("true", table.properties().get("write.parquet.bloom-filter-enabled.column.int")); + + List writtenRecords = ImmutableList.copyOf(IcebergGenerics.read(table).build()); + assertEquals(10, writtenRecords.size()); + } } 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..3aa6a5e94648 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(); + setDataSequenceNumber(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 testConstantsMapUsesExplicitSequenceNumberWhenDataSequenceIsUnavailable() { + 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 setDataSequenceNumber(DataFile dataFile, long dataSequenceNumber) + throws Exception { + Method method = dataFile.getClass().getMethod("setDataSequenceNumber", Long.class); + method.setAccessible(true); + try { + method.invoke(dataFile, dataSequenceNumber); + } 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/RecordWriterManagerTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java index f27a86cc72a2..03b3560f746a 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 @@ -42,7 +42,6 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -54,6 +53,7 @@ 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.hadoop.fs.Path; import org.apache.iceberg.AppendFiles; import org.apache.iceberg.DataFile; import org.apache.iceberg.FileFormat; @@ -69,7 +69,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; @@ -79,6 +78,10 @@ import org.apache.iceberg.types.Type; import org.apache.iceberg.types.Types; import org.apache.iceberg.util.DateTimeUtil; +import org.apache.parquet.hadoop.ParquetFileReader; +import org.apache.parquet.hadoop.metadata.BlockMetaData; +import org.apache.parquet.hadoop.metadata.ColumnChunkMetaData; +import org.apache.parquet.hadoop.util.HadoopInputFile; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -109,16 +112,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 +158,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 +171,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 +199,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 +211,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 +273,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 +335,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 +382,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 +420,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)); @@ -409,15 +432,8 @@ public void testSerializableDataFileRoundTripEquality() throws IOException { DataFile datafile = writer.getDataFile(); assertEquals(2L, datafile.recordCount()); - Map partitionFieldMap = new HashMap<>(); - for (PartitionField partitionField : PARTITION_SPEC.fields()) { - partitionFieldMap.put(partitionField.name(), partitionField); - } - - String partitionPath = - RecordWriterManager.getPartitionDataPath(partitionKey.toPath(), partitionFieldMap); DataFile roundTripDataFile = - SerializableDataFile.from(datafile, partitionPath) + SerializableDataFile.from(datafile, PARTITION_SPEC) .createDataFile(ImmutableMap.of(PARTITION_SPEC.specId(), PARTITION_SPEC)); checkDataFileEquality(datafile, roundTripDataFile); @@ -443,26 +459,25 @@ 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(); // fetch data file and its serializable version - Map partitionFieldMap = new HashMap<>(); - for (PartitionField partitionField : PARTITION_SPEC.fields()) { - partitionFieldMap.put(partitionField.name(), partitionField); - } - String partitionPath = - RecordWriterManager.getPartitionDataPath(partitionKey.toPath(), partitionFieldMap); DataFile datafile = writer.getDataFile(); - SerializableDataFile serializableDataFile = SerializableDataFile.from(datafile, partitionPath); + SerializableDataFile serializableDataFile = SerializableDataFile.from(datafile, PARTITION_SPEC); assertEquals(2L, datafile.recordCount()); 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 +488,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 +513,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 +591,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); @@ -618,7 +631,7 @@ public void testIdentityPartitioning() throws IOException { expectedPartitions.add(name + "=" + URLEncoder.encode(val, UTF_8.toString())); } String expectedPartitionPath = String.join("/", expectedPartitions); - assertEquals(expectedPartitionPath, dataFile.getPartitionPath()); + assertEquals(expectedPartitionPath, spec.partitionToPath(dataFile.partition(spec))); assertThat(dataFile.getPath(), containsString(expectedPartitionPath)); } @@ -664,16 +677,17 @@ 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); assertEquals(1, files.size()); SerializableDataFile dataFile = files.get(0); assertEquals(1, dataFile.getRecordCount()); + String partitionPath = spec.partitionToPath(dataFile.partition(spec)); for (Schema.Field field : bucketSchema.getFields()) { String expectedPartition = field.getName() + "_bucket"; - assertThat(dataFile.getPartitionPath(), containsString(expectedPartition)); + assertThat(partitionPath, containsString(expectedPartition)); assertThat(dataFile.getPath(), containsString(expectedPartition)); } } @@ -730,7 +744,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,15 +777,17 @@ 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)); + assertEquals(expectedPartition, spec.partitionToPath(dataFile.partition())); } @Rule public ExpectedException thrown = ExpectedException.none(); @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 +799,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 +862,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 +925,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 +1111,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 +1227,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 +1245,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 +1271,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,11 +1287,74 @@ 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); assertTrue( "Bundle 2 should produce data files", bundle2.getSerializableDataFiles().containsKey(dest)); } + + @Test + public void testWritePropertiesAppliedToParquetFiles() throws IOException { + Schema bloomSchema = + Schema.builder().addInt32Field("colWithBf").addInt32Field("colWithoutBf").build(); + org.apache.iceberg.Schema icebergBloomSchema = + IcebergUtils.beamSchemaToIcebergSchema(bloomSchema); + + TableIdentifier tableId = TableIdentifier.of("default", "test_write_properties"); + warehouse.createTable(tableId, icebergBloomSchema); + + Map writeProperties = + ImmutableMap.of( + "write.parquet.bloom-filter-enabled.column.colWithBf", "true", + "write.parquet.bloom-filter-enabled.column.colWithoutBf", "false"); + + IcebergDestination destination = + IcebergDestination.builder() + .setTableIdentifier(tableId) + .setFileFormat(FileFormat.PARQUET) + .build(); + WindowedValue dest = WindowedValues.valueInGlobalWindow(destination); + + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_bloom", Long.MAX_VALUE, 3, writeProperties); + for (int i = 0; i < 10; i++) { + Row row = Row.withSchema(bloomSchema).addValues(i, 100 + i).build(); + assertTrue(writerManager.write(dest, row)); + } + writerManager.close(); + + List dataFiles = writerManager.getSerializableDataFiles().get(dest); + assertEquals(1, dataFiles.size()); + + String dataFilePath = dataFiles.get(0).getPath(); + assertNotNull(dataFilePath); + + try (ParquetFileReader reader = + ParquetFileReader.open( + HadoopInputFile.fromPath(new Path(dataFilePath), new Configuration()))) { + List blocks = reader.getFooter().getBlocks(); + assertFalse("Parquet file should have at least one row group", blocks.isEmpty()); + + for (int i = 0; i < blocks.size(); i++) { + BlockMetaData block = blocks.get(i); + assertEquals("Each row group should have 2 columns", 2, block.getColumns().size()); + + for (ColumnChunkMetaData col : block.getColumns()) { + boolean hasBloomFilter = col.getBloomFilterOffset() > 0; + String colName = col.getPath().toDotString(); + if (colName.equals("colWithBf")) { + assertTrue( + "Column 'colWithBf' in row group " + i + " should have a bloom filter", + hasBloomFilter); + } else if (colName.equals("colWithoutBf")) { + assertFalse( + "Column 'colWithoutBf' in row group " + i + " should not have a bloom filter", + hasBloomFilter); + } + } + } + } + } } 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..e02cfaff0845 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,34 @@ */ package org.apache.beam.sdk.io.iceberg; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + 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.sdk.schemas.SchemaRegistry; +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.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.Schema; +import org.apache.iceberg.data.GenericRecord; +import org.apache.iceberg.types.Conversions; +import org.apache.iceberg.types.Types; import org.junit.Test; /** @@ -38,6 +59,7 @@ public class SerializableDataFileTest { .add("recordCount") .add("fileSizeInBytes") .add("partitionPath") + .add("jsonPartition") .add("partitionSpecId") .add("keyMetadata") .add("splitOffsets") @@ -47,6 +69,9 @@ public class SerializableDataFileTest { .add("nanValueCounts") .add("lowerBounds") .add("upperBounds") + .add("dataSequenceNumber") + .add("fileSequenceNumber") + .add("firstRowId") .build(); @Test @@ -73,4 +98,201 @@ public void testFieldsInEqualsMethodInSyncWithGetterFields() { + "to this test class's FIELDS_SET."); } } + + /** + * B13/A6: every field is pinned with {@code @SchemaFieldNumber} because Dataflow's in-place + * {@code --update} rejects a reordered schema or a changed field nullability. Lock the field + * count, order/names, and the two nullability-sensitive fields so any future edit trips review. + */ + @Test + public void schemaFieldNumbersArePinned() throws Exception { + org.apache.beam.sdk.schemas.Schema schema = + SchemaRegistry.createDefault().getSchema(SerializableDataFile.class); + assertEquals(18, schema.getFieldCount()); + assertEquals("path", schema.getField(0).getName()); + assertEquals("fileFormat", schema.getField(1).getName()); + assertEquals("recordCount", schema.getField(2).getName()); + assertEquals("fileSizeInBytes", schema.getField(3).getName()); + assertEquals("partitionPath", schema.getField(4).getName()); + assertEquals("partitionSpecId", schema.getField(5).getName()); + assertEquals("keyMetadata", schema.getField(6).getName()); + assertEquals("splitOffsets", schema.getField(7).getName()); + assertEquals("columnSizes", schema.getField(8).getName()); + assertEquals("valueCounts", schema.getField(9).getName()); + assertEquals("nullValueCounts", schema.getField(10).getName()); + assertEquals("nanValueCounts", schema.getField(11).getName()); + assertEquals("lowerBounds", schema.getField(12).getName()); + assertEquals("upperBounds", schema.getField(13).getName()); + assertEquals("dataSequenceNumber", schema.getField(14).getName()); + assertEquals("fileSequenceNumber", schema.getField(15).getName()); + assertEquals("firstRowId", schema.getField(16).getName()); + assertEquals("jsonPartition", schema.getField(17).getName()); + } + + /** + * 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, PartitionSpec.unpartitioned()); + + 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); + } + + /** + * F8: {@code from(DataFile, spec)} must populate BOTH the JSON partition (primary) and the + * partition path (fallback), so the deprecated {@code partitionPath} schema field stays non-null + * across releases (Dataflow's in-place pipeline update rejects a changed field nullability). + */ + @Test + public void fromPopulatesBothPartitionRepresentations() { + DataFile dataFile = + DataFiles.builder(PartitionSpec.unpartitioned()) + .withFormat(FileFormat.PARQUET) + .withPath("gs://test-bucket/data/f.parquet") + .withFileSizeInBytes(1L) + .withRecordCount(1L) + .build(); + + SerializableDataFile sdf = SerializableDataFile.from(dataFile, PartitionSpec.unpartitioned()); + + assertEquals( + "partition path must be populated (unpartitioned -> empty string), not null", + "", + sdf.getPartitionPath()); + assertNotNull("json partition must also be populated", sdf.getJsonPartition()); + } + + /** + * F6: a {@link DataFile} must be serialized with the EXACT spec it was written with — a + * mismatched spec id (usually a spec evolution on a shared/refreshed table between writing and + * serializing) must fail loudly rather than silently encode the partition under the wrong field + * ids. + */ + @Test + public void fromRejectsSpecIdMismatch() { + DataFile unpartitioned = + DataFiles.builder(PartitionSpec.unpartitioned()) + .withFormat(FileFormat.PARQUET) + .withPath("gs://test-bucket/data/f.parquet") + .withFileSizeInBytes(1L) + .withRecordCount(1L) + .build(); + Schema schema = + new Schema( + Types.NestedField.required(1, "id", Types.LongType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get())); + PartitionSpec otherSpec = PartitionSpec.builderFor(schema).identity("id").withSpecId(1).build(); + + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> SerializableDataFile.from(unpartitioned, otherSpec)); + assertTrue( + "message should explain the spec-id mismatch: " + ex.getMessage(), + ex.getMessage().contains("does not match")); + } + + /** + * B13/F8: elements encoded by a PRE-jsonPartition pipeline carry only {@code partitionPath} + * (field 4), with {@code jsonPartition} (field 14) null. {@code createDataFile} must reconstruct + * the partition via {@code withPartitionPath} rather than crash on the missing JSON. + */ + @Test + public void legacyElementWithoutJsonPartitionReconstructsViaPartitionPath() { + Schema schema = + new Schema( + Types.NestedField.required(1, "id", Types.LongType.get()), + Types.NestedField.required(2, "shard", Types.IntegerType.get())); + PartitionSpec spec = PartitionSpec.builderFor(schema).identity("shard").build(); + + SerializableDataFile legacy = + SerializableDataFile.builder() + .setPath("gs://test-bucket/data/legacy.parquet") + .setFileFormat("PARQUET") + .setRecordCount(1L) + .setFileSizeInBytes(1L) + .setPartitionPath("shard=5") + .setPartitionSpecId(spec.specId()) + .build(); // no setJsonPartition -> jsonPartition is null (pre-upgrade encoding) + + DataFile reconstructed = legacy.createDataFile(ImmutableMap.of(spec.specId(), spec)); + assertEquals("shard=5", spec.partitionToPath(reconstructed.partition())); + } + + /** + * F7: NaN / Infinity floating-point partition values don't round-trip through the JSON partition + * representation ({@code SingleValueParser.fromJson} rejects the quoted {@code "NaN"}). + * Reconstruct must fall back to the partition-path string, which handles them, rather than + * crash-looping the sink at commit time. + */ + @Test + public void nanFloatPartitionReconstructsViaPathFallback() { + Schema schema = + new Schema( + Types.NestedField.required(1, "f", Types.FloatType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get())); + PartitionSpec spec = PartitionSpec.builderFor(schema).identity("f").build(); + GenericRecord partition = GenericRecord.create(spec.partitionType()); + partition.setField("f", Float.NaN); + DataFile dataFile = + DataFiles.builder(spec) + .withFormat(FileFormat.PARQUET) + .withPath("gs://test-bucket/data/nan.parquet") + .withFileSizeInBytes(1L) + .withRecordCount(1L) + .withPartition(partition) + .build(); + + SerializableDataFile sdf = SerializableDataFile.from(dataFile, spec); + // Must reconstruct without throwing (JSON decode of NaN fails -> partition-path fallback). + DataFile reconstructed = sdf.createDataFile(ImmutableMap.of(spec.specId(), spec)); + + Object value = reconstructed.partition().get(0, Object.class); + assertTrue( + "partition value must round-trip as NaN", + value instanceof Float && Float.isNaN((Float) value)); + } } 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/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); + } +} 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/BigQueryMetastoreCatalogIT.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/BigQueryMetastoreCatalogIT.java index eb3ebfbf5219..a34e580d29b7 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/BigQueryMetastoreCatalogIT.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/BigQueryMetastoreCatalogIT.java @@ -58,6 +58,7 @@ public Catalog createCatalog() { .put("gcp_project", OPTIONS.getProject()) .put("gcp_location", "us-central1") .put("warehouse", warehouse) + .put("io-impl", "org.apache.iceberg.gcp.gcs.GCSFileIO") .build(), new Configuration()); } 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..2435cdb231da 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 @@ -56,13 +56,17 @@ import org.apache.beam.sdk.extensions.gcp.util.GcsUtil; import org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath; import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.cdc.IcebergCdcMetadataColumns; import org.apache.beam.sdk.managed.Managed; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; 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.MapElements; +import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.PeriodicImpulse; import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.transforms.SimpleFunction; @@ -74,10 +78,16 @@ import org.apache.beam.sdk.values.PCollection.IsBounded; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TypeDescriptors; +import org.apache.beam.sdk.values.ValueKind; 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.ImmutableSet; import org.apache.iceberg.AppendFiles; +import org.apache.iceberg.ChangelogOperation; import org.apache.iceberg.CombinedScanTask; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileFormat; import org.apache.iceberg.FileScanTask; import org.apache.iceberg.NullOrder; import org.apache.iceberg.PartitionSpec; @@ -85,15 +95,22 @@ import org.apache.iceberg.SortDirection; import org.apache.iceberg.SortOrder; import org.apache.iceberg.Table; +import org.apache.iceberg.TableProperties; import org.apache.iceberg.TableScan; 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.GenericAppenderFactory; +import org.apache.iceberg.data.GenericRecord; import org.apache.iceberg.data.IdentityPartitionConverters; import org.apache.iceberg.data.Record; import org.apache.iceberg.data.parquet.GenericParquetReaders; import org.apache.iceberg.data.parquet.GenericParquetWriter; +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.encryption.InputFilesDecryptor; import org.apache.iceberg.io.CloseableIterable; import org.apache.iceberg.io.DataWriter; @@ -106,8 +123,6 @@ import org.apache.iceberg.util.DateTimeUtil; import org.apache.iceberg.util.PartitionUtil; import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; import org.joda.time.Duration; import org.joda.time.Instant; import org.joda.time.LocalDate; @@ -272,11 +287,13 @@ public void cleanUp() throws Exception { .addArrayField("arr_long", Schema.FieldType.INT64) .addNullableRowField("nullable_row", NESTED_ROW_SCHEMA) .addNullableInt64Field("nullable_long") - .addDateTimeField("datetime_tz") + .addLogicalTypeField("datetime_tz", Timestamp.MICROS) .addLogicalTypeField("datetime", SqlTypes.DATETIME) .addLogicalTypeField("date", SqlTypes.DATE) .addLogicalTypeField("time", SqlTypes.TIME) .build(); + private static final Schema CDC_BEAM_SCHEMA = + Schema.builder().addInt64Field("id").addStringField("data").build(); private static final SimpleFunction ROW_FUNC = new SimpleFunction() { @@ -309,8 +326,10 @@ public Row apply(Long num) { .addValue(LongStream.range(0, num % 10).boxed().collect(Collectors.toList())) .addValue(num % 2 == 0 ? null : nestedRow) .addValue(num) - .addValue(new DateTime(timestampMillis).withZone(DateTimeZone.forOffsetHours(4))) - .addValue(DateTimeUtil.timestampFromMicros(timestampMillis * 1000)) + .addValue( + DateTimeUtil.timestamptzFromMicros(timestampMillis * 1000 + 123456789) + .toInstant()) + .addValue(DateTimeUtil.timestampFromMicros(timestampMillis * 1000 + 123456789)) .addValue(DateTimeUtil.dateFromDays(Integer.parseInt(strNum))) .addValue(DateTimeUtil.timeFromMicros(num)) .build(); @@ -318,7 +337,11 @@ 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)); + private static final org.apache.iceberg.Schema CDC_ICEBERG_SCHEMA = + new org.apache.iceberg.Schema( + beamSchemaToIcebergSchema(CDC_BEAM_SCHEMA).columns(), Collections.singleton(1)); protected static final SimpleFunction RECORD_FUNC = new SimpleFunction() { @Override @@ -450,7 +473,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())); @@ -543,7 +566,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())); @@ -559,6 +582,131 @@ public void testStreamingReadWithColumnPruning_drop() throws Exception { pipeline.run().waitUntilFinish(); } + @Test + public void testStreamingCdcReadMixedDeleteAndOverwriteSnapshots() throws Exception { + Table table = createCdcTable(); + DataFile firstFile = + commitCdcAppend( + table, + "cdc-first-data.parquet", + Arrays.asList( + cdcRecord(table.schema(), 1L, "first-file-update-before"), + cdcRecord(table.schema(), 2L, "first-file-delete"))); + Snapshot firstSnapshot = checkStateNotNull(table.currentSnapshot()); + + DataFile secondFile = + commitCdcAppend( + table, + "cdc-second-data.parquet", + Arrays.asList( + cdcRecord(table.schema(), 3L, "second-file-unchanged"), + cdcRecord(table.schema(), 4L, "second-file-update-before"))); + Snapshot secondSnapshot = checkStateNotNull(table.currentSnapshot()); + + DeleteFile equalityDelete = writeCdcEqualityDelete(table, "cdc-equality-delete.parquet", 2L); + DeleteFile positionDelete = + writeCdcPositionDelete(table, "cdc-position-delete.parquet", firstFile, 0L); + DataFile thirdFile = + writeCdcDataFile( + table, + "cdc-third-data.parquet", + Collections.singletonList(cdcRecord(table.schema(), 1L, "third-file-update-after"))); + table + .newRowDelta() + .addDeletes(equalityDelete) + .addDeletes(positionDelete) + .addRows(thirdFile) + .commit(); + table.refresh(); + Snapshot thirdSnapshot = checkStateNotNull(table.currentSnapshot()); + + DataFile fourthFile = + writeCdcDataFile( + table, + "cdc-fourth-data.parquet", + Arrays.asList( + cdcRecord(table.schema(), 3L, "second-file-unchanged"), + cdcRecord(table.schema(), 4L, "fourth-file-update-after"))); + table.newOverwrite().deleteFile(secondFile).addFile(fourthFile).commit(); + table.refresh(); + Snapshot fourthSnapshot = checkStateNotNull(table.currentSnapshot()); + + Map config = new HashMap<>(managedIcebergConfig(tableId())); + config.put("from_snapshot", firstSnapshot.snapshotId()); + config.put("to_snapshot", fourthSnapshot.snapshotId()); + config.put("streaming", true); + + PCollection rows = + pipeline.apply(Managed.read(ICEBERG_CDC).withConfig(config)).getSinglePCollection(); + + PCollection changes = rows.apply("Format CDC Changes", ParDo.of(new FormatCdcChange())); + + assertThat(rows.isBounded(), equalTo(UNBOUNDED)); + assertEquals(CDC_BEAM_SCHEMA, rows.getSchema()); + PAssert.that(changes) + .containsInAnyOrder( + cdcChange(ValueKind.INSERT, firstSnapshot, 1L, "first-file-update-before"), + cdcChange(ValueKind.INSERT, firstSnapshot, 2L, "first-file-delete"), + cdcChange(ValueKind.INSERT, secondSnapshot, 3L, "second-file-unchanged"), + cdcChange(ValueKind.INSERT, secondSnapshot, 4L, "second-file-update-before"), + cdcChange(ValueKind.UPDATE_BEFORE, thirdSnapshot, 1L, "first-file-update-before"), + cdcChange(ValueKind.UPDATE_AFTER, thirdSnapshot, 1L, "third-file-update-after"), + cdcChange(ValueKind.DELETE, thirdSnapshot, 2L, "first-file-delete"), + cdcChange(ValueKind.UPDATE_BEFORE, fourthSnapshot, 4L, "second-file-update-before"), + cdcChange(ValueKind.UPDATE_AFTER, fourthSnapshot, 4L, "fourth-file-update-after")); + pipeline.run().waitUntilFinish(); + } + + @Test + public void testCdcReadWithMetadataColumns() throws Exception { + Table table = + catalog.createTable( + TableIdentifier.parse(tableId()), + CDC_ICEBERG_SCHEMA, + PartitionSpec.unpartitioned(), + ImmutableMap.of(TableProperties.FORMAT_VERSION, "3")); + commitCdcAppend( + table, + "metadata-columns.parquet", + Arrays.asList(cdcRecord(table.schema(), 1L, "one"), cdcRecord(table.schema(), 2L, "two"))); + Snapshot snapshot = checkStateNotNull(table.currentSnapshot()); + long firstRowId = checkStateNotNull(snapshot.firstRowId()); + + Map config = new HashMap<>(managedIcebergConfig(tableId())); + config.put("to_snapshot", snapshot.snapshotId()); + config.put( + "include_metadata_columns", + Arrays.asList( + IcebergCdcMetadataColumns.CHANGE_TYPE, + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID, + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_SEQUENCE_NUMBER, + IcebergCdcMetadataColumns.ROW_ID, + IcebergCdcMetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER)); + + Schema outputSchema = + Schema.builder() + .addInt64Field("id") + .addStringField("data") + .addStringField(IcebergCdcMetadataColumns.CHANGE_TYPE) + .addInt64Field(IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID) + .addInt64Field(IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_SEQUENCE_NUMBER) + .addNullableField(IcebergCdcMetadataColumns.ROW_ID, Schema.FieldType.INT64) + .addNullableField( + IcebergCdcMetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER, Schema.FieldType.INT64) + .build(); + + PCollection rows = + pipeline.apply(Managed.read(ICEBERG_CDC).withConfig(config)).getSinglePCollection(); + + assertEquals(BOUNDED, rows.isBounded()); + assertEquals(outputSchema, rows.getSchema()); + PAssert.that(rows) + .containsInAnyOrder( + cdcMetadataRow(1L, "one", snapshot, firstRowId, outputSchema), + cdcMetadataRow(2L, "two", snapshot, firstRowId + 1, outputSchema)); + pipeline.run().waitUntilFinish(); + } + @Test public void testBatchReadBetweenSnapshots() throws Exception { runReadBetween(true, false); @@ -620,7 +768,9 @@ public void testWriteReadWithFilter() throws IOException { @Test public void testReadWriteStreaming() throws IOException { - Table table = catalog.createTable(TableIdentifier.parse(tableId()), ICEBERG_SCHEMA); + org.apache.iceberg.Schema schemaWithPk = + new org.apache.iceberg.Schema(ICEBERG_SCHEMA.columns(), ImmutableSet.of(1)); + Table table = catalog.createTable(TableIdentifier.parse(tableId()), schemaWithPk); List expectedRows = populateTable(table); Map config = managedIcebergConfig(tableId()); @@ -1034,7 +1184,9 @@ && checkStateNotNull(rec.getBoolean("bool_field")) == bool) } public void runReadBetween(boolean useSnapshotBoundary, boolean streaming) throws Exception { - Table table = catalog.createTable(TableIdentifier.parse(tableId()), ICEBERG_SCHEMA); + org.apache.iceberg.Schema schemaWithPk = + new org.apache.iceberg.Schema(ICEBERG_SCHEMA.columns(), ImmutableSet.of(1)); + Table table = catalog.createTable(TableIdentifier.parse(tableId()), schemaWithPk); populateTable(table, "a"); // first snapshot Thread.sleep(AFTER_UPDATE_SLEEP_MS); @@ -1067,6 +1219,133 @@ public void runReadBetween(boolean useSnapshotBoundary, boolean streaming) throw pipeline.run().waitUntilFinish(); } + private Table createCdcTable() { + return catalog.createTable( + TableIdentifier.parse(tableId()), + CDC_ICEBERG_SCHEMA, + PartitionSpec.unpartitioned(), + ImmutableMap.of( + TableProperties.FORMAT_VERSION, + "2", + TableProperties.SPLIT_SIZE, + "1", + TableProperties.DEFAULT_WRITE_METRICS_MODE, + "full")); + } + + private static String cdcChange(ValueKind valueKind, Snapshot snapshot, long id, String data) { + return String.format("%s:%d:%d:%s", valueKind, snapshot.timestampMillis(), id, data); + } + + private static Row cdcMetadataRow( + long id, String data, Snapshot snapshot, long rowId, Schema outputSchema) { + return Row.withSchema(outputSchema) + .addValues( + id, + data, + ChangelogOperation.INSERT.name(), + snapshot.snapshotId(), + snapshot.sequenceNumber(), + rowId, + snapshot.sequenceNumber()) + .build(); + } + + private static Record cdcRecord(org.apache.iceberg.Schema schema, long id, String data) { + GenericRecord record = GenericRecord.create(schema); + record.setField("id", id); + if (schema.findField("data") != null) { + record.setField("data", data); + } + return record; + } + + private DataFile commitCdcAppend(Table table, String filename, List records) + throws IOException { + DataFile dataFile = writeCdcDataFile(table, filename, records); + table.newFastAppend().appendFile(dataFile).commit(); + table.refresh(); + return dataFile; + } + + private DataFile writeCdcDataFile(Table table, String filename, List records) + throws IOException { + OutputFile file = + table.io().newOutputFile(table.location() + "/" + UUID.randomUUID() + "-" + filename); + DataWriter writer = + Parquet.writeData(file) + .schema(table.schema()) + .createWriterFunc(GenericParquetWriter::create) + .overwrite() + .withSpec(table.spec()) + .build(); + + try (writer) { + for (Record record : records) { + writer.write(record); + } + } + + return writer.toDataFile(); + } + + private DeleteFile writeCdcEqualityDelete(Table table, String filename, long id) + throws IOException { + org.apache.iceberg.Schema deleteSchema = table.schema().select("id"); + GenericAppenderFactory appenderFactory = + new GenericAppenderFactory(table.schema(), table.spec(), new int[] {1}, deleteSchema, null); + EqualityDeleteWriter writer = + appenderFactory.newEqDeleteWriter( + EncryptedFiles.plainAsEncryptedOutput( + table + .io() + .newOutputFile(table.location() + "/" + UUID.randomUUID() + "-" + filename)), + FileFormat.PARQUET, + null); + + try (writer) { + writer.write(cdcRecord(deleteSchema, id, null)); + } + + return writer.toDeleteFile(); + } + + private DeleteFile writeCdcPositionDelete( + Table table, String filename, DataFile dataFile, long... positions) throws IOException { + GenericAppenderFactory appenderFactory = + new GenericAppenderFactory(table.schema(), table.spec()); + PositionDeleteWriter writer = + appenderFactory.newPosDeleteWriter( + EncryptedFiles.plainAsEncryptedOutput( + table + .io() + .newOutputFile(table.location() + "/" + UUID.randomUUID() + "-" + filename)), + FileFormat.PARQUET, + null); + + try (writer) { + for (long position : positions) { + writer.write(PositionDelete.create().set(dataFile.location(), position)); + } + } + + return writer.toDeleteFile(); + } + + private static final class FormatCdcChange extends DoFn { + @ProcessElement + public void process( + @Element Row row, + ValueKind valueKind, + @DoFn.Timestamp Instant timestamp, + OutputReceiver outputReceiver) { + outputReceiver.output( + String.format( + "%s:%d:%d:%s", + valueKind, timestamp.getMillis(), row.getInt64("id"), row.getString("data"))); + } + } + @Test public void testWriteWithTableProperties() throws IOException { Map config = new HashMap<>(managedIcebergConfig(tableId())); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/RESTCatalogBLMSIT.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/RESTCatalogBLMSIT.java index c16df763333f..6934bf13f21c 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/RESTCatalogBLMSIT.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/RESTCatalogBLMSIT.java @@ -41,7 +41,6 @@ public static void setup() { .put("uri", "https://biglake.googleapis.com/iceberg/v1/restcatalog") .put("warehouse", BIGLAKE_WAREHOUSE) .put("header.x-goog-user-project", OPTIONS.getProject()) - .put("rest-metrics-reporting-enabled", "false") .put("io-impl", "org.apache.iceberg.gcp.gcs.GCSFileIO") .put("rest.auth.type", "org.apache.iceberg.gcp.auth.GoogleAuthManager") .build(); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ApplyWatermarkColumnTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ApplyWatermarkColumnTest.java new file mode 100644 index 000000000000..b28f5c2c91f8 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ApplyWatermarkColumnTest.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.assertThrows; + +import java.time.LocalDateTime; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.Timestamp; +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.DoFnTester; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.Reify; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TimestampedValue; +import org.joda.time.Instant; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ApplyWatermarkColumn}. */ +@RunWith(JUnit4.class) +public class ApplyWatermarkColumnTest { + @Rule public final transient TestPipeline pipeline = TestPipeline.create(); + + @Test + public void stampsRowsFromSupportedWatermarkTypes() { + assertWatermarkTimestamp( + "jodaDateTime", + Schema.builder().addStringField("id").addDateTimeField("wm").build(), + Row.withSchema(Schema.builder().addStringField("id").addDateTimeField("wm").build()) + .addValues("joda", new Instant(1_234L)) + .build(), + new Instant(1_234L)); + + assertWatermarkTimestamp( + "longMicros", + Schema.builder().addStringField("id").addInt64Field("wm").build(), + Row.withSchema(Schema.builder().addStringField("id").addInt64Field("wm").build()) + .addValues("long", 1_234_567L) + .build(), + new Instant(1_234L)); + + assertWatermarkTimestamp( + "localDateTime", + Schema.builder().addStringField("id").addLogicalTypeField("wm", SqlTypes.DATETIME).build(), + Row.withSchema( + Schema.builder() + .addStringField("id") + .addLogicalTypeField("wm", SqlTypes.DATETIME) + .build()) + .addValues("ldt", LocalDateTime.of(1969, 12, 31, 23, 59, 59, 123_000_000)) + .build(), + new Instant(-877L)); + + assertWatermarkTimestamp( + "javaInstant", + Schema.builder().addStringField("id").addLogicalTypeField("wm", Timestamp.MICROS).build(), + Row.withSchema( + Schema.builder() + .addStringField("id") + .addLogicalTypeField("wm", Timestamp.MICROS) + .build()) + .addValues("instant", java.time.Instant.parse("1969-12-31T23:59:59.123Z")) + .build(), + new Instant(-877L)); + + pipeline.run(); + } + + @Test + public void nullWatermarkValuePreservesInputTimestamp() { + Schema schema = + Schema.of( + Schema.Field.of("id", Schema.FieldType.STRING), + Schema.Field.nullable("wm", Schema.FieldType.DATETIME)); + Row row = Row.withSchema(schema).addValues("null", null).build(); + Instant inputTimestamp = new Instant(99L); + + PCollection output = + pipeline + .apply( + Create.timestamped(TimestampedValue.of(row, inputTimestamp)) + .withCoder(RowCoder.of(schema))) + .apply(ParDo.of(new ApplyWatermarkColumn("wm", "microseconds"))); + output.setCoder(RowCoder.of(schema)); + + PAssert.that(output.apply(Reify.timestamps())) + .containsInAnyOrder(TimestampedValue.of(row, inputTimestamp)); + + pipeline.run(); + } + + @Test + public void unsupportedWatermarkTypeThrows() { + Schema schema = Schema.builder().addStringField("wm").build(); + Row row = Row.withSchema(schema).addValue("2026-05-24T00:00:00Z").build(); + + assertThrows( + UnsupportedOperationException.class, + () -> { + try (DoFnTester tester = + DoFnTester.of(new ApplyWatermarkColumn("wm", "microseconds"))) { + tester.processElement(row); + } + }); + } + + @Test + public void missingWatermarkColumnThrows() { + Schema schema = Schema.builder().addStringField("other").build(); + Row row = Row.withSchema(schema).addValue("value").build(); + + assertThrows( + IllegalArgumentException.class, + () -> { + try (DoFnTester tester = + DoFnTester.of(new ApplyWatermarkColumn("wm", "microseconds"))) { + tester.processElement(row); + } + }); + } + + private void assertWatermarkTimestamp( + String name, Schema schema, Row row, Instant expectedTimestamp) { + PCollection output = + pipeline + .apply(name + "Create", Create.of(row).withCoder(RowCoder.of(schema))) + .apply( + name + "ApplyWatermark", ParDo.of(new ApplyWatermarkColumn("wm", "microseconds"))); + output.setCoder(RowCoder.of(schema)); + + PCollection> timestamps = + output.apply(name + "ReifyTimestamp", Reify.timestamps()); + PAssert.that(timestamps).containsInAnyOrder(TimestampedValue.of(row, expectedTimestamp)); + } +} 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..75bfaa1775da --- /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(), 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/CdcResolverTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcResolverTest.java new file mode 100644 index 000000000000..effcba475102 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcResolverTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.empty; +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link CdcResolver}. */ +@RunWith(JUnit4.class) +public class CdcResolverTest { + private static final TestResolver RESOLVER = new TestResolver(); + + @Test + public void duplicateDeleteInsertIsDropped() { + List emitted = + resolve( + Collections.singletonList(item("same", 7)), Collections.singletonList(item("same", 7))); + + assertThat(emitted, empty()); + } + + @Test + public void changedDeleteInsertBecomesUpdatePair() { + List emitted = + resolve( + Collections.singletonList(item("before", 1)), + Collections.singletonList(item("after", 2))); + + assertThat(emitted, contains("UPDATE_BEFORE:before", "UPDATE_AFTER:after")); + } + + @Test + public void duplicateUpdateAndSingletonsResolveByMultiplicity() { + List emitted = + resolve( + Arrays.asList(item("copy", 1), item("old", 2), item("deleted-only", 3)), + Arrays.asList(item("copy", 1), item("new", 4))); + + assertThat(emitted, contains("UPDATE_BEFORE:old", "UPDATE_AFTER:new", "DELETE:deleted-only")); + } + + @Test + public void hashCollisionOnlyConsumesEqualInsertOnce() { + List emitted = + resolve( + Arrays.asList(item("copy", 9), item("deleted-only", 9)), + Collections.singletonList(item("copy", 9))); + + assertThat(emitted, contains("DELETE:deleted-only")); + } + + @Test + public void hashMatchAloneDoesNotDeduplicate() { + List emitted = + resolve( + Collections.singletonList(item("before", 42)), + Collections.singletonList(item("after", 42))); + + assertThat(emitted, contains("UPDATE_BEFORE:before", "UPDATE_AFTER:after")); + } + + /** + * A PK group can hold several records on a side only when identifier values are duplicated. The + * pairing is arbitrary in that case, but it must not depend on the order the caller supplies. + */ + @Test + public void pairingDoesNotDependOnInputOrder() { + List inOrder = + resolve( + Arrays.asList(item("d1", 10), item("d2", 20)), + Arrays.asList(item("i1", 30), item("i2", 40))); + List insertsReversed = + resolve( + Arrays.asList(item("d1", 10), item("d2", 20)), + Arrays.asList(item("i2", 40), item("i1", 30))); + + assertThat( + inOrder, + contains("UPDATE_BEFORE:d1", "UPDATE_AFTER:i1", "UPDATE_BEFORE:d2", "UPDATE_AFTER:i2")); + assertEquals(inOrder, insertsReversed); + } + + /** When the two sides differ in size, input order must not decide which record is a DELETE. */ + @Test + public void unmatchedExtraDoesNotDependOnInputOrder() { + List inOrder = + resolve( + Arrays.asList(item("d1", 10), item("d2", 20)), + Collections.singletonList(item("i1", 30))); + List deletesReversed = + resolve( + Arrays.asList(item("d2", 20), item("d1", 10)), + Collections.singletonList(item("i1", 30))); + + assertThat(inOrder, contains("UPDATE_BEFORE:d1", "UPDATE_AFTER:i1", "DELETE:d2")); + assertEquals(inOrder, deletesReversed); + } + + private static Item item(String nonPkValue, int hash) { + return new Item(nonPkValue, hash); + } + + private static List resolve(List deletes, List inserts) { + List emitted = new ArrayList<>(); + RESOLVER.resolve( + deletes, inserts, (kind, item) -> emitted.add(kind.name() + ":" + item.nonPkValue)); + return emitted; + } + + private static class TestResolver extends CdcResolver { + @Override + protected int nonPkHash(Item element) { + return element.hash; + } + + @Override + protected boolean nonPkEquals(Item delete, Item insert) { + return delete.nonPkValue.equals(insert.nonPkValue); + } + } + + private static class Item { + private final String nonPkValue; + private final int hash; + + private Item(String nonPkValue, int hash) { + this.nonPkValue = nonPkValue; + this.hash = hash; + } + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogScannerTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogScannerTest.java new file mode 100644 index 000000000000..afb536278941 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ChangelogScannerTest.java @@ -0,0 +1,465 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.iceberg.types.Types.NestedField.optional; +import static org.apache.iceberg.types.Types.NestedField.required; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.io.iceberg.SerializableDataFile; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.OutputBuilder; +import org.apache.beam.sdk.values.TimestampedValue; +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.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.FileFormat; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.ExpressionParser; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.types.Comparators; +import org.apache.iceberg.types.Conversions; +import org.apache.iceberg.types.TypeUtil; +import org.apache.iceberg.types.Types; +import org.joda.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ChangelogScanner}. */ +@RunWith(JUnit4.class) +public class ChangelogScannerTest { + private static final Schema SINGLE_PK_SCHEMA = + new Schema( + ImmutableList.of( + required(1, "id", Types.LongType.get()), optional(2, "data", Types.StringType.get())), + ImmutableSet.of(1)); + private static final Schema COMPOSITE_PK_SCHEMA = + new Schema( + ImmutableList.of( + required(1, "account", Types.StringType.get()), + required(2, "sequence", Types.IntegerType.get()), + optional(3, "data", Types.StringType.get())), + ImmutableSet.of(1, 2)); + private static final Schema SINGLE_RECORD_ID_SCHEMA = recordIdSchema(SINGLE_PK_SCHEMA); + private static final Schema COMPOSITE_RECORD_ID_SCHEMA = recordIdSchema(COMPOSITE_PK_SCHEMA); + private static final PartitionSpec UNPARTITIONED_SPEC = PartitionSpec.unpartitioned(); + + @Test + public void analyzeFilesPrunesNonOverlappingOpposingTasksToUnidirectional() { + FakeAddedRowsTask insert = new FakeAddedRowsTask(dataFile("insert", 10L, 20L), 11L); + FakeDeletedDataFileTask delete = new FakeDeletedDataFileTask(dataFile("delete", 30L, 40L), 13L); + + ChangelogScanner.AnalysisResult result = + ChangelogScanner.analyzeFiles( + true, + ImmutableList.of(insert, delete), + SINGLE_RECORD_ID_SCHEMA, + comparator(SINGLE_RECORD_ID_SCHEMA)); + + assertThat(result.bidirectional, empty()); + assertThat(result.unidirectional, contains(delete, insert)); + assertNull(result.overlapLower); + assertNull(result.overlapUpper); + } + + @Test + public void analyzeFilesTreatsFilesTouchingAtOneKeyAsOverlapping() { + // The insert file's max PK equals the delete file's min PK, so they intersect at exactly one + // value. Bounds are inclusive file statistics, so this must not be pruned to unidirectional. + FakeAddedRowsTask insert = new FakeAddedRowsTask(dataFile("insert", 10L, 20L), 11L); + FakeDeletedDataFileTask delete = new FakeDeletedDataFileTask(dataFile("delete", 20L, 30L), 13L); + + ChangelogScanner.AnalysisResult result = + ChangelogScanner.analyzeFiles( + true, + ImmutableList.of(insert, delete), + SINGLE_RECORD_ID_SCHEMA, + comparator(SINGLE_RECORD_ID_SCHEMA)); + + assertThat(result.unidirectional, empty()); + assertThat(result.bidirectional, containsInAnyOrder(insert, delete)); + assertEquals(20L, record(result.overlapLower).getField("id")); + assertEquals(20L, record(result.overlapUpper).getField("id")); + } + + @Test + public void analyzeFilesFindsOverlapDespiteInputOrder() { + FakeDeletedDataFileTask laterDelete = + new FakeDeletedDataFileTask(dataFile("delete-later", 30L, 40L), 13L); + FakeDeletedDataFileTask overlappingDelete = + new FakeDeletedDataFileTask(dataFile("delete-overlap", 15L, 18L), 17L); + FakeAddedRowsTask insert = new FakeAddedRowsTask(dataFile("insert", 10L, 20L), 19L); + + ChangelogScanner.AnalysisResult result = + ChangelogScanner.analyzeFiles( + true, + ImmutableList.of(laterDelete, overlappingDelete, insert), + SINGLE_RECORD_ID_SCHEMA, + comparator(SINGLE_RECORD_ID_SCHEMA)); + + assertThat(result.unidirectional, contains(laterDelete)); + assertThat(result.bidirectional, containsInAnyOrder(overlappingDelete, insert)); + assertEquals(15L, record(result.overlapLower).getField("id")); + assertEquals(18L, record(result.overlapUpper).getField("id")); + } + + @Test + public void analyzeFilesUsesLexicographicCompositePrimaryKeyRanges() { + FakeAddedRowsTask insert = + new FakeAddedRowsTask( + dataFile( + COMPOSITE_PK_SCHEMA, + "insert", + ImmutableMap.of("account", "acct-a", "sequence", 2), + ImmutableMap.of("account", "acct-a", "sequence", 8), + 11L), + 11L); + FakeDeletedDataFileTask overlappingDelete = + new FakeDeletedDataFileTask( + dataFile( + COMPOSITE_PK_SCHEMA, + "delete-overlap", + ImmutableMap.of("account", "acct-a", "sequence", 5), + ImmutableMap.of("account", "acct-b", "sequence", 1), + 13L), + 13L); + FakeDeletedDataFileTask farDelete = + new FakeDeletedDataFileTask( + dataFile( + COMPOSITE_PK_SCHEMA, + "delete-far", + ImmutableMap.of("account", "acct-c", "sequence", 1), + ImmutableMap.of("account", "acct-c", "sequence", 2), + 17L), + 17L); + + ChangelogScanner.AnalysisResult result = + ChangelogScanner.analyzeFiles( + true, + ImmutableList.of(insert, farDelete, overlappingDelete), + COMPOSITE_RECORD_ID_SCHEMA, + comparator(COMPOSITE_RECORD_ID_SCHEMA)); + + assertThat(result.unidirectional, contains(farDelete)); + assertThat(result.bidirectional, containsInAnyOrder(insert, overlappingDelete)); + assertEquals("acct-a", record(result.overlapLower).getField("account").toString()); + assertEquals(5, record(result.overlapLower).getField("sequence")); + assertEquals("acct-a", record(result.overlapUpper).getField("account").toString()); + assertEquals(8, record(result.overlapUpper).getField("sequence")); + } + + @Test + public void analyzeFilesConservativelyRoutesAllTasksWhenMetricsAreMissing() { + FakeAddedRowsTask insert = new FakeAddedRowsTask(dataFile("insert", 10L, 20L), 11L); + FakeDeletedDataFileTask deleteWithMissingMetrics = + new FakeDeletedDataFileTask(dataFileWithoutBounds("delete-missing-metrics"), 13L); + + ChangelogScanner.AnalysisResult result = + ChangelogScanner.analyzeFiles( + true, // should catch missing metrics even if table claims it tracks them + ImmutableList.of(insert, deleteWithMissingMetrics), + SINGLE_RECORD_ID_SCHEMA, + comparator(SINGLE_RECORD_ID_SCHEMA)); + + assertThat(result.unidirectional, empty()); + assertThat(result.bidirectional, contains(insert, deleteWithMissingMetrics)); + assertNull(result.overlapLower); + assertNull(result.overlapUpper); + } + + @Test + public void taskBatcherFlushesAtSplitBoundariesWithoutEmptyBatches() { + CapturingOutputReceiver out = new CapturingOutputReceiver(); + ChangelogScanner.TaskBatcher batcher = + new ChangelogScanner.TaskBatcher("default.table", 1234L, 100L, out); + SerializableChangelogTask first = serializableTask("first", 40L); + SerializableChangelogTask second = serializableTask("second", 60L); + SerializableChangelogTask third = serializableTask("third", 1L); + + batcher.add(first, 1L, 40L); + batcher.add(second, 1L, 60L); + assertThat(out.values, empty()); + + batcher.add(third, 1L, 1L); + assertEquals(1, out.values.size()); + batcher.flush(); + + assertEquals(2, out.values.size()); + assertEquals(2, batcher.totalSplits); + assertEquals(new Instant(1234L), out.values.get(0).getTimestamp()); + assertThat(tasksInOutput(out, 0), contains(first, second)); + assertThat(tasksInOutput(out, 1), contains(third)); + } + + @Test + public void taskBatcherAllowsOversizeSingleTaskWithoutEmittingEmptyBatch() { + CapturingOutputReceiver out = new CapturingOutputReceiver(); + ChangelogScanner.TaskBatcher batcher = + new ChangelogScanner.TaskBatcher("default.table", 1234L, 100L, out); + SerializableChangelogTask oversize = serializableTask("oversize", 150L); + + batcher.add(oversize, 1L, 150L); + assertThat(out.values, empty()); + + batcher.flush(); + + assertEquals(1, out.values.size()); + assertEquals(1, batcher.totalSplits); + assertThat(tasksInOutput(out, 0), contains(oversize)); + } + + private static Comparator comparator(Schema schema) { + return Comparators.forType(schema.asStruct()); + } + + private static Schema recordIdSchema(Schema schema) { + return TypeUtil.select(schema, schema.identifierFieldIds()); + } + + private static Record record(StructLike structLike) { + return (Record) structLike; + } + + private static List tasksInOutput( + CapturingOutputReceiver out, int index) { + return out.values.get(index).getValue().getValue(); + } + + private static DataFile dataFile(String name, long lower, long upper) { + return dataFile(UNPARTITIONED_SPEC, null, name, lower, upper); + } + + private static DataFile dataFile( + PartitionSpec spec, StructLike partition, String name, long lower, long upper) { + return dataFile( + SINGLE_PK_SCHEMA, + spec, + partition, + name, + ImmutableMap.of("id", lower), + ImmutableMap.of("id", upper), + 100L); + } + + private static DataFile dataFile( + Schema schema, String name, Map lower, Map upper, long size) { + return dataFile(schema, UNPARTITIONED_SPEC, null, name, lower, upper, size); + } + + private static DataFile dataFile( + Schema schema, + PartitionSpec spec, + StructLike partition, + String name, + Map lower, + Map upper, + long size) { + DataFiles.Builder builder = + DataFiles.builder(spec) + .withFormat(FileFormat.PARQUET) + .withPath("gs:://bucket/data/" + name + ".parquet") + .withFileSizeInBytes(size) + .withMetrics( + new Metrics( + 1L, null, null, null, null, bounds(schema, lower), bounds(schema, upper))); + if (partition != null) { + builder.withPartition(partition); + } + return builder.build(); + } + + private static DataFile dataFileWithoutBounds(String name) { + return DataFiles.builder(UNPARTITIONED_SPEC) + .withFormat(FileFormat.PARQUET) + .withPath("gs:://bucket/data/" + name + ".parquet") + .withFileSizeInBytes(100L) + .withMetrics(new Metrics(1L, null, null, null, null, null, null)) + .build(); + } + + private static Map bounds(Schema schema, Map values) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Types.NestedField field : schema.columns()) { + if (values.containsKey(field.name())) { + builder.put( + field.fieldId(), Conversions.toByteBuffer(field.type(), values.get(field.name()))); + } + } + return builder.build(); + } + + private static SerializableChangelogTask serializableTask(String name, long length) { + DataFile file = + DataFiles.builder(UNPARTITIONED_SPEC) + .withFormat(FileFormat.PARQUET) + .withPath("gs:://bucket/data/" + name + ".parquet") + .withFileSizeInBytes(length) + .withMetrics(new Metrics(1L, null, null, null, null, null, null)) + .build(); + return SerializableChangelogTask.builder() + .setType(SerializableChangelogTask.Type.ADDED_ROWS) + .setDataFile(SerializableDataFile.from(file, PartitionSpec.unpartitioned(), false)) + .setSpecId(UNPARTITIONED_SPEC.specId()) + .setOperation(ChangelogOperation.INSERT) + .setOrdinal(0) + .setCommitSnapshotId(1L) + .setStart(0L) + .setLength(length) + .setJsonExpression(ExpressionParser.toJson(Expressions.alwaysTrue())) + .build(); + } + + private abstract static class FakeContentTask + implements ChangelogScanTask, ContentScanTask { + private final DataFile file; + private final PartitionSpec spec; + private final StructLike partition; + private final long length; + + FakeContentTask(DataFile file, long length) { + this(file, UNPARTITIONED_SPEC, file.partition(), length); + } + + FakeContentTask(DataFile file, PartitionSpec spec, StructLike partition, long length) { + this.file = file; + this.spec = spec; + this.partition = partition; + this.length = length; + } + + @Override + public DataFile file() { + return file; + } + + @Override + public PartitionSpec spec() { + return spec; + } + + @Override + public StructLike partition() { + return partition; + } + + @Override + public long start() { + return 0L; + } + + @Override + public long length() { + return length; + } + + @Override + public Expression residual() { + return Expressions.alwaysTrue(); + } + + @Override + public int changeOrdinal() { + return 0; + } + + @Override + public long commitSnapshotId() { + return 1L; + } + } + + private static class FakeAddedRowsTask extends FakeContentTask implements AddedRowsScanTask { + FakeAddedRowsTask(DataFile file, long length) { + super(file, length); + } + + @Override + public List deletes() { + return Collections.emptyList(); + } + + @Override + public ChangelogOperation operation() { + return ChangelogOperation.INSERT; + } + } + + private static class FakeDeletedDataFileTask extends FakeContentTask + implements DeletedDataFileScanTask { + FakeDeletedDataFileTask(DataFile file, long length) { + super(file, length); + } + + @Override + public List existingDeletes() { + return Collections.emptyList(); + } + + @Override + public ChangelogOperation operation() { + return ChangelogOperation.DELETE; + } + } + + private static final class CapturingOutputReceiver + implements DoFn.OutputReceiver>> { + private final ImmutableList.Builder< + TimestampedValue>>> + builder = ImmutableList.builder(); + private List>>> + values = ImmutableList.of(); + + @Override + public OutputBuilder>> builder( + KV> value) { + throw new UnsupportedOperationException("Use outputWithTimestamp in this test receiver."); + } + + @Override + public void outputWithTimestamp( + KV> value, Instant timestamp) { + builder.add(TimestampedValue.of(value, timestamp)); + values = builder.build(); + } + } +} 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/IncrementalChangelogSourceTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSourceTest.java new file mode 100644 index 000000000000..14e7208dad0c --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSourceTest.java @@ -0,0 +1,514 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.iceberg.types.Types.NestedField.optional; +import static org.apache.iceberg.types.Types.NestedField.required; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.io.iceberg.IcebergCatalogConfig; +import org.apache.beam.sdk.io.iceberg.IcebergIO.ReadRows.StartingStrategy; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.TestDataWarehouse; +import org.apache.beam.sdk.io.iceberg.TestFixtures; +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.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.Reify; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollection.IsBounded; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TimestampedValue; +import org.apache.beam.sdk.values.ValueKind; +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.iceberg.ChangelogOperation; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.Table; +import org.apache.iceberg.TableProperties; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.types.Types; +import org.joda.time.Instant; +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; + +/** Integration tests for {@link IncrementalChangelogSource}. */ +@RunWith(JUnit4.class) +public class IncrementalChangelogSourceTest { + private static final org.apache.iceberg.Schema CDC_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + required(1, "id", Types.LongType.get()), optional(2, "data", Types.StringType.get())), + ImmutableSet.of(1)); + + private static final org.apache.iceberg.Schema EVENT_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + required(1, "id", Types.LongType.get()), + optional(2, "data", Types.StringType.get()), + required(3, "event_time", Types.TimestampType.withoutZone())), + 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(); + @Rule public TestPipeline pipeline = TestPipeline.create(); + + @Test + public void boundedSnapshotRangeEmitsOnlyRequestedSnapshotsWithProjectedSchema() + throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + commitAppend(table, "s1.parquet", records(1L, "one")); + commitAppend(table, "s2.parquet", records(2L, "two")); + commitAppend(table, "s3.parquet", records(3L, "three")); + commitAppend(table, "s4.parquet", records(4L, "four")); + List snapshots = Lists.newArrayList(table.snapshots()); + + IcebergScanConfig scanConfig = + baseConfigBuilder(table, tableId) + .setKeepFields(ImmutableList.of("id")) + .setFromSnapshotInclusive(snapshots.get(1).snapshotId()) + .setToSnapshot(snapshots.get(2).snapshotId()) + .build(); + Schema projectedSchema = Schema.builder().addInt64Field("id").build(); + + PCollection rows = pipeline.apply(new IncrementalChangelogSource(scanConfig)); + + assertThat(rows.isBounded(), equalTo(IsBounded.BOUNDED)); + assertEquals(projectedSchema, rows.getSchema()); + PAssert.that(rows) + .containsInAnyOrder( + Row.withSchema(projectedSchema).addValue(2L).build(), + Row.withSchema(projectedSchema).addValue(3L).build()); + + pipeline.run().waitUntilFinish(); + } + + @Test + public void metadataColumnsAreAppendedToProjectedRecord() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tablePropertiesV3()); + DataFile file1 = + commitAppend( + table, + testName.getMethodName() + "file1.parquet", + Arrays.asList( + record(1L, "one"), record(2L, "two"), record(3L, "three"), record(4L, "four"))); + + DataFile file2 = + warehouse.writeRecords( + testName.getMethodName() + "file2.parquet", + table.schema(), + PartitionSpec.unpartitioned(), + null, + ImmutableList.of(record(3L, "three_new"), record(4L, "four_new"))); + table.newOverwrite().deleteFile(file1).addFile(file2).commit(); + table.refresh(); + + List snapshots = Lists.newArrayList(table.snapshots()); + long snap1Id = snapshots.get(0).snapshotId(); + long snap1Seq = snapshots.get(0).sequenceNumber(); + long file1Seq = snapshots.get(0).sequenceNumber(); + long snap1FirstRowId = snapshots.get(0).firstRowId(); + + long snap2Id = snapshots.get(1).snapshotId(); + long snap2Seq = snapshots.get(1).sequenceNumber(); + long file2Seq = snapshots.get(1).sequenceNumber(); + long snap2FirstRowId = snapshots.get(1).firstRowId(); + + IcebergScanConfig scanConfig = + baseConfigBuilder(table, tableId) + .setKeepFields(ImmutableList.of("id")) + .setFromSnapshotInclusive(snapshots.get(0).snapshotId()) + .setToSnapshot(snapshots.get(1).snapshotId()) + .setMetadataColumns( + ImmutableList.of( + IcebergCdcMetadataColumns.CHANGE_TYPE, + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID, + IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_SEQUENCE_NUMBER, + IcebergCdcMetadataColumns.ROW_ID, + IcebergCdcMetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER)) + .build(); + Schema outputSchema = + Schema.builder() + .addInt64Field("id") + .addStringField(IcebergCdcMetadataColumns.CHANGE_TYPE) + .addInt64Field(IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_ID) + .addInt64Field(IcebergCdcMetadataColumns.COMMIT_SNAPSHOT_SEQUENCE_NUMBER) + .addNullableField(IcebergCdcMetadataColumns.ROW_ID, Schema.FieldType.INT64) + .addNullableField( + IcebergCdcMetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER, Schema.FieldType.INT64) + .build(); + + PCollection rows = pipeline.apply(new IncrementalChangelogSource(scanConfig)); + + assertEquals(outputSchema, rows.getSchema()); + PAssert.that(rows) + .containsInAnyOrder( + // snapshot 1: insert data file 1 + row( + 1L, + ChangelogOperation.INSERT, + snap1Id, + snap1Seq, + snap1FirstRowId, + file1Seq, + outputSchema), + row( + 2L, + ChangelogOperation.INSERT, + snap1Id, + snap1Seq, + snap1FirstRowId + 1, + file1Seq, + outputSchema), + row( + 3L, + ChangelogOperation.INSERT, + snap1Id, + snap1Seq, + snap1FirstRowId + 2, + file1Seq, + outputSchema), + row( + 4L, + ChangelogOperation.INSERT, + snap1Id, + snap1Seq, + snap1FirstRowId + 3, + file1Seq, + outputSchema), + // snapshot 2: delete data file 1 + row( + 1L, + ChangelogOperation.DELETE, + snap2Id, + snap2Seq, + snap1FirstRowId, + file1Seq, + outputSchema), + row( + 2L, + ChangelogOperation.DELETE, + snap2Id, + snap2Seq, + snap1FirstRowId + 1, + file1Seq, + outputSchema), + row( + 3L, + ChangelogOperation.UPDATE_BEFORE, + snap2Id, + snap2Seq, + snap1FirstRowId + 2, + file1Seq, + outputSchema), + row( + 4L, + ChangelogOperation.UPDATE_BEFORE, + snap2Id, + snap2Seq, + snap1FirstRowId + 3, + file1Seq, + outputSchema), + // snapshot 2: insert data file 2 + row( + 3L, + ChangelogOperation.UPDATE_AFTER, + snap2Id, + snap2Seq, + snap2FirstRowId, + file2Seq, + outputSchema), + row( + 4L, + ChangelogOperation.UPDATE_AFTER, + snap2Id, + snap2Seq, + snap2FirstRowId + 1, + file2Seq, + outputSchema)); + + pipeline.run().waitUntilFinish(); + } + + private Row row( + long id, + ChangelogOperation operation, + long snapshotId, + long snapshotSequence, + long rowId, + long lastUpdatedSequence, + Schema outputSchema) { + return Row.withSchema(outputSchema) + .addValues(id, operation.name(), snapshotId, snapshotSequence, rowId, lastUpdatedSequence) + .build(); + } + + @Test + public void streamingSnapshotRangeTerminatesWithoutDuplicates() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + commitAppend(table, "s1.parquet", records(1L, "one")); + commitAppend(table, "s2.parquet", records(2L, "two")); + commitAppend(table, "s3.parquet", records(3L, "three")); + + IcebergScanConfig scanConfig = + baseConfigBuilder(table, tableId) + .setStreaming(true) + .setStartingStrategy(StartingStrategy.EARLIEST) + .setToSnapshot(table.currentSnapshot().snapshotId()) + .build(); + Schema rowSchema = IcebergUtils.icebergSchemaToBeamSchema(CDC_SCHEMA); + + PCollection rows = pipeline.apply(new IncrementalChangelogSource(scanConfig)); + + assertThat(rows.isBounded(), equalTo(IsBounded.UNBOUNDED)); + PAssert.that(rows) + .containsInAnyOrder( + Row.withSchema(rowSchema).addValues(1L, "one").build(), + Row.withSchema(rowSchema).addValues(2L, "two").build(), + Row.withSchema(rowSchema).addValues(3L, "three").build()); + + pipeline.run().waitUntilFinish(); + } + + @Test + public void overwriteUpdatePairsAreResolvedWithinSnapshotWindow() throws Exception { + TableIdentifier tableId = tableId(); + Table table = + warehouse.createTable( + tableId, + CDC_SCHEMA, + null, + ImmutableMap.of(TableProperties.FORMAT_VERSION, "2", TableProperties.SPLIT_SIZE, "1")); + DataFile oldFile = + commitAppend( + table, "old.parquet", ImmutableList.of(record(1L, "before"), record(2L, "same"))); + DataFile newFile = + warehouse.writeRecords( + testName.getMethodName() + "-new.parquet", + table.schema(), + ImmutableList.of(record(1L, "after"), record(2L, "same"))); + table.newOverwrite().deleteFile(oldFile).addFile(newFile).commit(); + table.refresh(); + + IcebergScanConfig scanConfig = + baseConfigBuilder(table, tableId) + .setFromSnapshotInclusive(table.currentSnapshot().snapshotId()) + .setToSnapshot(table.currentSnapshot().snapshotId()) + .build(); + + PCollection changes = + pipeline + .apply(new IncrementalChangelogSource(scanConfig)) + .apply("Format Changes", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(changes).containsInAnyOrder("UPDATE_BEFORE:1:before", "UPDATE_AFTER:1:after"); + + pipeline.run().waitUntilFinish(); + } + + @Test + public void watermarkColumnRestampsProjectedRows() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, EVENT_SCHEMA, null, tableProperties()); + java.time.Instant firstEventInstant = + java.time.Instant.ofEpochMilli(System.currentTimeMillis() - 1_000L); + java.time.Instant secondEventInstant = firstEventInstant.plusMillis(5_000L); + LocalDateTime firstEvent = LocalDateTime.ofInstant(firstEventInstant, ZoneOffset.UTC); + LocalDateTime secondEvent = LocalDateTime.ofInstant(secondEventInstant, ZoneOffset.UTC); + commitAppend( + table, + "events.parquet", + ImmutableList.of(eventRecord(1L, "one", firstEvent), eventRecord(2L, "two", secondEvent))); + + IcebergScanConfig scanConfig = + baseConfigBuilder(table, tableId) + .setKeepFields(ImmutableList.of("id", "event_time")) + .setWatermarkColumn("event_time") + .setToSnapshot(table.currentSnapshot().snapshotId()) + .build(); + Schema projectedSchema = + Schema.builder() + .addInt64Field("id") + .addLogicalTypeField( + "event_time", org.apache.beam.sdk.schemas.logicaltypes.SqlTypes.DATETIME) + .build(); + + PCollection rows = pipeline.apply(new IncrementalChangelogSource(scanConfig)); + + assertEquals(projectedSchema, rows.getSchema()); + PAssert.that(rows.apply(Reify.timestamps())) + .containsInAnyOrder( + TimestampedValue.of( + Row.withSchema(projectedSchema).addValues(1L, firstEvent).build(), + new Instant(firstEventInstant.toEpochMilli())), + TimestampedValue.of( + Row.withSchema(projectedSchema).addValues(2L, secondEvent).build(), + new Instant(secondEventInstant.toEpochMilli()))); + + pipeline.run().waitUntilFinish(); + } + + /** + * Bi-directional rows are windowed per snapshot with zero allowed lateness, so a record that + * reaches the CoGroupByKey after the watermark has passed its snapshot's window is dropped + * silently. Drive several consecutive snapshots through the shuffle path and assert that + * advancing past one snapshot never discards an earlier one's records. + */ + @Test + public void consecutiveSnapshotsThroughShuffleDropNoRecords() throws Exception { + assertNoRecordsDroppedAcrossSnapshots(false); + } + + /** Same, but driven by the per-snapshot watermark from {@link WatchForSnapshotsSdf}. */ + @Test + public void consecutiveSnapshotsThroughShuffleDropNoRecordsWhenStreaming() throws Exception { + assertNoRecordsDroppedAcrossSnapshots(true); + } + + private void assertNoRecordsDroppedAcrossSnapshots(boolean streaming) throws Exception { + TableIdentifier tableId = tableId(); + // SPLIT_SIZE=1 forces every bi-directional group onto the CoGroupByKey path rather than + // LocalResolveDoFn, so the windowing/lateness behaviour is what's under test. + Table table = + warehouse.createTable( + tableId, + CDC_SCHEMA, + null, + ImmutableMap.of(TableProperties.FORMAT_VERSION, "2", TableProperties.SPLIT_SIZE, "1")); + + DataFile v1 = commitAppend(table, "v1.parquet", ImmutableList.of(record(1L, "v1"))); + DataFile v2 = commitOverwrite(table, "v2.parquet", v1, record(1L, "v2")); + commitOverwrite(table, "v3.parquet", v2, record(1L, "v3")); + + IcebergScanConfig scanConfig = + baseConfigBuilder(table, tableId) + .setStreaming(streaming) + .setStartingStrategy(StartingStrategy.EARLIEST) + .setToSnapshot(table.currentSnapshot().snapshotId()) + .build(); + + PCollection changes = + pipeline + .apply(new IncrementalChangelogSource(scanConfig)) + .apply("Format Changes", ParDo.of(new FormatValueKindAndRow())); + + PAssert.that(changes) + .containsInAnyOrder( + "INSERT:1:v1", + "UPDATE_BEFORE:1:v1", + "UPDATE_AFTER:1:v2", + "UPDATE_BEFORE:1:v2", + "UPDATE_AFTER:1:v3"); + + pipeline.run().waitUntilFinish(); + } + + private DataFile commitOverwrite( + Table table, String fileName, DataFile replaced, Record replacement) throws IOException { + DataFile file = + warehouse.writeRecords( + testName.getMethodName() + "-" + fileName, + table.schema(), + ImmutableList.of(replacement)); + table.newOverwrite().deleteFile(replaced).addFile(file).commit(); + table.refresh(); + return file; + } + + private TableIdentifier tableId() { + return TableIdentifier.of("default", testName.getMethodName()); + } + + private IcebergScanConfig.Builder baseConfigBuilder(Table table, TableIdentifier tableId) { + return IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(table.schema())) + .setUseCdc(true); + } + + private static Map tableProperties() { + return ImmutableMap.of(TableProperties.FORMAT_VERSION, "2"); + } + + private static Map tablePropertiesV3() { + return ImmutableMap.of(TableProperties.FORMAT_VERSION, "3"); + } + + private DataFile commitAppend(Table table, String fileName, List records) + throws IOException { + DataFile file = + warehouse.writeRecords(testName.getMethodName() + "-" + fileName, table.schema(), records); + table.newFastAppend().appendFile(file).commit(); + table.refresh(); + return file; + } + + private static List records(long id, String data) { + return ImmutableList.of(record(id, data)); + } + + private static Record record(long id, String data) { + return TestFixtures.createRecord(CDC_SCHEMA, ImmutableMap.of("id", id, "data", data)); + } + + private static Record eventRecord(long id, String data, LocalDateTime eventTime) { + return TestFixtures.createRecord( + EVENT_SCHEMA, ImmutableMap.of("id", id, "data", data, "event_time", eventTime)); + } + + private static final class FormatValueKindAndRow extends DoFn { + @ProcessElement + public void process( + @Element Row row, ValueKind valueKind, OutputReceiver outputReceiver) { + outputReceiver.output( + valueKind.name() + ":" + row.getInt64("id") + ":" + row.getString("data")); + } + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/LocalResolveDoFnTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/LocalResolveDoFnTest.java new file mode 100644 index 000000000000..2dfbb854b374 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/LocalResolveDoFnTest.java @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.Map; +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.TestDataWarehouse; +import org.apache.beam.sdk.transforms.DoFnTester; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TimestampedValue; +import org.apache.beam.sdk.values.ValueInSingleWindow; +import org.apache.beam.sdk.values.ValueKind; +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.Table; +import org.apache.iceberg.TableProperties; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.GenericRecord; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.expressions.ExpressionParser; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.types.Types; +import org.joda.time.Instant; +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; + +/** Integration tests for {@link LocalResolveDoFn}. */ +@RunWith(JUnit4.class) +public class LocalResolveDoFnTest { + 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, "visible", Types.StringType.get()), + Types.NestedField.optional(3, "hidden", Types.StringType.get())), + ImmutableSet.of(1)); + + private static final org.apache.iceberg.Schema FIXED_CDC_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.required(1, "id", Types.LongType.get()), + Types.NestedField.optional(2, "visible", Types.StringType.get()), + Types.NestedField.optional(3, "data", Types.FixedType.ofLength(4))), + 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 copyOnWriteRewriteOfIdenticalRowsIsDropped() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + IcebergScanConfig scanConfig = scanConfig(table, tableId); + DataFile oldFile = + warehouse.writeRecords( + testName.getMethodName() + "-old.parquet", + table.schema(), + ImmutableList.of(record(1L, "shown", "same-hidden"))); + DataFile newFile = + warehouse.writeRecords( + testName.getMethodName() + "-new.parquet", + table.schema(), + ImmutableList.of(record(1L, "shown", "same-hidden"))); + + List> output = + process( + scanConfig, + descriptor(tableId, 1L, 1L), + ImmutableList.of( + task(SerializableChangelogTask.Type.DELETED_FILE, oldFile, table, 300L), + task(SerializableChangelogTask.Type.ADDED_ROWS, newFile, table, 300L)), + new Instant(0L)); + + assertThat(output, empty()); + } + + @Test + public void hiddenOnlyUpdateIsResolvedBeforeProjection() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + IcebergScanConfig scanConfig = scanConfig(table, tableId); + DataFile oldFile = + warehouse.writeRecords( + testName.getMethodName() + "-old.parquet", + table.schema(), + ImmutableList.of(record(1L, "shown", "old-hidden"))); + DataFile newFile = + warehouse.writeRecords( + testName.getMethodName() + "-new.parquet", + table.schema(), + ImmutableList.of(record(1L, "shown", "new-hidden"))); + Instant timestamp = new Instant(1234L); + + List> output = + process( + scanConfig, + descriptor(tableId, 1L, 1L), + ImmutableList.of( + task(SerializableChangelogTask.Type.DELETED_FILE, oldFile, table, 301L), + task(SerializableChangelogTask.Type.ADDED_ROWS, newFile, table, 301L)), + timestamp); + + assertThat( + output.stream().map(LocalResolveDoFnTest::kindAndProjectedRow).collect(Collectors.toList()), + contains("UPDATE_BEFORE:1:shown:2", "UPDATE_AFTER:1:shown:2")); + assertEquals( + ImmutableList.of(timestamp, timestamp), + output.stream().map(ValueInSingleWindow::getTimestamp).collect(Collectors.toList())); + } + + @Test + public void copyOnWriteRewriteWithFixedColumnIsDropped() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, FIXED_CDC_SCHEMA, null, tableProperties()); + IcebergScanConfig scanConfig = scanConfig(table, tableId); + // Distinct byte[] instances with identical content. The CoW no-op is only dropped when the + // `fixed` column is hashed/compared by content; an identity hashCode would leak a spurious + // UPDATE pair. + DataFile oldFile = + warehouse.writeRecords( + testName.getMethodName() + "-old.parquet", + table.schema(), + ImmutableList.of(fixedRecord(1L, "shown", new byte[] {1, 2, 3, 4}))); + DataFile newFile = + warehouse.writeRecords( + testName.getMethodName() + "-new.parquet", + table.schema(), + ImmutableList.of(fixedRecord(1L, "shown", new byte[] {1, 2, 3, 4}))); + + List> output = + process( + scanConfig, + descriptor(tableId, 1L, 1L), + ImmutableList.of( + task(SerializableChangelogTask.Type.DELETED_FILE, oldFile, table, 300L), + task(SerializableChangelogTask.Type.ADDED_ROWS, newFile, table, 300L)), + new Instant(0L)); + + assertThat(output, empty()); + } + + @Test + public void differingFixedColumnBecomesUpdatePair() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, FIXED_CDC_SCHEMA, null, tableProperties()); + IcebergScanConfig scanConfig = scanConfig(table, tableId); + // Same PK and projected fields, but the `fixed` column differs, so this must NOT be treated as + // a CoW duplicate -- guards against the fixed column being ignored during resolution. + DataFile oldFile = + warehouse.writeRecords( + testName.getMethodName() + "-old.parquet", + table.schema(), + ImmutableList.of(fixedRecord(1L, "shown", new byte[] {1, 2, 3, 4}))); + DataFile newFile = + warehouse.writeRecords( + testName.getMethodName() + "-new.parquet", + table.schema(), + ImmutableList.of(fixedRecord(1L, "shown", new byte[] {5, 6, 7, 8}))); + + List> output = + process( + scanConfig, + descriptor(tableId, 1L, 1L), + ImmutableList.of( + task(SerializableChangelogTask.Type.DELETED_FILE, oldFile, table, 302L), + task(SerializableChangelogTask.Type.ADDED_ROWS, newFile, table, 302L)), + new Instant(0L)); + + assertThat( + output.stream().map(LocalResolveDoFnTest::kindAndProjectedRow).collect(Collectors.toList()), + contains("UPDATE_BEFORE:1:shown:2", "UPDATE_AFTER:1:shown:2")); + } + + @Test + public void recordsOnOverlapBoundsAreResolvedAsUpdates() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + IcebergScanConfig scanConfig = scanConfig(table, tableId); + DataFile oldFile = + warehouse.writeRecords( + testName.getMethodName() + "-old.parquet", + table.schema(), + ImmutableList.of(record(1L, "shown", "old"), record(2L, "shown", "old"))); + DataFile newFile = + warehouse.writeRecords( + testName.getMethodName() + "-new.parquet", + table.schema(), + ImmutableList.of(record(1L, "shown", "new"), record(2L, "shown", "new"))); + + List> output = + process( + scanConfig, + descriptor(tableId, 1L, 2L), + ImmutableList.of( + task(SerializableChangelogTask.Type.DELETED_FILE, oldFile, table, 303L), + task(SerializableChangelogTask.Type.ADDED_ROWS, newFile, table, 303L)), + new Instant(0L)); + + assertThat( + output.stream().map(LocalResolveDoFnTest::kindAndProjectedRow).collect(Collectors.toList()), + containsInAnyOrder( + "UPDATE_BEFORE:1:shown:2", + "UPDATE_AFTER:1:shown:2", + "UPDATE_BEFORE:2:shown:2", + "UPDATE_AFTER:2:shown:2")); + } + + private List> process( + IcebergScanConfig scanConfig, + ChangelogDescriptor descriptor, + List tasks, + Instant timestamp) + throws Exception { + try (DoFnTester>, Row> tester = + DoFnTester.of(new LocalResolveDoFn(scanConfig))) { + tester.processTimestampedElement(TimestampedValue.of(KV.of(descriptor, tasks), timestamp)); + return tester.getMutableOutput(tester.getMainOutputTag()); + } + } + + private TableIdentifier tableId() { + return TableIdentifier.of("default", testName.getMethodName()); + } + + private IcebergScanConfig scanConfig(Table table, TableIdentifier tableId) { + return IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(table.schema())) + .setKeepFields(ImmutableList.of("id", "visible")) + .setUseCdc(true) + .build(); + } + + private static ChangelogDescriptor descriptor( + TableIdentifier tableId, long lowerInclusive, long upperInclusive) { + org.apache.beam.sdk.schemas.Schema pkSchema = + org.apache.beam.sdk.schemas.Schema.builder().addInt64Field("id").build(); + return ChangelogDescriptor.builder() + .setTableIdentifierString(tableId.toString()) + .setSnapshotSequenceNumber(1) + .setCommitSnapshotId(1) + .setOverlapLower(Row.withSchema(pkSchema).addValue(lowerInclusive).build()) + .setOverlapUpper(Row.withSchema(pkSchema).addValue(upperInclusive).build()) + .build(); + } + + private static Record record(long id, String visible, String hidden) { + GenericRecord record = GenericRecord.create(CDC_SCHEMA); + record.setField("id", id); + record.setField("visible", visible); + record.setField("hidden", hidden); + return record; + } + + private static Record fixedRecord(long id, String visible, byte[] data) { + GenericRecord record = GenericRecord.create(FIXED_CDC_SCHEMA); + record.setField("id", id); + record.setField("visible", visible); + record.setField("data", data); + return record; + } + + private static SerializableChangelogTask task( + SerializableChangelogTask.Type type, DataFile dataFile, Table table, long snapshotId) { + return SerializableChangelogTask.builder() + .setType(type) + .setDataFile(dataFile, table.spec(), true) + .setAddedDeletes(ImmutableList.of()) + .setExistingDeletes(ImmutableList.of()) + .setSpecId(table.spec().specId()) + .setOperation( + type == SerializableChangelogTask.Type.ADDED_ROWS + ? ChangelogOperation.INSERT + : ChangelogOperation.DELETE) + .setOrdinal(0) + .setCommitSnapshotId(snapshotId) + .setStart(0L) + .setLength(dataFile.fileSizeInBytes()) + .setJsonExpression(ExpressionParser.toJson(Expressions.alwaysTrue())) + .build(); + } + + private static Map tableProperties() { + return ImmutableMap.of(TableProperties.FORMAT_VERSION, "2"); + } + + private static String kindAndProjectedRow(ValueInSingleWindow value) { + ValueKind kind = value.getValueKind(); + Row row = value.getValue(); + return kind.name() + + ":" + + row.getInt64("id") + + ":" + + row.getString("visible") + + ":" + + row.getSchema().getFieldCount(); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/OverlapRangeTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/OverlapRangeTest.java new file mode 100644 index 000000000000..69e47182e173 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/OverlapRangeTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +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.TestDataWarehouse; +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.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.GenericRecord; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.types.Types; +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; + +/** Unit tests for {@link OverlapRange}. */ +@RunWith(JUnit4.class) +public class OverlapRangeTest { + private static final org.apache.iceberg.Schema SINGLE_PK_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get())), + ImmutableSet.of(1)); + + private static final org.apache.iceberg.Schema COMPOSITE_PK_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.optional(3, "data", Types.StringType.get()), + Types.NestedField.required(1, "account", Types.StringType.get()), + Types.NestedField.optional(4, "extra", Types.IntegerType.get()), + Types.NestedField.required(2, "sequence", Types.IntegerType.get())), + ImmutableSet.of(1, 2)); + + @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); + @Rule public TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); + @Rule public final TestName testName = new TestName(); + + @Test + public void containsUsesInclusiveSingleColumnBounds() throws Exception { + OverlapRange range = overlapRange(SINGLE_PK_SCHEMA); + StructLike lower = range.toStructLike(pkRow(range.recordIdSchema(), 10)); + StructLike upper = range.toStructLike(pkRow(range.recordIdSchema(), 20)); + + assertFalse(range.contains(singlePkRecord(9), lower, upper)); + assertTrue(range.contains(singlePkRecord(10), lower, upper)); + assertTrue(range.contains(singlePkRecord(15), lower, upper)); + assertTrue(range.contains(singlePkRecord(20), lower, upper)); + assertFalse(range.contains(singlePkRecord(21), lower, upper)); + } + + @Test + public void containsUsesLexicographicCompositeBounds() throws Exception { + OverlapRange range = overlapRange(COMPOSITE_PK_SCHEMA); + StructLike lower = range.toStructLike(pkRow(range.recordIdSchema(), "a", 2)); + StructLike upper = range.toStructLike(pkRow(range.recordIdSchema(), "b", 1)); + + assertFalse(range.contains(compositePkRecord("a", 1), lower, upper)); + assertTrue(range.contains(compositePkRecord("a", 2), lower, upper)); + assertTrue(range.contains(compositePkRecord("a", 9), lower, upper)); + assertTrue(range.contains(compositePkRecord("b", 0), lower, upper)); + assertTrue(range.contains(compositePkRecord("b", 1), lower, upper)); + assertFalse(range.contains(compositePkRecord("b", 2), lower, upper)); + } + + @Test + public void nullBoundsAreConservative() throws Exception { + OverlapRange range = overlapRange(SINGLE_PK_SCHEMA); + StructLike lower = range.toStructLike(pkRow(range.recordIdSchema(), 10)); + StructLike upper = range.toStructLike(pkRow(range.recordIdSchema(), 20)); + + assertNull(range.toStructLike(null)); + assertTrue(range.contains(singlePkRecord(1), null, upper)); + assertTrue(range.contains(singlePkRecord(100), lower, null)); + assertTrue(range.contains(singlePkRecord(100), null, null)); + } + + @Test + public void recordIdProjectionUsesIdentifierFieldsFromFullRecord() throws Exception { + OverlapRange range = overlapRange(COMPOSITE_PK_SCHEMA); + StructLike lower = range.toStructLike(pkRow(range.recordIdSchema(), "acct", 7)); + StructLike upper = range.toStructLike(pkRow(range.recordIdSchema(), "acct", 7)); + + assertTrue(range.contains(compositePkRecord("acct", 7), lower, upper)); + + assertEquals("acct", range.recordIdProjection().get(0, String.class)); + assertEquals(7, (int) range.recordIdProjection().get(1, Integer.class)); + } + + private OverlapRange overlapRange(org.apache.iceberg.Schema schema) throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + IcebergCatalogConfig catalogConfig = + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build(); + catalogConfig.catalog().createTable(tableId, schema); + IcebergScanConfig scanConfig = + IcebergScanConfig.builder() + .setCatalogConfig(catalogConfig) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(schema)) + .setUseCdc(true) + .build(); + return OverlapRange.forScanConfig(scanConfig); + } + + private static Row pkRow(Schema recordIdSchema, Object... values) { + return Row.withSchema(IcebergUtils.icebergSchemaToBeamSchema(recordIdSchema)) + .addValues(values) + .build(); + } + + private static Record singlePkRecord(int id) { + GenericRecord record = GenericRecord.create(SINGLE_PK_SCHEMA); + record.setField("id", id); + record.setField("data", "v" + id); + return record; + } + + private static Record compositePkRecord(String account, int sequence) { + GenericRecord record = GenericRecord.create(COMPOSITE_PK_SCHEMA); + record.setField("data", "payload"); + record.setField("account", account); + record.setField("extra", 100); + record.setField("sequence", sequence); + return record; + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ReadFromChangelogsTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ReadFromChangelogsTest.java new file mode 100644 index 000000000000..7a1e71d3d80b --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ReadFromChangelogsTest.java @@ -0,0 +1,366 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.Map; +import java.util.stream.Collectors; +import org.apache.beam.sdk.coders.KvCoder; +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.sdk.schemas.Schema; +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.values.KV; +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.ValueKind; +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.TableProperties; +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.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.types.TypeUtil; +import org.apache.iceberg.types.Types; +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 ReadFromChangelogs}. */ +@RunWith(JUnit4.class) +public class ReadFromChangelogsTest { + 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, "visible", Types.StringType.get()), + Types.NestedField.optional(3, "hidden", 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(); + @Rule public TestPipeline pipeline = TestPipeline.create(); + + @Test + public void unidirectionalTasksEmitProjectedRowsOnly() throws IOException { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + IcebergScanConfig scanConfig = scanConfig(table, tableId, ImmutableList.of("id", "visible")); + + DataFile addedFile = + warehouse.writeRecords( + testName.getMethodName() + "-added.parquet", + table.schema(), + ImmutableList.of(record(10L, "added", "added-hidden"))); + DataFile deletedRowsFile = + warehouse.writeRecords( + testName.getMethodName() + "-deleted-rows.parquet", + table.schema(), + ImmutableList.of( + record(20L, "deleted-row", "deleted-row-hidden"), + record(21L, "not-deleted", "not-deleted-hidden"))); + DeleteFile addedPositionDelete = + writePositionDelete(table, deletedRowsFile, "deleted-rows-pos-delete.parquet", 0L); + DataFile deletedFile = + warehouse.writeRecords( + testName.getMethodName() + "-deleted-file.parquet", + table.schema(), + ImmutableList.of(record(30L, "deleted-file", "deleted-file-hidden"))); + + List tasks = + ImmutableList.of( + task( + SerializableChangelogTask.Type.ADDED_ROWS, + addedFile, + ImmutableList.of(), + ImmutableList.of(), + table, + 100L), + task( + SerializableChangelogTask.Type.DELETED_ROWS, + deletedRowsFile, + ImmutableList.of(addedPositionDelete), + ImmutableList.of(), + table, + 100L), + task( + SerializableChangelogTask.Type.DELETED_FILE, + deletedFile, + ImmutableList.of(), + ImmutableList.of(), + table, + 100L)); + + ReadFromChangelogs.Output output = + input(ImmutableList.of(KV.of(descriptor(), tasks)), ImmutableList.of()) + .apply(new ReadFromChangelogs(scanConfig)); + + assertEquals( + IcebergUtils.icebergSchemaToBeamSchema(scanConfig.getProjectedSchema()), + output.uniDirectionalRows().getSchema()); + PAssert.that( + output.uniDirectionalRows().apply("Format Unidirectional", ParDo.of(new FormatRow()))) + .containsInAnyOrder( + "INSERT:10:added:2", "DELETE:20:deleted-row:2", "DELETE:30:deleted-file:2"); + PAssert.that(output.biDirectionalInserts()).empty(); + PAssert.that(output.biDirectionalDeletes()).empty(); + + pipeline.run().waitUntilFinish(); + } + + @Test + public void bidirectionalTasksKeepFullRowsForDownstreamResolution() throws IOException { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + IcebergScanConfig scanConfig = scanConfig(table, tableId, ImmutableList.of("id", "visible")); + DataFile oldFile = + warehouse.writeRecords( + testName.getMethodName() + "-old.parquet", + table.schema(), + ImmutableList.of(record(1L, "shown", "old-hidden"))); + DataFile newFile = + warehouse.writeRecords( + testName.getMethodName() + "-new.parquet", + table.schema(), + ImmutableList.of(record(1L, "shown", "new-hidden"))); + List tasks = + ImmutableList.of( + task( + SerializableChangelogTask.Type.DELETED_FILE, + oldFile, + ImmutableList.of(), + ImmutableList.of(), + table, + 200L), + task( + SerializableChangelogTask.Type.ADDED_ROWS, + newFile, + ImmutableList.of(), + ImmutableList.of(), + table, + 200L)); + + ReadFromChangelogs.Output output = + input(ImmutableList.of(), ImmutableList.of(KV.of(descriptor(200L, 200L, 1L, 1L), tasks))) + .apply(new ReadFromChangelogs(scanConfig)); + + PAssert.that(output.uniDirectionalRows()).empty(); + PAssert.that( + output.biDirectionalDeletes().apply("Format Deletes", ParDo.of(new FormatKeyedRow()))) + .containsInAnyOrder("DELETE:200:200:1:shown:old-hidden:3"); + PAssert.that( + output.biDirectionalInserts().apply("Format Inserts", ParDo.of(new FormatKeyedRow()))) + .containsInAnyOrder("INSERT:200:200:1:shown:new-hidden:3"); + + pipeline.run().waitUntilFinish(); + } + + private PCollectionTuple input( + List>> unidirectional, + List>> largeBidirectional) { + Schema rowIdBeamSchema = + IcebergUtils.icebergSchemaToBeamSchema( + TypeUtil.select(CDC_SCHEMA, CDC_SCHEMA.identifierFieldIds())); + KvCoder> coder = + ChangelogScanner.coder(rowIdBeamSchema); + PCollection>> uni = + unidirectional.isEmpty() + ? pipeline.apply("Empty Unidirectional", Create.empty(coder)) + : pipeline.apply("Create Unidirectional", Create.of(unidirectional).withCoder(coder)); + PCollection>> large = + largeBidirectional.isEmpty() + ? pipeline.apply("Empty Large Bidirectional", Create.empty(coder)) + : pipeline.apply( + "Create Large Bidirectional", Create.of(largeBidirectional).withCoder(coder)); + return PCollectionTuple.of(ChangelogScanner.UNIDIRECTIONAL_TASKS, uni) + .and(ChangelogScanner.LARGE_BIDIRECTIONAL_TASKS, large); + } + + private TableIdentifier tableId() { + return TableIdentifier.of("default", testName.getMethodName()); + } + + private IcebergScanConfig scanConfig( + Table table, TableIdentifier tableId, List keepFields) { + return IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(table.schema())) + .setKeepFields(keepFields) + .setUseCdc(true) + .build(); + } + + private ChangelogDescriptor descriptor() { + return ChangelogDescriptor.builder() + .setTableIdentifierString(tableId().toString()) + .setSnapshotSequenceNumber(100L) + .setCommitSnapshotId(100L) + .build(); + } + + private ChangelogDescriptor descriptor( + long sequenceNumber, long snapshotId, long lowerInclusive, long upperInclusive) { + Schema pkSchema = Schema.builder().addInt64Field("id").build(); + return ChangelogDescriptor.builder() + .setTableIdentifierString(tableId().toString()) + .setSnapshotSequenceNumber(sequenceNumber) + .setCommitSnapshotId(snapshotId) + .setOverlapLower(Row.withSchema(pkSchema).addValue(lowerInclusive).build()) + .setOverlapUpper(Row.withSchema(pkSchema).addValue(upperInclusive).build()) + .build(); + } + + private static Record record(long id, String visible, String hidden) { + GenericRecord record = GenericRecord.create(CDC_SCHEMA); + record.setField("id", id); + record.setField("visible", visible); + record.setField("hidden", hidden); + 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 { + for (long position : positions) { + writer.write(PositionDelete.create().set(dataFile.location(), position)); + } + } finally { + writer.close(); + } + return writer.toDeleteFile(); + } + + private static SerializableChangelogTask task( + SerializableChangelogTask.Type type, + DataFile dataFile, + List addedDeletes, + List existingDeletes, + Table table, + long snapshotId) { + return SerializableChangelogTask.builder() + .setType(type) + .setDataFile(dataFile, table.spec(), 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(snapshotId) + .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 Map tableProperties() { + return ImmutableMap.of(TableProperties.FORMAT_VERSION, "2"); + } + + private static class FormatRow extends DoFn { + @ProcessElement + public void process(@Element Row row, ValueKind kind, OutputReceiver out) { + out.output( + kind.name() + + ":" + + row.getInt64("id") + + ":" + + row.getString("visible") + + ":" + + row.getSchema().getFieldCount()); + } + } + + private static class FormatKeyedRow extends DoFn, String> { + @ProcessElement + public void process( + @Element KV element, ValueKind kind, OutputReceiver out) { + Row row = element.getValue(); + CdcRowDescriptor descriptor = element.getKey(); + out.output( + kind.name() + + ":" + + descriptor.getCommitSnapshotId() + + ":" + + descriptor.getSnapshotSequenceNumber() + + ":" + + descriptor.getPrimaryKey().getInt64("id") + + ":" + + row.getString("visible") + + ":" + + row.getString("hidden") + + ":" + + row.getSchema().getFieldCount()); + } + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ResolveChangesTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ResolveChangesTest.java new file mode 100644 index 000000000000..4d9e709b8d42 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/ResolveChangesTest.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.cdc; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collections; +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.TestDataWarehouse; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.transforms.DoFnTester; +import org.apache.beam.sdk.transforms.join.CoGbkResult; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TimestampedValue; +import org.apache.beam.sdk.values.ValueInSingleWindow; +import org.apache.beam.sdk.values.ValueKind; +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.catalog.TableIdentifier; +import org.apache.iceberg.types.Types; +import org.joda.time.Instant; +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; + +/** Unit tests for {@link ResolveChanges}. */ +@RunWith(JUnit4.class) +public class ResolveChangesTest { + private static final org.apache.iceberg.Schema SIMPLE_ICEBERG_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get())), + ImmutableSet.of(1)); + private static final Schema SIMPLE_BEAM_SCHEMA = + IcebergUtils.icebergSchemaToBeamSchema(SIMPLE_ICEBERG_SCHEMA); + private static final Schema PK_SCHEMA = Schema.builder().addInt32Field("id").build(); + + @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); + @Rule public TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); + @Rule public final TestName testName = new TestName(); + + @Test + public void fullRowDuplicateDeleteInsertEmitsNothing() throws Exception { + Row duplicate = simpleRow(1, "duplicate"); + + List> output = + process( + SIMPLE_ICEBERG_SCHEMA, + pkRow(1), + Collections.singletonList(duplicate), + Collections.singletonList(duplicate), + new Instant(0L)); + + assertThat(output, empty()); + } + + @Test + public void updatePairAndExtraRowsPreserveKindsAndTimestamp() throws Exception { + Instant timestamp = new Instant(123L); + Row before = simpleRow(1, "before"); + Row after = simpleRow(1, "after"); + Row extraDelete = simpleRow(1, "deleted-only"); + + List> output = + process( + SIMPLE_ICEBERG_SCHEMA, + pkRow(1), + Arrays.asList(before, extraDelete), + Collections.singletonList(after), + timestamp); + + // Simulates an unusual case where two deletes share a PK. Which one pairs with the insert is + // arbitrary because Iceberg doesn't track lineage within a commit; the resolver orders both + // sides by nonPkHash so the choice is at least deterministic. These values are picked so that + // ordering matches the intuitive reading ("before" hashes below "deleted-only"). + assertThat( + output.stream().map(ResolveChangesTest::kindAndData).collect(Collectors.toList()), + contains("UPDATE_BEFORE:before", "UPDATE_AFTER:after", "DELETE:deleted-only")); + assertEquals( + Collections.nCopies(3, timestamp), + output.stream().map(ValueInSingleWindow::getTimestamp).collect(Collectors.toList())); + } + + @Test + public void duplicateDetectionUsesDeepEqualityForNestedValues() throws Exception { + org.apache.iceberg.Schema icebergSchema = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional( + 2, + "nested", + Types.StructType.of( + Types.NestedField.optional(3, "name", Types.StringType.get()))), + Types.NestedField.optional( + 4, "items", Types.ListType.ofOptional(5, Types.StringType.get())), + Types.NestedField.optional( + 6, + "attrs", + Types.MapType.ofOptional( + 7, 8, Types.StringType.get(), Types.IntegerType.get())), + Types.NestedField.optional(9, "payload", Types.BinaryType.get()), + Types.NestedField.optional(10, "nullable", Types.StringType.get())), + ImmutableSet.of(1)); + Schema beamSchema = IcebergUtils.icebergSchemaToBeamSchema(icebergSchema); + Schema nestedSchema = beamSchema.getField("nested").getType().getRowSchema(); + Row delete = + Row.withSchema(beamSchema) + .addValues( + 1, + Row.withSchema(nestedSchema).addValue("same").build(), + ImmutableList.of("a", "b"), + ImmutableMap.of("x", 1), + new byte[] {1, 2, 3}, + null) + .build(); + Row insert = + Row.withSchema(beamSchema) + .addValues( + 1, + Row.withSchema(nestedSchema).addValue("same").build(), + ImmutableList.of("a", "b"), + ImmutableMap.of("x", 1), + new byte[] {1, 2, 3}, + null) + .build(); + + List> output = + process( + icebergSchema, + pkRow(1), + Collections.singletonList(delete), + Collections.singletonList(insert), + new Instant(0L)); + + assertThat(output, empty()); + } + + private List> process( + org.apache.iceberg.Schema icebergSchema, + Row pk, + List deletes, + List inserts, + Instant timestamp) + throws Exception { + CoGbkResult result = + CoGbkResult.of(ResolveChanges.DELETES, deletes).and(ResolveChanges.INSERTS, inserts); + try (DoFnTester, Row> tester = + DoFnTester.of(new ResolveChanges(scanConfig(icebergSchema)))) { + tester.processTimestampedElement( + TimestampedValue.of( + KV.of( + CdcRowDescriptor.builder() + .setCommitSnapshotId(123) + .setSnapshotSequenceNumber(456) + .setPrimaryKey(pk) + .build(), + result), + timestamp)); + return tester.getMutableOutput(tester.getMainOutputTag()); + } + } + + private IcebergScanConfig scanConfig(org.apache.iceberg.Schema icebergSchema) { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + IcebergCatalogConfig catalogConfig = + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build(); + catalogConfig.catalog().createTable(tableId, icebergSchema); + return IcebergScanConfig.builder() + .setCatalogConfig(catalogConfig) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(icebergSchema)) + .setUseCdc(true) + .build(); + } + + private static Row simpleRow(int id, String data) { + return Row.withSchema(SIMPLE_BEAM_SCHEMA).addValues(id, data).build(); + } + + private static Row pkRow(int id) { + return Row.withSchema(PK_SCHEMA).addValue(id).build(); + } + + private static String kindAndData(ValueInSingleWindow value) { + ValueKind kind = value.getValueKind(); + return kind.name() + ":" + value.getValue().getString("data"); + } +} 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..32d0fc691c01 --- /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, PartitionSpec.unpartitioned(), 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; + } + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SnapshotWindowFnTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SnapshotWindowFnTest.java new file mode 100644 index 000000000000..c3c081c1eed7 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SnapshotWindowFnTest.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 static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.util.Collection; +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.joda.time.Duration; +import org.joda.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link SnapshotWindowFn}. */ +@RunWith(JUnit4.class) +public class SnapshotWindowFnTest { + @Test + public void identicalTimestampsShareWindowAndAdjacentTimestampsDoNot() throws Exception { + SnapshotWindowFn fn = new SnapshotWindowFn(); + Instant timestamp = new Instant(1_000L); + + IntervalWindow first = onlyWindow(fn, timestamp); + IntervalWindow second = onlyWindow(fn, timestamp); + IntervalWindow adjacent = onlyWindow(fn, timestamp.plus(Duration.millis(1))); + + assertEquals(new IntervalWindow(timestamp, timestamp.plus(Duration.millis(1))), first); + assertEquals(first, second); + assertNotEquals(first, adjacent); + assertEquals( + new IntervalWindow(timestamp.plus(Duration.millis(1)), timestamp.plus(Duration.millis(2))), + adjacent); + } + + @Test + public void sideInputMappingStartsAtMainWindowMaxTimestamp() { + SnapshotWindowFn fn = new SnapshotWindowFn(); + IntervalWindow mainWindow = new IntervalWindow(new Instant(10L), new Instant(20L)); + + IntervalWindow sideInputWindow = fn.getDefaultWindowMappingFn().getSideInputWindow(mainWindow); + + assertEquals( + new IntervalWindow( + mainWindow.maxTimestamp(), mainWindow.maxTimestamp().plus(Duration.millis(1L))), + sideInputWindow); + } + + @SuppressWarnings("NonCanonicalType") + private static IntervalWindow onlyWindow(SnapshotWindowFn fn, Instant timestamp) + throws Exception { + Collection windows = + fn.assignWindows( + fn.new AssignContext() { + @Override + public Object element() { + return "element"; + } + + @Override + public Instant timestamp() { + return timestamp; + } + + @Override + public BoundedWindow window() { + return GlobalWindow.INSTANCE; + } + }); + assertThat( + windows, contains(new IntervalWindow(timestamp, timestamp.plus(Duration.millis(1L))))); + return windows.iterator().next(); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/WatchForSnapshotsSdfTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/WatchForSnapshotsSdfTest.java new file mode 100644 index 000000000000..02bd33c3bcec --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/WatchForSnapshotsSdfTest.java @@ -0,0 +1,304 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.iceberg.types.Types.NestedField.optional; +import static org.apache.iceberg.types.Types.NestedField.required; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.beam.sdk.io.iceberg.IcebergCatalogConfig; +import org.apache.beam.sdk.io.iceberg.IcebergIO.ReadRows.StartingStrategy; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.TestDataWarehouse; +import org.apache.beam.sdk.io.iceberg.TestFixtures; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.splittabledofn.ManualWatermarkEstimator; +import org.apache.beam.sdk.values.OutputBuilder; +import org.apache.beam.sdk.values.TimestampedValue; +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.iceberg.DataFile; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.Table; +import org.apache.iceberg.TableProperties; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.types.Types; +import org.joda.time.Duration; +import org.joda.time.Instant; +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; + +/** Unit tests for {@link WatchForSnapshotsSdf}. */ +@RunWith(JUnit4.class) +public class WatchForSnapshotsSdfTest { + private static final Schema CDC_SCHEMA = + new Schema( + ImmutableList.of( + required(1, "id", Types.LongType.get()), 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 earliestStreamingRestrictionEmitsSnapshotsInSequenceOrder() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + commitAppend(table, "s1.parquet", records("one", 1L)); + commitAppend(table, "s2.parquet", records("two", 2L)); + commitAppend(table, "s3.parquet", records("three", 3L)); + List snapshots = Lists.newArrayList(table.snapshots()); + + WatchForSnapshotsSdf sdf = + new WatchForSnapshotsSdf( + scanConfigBuilder(table, tableId) + .setStreaming(true) + .setStartingStrategy(StartingStrategy.EARLIEST) + .setPollInterval(Duration.millis(1L)) + .build()); + + OffsetRange restriction = sdf.initialRestriction(); + assertEquals(1L, restriction.getFrom()); + assertEquals(Long.MAX_VALUE, restriction.getTo()); + + CapturingOutputReceiver out = new CapturingOutputReceiver(); + ManualWatermarkEstimator watermark = + sdf.newWatermarkEstimator(sdf.initialWatermarkState()); + DoFn.ProcessContinuation continuation = + sdf.process(sdf.newTracker(restriction), watermark, out); + + Long[] expectedSnapshotIds = snapshots.stream().map(Snapshot::snapshotId).toArray(Long[]::new); + List actualSnapshotIds = + out.values.stream().map(TimestampedValue::getValue).collect(Collectors.toList()); + assertTrue(continuation.shouldResume()); + assertEquals(Duration.millis(1L), continuation.resumeDelay()); + assertThat(actualSnapshotIds, contains(expectedSnapshotIds)); + // The watermark lands just past the last snapshot's timestamp, so its 1ms window can fire as + // soon as its records drain. + assertEquals( + Instant.ofEpochMilli(snapshots.get(snapshots.size() - 1).timestampMillis()) + .plus(Duration.millis(1L)), + watermark.currentWatermark()); + } + + @Test + public void boundedSnapshotRangeUsesInclusiveLowerAndExclusiveUpperSequence() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + commitAppend(table, "s1.parquet", records("one", 1L)); + commitAppend(table, "s2.parquet", records("two", 2L)); + commitAppend(table, "s3.parquet", records("three", 3L)); + commitAppend(table, "s4.parquet", records("four", 4L)); + List snapshots = Lists.newArrayList(table.snapshots()); + Snapshot second = snapshots.get(1); + Snapshot third = snapshots.get(2); + + WatchForSnapshotsSdf sdf = + new WatchForSnapshotsSdf( + scanConfigBuilder(table, tableId) + .setStreaming(true) + .setFromSnapshotInclusive(second.snapshotId()) + .setToSnapshot(third.snapshotId()) // this is inclusive + .setPollInterval(Duration.standardSeconds(30L)) + .build()); + + OffsetRange restriction = sdf.initialRestriction(); + assertEquals(second.sequenceNumber(), restriction.getFrom()); + assertEquals(third.sequenceNumber() + 1, restriction.getTo()); + + CapturingOutputReceiver out = new CapturingOutputReceiver(); + DoFn.ProcessContinuation continuation = + sdf.process( + sdf.newTracker(restriction), + sdf.newWatermarkEstimator(sdf.initialWatermarkState()), + out); + + List outputSnapshotIds = + out.values.stream().map(TimestampedValue::getValue).collect(Collectors.toList()); + + assertFalse(continuation.shouldResume()); + assertThat(outputSnapshotIds, contains(second.snapshotId(), third.snapshotId())); + assertEquals(2, out.values.size()); + assertEquals(Instant.ofEpochMilli(second.timestampMillis()), out.values.get(0).getTimestamp()); + assertEquals(Instant.ofEpochMilli(third.timestampMillis()), out.values.get(1).getTimestamp()); + } + + @Test + public void streamingDefaultStartsAtLatestSnapshotAndEarliestStartsAtFirst() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + commitAppend(table, "s1.parquet", records("one", 1L)); + commitAppend(table, "s2.parquet", records("two", 2L)); + Snapshot latest = table.currentSnapshot(); + + WatchForSnapshotsSdf defaultSdf = + new WatchForSnapshotsSdf( + scanConfigBuilder(table, tableId) + .setStreaming(true) + .setPollInterval(Duration.standardSeconds(1L)) + .build()); + WatchForSnapshotsSdf earliestSdf = + new WatchForSnapshotsSdf( + scanConfigBuilder(table, tableId) + .setStreaming(true) + .setStartingStrategy(StartingStrategy.EARLIEST) + .setPollInterval(Duration.standardSeconds(1L)) + .build()); + + assertEquals(latest.sequenceNumber(), defaultSdf.initialRestriction().getFrom()); + assertEquals(1L, earliestSdf.initialRestriction().getFrom()); + } + + @Test + public void emptyTableReturnsResumeAndAdvancesIdleWatermark() { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + Duration pollInterval = Duration.millis(25L); + WatchForSnapshotsSdf sdf = + new WatchForSnapshotsSdf( + scanConfigBuilder(table, tableId) + .setStreaming(true) + .setPollInterval(pollInterval) + .build()); + ManualWatermarkEstimator watermark = + sdf.newWatermarkEstimator(sdf.initialWatermarkState()); + Instant beforeProcess = Instant.now(); + CapturingOutputReceiver out = new CapturingOutputReceiver(); + + DoFn.ProcessContinuation continuation = + sdf.process(sdf.newTracker(sdf.initialRestriction()), watermark, out); + + assertTrue(continuation.shouldResume()); + assertEquals(pollInterval, continuation.resumeDelay()); + assertThat(out.values, empty()); + // The idle bump advances the watermark to now() - pollInterval. + assertThat( + watermark.currentWatermark(), + greaterThan(beforeProcess.minus(pollInterval).minus(Duration.millis(1L)))); + assertThat(watermark.currentWatermark(), lessThanOrEqualTo(Instant.now())); + } + + /** + * A snapshot discovered after the watermark has already advanced past its commit time (e.g. the + * idle bump ran ahead) must be emitted with a clamped timestamp rather than behind the watermark, + * where its records would be silently dropped as late data. + */ + @Test + public void lateDiscoveredSnapshotsAreClampedToTheWatermark() throws Exception { + TableIdentifier tableId = tableId(); + Table table = warehouse.createTable(tableId, CDC_SCHEMA, null, tableProperties()); + commitAppend(table, "s1.parquet", records("one", 1L)); + commitAppend(table, "s2.parquet", records("two", 2L)); + + WatchForSnapshotsSdf sdf = + new WatchForSnapshotsSdf( + scanConfigBuilder(table, tableId) + .setStreaming(true) + .setStartingStrategy(StartingStrategy.EARLIEST) + .setPollInterval(Duration.millis(1L)) + .build()); + // Seed a watermark ahead of both commit timestamps, as if the idle bump ran before discovery. + Instant seededWatermark = Instant.now().plus(Duration.standardHours(1)); + ManualWatermarkEstimator watermark = sdf.newWatermarkEstimator(seededWatermark); + CapturingOutputReceiver out = new CapturingOutputReceiver(); + + sdf.process(sdf.newTracker(sdf.initialRestriction()), watermark, out); + + // Neither snapshot is emitted behind the seeded watermark; each lands 1ms after the previous. + assertEquals(2, out.values.size()); + assertEquals(seededWatermark, out.values.get(0).getTimestamp()); + assertEquals(seededWatermark.plus(Duration.millis(1L)), out.values.get(1).getTimestamp()); + assertEquals(seededWatermark.plus(Duration.millis(2L)), watermark.currentWatermark()); + } + + private TableIdentifier tableId() { + return TableIdentifier.of("default", testName.getMethodName()); + } + + private IcebergScanConfig.Builder scanConfigBuilder(Table table, TableIdentifier tableId) { + return IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(table.schema())) + .setUseCdc(true); + } + + private static Map tableProperties() { + return ImmutableMap.of(TableProperties.FORMAT_VERSION, "2"); + } + + private void commitAppend(Table table, String fileName, List records) throws IOException { + DataFile file = + warehouse.writeRecords(testName.getMethodName() + "-" + fileName, table.schema(), records); + table.newFastAppend().appendFile(file).commit(); + table.refresh(); + } + + private static List records(String data, long... ids) { + ImmutableList.Builder records = ImmutableList.builder(); + for (long id : ids) { + records.add(TestFixtures.createRecord(CDC_SCHEMA, ImmutableMap.of("id", id, "data", data))); + } + return records.build(); + } + + private static final class CapturingOutputReceiver implements DoFn.OutputReceiver { + private final ImmutableList.Builder> builder = ImmutableList.builder(); + private List> values = ImmutableList.of(); + + @Override + public OutputBuilder builder(Long value) { + throw new UnsupportedOperationException("Use outputWithTimestamp in this test receiver."); + } + + @Override + public void outputWithTimestamp(Long value, Instant timestamp) { + builder.add(TimestampedValue.of(value, timestamp)); + values = builder.build(); + } + } +} diff --git a/sdks/java/io/influxdb/build.gradle b/sdks/java/io/influxdb/build.gradle index dc6c85059cdb..888c54e71ee1 100644 --- a/sdks/java/io/influxdb/build.gradle +++ b/sdks/java/io/influxdb/build.gradle @@ -10,7 +10,7 @@ * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an AS IS BASIS, +* distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. @@ -30,16 +30,8 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:4.6.0" implementation library.java.vendored_guava_32_1_2_jre testImplementation library.java.junit - // TODO(https://github.com/apache/beam/issues/34056) remove powermock once removed static mocks. - testImplementation "org.powermock:powermock-module-junit4:2.0.9" - testImplementation "org.powermock:powermock-api-mockito2:2.0.9" testImplementation library.java.mockito_core testRuntimeOnly project(path: ":runners:direct-java", configuration: "shadow") testImplementation project(path: ":sdks:java:extensions:avro", configuration: "testRuntimeMigration") testImplementation project(path: ":sdks:java:io:common") } - -configurations.testRuntimeClasspath { - // Pin mockito-core 3 for now due to powermock not support mockito 4 - resolutionStrategy.force "org.mockito:mockito-core:3.7.7" -} diff --git a/sdks/java/io/influxdb/src/main/java/org/apache/beam/sdk/io/influxdb/InfluxDbIO.java b/sdks/java/io/influxdb/src/main/java/org/apache/beam/sdk/io/influxdb/InfluxDbIO.java index a2aef8766921..71d9a3e721d0 100644 --- a/sdks/java/io/influxdb/src/main/java/org/apache/beam/sdk/io/influxdb/InfluxDbIO.java +++ b/sdks/java/io/influxdb/src/main/java/org/apache/beam/sdk/io/influxdb/InfluxDbIO.java @@ -45,6 +45,7 @@ import org.apache.beam.sdk.values.PBegin; 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.annotations.VisibleForTesting; import org.checkerframework.checker.nullness.qual.Nullable; import org.influxdb.InfluxDB; import org.influxdb.InfluxDB.ConsistencyLevel; @@ -565,12 +566,20 @@ private void flush() { @AutoValue public abstract static class DataSourceConfiguration implements Serializable { + @VisibleForTesting + @FunctionalInterface + public interface ConnectionFactory extends Serializable { + InfluxDB create(DataSourceConfiguration configuration, boolean disableCertificateValidation); + } + abstract ValueProvider url(); abstract ValueProvider userName(); abstract ValueProvider password(); + abstract @Nullable ConnectionFactory connectionFactory(); + abstract Builder builder(); @AutoValue.Builder @@ -582,6 +591,8 @@ abstract static class Builder { abstract Builder setPassword(ValueProvider password); + abstract Builder setConnectionFactory(@Nullable ConnectionFactory connectionFactory); + abstract DataSourceConfiguration build(); } @@ -594,6 +605,14 @@ public static DataSourceConfiguration create( .build(); } + /** + * For testing only. Note that userName and password are ignored when connectionFactory is set. + */ + @VisibleForTesting + DataSourceConfiguration withConnectionFactory(ConnectionFactory connectionFactory) { + return builder().setConnectionFactory(connectionFactory).build(); + } + void populateDisplayData(DisplayData.Builder builder) { builder.add(DisplayData.item("url", url())); builder.add(DisplayData.item("userName", userName())); @@ -642,6 +661,9 @@ private static boolean checkDatabase( public static InfluxDB getConnection( DataSourceConfiguration configuration, boolean disableCertificateValidation) { + if (configuration.connectionFactory() != null) { + return configuration.connectionFactory().create(configuration, disableCertificateValidation); + } if (!disableCertificateValidation) { return InfluxDBFactory.connect( configuration.url().get(), diff --git a/sdks/java/io/influxdb/src/test/java/org/apache/beam/sdk/io/influxdb/InfluxDbIOTest.java b/sdks/java/io/influxdb/src/test/java/org/apache/beam/sdk/io/influxdb/InfluxDbIOTest.java index 78579b82c93d..a54eec72e41c 100644 --- a/sdks/java/io/influxdb/src/test/java/org/apache/beam/sdk/io/influxdb/InfluxDbIOTest.java +++ b/sdks/java/io/influxdb/src/test/java/org/apache/beam/sdk/io/influxdb/InfluxDbIOTest.java @@ -18,8 +18,6 @@ package org.apache.beam.sdk.io.influxdb; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.powermock.api.mockito.PowerMockito.doReturn; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -28,10 +26,11 @@ import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; -import okhttp3.OkHttpClient; +import java.util.concurrent.atomic.AtomicReference; import org.apache.beam.sdk.PipelineResult; import org.apache.beam.sdk.PipelineResult.State; import org.apache.beam.sdk.io.influxdb.InfluxDbIO.DataSourceConfiguration; +import org.apache.beam.sdk.io.influxdb.InfluxDbIO.DataSourceConfiguration.ConnectionFactory; import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; @@ -39,43 +38,71 @@ import org.apache.beam.sdk.transforms.Create; import org.apache.beam.sdk.values.PCollection; import org.influxdb.InfluxDB; -import org.influxdb.InfluxDBFactory; import org.influxdb.dto.Query; import org.influxdb.dto.QueryResult; import org.influxdb.dto.QueryResult.Series; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({ - org.influxdb.InfluxDBFactory.class, - org.apache.beam.sdk.io.influxdb.InfluxDbIO.InfluxDBSource.class -}) + public class InfluxDbIOTest { @Rule public final TestPipeline pipeline = TestPipeline.create(); + // ACTIVE_MOCK provides access to the mock InfluxDB instance on worker threads after + // TestConnectionFactory is deserialized, avoiding non-serializable Mockito state. + private static final AtomicReference ACTIVE_MOCK = new AtomicReference<>(); + + private InfluxDB influxDb; + @Before public void setupTest() { - PowerMockito.mockStatic(InfluxDBFactory.class); + influxDb = Mockito.mock(InfluxDB.class); + ACTIVE_MOCK.set(influxDb); + } + + @After + public void tearDown() { + ACTIVE_MOCK.set(null); + } + + private DataSourceConfiguration getDataSourceConfiguration( + String influxHost, String userName, String password) { + return DataSourceConfiguration.create( + StaticValueProvider.of(influxHost), + StaticValueProvider.of(userName), + StaticValueProvider.of(password)) + .withConnectionFactory(new TestConnectionFactory(influxDb, influxHost, userName, password)); + } + + private static class TestConnectionFactory implements ConnectionFactory { + private final String expectedUrl; + private final String expectedUser; + private final String expectedPassword; + private final transient InfluxDB mockInstance; + + TestConnectionFactory( + InfluxDB mockInstance, String expectedUrl, String expectedUser, String expectedPassword) { + this.mockInstance = mockInstance; + this.expectedUrl = expectedUrl; + this.expectedUser = expectedUser; + this.expectedPassword = expectedPassword; + } + + @Override + public InfluxDB create(DataSourceConfiguration config, boolean disableCert) { + Assert.assertEquals(expectedUrl, config.url().get()); + Assert.assertEquals(expectedUser, config.userName().get()); + Assert.assertEquals(expectedPassword, config.password().get()); + return mockInstance != null ? mockInstance : ACTIVE_MOCK.get(); + } } @Test public void validateWriteTest() { - InfluxDB influxDb = Mockito.mock(InfluxDB.class); - PowerMockito.when( - InfluxDBFactory.connect( - anyString(), anyString(), anyString(), any(OkHttpClient.Builder.class))) - .thenReturn(influxDb); - PowerMockito.when(InfluxDBFactory.connect(anyString(), anyString(), anyString())) - .thenReturn(influxDb); String influxHost = "http://localhost"; String userName = "admin"; String password = "admin"; @@ -84,7 +111,9 @@ public void validateWriteTest() { Mockito.doAnswer(invocation -> countInvocation.getAndIncrement()) .when(influxDb) .write(any(List.class)); - doReturn(getDatabase(influxDatabaseName)).when(influxDb).query(new Query("SHOW DATABASES")); + Mockito.doReturn(getDatabase(influxDatabaseName)) + .when(influxDb) + .query(new Query("SHOW DATABASES")); final int numOfElementsToWrite = 1000; pipeline .apply("Generate data", Create.of(GenerateData.getMetric("test_m", numOfElementsToWrite))) @@ -92,10 +121,7 @@ public void validateWriteTest() { "Write data to InfluxDB", InfluxDbIO.write() .withDataSourceConfiguration( - DataSourceConfiguration.create( - StaticValueProvider.of(influxHost), - StaticValueProvider.of(userName), - StaticValueProvider.of(password))) + getDataSourceConfiguration(influxHost, userName, password)) .withDatabase(influxDatabaseName)); PipelineResult result = pipeline.run(); Assert.assertEquals(State.DONE, result.waitUntilFinish()); @@ -108,20 +134,17 @@ public void validateReadTest() { String userName = "admin"; String password = "admin"; String influxDatabaseName = "testDataBase"; - InfluxDB influxDb = Mockito.mock(InfluxDB.class); - PowerMockito.when( - InfluxDBFactory.connect( - anyString(), anyString(), anyString(), any(OkHttpClient.Builder.class))) - .thenReturn(influxDb); - PowerMockito.when(InfluxDBFactory.connect(anyString(), anyString(), anyString())) - .thenReturn(influxDb); - doReturn(getDatabase(influxDatabaseName)).when(influxDb).query(new Query("SHOW DATABASES")); - doReturn(getDatabase(influxDatabaseName)).when(influxDb).query(new Query("SHOW SHARDS")); - doReturn(mockResultForNumberAndSizeOfBlocks()) + Mockito.doReturn(getDatabase(influxDatabaseName)) + .when(influxDb) + .query(new Query("SHOW DATABASES")); + Mockito.doReturn(getDatabase(influxDatabaseName)) + .when(influxDb) + .query(new Query("SHOW SHARDS")); + Mockito.doReturn(mockResultForNumberAndSizeOfBlocks()) .when(influxDb) .query(new Query("EXPLAIN SELECT * FROM cpu", influxDatabaseName)); - doReturn(mockResult("cpu", 20)) + Mockito.doReturn(mockResult("cpu", 20)) .when(influxDb) .query(new Query("SELECT * FROM cpu", influxDatabaseName)); @@ -131,10 +154,7 @@ public void validateReadTest() { "Read data to InfluxDB", InfluxDbIO.read() .withDataSourceConfiguration( - DataSourceConfiguration.create( - StaticValueProvider.of(influxHost), - StaticValueProvider.of(userName), - StaticValueProvider.of(password))) + getDataSourceConfiguration(influxHost, userName, password)) .withDatabase(influxDatabaseName) .withQuery("SELECT * FROM cpu")) .apply(Count.globally()); diff --git a/sdks/java/io/jms/build.gradle b/sdks/java/io/jms/build.gradle index 24a195e63f14..369d02b37d3e 100644 --- a/sdks/java/io/jms/build.gradle +++ b/sdks/java/io/jms/build.gradle @@ -33,6 +33,12 @@ dependencies { implementation library.java.slf4j_api implementation library.java.joda_time implementation "org.apache.geronimo.specs:geronimo-jms_2.0_spec:1.0-alpha-2" + // Don't put proprietary licensed ibm mq client into runtimeClasspath + // (affects expansion service shadow jar) + compileOnly("com.ibm.mq:com.ibm.mq.allclient:9.3.0.25") { + // duplicating geronimo-jms_2.0_spec + exclude group: "javax.jms", module: "javax.jms-api" + } testImplementation library.java.activemq_amqp testImplementation library.java.activemq_broker testImplementation library.java.activemq_jaas diff --git a/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/BeamGenericJmsConnectionFactory.java b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/BeamGenericJmsConnectionFactory.java new file mode 100644 index 000000000000..d14c8c8addc3 --- /dev/null +++ b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/BeamGenericJmsConnectionFactory.java @@ -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. + */ +package org.apache.beam.sdk.io.jms; + +import java.io.Serializable; +import javax.jms.ConnectionFactory; + +/** + * An interface for creating custom JMS {@link ConnectionFactory} instances. + * + *

Expansion service users connecting to JMS brokers other than the built-in supported ones + * (ActiveMQ, Qpid, IBM MQ) can implement this interface and specify their implementation class name + * in {@link ConnectionConfiguration}. + * + *

The implementation must have a public default constructor. + */ +@FunctionalInterface +public interface BeamGenericJmsConnectionFactory extends Serializable { + + /** + * Creates a {@link ConnectionFactory} using the given {@link ConnectionConfiguration}. + * + * @param config the JMS connection configuration + * @return configured JMS {@link ConnectionFactory} + */ + ConnectionFactory createConnectionFactory(ConnectionConfiguration config) throws Exception; +} diff --git a/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/ConnectionConfiguration.java b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/ConnectionConfiguration.java new file mode 100644 index 000000000000..f534f8a07313 --- /dev/null +++ b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/ConnectionConfiguration.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.jms; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; +import com.ibm.mq.jms.MQConnectionFactory; +import com.ibm.msg.client.wmq.WMQConstants; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.List; +import javax.jms.ConnectionFactory; +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.vendor.guava.v32_1_2_jre.com.google.common.base.Splitter; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** A POJO describing a JMS connection, used by SchemaTransformProvider. */ +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class ConnectionConfiguration implements Serializable { + private static final Logger LOG = LoggerFactory.getLogger(ConnectionConfiguration.class); + + public static Builder builder() { + return new AutoValue_ConnectionConfiguration.Builder(); + } + + public static ConnectionConfiguration create( + String serverUri, @Nullable String connectionFactoryClassName) { + checkArgument(serverUri != null, "serverUri can not be null"); + return builder() + .setServerUri(serverUri) + .setConnectionFactoryClassName(connectionFactoryClassName) + .build(); + } + + public static ConnectionConfiguration create(String serverUri) { + return create(serverUri, null); + } + + @SchemaFieldDescription("The JMS broker URI.") + public abstract String getServerUri(); + + @SchemaFieldDescription("The JMS ConnectionFactory class name.") + public abstract @Nullable String getConnectionFactoryClassName(); + + @SchemaFieldDescription("The username to connect to the JMS broker.") + public abstract @Nullable String getUsername(); + + @SchemaFieldDescription("The password to connect to the JMS broker.") + public abstract @Nullable String getPassword(); + + public ConnectionConfiguration withUsername(String username) { + return toBuilder().setUsername(username).build(); + } + + public ConnectionConfiguration withPassword(String password) { + return toBuilder().setPassword(password).build(); + } + + public ConnectionConfiguration withConnectionFactoryClassName(String connectionFactoryClassName) { + return toBuilder().setConnectionFactoryClassName(connectionFactoryClassName).build(); + } + + abstract Builder toBuilder(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setServerUri(String serverUri); + + public abstract Builder setConnectionFactoryClassName( + @Nullable String connectionFactoryClassName); + + public abstract Builder setUsername(@Nullable String username); + + public abstract Builder setPassword(@Nullable String password); + + public abstract ConnectionConfiguration build(); + } + + public ConnectionFactory createConnectionFactory() { + String className = getConnectionFactoryClassName(); + // Default to ActiveMQ + if (className == null || className.isEmpty()) { + className = "org.apache.activemq.ActiveMQConnectionFactory"; + } + Class clazz; + Class factoryClass; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException( + String.format( + "ConnectionFactory %s does not exist. If using expansion service, attach the connection factory jar as part of its invocation classpath.", + className), + e); + } + if (BeamGenericJmsConnectionFactory.class.isAssignableFrom(clazz)) { + factoryClass = (Class) clazz; + } else if (className.contains("org.apache.activemq.ActiveMQConnectionFactory") + || className.contains("org.apache.qpid.jms")) { + // Connectors supported by StandardJmsConnectionFactory + factoryClass = StandardJmsConnectionFactory.class; + } else if (className.contains("com.ibm.mq")) { + factoryClass = IbmMqJmsConnectionFactory.class; + } else { + // Attempt to use StandardJmsConnectionFactory.class; + factoryClass = StandardJmsConnectionFactory.class; + } + try { + BeamGenericJmsConnectionFactory factory = factoryClass.getDeclaredConstructor().newInstance(); + return factory.createConnectionFactory(this); + } catch (Exception e) { + throw new IllegalArgumentException( + "Unable to instantiate JMS ConnectionFactory of class " + + className + + ". Must be a supported provider (ActiveMQ, Qpid, IBM MQ) or implement BeamGenericJmsConnectionFactory.", + e); + } + } + + /** + * A {@link BeamGenericJmsConnectionFactory} implementation for standard JMS connection factories. + */ + public static class StandardJmsConnectionFactory implements BeamGenericJmsConnectionFactory { + + @Override + public ConnectionFactory createConnectionFactory(ConnectionConfiguration config) + throws Exception { + String className = config.getConnectionFactoryClassName(); + if (className == null || className.isEmpty()) { + className = "org.apache.activemq.ActiveMQConnectionFactory"; + } + Class clazz = Class.forName(className); + String uri = config.getServerUri(); + String username = config.getUsername(); + String password = config.getPassword(); + + if (username != null && password != null) { + try { + return (ConnectionFactory) + clazz + .getConstructor(String.class, String.class, String.class) + .newInstance(username, password, uri); + } catch (NoSuchMethodException e) { + // Fall through to 1-arg or 0-arg constructor + setters + } + } + ConnectionFactory cf; + try { + cf = (ConnectionFactory) clazz.getConstructor(String.class).newInstance(uri); + } catch (NoSuchMethodException e) { + cf = (ConnectionFactory) clazz.getConstructor().newInstance(); + } + + if (username != null && password != null) { + boolean setUsernameSuccess = + // ActiveMQ (capital N) + invokeMethodIfExists(cf, "setUserName", String.class, username) + // Qpid (lowercase n) + || invokeMethodIfExists(cf, "setUsername", String.class, username); + boolean setPasswordSuccess = + invokeMethodIfExists(cf, "setPassword", String.class, password); + + if (!setUsernameSuccess || !setPasswordSuccess) { + LOG.warn("Unable to set username/password on JMS ConnectionFactory of class {}", clazz); + } + } + return cf; + } + + private static boolean invokeMethodIfExists( + Object target, String methodName, Class paramType, Object arg) { + try { + Method m = target.getClass().getMethod(methodName, paramType); + m.invoke(target, arg); + return true; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + return false; + } + } + } + + /** A {@link BeamGenericJmsConnectionFactory} implementation for IBM MQ. */ + public static class IbmMqJmsConnectionFactory implements BeamGenericJmsConnectionFactory { + + @Override + public ConnectionFactory createConnectionFactory(ConnectionConfiguration config) + throws Exception { + MQConnectionFactory cf = new MQConnectionFactory(); + cf.setTransportType(WMQConstants.WMQ_CM_CLIENT); + + String uri = config.getServerUri(); + if (!Strings.isNullOrEmpty(uri)) { + URI parsedUri = new URI(uri); + String host = parsedUri.getHost(); + int port = parsedUri.getPort(); + if (host != null) { + cf.setHostName(host); + } + if (port > 0) { + cf.setPort(port); + } + if (parsedUri.getQuery() != null) { + for (String param : Splitter.on('&').split(parsedUri.getQuery())) { + List pair = Splitter.on('=').splitToList(param); + if (pair.size() == 2) { + if ("channel".equalsIgnoreCase(pair.get(0))) { + cf.setChannel(pair.get(1)); + } else if ("queueManager".equalsIgnoreCase(pair.get(0))) { + cf.setQueueManager(pair.get(1)); + } + } + } + } + } + + String username = config.getUsername(); + if (username != null) { + cf.setBooleanProperty(WMQConstants.USER_AUTHENTICATION_MQCSP, true); + cf.setStringProperty(WMQConstants.USERID, username); + String password = config.getPassword(); + if (password != null) { + cf.setStringProperty(WMQConstants.PASSWORD, password); + } + } + return cf; + } + } +} diff --git a/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java index 276d61ae5ef5..3a58d28c1d93 100644 --- a/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java +++ b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsCheckpointMark.java @@ -19,13 +19,17 @@ import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.Session; import org.apache.beam.sdk.io.UnboundedSource; +import org.apache.beam.sdk.io.jms.JmsIO.AcknowledgeMode; 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; @@ -41,29 +45,32 @@ class JmsCheckpointMark implements UnboundedSource.CheckpointMark, Serializable private static final Logger LOG = LoggerFactory.getLogger(JmsCheckpointMark.class); private Instant oldestMessageTimestamp; - private transient @Nullable Message lastMessage; + private transient @Nullable List messages; private transient @Nullable MessageConsumer consumer; private transient @Nullable Session session; + private transient @Nullable AtomicInteger activeCheckpoints; private JmsCheckpointMark( Instant oldestMessageTimestamp, - @Nullable Message lastMessage, + @Nullable List messages, @Nullable MessageConsumer consumer, - @Nullable Session session) { + @Nullable Session session, + @Nullable AtomicInteger activeCheckpoints) { this.oldestMessageTimestamp = oldestMessageTimestamp; - this.lastMessage = lastMessage; + this.messages = messages; this.consumer = consumer; this.session = session; + this.activeCheckpoints = activeCheckpoints; } /** Acknowledge all outstanding message. */ @Override public void finalizeCheckpoint() { try { - // Jms spec will implicitly acknowledge _all_ messaged already received by the same - // session if one message in this session is being acknowledged. - if (lastMessage != null) { - lastMessage.acknowledge(); + if (messages != null) { + for (Message message : messages) { + message.acknowledge(); + } } } catch (JMSException e) { // The effect of this is message not get acknowledged and thus will be redelivered. It is @@ -93,14 +100,37 @@ public void finalizeCheckpoint() { LOG.info("Error closing JMS session. It may have already been closed."); } } + + if (activeCheckpoints != null) { + activeCheckpoints.decrementAndGet(); + } + } + + @VisibleForTesting + @Nullable + List getMessages() { + return messages; + } + + @VisibleForTesting + @Nullable + Session getSession() { + return session; + } + + @VisibleForTesting + @Nullable + MessageConsumer getConsumer() { + return consumer; } // set an empty list to messages when deserialize private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); - lastMessage = null; + messages = null; session = null; + consumer = null; } @Override @@ -120,8 +150,8 @@ public int hashCode() { return Objects.hash(oldestMessageTimestamp); } - static Preparer newPreparer() { - return new Preparer(); + static Preparer newPreparer(AcknowledgeMode acknowledgeMode) { + return new Preparer(acknowledgeMode); } /** @@ -129,15 +159,18 @@ static Preparer newPreparer() { */ static class Preparer { private Instant oldestMessageTimestamp = Instant.now(); - private transient @Nullable Message lastMessage = null; + private transient List messages = new ArrayList<>(); + private final AcknowledgeMode acknowledgeMode; @VisibleForTesting transient boolean discarded = false; @VisibleForTesting final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private Preparer() {} + private Preparer(AcknowledgeMode acknowledgeMode) { + this.acknowledgeMode = acknowledgeMode; + } - void add(Message message) throws Exception { + void add(Message message) throws JMSException { lock.writeLock().lock(); try { if (discarded) { @@ -149,7 +182,18 @@ void add(Message message) throws Exception { if (currentMessageTimestamp.isBefore(oldestMessageTimestamp)) { oldestMessageTimestamp = currentMessageTimestamp; } - lastMessage = message; + if (acknowledgeMode == AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE) { + messages.add(message); + } else { + // Jms spec will implicitly acknowledge _all_ messaged already received by the same + // session if one message in this session is being acknowledged. Only need to ack + // last seen one. + if (messages.isEmpty()) { + messages.add(message); + } else { + messages.set(0, message); + } + } } finally { lock.writeLock().unlock(); } @@ -167,6 +211,7 @@ Instant getOldestMessageTimestamp() { void discard() { lock.writeLock().lock(); try { + messages.clear(); this.discarded = true; } finally { lock.writeLock().unlock(); @@ -175,21 +220,43 @@ void discard() { /** * Create a new checkpoint mark based on the current preparer. This will reset the messages held - * by the preparer, and the owner of the preparer is responsible to create a new Jms session - * after this call. + * by the preparer. If AcknowledgeMode is CLIENT_ACKNOWLEDGE, the owner of the preparer is + * responsible to create a new Jms session after this call. */ - JmsCheckpointMark newCheckpoint(@Nullable MessageConsumer consumer, @Nullable Session session) { + JmsCheckpointMark newCheckpoint( + @Nullable MessageConsumer consumer, + @Nullable Session session, + @Nullable AcknowledgeMode acknowledgeMode, + @Nullable AtomicInteger activeCheckpoints) { JmsCheckpointMark checkpointMark; lock.writeLock().lock(); try { if (discarded) { - lastMessage = null; + messages.clear(); checkpointMark = this.emptyCheckpoint(); } else { + List messagesCopy = null; + MessageConsumer consumerToPass = null; + Session sessionToPass = null; + if (!messages.isEmpty()) { + messagesCopy = new ArrayList<>(messages); + } + if (acknowledgeMode == AcknowledgeMode.CLIENT_ACKNOWLEDGE) { + consumerToPass = consumer; + sessionToPass = session; + } checkpointMark = - new JmsCheckpointMark(oldestMessageTimestamp, lastMessage, consumer, session); - lastMessage = null; + new JmsCheckpointMark( + oldestMessageTimestamp, + messagesCopy, + consumerToPass, + sessionToPass, + activeCheckpoints); + messages.clear(); oldestMessageTimestamp = Instant.now(); + if (activeCheckpoints != null) { + activeCheckpoints.incrementAndGet(); + } } } finally { lock.writeLock().unlock(); @@ -198,11 +265,11 @@ JmsCheckpointMark newCheckpoint(@Nullable MessageConsumer consumer, @Nullable Se } JmsCheckpointMark emptyCheckpoint() { - return new JmsCheckpointMark(oldestMessageTimestamp, null, null, null); + return new JmsCheckpointMark(oldestMessageTimestamp, null, null, null, null); } boolean isEmpty() { - return lastMessage == null; + return messages.isEmpty(); } } } diff --git a/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsIO.java b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsIO.java index 5deb7a2b62c7..c3cf0a2ac253 100644 --- a/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsIO.java +++ b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsIO.java @@ -35,6 +35,7 @@ import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import javax.jms.Connection; import javax.jms.ConnectionFactory; @@ -118,6 +119,22 @@ * * } * + *

Acknowledgment Modes and Client Prefetch Configuration

+ * + *

By default, {@link JmsIO} consumes messages using {@link AcknowledgeMode#CLIENT_ACKNOWLEDGE} + * where a new {@link javax.jms.Session} is created for each checkpoint to prevent premature + * acknowledgments across bundles. When using {@link AcknowledgeMode#CLIENT_ACKNOWLEDGE}, if your + * JMS broker or client library utilizes client-side message prefetch buffers (such as Apache + * ActiveMQ), you should configure {@code prefetch=0} on your {@link javax.jms.ConnectionFactory} + * (e.g., via {@code ?jms.prefetchPolicy.all=0} in the broker URL or {@code + * ActiveMQPrefetchPolicy.setAll(0)}). Otherwise, unconsumed messages could be held inside old + * consumers in low throughput scenario and could lead to message backlog. + * + *

Alternatively, if your JMS broker supports individual message acknowledgment (such as ActiveMQ + * or Amazon MQ {@code ActiveMQSession.INDIVIDUAL_ACKNOWLEDGE = 4}), you can specify {@link + * Read#withAcknowledgeMode(AcknowledgeMode)} with {@link AcknowledgeMode#INDIVIDUAL_ACKNOWLEDGE}. + * In this mode, a single shared session and consumer are reused across all checkpoints. + * *

Writing to a JMS destination

* *

JmsIO sink supports writing text messages to a JMS destination on a broker. To configure a JMS @@ -146,10 +163,11 @@ public static Read read() { .setCoder(SerializableCoder.of(JmsRecord.class)) .setCloseTimeout(DEFAULT_CLOSE_TIMEOUT) .setRequiresDeduping(false) + .setAcknowledgeMode(AcknowledgeMode.CLIENT_ACKNOWLEDGE) .setMessageMapper( new MessageMapper() { @Override - public JmsRecord mapMessage(Message message) throws Exception { + public JmsRecord mapMessage(Message message) throws JMSException { TextMessage textMessage = (TextMessage) message; Map properties = new HashMap<>(); @SuppressWarnings("rawtypes") @@ -182,6 +200,7 @@ public static Read readMessage() { .setMaxNumRecords(Long.MAX_VALUE) .setCloseTimeout(DEFAULT_CLOSE_TIMEOUT) .setRequiresDeduping(false) + .setAcknowledgeMode(AcknowledgeMode.CLIENT_ACKNOWLEDGE) .build(); } @@ -261,6 +280,10 @@ ConnectionFactory getConnectionFactory() { abstract boolean isRequiresDeduping(); + abstract AcknowledgeMode getAcknowledgeMode(); + + abstract @Nullable Integer getIndividualAcknowledgeModeCode(); + abstract Builder builder(); @AutoValue.Builder @@ -292,6 +315,10 @@ abstract Builder setConnectionFactoryProviderFn( abstract Builder setRequiresDeduping(boolean requiresDeduping); + abstract Builder setAcknowledgeMode(AcknowledgeMode acknowledgeMode); + + abstract Builder setIndividualAcknowledgeModeCode(Integer individualAcknowledgeModeCode); + abstract Read build(); } @@ -340,6 +367,21 @@ public Read withConnectionFactoryProviderFn( return builder().setConnectionFactoryProviderFn(connectionFactoryProviderFn).build(); } + public Read withConnectionConfiguration(ConnectionConfiguration configuration) { + checkArgument(configuration != null, "configuration can not be null"); + Read read = + this.withConnectionFactoryProviderFn( + (SerializableFunction) + __ -> configuration.createConnectionFactory()); + if (configuration.getUsername() != null) { + read = read.withUsername(configuration.getUsername()); + } + if (configuration.getPassword() != null) { + read = read.withPassword(configuration.getPassword()); + } + return read; + } + /** * Specify the JMS queue destination name where to read messages from. The {@link JmsIO.Read} * acts as a consumer on the queue. @@ -484,6 +526,29 @@ public Read withRequiresDeduping() { return builder().setRequiresDeduping(true).build(); } + /** + * Specify the {@link AcknowledgeMode} used for consuming and acknowledging JMS messages. + * + *

To use {@link AcknowledgeMode#INDIVIDUAL_ACKNOWLEDGE}, providers other than ActiveMQ, + * ActiveMQ Artemis, Qpid JMS, require configuring {@link #withIndividualAcknowledgeModeCode} + * explicitly. + */ + public Read withAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + checkArgument(acknowledgeMode != null, "acknowledgeMode can not be null"); + return builder().setAcknowledgeMode(acknowledgeMode).build(); + } + + /** + * Specify the custom integer code for individual message acknowledgment when using {@link + * AcknowledgeMode#INDIVIDUAL_ACKNOWLEDGE}. + * + *

Different JMS providers use different proprietary integer constants for individual + * acknowledgment (e.g., ActiveMQ uses 4, Qpid JMS / ActiveMQ Artemis use 101). + */ + public Read withIndividualAcknowledgeModeCode(int individualAcknowledgeModeCode) { + return builder().setIndividualAcknowledgeModeCode(individualAcknowledgeModeCode).build(); + } + @Override public PCollection expand(PBegin input) { checkArgument( @@ -515,6 +580,7 @@ public void populateDisplayData(DisplayData.Builder builder) { super.populateDisplayData(builder); builder.addIfNotNull(DisplayData.item("queue", getQueue())); builder.addIfNotNull(DisplayData.item("topic", getTopic())); + builder.add(DisplayData.item("acknowledgeMode", getAcknowledgeMode().name())); } /////////////////////////////////////////////////////////////////////////////////////// @@ -530,6 +596,35 @@ UnboundedSource createSource() { private JmsIO() {} + public enum AcknowledgeMode { + /** + * Acknowledge on checkpoint finalization with session isolation across checkpoints. Due to that + * runners may or may not finalize checkpoint in timely minor, unacked messages in unclosed + * consumer could be stuck when source throughput is zero. When using this mode in low + * throughput use cases, client-side prefetch buffers should be disabled (e.g. set {@code + * prefetch=0} for ActiveMQ) in connection factory properties. + */ + CLIENT_ACKNOWLEDGE, + + /** + * CLIENT_ACKNOWLEDGE but without session isolation across checkpoints. Acknowledging a message + * implicitly acknowledge all messages received up to that point per JMS spec. Best for + * performance but not safe on worker crash or scaling down. Good when best effort delivery is + * acceptable. + */ + CLIENT_ACKNOWLEDGE_UNSAFE, + + /** + * Acknowledge messages individually on checkpoint finalization. Recommended for JMS providers + * that support individual message acknowledgment (e.g., ActiveMQ, Amazon MQ, Artemis, Qpid + * JMS). + */ + INDIVIDUAL_ACKNOWLEDGE + + // When adding new AcknowledgeMode enum, update getAckModeCode(AcknowledgeMode mode) to handle + // the new mode. + } + /** * An interface used by {@link JmsIO.Read} for converting each jms {@link Message} into an element * of the resulting {@link PCollection}. @@ -601,10 +696,13 @@ static class UnboundedJmsReader extends UnboundedReader { private byte[] currentID; private long receiveTimeoutMillis; private PipelineOptions options; + // Acknowlging messages need open consumer. Tracking active checkpoints allows delayed close of + // session and consumer. + private final AtomicInteger activeCheckpoints = new AtomicInteger(0); public UnboundedJmsReader(UnboundedJmsSource source, PipelineOptions options) { this.source = source; - this.checkpointMarkPreparer = JmsCheckpointMark.newPreparer(); + this.checkpointMarkPreparer = JmsCheckpointMark.newPreparer(source.spec.getAcknowledgeMode()); this.currentMessage = null; this.currentID = EMPTY; this.options = options; @@ -613,7 +711,8 @@ public UnboundedJmsReader(UnboundedJmsSource source, PipelineOptions options) /** recreate session and consumer. */ private synchronized void recreateSession() throws IOException { try { - this.session = this.connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); + int ackMode = getAckModeCode(source.spec.getAcknowledgeMode()); + this.session = this.connection.createSession(false, ackMode); } catch (Exception e) { throw new IOException("Error creating JMS session", e); } @@ -634,6 +733,34 @@ private synchronized void recreateSession() throws IOException { } } + private int getAckModeCode(AcknowledgeMode mode) { + if (mode == AcknowledgeMode.CLIENT_ACKNOWLEDGE + || mode == AcknowledgeMode.CLIENT_ACKNOWLEDGE_UNSAFE) { + return Session.CLIENT_ACKNOWLEDGE; + } else if (mode == AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE) { + Integer configuredCode = source.spec.getIndividualAcknowledgeModeCode(); + if (configuredCode != null) { + return configuredCode; + } + String connectionClassName = this.connection.getClass().getName(); + if (connectionClassName.contains("org.apache.activemq.ActiveMQConnection")) { + return 4; + } else if (connectionClassName.contains("org.apache.qpid.jms")) { + return 101; + } else if (connectionClassName.contains("org.apache.activemq.artemis")) { + return 101; + } else { + throw new IllegalArgumentException( + String.format( + "Unknown JMS provider '%s' for INDIVIDUAL_ACKNOWLEDGE. " + + "Please specify the code explicitly via Read#withIndividualAcknowledgeModeCode(int).", + connectionClassName)); + } + } else { + throw new IllegalArgumentException(String.format("Unknown AcknowledgeMode: %s", mode)); + } + } + @Override public boolean start() throws IOException { Read spec = source.spec; @@ -752,16 +879,23 @@ public CheckpointMark getCheckpointMark() { MessageConsumer consumerToClose; Session sessionTofinalize; + AcknowledgeMode mode = source.spec.getAcknowledgeMode(); synchronized (this) { - consumerToClose = consumer; - sessionTofinalize = session; - } - try { - recreateSession(); - } catch (IOException e) { - throw new RuntimeException(e); + if (mode == AcknowledgeMode.CLIENT_ACKNOWLEDGE) { + consumerToClose = consumer; + sessionTofinalize = session; + try { + recreateSession(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + consumerToClose = null; + sessionTofinalize = null; + } } - return checkpointMarkPreparer.newCheckpoint(consumerToClose, sessionTofinalize); + return checkpointMarkPreparer.newCheckpoint( + consumerToClose, sessionTofinalize, mode, activeCheckpoints); } @Override @@ -783,21 +917,44 @@ public void close() { private void doClose() { try { closeAutoscaler(); - closeConsumer(); - ScheduledExecutorService executorService = - options.as(ExecutorOptions.class).getScheduledExecutorService(); - executorService.schedule( - () -> { - LOG.debug("Closing connection after delay {}", source.spec.getCloseTimeout()); - // Discard the checkpoints and set the reader as inactive - checkpointMarkPreparer.discard(); - closeSession(); - closeConnection(); - }, - source.spec.getCloseTimeout().getMillis(), - TimeUnit.MILLISECONDS); + // Discard the checkpoints and set the reader as inactive + checkpointMarkPreparer.discard(); + if (source.spec.getAcknowledgeMode() == AcknowledgeMode.CLIENT_ACKNOWLEDGE) { + // checkpointMark holds session in CLIENT_ACKNOWLEDGE mode. Therefore + // we can close consumer and session immediately. + closeConsumer(); + closeSession(); + } + if (activeCheckpoints.get() == 0) { + closeConsumer(); + closeSession(); + closeConnection(); + } else { + ScheduledExecutorService executorService = + options.as(ExecutorOptions.class).getScheduledExecutorService(); + long deadline = System.currentTimeMillis() + source.spec.getCloseTimeout().getMillis(); + long pollInterval = 1L; + executorService.schedule( + new Runnable() { + @Override + public void run() { + if (activeCheckpoints.get() == 0 || System.currentTimeMillis() >= deadline) { + LOG.debug( + "Closing connection after checkpoints finalized or timeout: {}", + source.spec.getCloseTimeout()); + closeConsumer(); + closeSession(); + closeConnection(); + } else { + executorService.schedule(this, pollInterval, TimeUnit.SECONDS); + } + } + }, + pollInterval, + TimeUnit.SECONDS); + } } catch (Exception e) { - LOG.error("Error closing reader", e); + LOG.warn("Error closing reader", e); } } @@ -809,7 +966,7 @@ private void closeConnection() { connection = null; } } catch (Exception e) { - LOG.error("Error closing connection", e); + LOG.warn("Error closing connection", e); } } @@ -964,6 +1121,21 @@ public Write withConnectionFactoryProviderFn( return builder().setConnectionFactoryProviderFn(connectionFactoryProviderFn).build(); } + public Write withConnectionConfiguration(ConnectionConfiguration configuration) { + checkArgument(configuration != null, "configuration can not be null"); + Write write = + this.withConnectionFactoryProviderFn( + (SerializableFunction) + __ -> configuration.createConnectionFactory()); + if (configuration.getUsername() != null) { + write = write.withUsername(configuration.getUsername()); + } + if (configuration.getPassword() != null) { + write = write.withPassword(configuration.getPassword()); + } + return write; + } + /** * Specify the JMS queue destination name where to send messages to. The {@link JmsIO.Write} * acts as a producer on the queue. diff --git a/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsReadSchemaTransformProvider.java b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsReadSchemaTransformProvider.java new file mode 100644 index 000000000000..c34f38f58e05 --- /dev/null +++ b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsReadSchemaTransformProvider.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.jms; + +import static org.apache.beam.sdk.io.jms.JmsReadSchemaTransformProvider.ReadConfiguration; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +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 JmsReadSchemaTransformProvider + extends TypedSchemaTransformProvider { + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class ReadConfiguration implements Serializable { + public static Builder builder() { + return new AutoValue_JmsReadSchemaTransformProvider_ReadConfiguration.Builder(); + } + + @SchemaFieldDescription( + "Configuration options to set up the JMS connection.\nNote: if " + + "connection factory class name is set, spin up a persistent expansion service with " + + "the provider client JAR on the classpath:\n" + + "java -cp :\n" + + "org.apache.beam.sdk.expansion.service.ExpansionService \n" + + "and pass expansion_service='localhost:' to the transform. Currently, only " + + "ActiveMQ (org.apache.activemq.ActiveMQConnectionFactory) is embedded into the expansion service") + public abstract ConnectionConfiguration getConnectionConfiguration(); + + @SchemaFieldDescription( + "The JMS queue to read from. Exclusively one of queue or topic must be specified.") + @Nullable + public abstract String getQueue(); + + @SchemaFieldDescription( + "The JMS topic to read from. Exclusively one of queue or topic must be specified.") + @Nullable + public abstract String getTopic(); + + @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(); + + @SchemaFieldDescription("Close timeout for the JMS connection in seconds.") + @Nullable + public abstract Long getCloseTimeoutSeconds(); + + @SchemaFieldDescription( + "The JMS acknowledge mode: CLIENT_ACKNOWLEDGE, CLIENT_ACKNOWLEDGE_UNSAFE, or INDIVIDUAL_ACKNOWLEDGE.") + @Nullable + public abstract String getAcknowledgeMode(); + + @SchemaFieldDescription( + "The proprietary integer code for individual message acknowledgment when using INDIVIDUAL_ACKNOWLEDGE.") + @Nullable + public abstract Integer getIndividualAcknowledgeModeCode(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConnectionConfiguration( + ConnectionConfiguration connectionConfiguration); + + public abstract Builder setQueue(String queue); + + public abstract Builder setTopic(String topic); + + public abstract Builder setMaxNumRecords(Long maxNumRecords); + + public abstract Builder setMaxReadTimeSeconds(Long maxReadTimeSeconds); + + public abstract Builder setCloseTimeoutSeconds(Long closeTimeoutSeconds); + + public abstract Builder setAcknowledgeMode(String acknowledgeMode); + + public abstract Builder setIndividualAcknowledgeModeCode( + Integer individualAcknowledgeModeCode); + + public abstract ReadConfiguration build(); + } + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:jms_read:v1"; + } + + @Override + public String description() { + return "Reads messages from a JMS broker and outputs each message as a single `payload` " + + "(string) field.\n" + + "\n" + + "By default the read is unbounded (streaming): it keeps consuming messages from the " + + "specified queue or topic until the pipeline is stopped. Setting `maxNumRecords` and/or " + + "`maxReadTimeSeconds` bounds the read, producing a bounded (batch) PCollection."; + } + + @Override + public List outputCollectionNames() { + return Collections.singletonList("output"); + } + + @Override + protected SchemaTransform from(ReadConfiguration configuration) { + return new JmsReadSchemaTransform(configuration); + } + + private static class JmsReadSchemaTransform extends SchemaTransform { + private final ReadConfiguration config; + + JmsReadSchemaTransform(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()); + + Preconditions.checkArgument( + (config.getQueue() != null) != (config.getTopic() != null), + "Exactly one of queue or topic must be specified."); + + JmsIO.Read readTransform = + JmsIO.read().withConnectionConfiguration(config.getConnectionConfiguration()); + + String queue = config.getQueue(); + if (queue != null) { + readTransform = readTransform.withQueue(queue); + } + String topic = config.getTopic(); + if (topic != null) { + readTransform = readTransform.withTopic(topic); + } + + Long maxRecords = config.getMaxNumRecords(); + Long maxReadTime = config.getMaxReadTimeSeconds(); + if (maxRecords != null) { + readTransform = readTransform.withMaxNumRecords(maxRecords); + } + if (maxReadTime != null) { + readTransform = readTransform.withMaxReadTime(Duration.standardSeconds(maxReadTime)); + } + + Long closeTimeout = config.getCloseTimeoutSeconds(); + if (closeTimeout != null) { + readTransform = readTransform.withCloseTimeout(Duration.standardSeconds(closeTimeout)); + } + + String ackMode = config.getAcknowledgeMode(); + if (ackMode != null) { + readTransform = + readTransform.withAcknowledgeMode(JmsIO.AcknowledgeMode.valueOf(ackMode.toUpperCase())); + } + + Integer indAckCode = config.getIndividualAcknowledgeModeCode(); + if (indAckCode != null) { + readTransform = readTransform.withIndividualAcknowledgeModeCode(indAckCode); + } + + Schema outputSchema = Schema.builder().addStringField("payload").build(); + + PCollection outputRows = + input + .getPipeline() + .apply(readTransform) + .apply( + "Wrap in Beam Rows", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element JmsRecord record, OutputReceiver outputReceiver) { + String payload = record.getPayload(); + if (payload == null) { + payload = ""; + } + outputReceiver.output( + Row.withSchema(outputSchema).addValue(payload).build()); + } + })) + .setRowSchema(outputSchema); + + return PCollectionRowTuple.of("output", outputRows); + } + } +} diff --git a/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsWriteSchemaTransformProvider.java b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsWriteSchemaTransformProvider.java new file mode 100644 index 000000000000..34316c9a8c1e --- /dev/null +++ b/sdks/java/io/jms/src/main/java/org/apache/beam/sdk/io/jms/JmsWriteSchemaTransformProvider.java @@ -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. + */ +package org.apache.beam.sdk.io.jms; + +import static org.apache.beam.sdk.io.jms.JmsWriteSchemaTransformProvider.WriteConfiguration; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +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 JmsWriteSchemaTransformProvider + extends TypedSchemaTransformProvider { + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class WriteConfiguration implements Serializable { + public static Builder builder() { + return new AutoValue_JmsWriteSchemaTransformProvider_WriteConfiguration.Builder(); + } + + @SchemaFieldDescription( + "Configuration options to set up the JMS connection.\nNote: if " + + "connection factory class name is set, spin up a persistent expansion service with " + + "the provider client JAR on the classpath:\n" + + "java -cp :\n" + + "org.apache.beam.sdk.expansion.service.ExpansionService \n" + + "and pass expansion_service='localhost:' to the transform. Currently, only " + + "ActiveMQ (org.apache.activemq.ActiveMQConnectionFactory) is embedded into the expansion service") + public abstract ConnectionConfiguration getConnectionConfiguration(); + + @SchemaFieldDescription( + "The JMS queue to write to. Exclusively one of queue or topic must be specified.") + @Nullable + public abstract String getQueue(); + + @SchemaFieldDescription( + "The JMS topic to write to. Exclusively one of queue or topic must be specified.") + @Nullable + public abstract String getTopic(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConnectionConfiguration( + ConnectionConfiguration connectionConfiguration); + + public abstract Builder setQueue(String queue); + + public abstract Builder setTopic(String topic); + + public abstract WriteConfiguration build(); + } + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:jms_write:v1"; + } + + @Override + public String description() { + return "Publishes messages to a JMS broker. Expects an input PCollection of rows with a " + + "`payload` (string) or `bytes` field, each of which is published as one JMS TextMessage.\n" + + "\n" + + "Works with both bounded (batch) and unbounded (streaming) input PCollections."; + } + + @Override + public List inputCollectionNames() { + return Collections.singletonList("input"); + } + + @Override + protected SchemaTransform from(WriteConfiguration configuration) { + return new JmsWriteSchemaTransform(configuration); + } + + private static class JmsWriteSchemaTransform extends SchemaTransform { + private final WriteConfiguration config; + + JmsWriteSchemaTransform(WriteConfiguration configuration) { + this.config = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + PCollection inputRows = input.getSinglePCollection(); + + Preconditions.checkArgument( + (config.getQueue() != null) != (config.getTopic() != null), + "Exactly one of queue or topic must be specified."); + + int fieldIndex = -1; + boolean isBytes = false; + Schema schema = inputRows.getSchema(); + if (schema.hasField("payload") + && schema.getField("payload").getType().equals(Schema.FieldType.STRING)) { + fieldIndex = schema.indexOf("payload"); + } else if (schema.hasField("bytes") + && schema.getField("bytes").getType().equals(Schema.FieldType.BYTES)) { + fieldIndex = schema.indexOf("bytes"); + isBytes = true; + } else if (schema.getFieldCount() == 1 + && schema.getField(0).getType().equals(Schema.FieldType.STRING)) { + fieldIndex = 0; + } else if (schema.getFieldCount() == 1 + && schema.getField(0).getType().equals(Schema.FieldType.BYTES)) { + fieldIndex = 0; + isBytes = true; + } else { + throw new IllegalStateException( + String.format( + "Expected input Schema to have a 'payload' (STRING) or 'bytes' (BYTES) field, or" + + " a single string/bytes field, but received: %s", + schema)); + } + + JmsIO.Write writeTransform = + JmsIO.write() + .withConnectionConfiguration(config.getConnectionConfiguration()) + .withValueMapper(new TextMessageMapper()); + + String queue = config.getQueue(); + if (queue != null) { + writeTransform = writeTransform.withQueue(queue); + } + String topic = config.getTopic(); + if (topic != null) { + writeTransform = writeTransform.withTopic(topic); + } + + final int idx = fieldIndex; + final boolean bytesField = isBytes; + inputRows + .apply( + "Extract payload", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element Row row, OutputReceiver outputReceiver) { + if (bytesField) { + byte[] bytes = + org.apache.beam.sdk.util.Preconditions.checkStateNotNull( + row.getBytes(idx)); + outputReceiver.output(new String(bytes, StandardCharsets.UTF_8)); + } else { + String payload = + org.apache.beam.sdk.util.Preconditions.checkStateNotNull( + row.getString(idx)); + outputReceiver.output(payload); + } + } + })) + .apply(writeTransform); + + return PCollectionRowTuple.empty(inputRows.getPipeline()); + } + } +} diff --git a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/CommonJms.java b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/CommonJms.java index 1d1245e6877d..16173a36248c 100644 --- a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/CommonJms.java +++ b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/CommonJms.java @@ -26,7 +26,6 @@ import javax.jms.BytesMessage; import javax.jms.ConnectionFactory; import javax.jms.Message; -import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.broker.BrokerPlugin; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.security.AuthenticationUser; @@ -91,12 +90,16 @@ public CommonJms( this.connectionFactoryClass = connectionFactoryClass; } + private boolean shouldAppendPort() { + return brokerPort != null && brokerPort != 0; + } + void startBroker() throws Exception { broker = new BrokerService(); broker.setUseJmx(false); broker.setPersistenceAdapter(new MemoryPersistenceAdapter()); TransportFactory.registerTransportFactory("amqp", new AmqpTransportFactory()); - if (connectionFactoryClass != ActiveMQConnectionFactory.class) { + if (shouldAppendPort()) { broker.addConnector(String.format("%s:%d?transport.transformer=jms", brokerUrl, brokerPort)); } else { broker.addConnector(brokerUrl); @@ -119,10 +122,18 @@ void startBroker() throws Exception { broker.waitUntilStarted(); } + private String getBrokerUrlWithPort() { + if (shouldAppendPort()) { + return String.format("%s:%d", brokerUrl, brokerPort); + } else { + return brokerUrl; + } + } + ConnectionFactory createConnectionFactory() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { - return connectionFactoryClass.getConstructor(String.class).newInstance(brokerUrl); + return connectionFactoryClass.getConstructor(String.class).newInstance(getBrokerUrlWithPort()); } ConnectionFactory createConnectionFactoryWithSyncAcksAndWithoutPrefetch() @@ -130,13 +141,15 @@ ConnectionFactory createConnectionFactoryWithSyncAcksAndWithoutPrefetch() IllegalAccessException { return connectionFactoryClass .getConstructor(String.class) - .newInstance(brokerUrl + BROKER_WITHOUT_PREFETCH_PARAM + forceAsyncAcksParam); + .newInstance(getBrokerUrlWithPort() + BROKER_WITHOUT_PREFETCH_PARAM + forceAsyncAcksParam); } void stopBroker() throws Exception { - broker.stop(); - broker.waitUntilStopped(); - broker = null; + if (broker != null) { + broker.stop(); + broker.waitUntilStopped(); + broker = null; + } } Class getConnectionFactoryClass() { diff --git a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/ConnectionConfigurationTest.java b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/ConnectionConfigurationTest.java new file mode 100644 index 000000000000..7aabc683855d --- /dev/null +++ b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/ConnectionConfigurationTest.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.jms; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.qpid.jms.JmsConnectionFactory; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link ConnectionConfiguration}. */ +@RunWith(JUnit4.class) +public class ConnectionConfigurationTest { + + public static class CustomTestConnectionFactory implements BeamGenericJmsConnectionFactory { + @Override + public ConnectionFactory createConnectionFactory(ConnectionConfiguration config) { + return new DummyConnectionFactory( + config.getServerUri(), config.getUsername(), config.getPassword()); + } + } + + public static class DummyConnectionFactory implements ConnectionFactory { + private final String serverUri; + private final String username; + private final String password; + + public DummyConnectionFactory( + String serverUri, @Nullable String username, @Nullable String password) { + this.serverUri = serverUri; + this.username = username; + this.password = password; + } + + public String getServerUri() { + return serverUri; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @Override + public Connection createConnection() throws JMSException { + return null; + } + + @Override + public Connection createConnection(String username, String password) throws JMSException { + return null; + } + + @Override + public javax.jms.JMSContext createContext() { + return null; + } + + @Override + public javax.jms.JMSContext createContext(int sessionMode) { + return null; + } + + @Override + public javax.jms.JMSContext createContext(String username, String password) { + return null; + } + + @Override + public javax.jms.JMSContext createContext(String username, String password, int sessionMode) { + return null; + } + } + + @Test + public void testDefaultActiveMQConnectionFactory() { + ConnectionConfiguration config = ConnectionConfiguration.create("vm://localhost"); + ConnectionFactory cf = config.createConnectionFactory(); + assertNotNull(cf); + assertTrue(cf instanceof ActiveMQConnectionFactory); + } + + @Test + public void testQpidConnectionFactory() { + ConnectionConfiguration config = + ConnectionConfiguration.create("amqp://localhost") + .withConnectionFactoryClassName("org.apache.qpid.jms.JmsConnectionFactory"); + ConnectionFactory cf = config.createConnectionFactory(); + assertNotNull(cf); + assertTrue(cf instanceof JmsConnectionFactory); + } + + @Test + public void testCustomBeamGenericJmsConnectionFactory() { + ConnectionConfiguration config = + ConnectionConfiguration.create("custom://localhost") + .withConnectionFactoryClassName(CustomTestConnectionFactory.class.getName()) + .withUsername("testUser") + .withPassword("testPass"); + ConnectionFactory cf = config.createConnectionFactory(); + assertNotNull(cf); + assertTrue(cf instanceof DummyConnectionFactory); + DummyConnectionFactory dummy = (DummyConnectionFactory) cf; + assertEquals("custom://localhost", dummy.getServerUri()); + assertEquals("testUser", dummy.getUsername()); + assertEquals("testPass", dummy.getPassword()); + } + + @Test + public void testClientsNotSlippedIntoRuntimeDependencies() { + // Verify IBM MQ client is not present on runtime classpath + assertThrows( + ClassNotFoundException.class, () -> Class.forName("com.ibm.mq.jms.MQConnectionFactory")); + + ConnectionConfiguration config = + ConnectionConfiguration.create("tcp://localhost:1414") + .withConnectionFactoryClassName("com.ibm.mq.jms.MQConnectionFactory"); + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, config::createConnectionFactory); + assertTrue(exception.getCause() instanceof ClassNotFoundException); + } + + @Test + public void testUnsupportedConnectionFactoryClass() { + ConnectionConfiguration config = + ConnectionConfiguration.create("tcp://localhost") + .withConnectionFactoryClassName("java.lang.String"); + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, config::createConnectionFactory); + assertTrue(exception.getMessage().contains("BeamGenericJmsConnectionFactory")); + } +} diff --git a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsIOIT.java b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsIOIT.java index 266d04342d1f..3dbb20775f71 100644 --- a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsIOIT.java +++ b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsIOIT.java @@ -20,8 +20,8 @@ import static org.apache.beam.sdk.io.jms.CommonJms.PASSWORD; import static org.apache.beam.sdk.io.jms.CommonJms.QUEUE; import static org.apache.beam.sdk.io.jms.CommonJms.USERNAME; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.Serializable; @@ -29,8 +29,10 @@ import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import javax.jms.Connection; import javax.jms.ConnectionFactory; @@ -46,6 +48,7 @@ import org.apache.beam.sdk.io.GenerateSequence; import org.apache.beam.sdk.io.common.IOITHelper; import org.apache.beam.sdk.io.common.IOTestPipelineOptions; +import org.apache.beam.sdk.io.common.NetworkTestHelper; import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Metrics; import org.apache.beam.sdk.options.Default; @@ -63,6 +66,7 @@ import org.apache.qpid.jms.JmsConnectionFactory; import org.joda.time.Duration; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -142,105 +146,174 @@ public interface JmsIOITOptions extends IOTestPipelineOptions, StreamingOptions @Rule public transient TestPipeline pipelineWrite = TestPipeline.create(); @Rule public transient TestPipeline pipelineRead = TestPipeline.create(); - @Parameterized.Parameters(name = "with client class {3}") + @Parameterized.Parameters(name = "with client class {2}") public static Collection connectionFactories() { return ImmutableList.of( - new Object[] { - "vm://localhost", 5672, "jms.sendAcksAsync=false", ActiveMQConnectionFactory.class - }); - // TODO(https://github.com/apache/beam/issues/26175) Test failure on direct runner due to - // JmsIO read on amqp slow on CI (passed locally) - // new Object[] { - // "amqp://localhost", 5672, "jms.forceAsyncAcks=false", JmsConnectionFactory.class - // }); + new Object[] {"vm://localhost", "jms.sendAcksAsync=false", ActiveMQConnectionFactory.class}, + new Object[] {"amqp://localhost", "jms.forceAsyncAcks=false", JmsConnectionFactory.class}); } - private final CommonJms commonJms; + private static final Map BROKERS = new ConcurrentHashMap<>(); + + private final String brokerUrl; + private final Integer brokerPort; + private final String forceAsyncAcksParam; + private final Class connectionFactoryClassParam; + private CommonJms commonJms; private ConnectionFactory connectionFactory; private Class connectionFactoryClass; public JmsIOIT( String brokerUrl, - Integer brokerPort, String forceAsyncAcksParam, Class connectionFactoryClass) { - this.commonJms = - new CommonJms( - OPTIONS.isLocalJmsBrokerEnabled() ? brokerUrl : OPTIONS.getJmsBrokerHost(), - OPTIONS.isLocalJmsBrokerEnabled() ? brokerPort : OPTIONS.getJmsBrokerPort(), - forceAsyncAcksParam, - connectionFactoryClass); + this.brokerUrl = brokerUrl; + if (OPTIONS.isLocalJmsBrokerEnabled()) { + try { + this.brokerPort = NetworkTestHelper.getAvailableLocalPort(); + } catch (IOException e) { + throw new RuntimeException("Failed to find available port", e); + } + } else { + this.brokerPort = OPTIONS.getJmsBrokerPort(); + } + this.forceAsyncAcksParam = forceAsyncAcksParam; + this.connectionFactoryClassParam = connectionFactoryClass; } @Before public void setup() throws Exception { if (OPTIONS.isLocalJmsBrokerEnabled()) { - this.commonJms.startBroker(); - connectionFactory = this.commonJms.createConnectionFactory(); - connectionFactoryClass = this.commonJms.getConnectionFactoryClass(); - // use a small number of record for local integration test + String key = brokerUrl + ":" + connectionFactoryClassParam.getName(); + commonJms = + BROKERS.computeIfAbsent( + key, + k -> { + CommonJms broker = + new CommonJms( + brokerUrl, brokerPort, forceAsyncAcksParam, connectionFactoryClassParam); + try { + broker.startBroker(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return broker; + }); OPTIONS.setNumberOfRecords(10000); + } else { + commonJms = + new CommonJms( + OPTIONS.getJmsBrokerHost(), + OPTIONS.getJmsBrokerPort(), + forceAsyncAcksParam, + connectionFactoryClassParam); } } + @AfterClass + public static void afterClass() throws Exception { + for (CommonJms broker : BROKERS.values()) { + try { + broker.stopBroker(); + } catch (Exception e) { + // ignore errors on shutdown + } + } + BROKERS.clear(); + } + + private void setupConnection(JmsIO.AcknowledgeMode acknowledgeMode) throws Exception { + if (acknowledgeMode == JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE) { + connectionFactory = this.commonJms.createConnectionFactoryWithSyncAcksAndWithoutPrefetch(); + } else { + connectionFactory = this.commonJms.createConnectionFactory(); + } + connectionFactoryClass = this.commonJms.getConnectionFactoryClass(); + } + @After public void tearDown() throws Exception { - if (OPTIONS.isLocalJmsBrokerEnabled()) { - this.commonJms.stopBroker(); - connectionFactory = null; - connectionFactoryClass = null; - } + connectionFactory = null; + connectionFactoryClass = null; + } + + @Test + public void testPublishingThenReadingAll() throws Exception { + runPublishingThenReadingAll(JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE); + } + + @Test + public void testPublishingThenReadingAllIndividualAcknowledge() throws Exception { + runPublishingThenReadingAll(JmsIO.AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE); } @Test - public void testPublishingThenReadingAll() throws IOException, JMSException { - PipelineResult writeResult = publishingMessages(); + public void testPublishingThenReadingAllClientAcknowledgeUnsafe() throws Exception { + runPublishingThenReadingAll(JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE_UNSAFE); + } + + private void runPublishingThenReadingAll(JmsIO.AcknowledgeMode acknowledgeMode) throws Exception { + setupConnection(acknowledgeMode); + String queue = QUEUE + "_" + acknowledgeMode.name(); + PipelineResult writeResult = publishingMessages(queue); PipelineResult.State writeState = writeResult.waitUntilFinish(); assertNotEquals(PipelineResult.State.FAILED, writeState); - PipelineResult readResult = readMessages(); - PipelineResult.State readState = - readResult.waitUntilFinish(Duration.standardSeconds(OPTIONS.getReadTimeout())); - // A workaround to stop the pipeline for waiting for too long + PipelineResult readResult = readMessages(acknowledgeMode, queue); + MetricsReader metricsReader = new MetricsReader(readResult, NAMESPACE); + long startTime = System.currentTimeMillis(); + long timeoutMillis = OPTIONS.getReadTimeout() * 1000L; + PipelineResult.State readState = readResult.getState(); + while (System.currentTimeMillis() - startTime < timeoutMillis + && (readState == null || !readState.isTerminal())) { + Thread.sleep(500); + readState = readResult.getState(); + if (readState != null && readState.isTerminal()) { + break; + } + long actualRecords = metricsReader.getCounterMetric(READ_ELEMENT_METRIC_NAME); + if (actualRecords >= OPTIONS.getNumberOfRecords()) { + int unackRecords = countRemain(queue); + if (unackRecords == 0) { + readResult.cancel(); + readState = readResult.getState(); + break; + } + } + } cancelIfTimeouted(readResult, readState); assertNotEquals(PipelineResult.State.FAILED, readState); - MetricsReader metricsReader = new MetricsReader(readResult, NAMESPACE); long actualRecords = metricsReader.getCounterMetric(READ_ELEMENT_METRIC_NAME); - - // TODO(yathu) resolve pending messages with direct runner then we can simply assert - // actual-records == total-records. - // Due to direct runner only finalize checkpoint at very end, there are open consumers (may - // with buffer) and O(open_consumer) message won't get delivered to other session. - int unackRecords = countRemain(QUEUE); - assertTrue( - String.format("Too many unacknowledged messages: %d", unackRecords), - unackRecords < OPTIONS.getNumberOfRecords() * 0.003); - - // acknowledged records - int ackRecords = OPTIONS.getNumberOfRecords() - unackRecords; - assertTrue( - String.format( - "actual number of records %d smaller than expected: %d.", actualRecords, ackRecords), - ackRecords <= actualRecords); + int unackRecords = countRemain(queue); + assertEquals("All messages should be acknowledged", 0, unackRecords); + assertEquals("All records should be read", (long) OPTIONS.getNumberOfRecords(), actualRecords); collectAndPublishMetrics(writeResult, readResult); } private void cancelIfTimeouted(PipelineResult readResult, PipelineResult.State readState) throws IOException { - if (readState == null) { + if (readState == null || !readState.isTerminal()) { readResult.cancel(); } } - private PipelineResult readMessages() { + private PipelineResult readMessages(JmsIO.AcknowledgeMode acknowledgeMode, String queue) { pipelineRead.getOptions().as(JmsIOITOptions.class).setStreaming(true); pipelineRead.getOptions().as(JmsIOITOptions.class).setBlockOnRun(false); - JmsIO.Read jmsIORead = JmsIO.readMessage(); + JmsIO.Read jmsIORead = + JmsIO.readMessage() + .withAcknowledgeMode(acknowledgeMode) + // Decrease withCloseTimeout to be smaller than pipeline timeout. Direct runner randomly + // closes reader causing cached pending consumer hanging until closeTimeout + .withCloseTimeout(Duration.standardSeconds(10)); if (pipelineRead.getOptions().as(JmsIOITOptions.class).getUseConnectionFactoryProviderFn()) { jmsIORead = jmsIORead.withConnectionFactoryProviderFn( - CommonJms.toSerializableFunction(commonJms::createConnectionFactory)); + CommonJms.toSerializableFunction( + acknowledgeMode == JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE + ? commonJms::createConnectionFactoryWithSyncAcksAndWithoutPrefetch + : commonJms::createConnectionFactory)); } else { jmsIORead = jmsIORead.withConnectionFactory(connectionFactory); } @@ -248,7 +321,7 @@ private PipelineResult readMessages() { .apply( "Read Messages", jmsIORead - .withQueue(QUEUE) + .withQueue(queue) .withUsername(USERNAME) .withPassword(PASSWORD) .withCoder(SerializableCoder.of(String.class)) @@ -258,7 +331,7 @@ private PipelineResult readMessages() { return pipelineRead.run(); } - private PipelineResult publishingMessages() { + private PipelineResult publishingMessages(String queue) { JmsIO.Write jmsIOWrite = JmsIO.write(); if (pipelineWrite.getOptions().as(JmsIOITOptions.class).getUseConnectionFactoryProviderFn()) { @@ -276,7 +349,7 @@ private PipelineResult publishingMessages() { .apply( "Publish to Jms Broker", jmsIOWrite - .withQueue(QUEUE) + .withQueue(queue) .withUsername(USERNAME) .withPassword(PASSWORD) .withValueMapper(new TextMessageMapper())); @@ -318,17 +391,19 @@ private Function getTimeMetric( } private int countRemain(String queue) throws JMSException { - Connection connection = connectionFactory.createConnection(USERNAME, PASSWORD); - connection.start(); - Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); - QueueBrowser browser = session.createBrowser(session.createQueue(queue)); - Enumeration messages = browser.getEnumeration(); - int count = 0; - while (messages.hasMoreElements()) { - messages.nextElement(); - count++; + try (Connection connection = connectionFactory.createConnection(USERNAME, PASSWORD)) { + connection.start(); + try (Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); + QueueBrowser browser = session.createBrowser(session.createQueue(queue))) { + Enumeration messages = browser.getEnumeration(); + int count = 0; + while (messages.hasMoreElements()) { + messages.nextElement(); + count++; + } + return count; + } } - return count; } static class ToString extends DoFn { diff --git a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsIOTest.java b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsIOTest.java index b3233f866172..d23b33873e14 100644 --- a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsIOTest.java +++ b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsIOTest.java @@ -32,29 +32,21 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isA; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.core.StringContains.containsString; import static org.hamcrest.object.HasToString.hasToString; 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.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.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import java.io.IOException; -import java.io.NotSerializableException; import java.io.Serializable; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -67,8 +59,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import javax.jms.BytesMessage; @@ -85,30 +75,27 @@ import org.apache.activemq.command.ActiveMQMessage; import org.apache.activemq.util.Callback; import org.apache.beam.sdk.PipelineResult; -import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.SerializableCoder; import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.io.UnboundedSource; import org.apache.beam.sdk.io.UnboundedSource.CheckpointMark; +import org.apache.beam.sdk.io.common.NetworkTestHelper; import org.apache.beam.sdk.io.jms.JmsIO.UnboundedJmsReader; import org.apache.beam.sdk.metrics.MetricNameFilter; import org.apache.beam.sdk.metrics.MetricQueryResults; import org.apache.beam.sdk.metrics.MetricsFilter; -import org.apache.beam.sdk.options.ExecutorOptions; -import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.testing.CoderProperties; 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.SerializableBiFunction; -import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Throwables; import org.apache.qpid.jms.JmsAcknowledgeCallback; import org.apache.qpid.jms.JmsConnectionFactory; import org.apache.qpid.jms.message.JmsTextMessage; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.junit.After; import org.junit.Before; @@ -116,7 +103,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -151,39 +137,42 @@ private static TextMessage createTextMessage(Session session, long messageIndex) RetryConfiguration.create(1, Duration.standardSeconds(1), null); @Rule public final transient TestPipeline pipeline = TestPipeline.create(); - @Parameterized.Parameters(name = "with client class {3}") + @Parameterized.Parameters(name = "with client class {2}") public static Collection connectionFactories() { return Arrays.asList( - new Object[] { - "vm://localhost", 5672, "jms.sendAcksAsync=false", ActiveMQConnectionFactory.class - }, - new Object[] { - "amqp://localhost", 5672, "jms.forceAsyncAcks=false", JmsConnectionFactory.class - }); + new Object[] {"vm://localhost", "jms.sendAcksAsync=false", ActiveMQConnectionFactory.class}, + new Object[] {"amqp://localhost", "jms.forceAsyncAcks=false", JmsConnectionFactory.class}); } - private final CommonJms commonJms; - private final ConnectionFactory connectionFactory; + private CommonJms commonJms; + private ConnectionFactory connectionFactory; private final Class connectionFactoryClass; - private final ConnectionFactory connectionFactoryWithSyncAcksAndWithoutPrefetch; + private ConnectionFactory connectionFactoryWithSyncAcksAndWithoutPrefetch; + private String brokerUrl; + private String forceAsyncAcksParam; + private int brokerPort; public JmsIOTest( String brokerUrl, - Integer brokerPort, String forceAsyncAcksParam, - Class connectionFactoryClass) - throws InvocationTargetException, NoSuchMethodException, InstantiationException, - IllegalAccessException { - this.commonJms = - new CommonJms(brokerUrl, brokerPort, forceAsyncAcksParam, connectionFactoryClass); + Class connectionFactoryClass) { + this.brokerUrl = brokerUrl; + this.forceAsyncAcksParam = forceAsyncAcksParam; this.connectionFactoryClass = connectionFactoryClass; - this.connectionFactory = commonJms.createConnectionFactory(); - this.connectionFactoryWithSyncAcksAndWithoutPrefetch = - commonJms.createConnectionFactoryWithSyncAcksAndWithoutPrefetch(); + try { + this.brokerPort = NetworkTestHelper.getAvailableLocalPort(); + } catch (IOException e) { + throw new RuntimeException("Failed to find available port", e); + } } @Before public void beforeEach() throws Exception { + this.commonJms = + new CommonJms(brokerUrl, brokerPort, forceAsyncAcksParam, connectionFactoryClass); + this.connectionFactory = commonJms.createConnectionFactory(); + this.connectionFactoryWithSyncAcksAndWithoutPrefetch = + commonJms.createConnectionFactoryWithSyncAcksAndWithoutPrefetch(); this.commonJms.startBroker(); } @@ -248,20 +237,6 @@ public void testReadMessages() throws Exception { assertQueueIsEmpty(); } - @Test - public void testPipelineWithNonSerializableCF() { - SerializableUtils.ensureSerializable( - JmsIO.read() - .withConnectionFactoryProviderFn(__ -> new MockNonSerializableConnectionFactory())); - try { - SerializableUtils.ensureSerializable( - JmsIO.read().withConnectionFactory(new MockNonSerializableConnectionFactory())); - fail(); - } catch (Exception e) { - assertThat(Throwables.getRootCause(e), isA(NotSerializableException.class)); - } - } - @Test public void testReadMessagesWithCFProviderFn() throws Exception { long count = 5; @@ -518,32 +493,6 @@ public void testWriteDynamicMessage() throws Exception { assertEquals(100, count); } - @Test - public void testSplitForQueue() throws Exception { - JmsIO.Read read = JmsIO.read().withQueue(QUEUE); - PipelineOptions pipelineOptions = PipelineOptionsFactory.create(); - int desiredNumSplits = 5; - JmsIO.UnboundedJmsSource initialSource = new JmsIO.UnboundedJmsSource(read); - List splits = initialSource.split(desiredNumSplits, pipelineOptions); - // in the case of a queue, we have concurrent consumers by default, so the initial number - // splits is equal to the desired number of splits - assertEquals(desiredNumSplits, splits.size()); - } - - @Test - public void testSplitForTopic() throws Exception { - JmsIO.Read read = JmsIO.read().withTopic(TOPIC); - PipelineOptions pipelineOptions = PipelineOptionsFactory.create(); - int desiredNumSplits = 5; - JmsIO.UnboundedJmsSource initialSource = new JmsIO.UnboundedJmsSource(read); - List splits = initialSource.split(desiredNumSplits, pipelineOptions); - // in the case of a topic, we can have only a unique subscriber on the topic per pipeline - // else it means we can have duplicate messages (all subscribers on the topic receive every - // message). - // So, whatever the desizedNumSplits is, the actual number of splits should be 1. - assertEquals(1, splits.size()); - } - private boolean advanceWithRetry(UnboundedSource.UnboundedReader reader) throws IOException { for (int attempt = 0; attempt < 10; attempt++) { if (reader.advance()) { @@ -610,6 +559,12 @@ public void testCheckpointMarkAndFinalizeSeparately() throws Exception { // get checkpoint mark after consumed 4 messages CheckpointMark mark = reader.getCheckpointMark(); + JmsCheckpointMark jmsMark = (JmsCheckpointMark) mark; + // In CLIENT_ACKNOWLEDGE mode, session/consumer are recreated on checkpoint: + assertNotNull(jmsMark.getConsumer()); + assertNotNull(jmsMark.getSession()); + assertNotNull(jmsMark.getMessages()); + assertEquals(1, jmsMark.getMessages().size()); // consume two more messages after checkpoint made reader.advance(); @@ -625,7 +580,62 @@ public void testCheckpointMarkAndFinalizeSeparately() throws Exception { assertEquals(7, count(QUEUE)); } + @Test + public void testCheckpointMarkAndFinalizeSeparatelyIndividualAcknowledge() throws Exception { + UnboundedJmsReader reader = setupReaderForTest(JmsIO.AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE); + + assertTrue(reader.start()); + assertTrue(advanceWithRetry(reader)); + assertTrue(advanceWithRetry(reader)); + + CheckpointMark mark = reader.getCheckpointMark(); + JmsCheckpointMark jmsMark = (JmsCheckpointMark) mark; + assertNull(jmsMark.getConsumer()); + assertNull(jmsMark.getSession()); + assertNotNull(jmsMark.getMessages()); + assertEquals(3, jmsMark.getMessages().size()); + + reader.advance(); + reader.advance(); + + assertEquals(10, count(QUEUE)); + mark.finalizeCheckpoint(); + + // Verify only checkpointed messages are acknowledged + assertEquals(7, count(QUEUE)); + } + + @Test + public void testCheckpointMarkAndFinalizeSeparatelyClientAcknowledgeUnsafe() throws Exception { + UnboundedJmsReader reader = setupReaderForTest(JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE_UNSAFE); + + assertTrue(reader.start()); + assertTrue(advanceWithRetry(reader)); + assertTrue(advanceWithRetry(reader)); + + CheckpointMark mark = reader.getCheckpointMark(); + JmsCheckpointMark jmsMark = (JmsCheckpointMark) mark; + assertNull(jmsMark.getConsumer()); + assertNull(jmsMark.getSession()); + assertNotNull(jmsMark.getMessages()); + assertEquals(1, jmsMark.getMessages().size()); + + reader.advance(); + reader.advance(); + + assertEquals(10, count(QUEUE)); + mark.finalizeCheckpoint(); + + // Verify all messages consumed on the session up to checkpoint are acknowledged + assertEquals(5, count(QUEUE)); + } + private JmsIO.UnboundedJmsReader setupReaderForTest() throws JMSException { + return setupReaderForTest(null); + } + + private JmsIO.UnboundedJmsReader setupReaderForTest( + JmsIO.@Nullable AcknowledgeMode acknowledgeMode) throws JMSException { // we are using no prefetch here // prefetch is an ActiveMQ feature: to make efficient use of network resources the broker // utilizes a 'push' model to dispatch messages to consumers. However, in the case of our @@ -652,6 +662,9 @@ private JmsIO.UnboundedJmsReader setupReaderForTest() throws JMSException { .withUsername(USERNAME) .withPassword(PASSWORD) .withQueue(QUEUE); + if (acknowledgeMode != null) { + spec = spec.withAcknowledgeMode(acknowledgeMode); + } JmsIO.UnboundedJmsSource source = new JmsIO.UnboundedJmsSource(spec); JmsIO.UnboundedJmsReader reader = source.createReader(PipelineOptionsFactory.create(), null); return reader; @@ -765,15 +778,6 @@ public void testCheckpointMarkSafety() throws Exception { runner.join(); } - /** Test the checkpoint mark default coder, which is actually AvroCoder. */ - @Test - public void testCheckpointMarkDefaultCoder() throws Exception { - JmsCheckpointMark jmsCheckpointMark = JmsCheckpointMark.newPreparer().newCheckpoint(null, null); - Coder coder = new JmsIO.UnboundedJmsSource(null).getCheckpointMarkCoder(); - CoderProperties.coderSerializable(coder); - CoderProperties.coderDecodeEncodeEqual(coder, jmsCheckpointMark); - } - @Test public void testDefaultAutoscaler() throws IOException { JmsIO.Read spec = @@ -818,50 +822,6 @@ public void testCustomAutoscaler() throws IOException { verify(autoScaler, times(1)).stop(); } - @Test - public void testCloseWithTimeout() throws IOException { - Duration closeTimeout = Duration.millis(2000L); - JmsIO.Read spec = - JmsIO.read() - .withConnectionFactory(connectionFactory) - .withUsername(USERNAME) - .withPassword(PASSWORD) - .withQueue(QUEUE) - .withCloseTimeout(closeTimeout); - - JmsIO.UnboundedJmsSource source = new JmsIO.UnboundedJmsSource(spec); - - ScheduledExecutorService mockScheduledExecutorService = - Mockito.mock(ScheduledExecutorService.class); - ExecutorOptions options = PipelineOptionsFactory.as(ExecutorOptions.class); - options.setScheduledExecutorService(mockScheduledExecutorService); - ArgumentCaptor runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); - when(mockScheduledExecutorService.schedule( - runnableArgumentCaptor.capture(), anyLong(), any(TimeUnit.class))) - .thenReturn(null /* unused */); - - JmsIO.UnboundedJmsReader reader = source.createReader(options, null); - reader.start(); - assertFalse(getDiscardedValue(reader)); - reader.close(); - assertFalse(getDiscardedValue(reader)); - verify(mockScheduledExecutorService) - .schedule(any(Runnable.class), eq(closeTimeout.getMillis()), eq(TimeUnit.MILLISECONDS)); - runnableArgumentCaptor.getValue().run(); - assertTrue(getDiscardedValue(reader)); - verifyNoMoreInteractions(mockScheduledExecutorService); - } - - private boolean getDiscardedValue(JmsIO.UnboundedJmsReader reader) { - JmsCheckpointMark.Preparer preparer = reader.checkpointMarkPreparer; - preparer.lock.readLock().lock(); - try { - return preparer.discarded; - } finally { - preparer.lock.readLock().unlock(); - } - } - @Test public void testDiscardCheckpointMark() throws Exception { Connection connection = @@ -982,7 +942,8 @@ public void testWriteMessageWithRetryPolicyReachesLimit() throws Exception { int maxPublicationAttempts = 2; List data = Collections.singletonList(messageText); RetryConfiguration retryConfiguration = - RetryConfiguration.create(maxPublicationAttempts, null, null); + RetryConfiguration.create( + maxPublicationAttempts, Duration.standardSeconds(5), Duration.millis(10L)); WriteJmsResult output = pipeline @@ -1039,7 +1000,8 @@ public void testWriteMessagesWithErrors() throws Exception { List data = Arrays.asList("Message 1", "Message 2", "Message 3", "Message 4"); RetryConfiguration retryConfiguration = - RetryConfiguration.create(maxPublicationAttempts, null, null); + RetryConfiguration.create( + maxPublicationAttempts, Duration.standardSeconds(5), Duration.millis(10L)); WriteJmsResult output = pipeline diff --git a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsLocalTest.java b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsLocalTest.java new file mode 100644 index 000000000000..4ea8f6d317a7 --- /dev/null +++ b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsLocalTest.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.jms; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.isA; +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 java.io.IOException; +import java.io.NotSerializableException; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.Queue; +import javax.jms.Session; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.options.ExecutorOptions; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.testing.CoderProperties; +import org.apache.beam.sdk.util.SerializableUtils; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Throwables; +import org.joda.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +/** Local unit tests for {@link JmsIO} that do not require an active JMS broker. */ +@RunWith(JUnit4.class) +public class JmsLocalTest { + + private static final String QUEUE = "queue"; + private static final String TOPIC = "topic"; + + @Test + public void testPipelineWithNonSerializableCF() { + SerializableUtils.ensureSerializable( + JmsIO.read() + .withConnectionFactoryProviderFn(__ -> new MockNonSerializableConnectionFactory())); + try { + SerializableUtils.ensureSerializable( + JmsIO.read().withConnectionFactory(new MockNonSerializableConnectionFactory())); + fail(); + } catch (Exception e) { + assertThat(Throwables.getRootCause(e), isA(NotSerializableException.class)); + } + } + + @Test + public void testSplitForQueue() throws Exception { + JmsIO.Read read = JmsIO.read().withQueue(QUEUE); + PipelineOptions pipelineOptions = PipelineOptionsFactory.create(); + int desiredNumSplits = 5; + JmsIO.UnboundedJmsSource initialSource = new JmsIO.UnboundedJmsSource<>(read); + List> splits = + initialSource.split(desiredNumSplits, pipelineOptions); + assertEquals(desiredNumSplits, splits.size()); + } + + @Test + public void testSplitForTopic() throws Exception { + JmsIO.Read read = JmsIO.read().withTopic(TOPIC); + PipelineOptions pipelineOptions = PipelineOptionsFactory.create(); + int desiredNumSplits = 5; + JmsIO.UnboundedJmsSource initialSource = new JmsIO.UnboundedJmsSource<>(read); + List> splits = + initialSource.split(desiredNumSplits, pipelineOptions); + assertEquals(1, splits.size()); + } + + @Test + public void testPublisherWithRetryConfiguration() { + RetryConfiguration retryPolicy = + RetryConfiguration.create(5, Duration.standardSeconds(15), null); + JmsIO.Write publisher = + JmsIO.write() + .withConnectionFactory(new ActiveMQConnectionFactory("vm://localhost")) + .withRetryConfiguration(retryPolicy) + .withQueue(QUEUE) + .withUsername("user") + .withPassword("password"); + assertEquals( + publisher.getRetryConfiguration(), + RetryConfiguration.create(5, Duration.standardSeconds(15), null)); + } + + @Test + public void testJmsCheckpointMarkIndividualAcknowledgeAllMessages() throws Exception { + Message msg1 = Mockito.mock(Message.class); + Message msg2 = Mockito.mock(Message.class); + Message msg3 = Mockito.mock(Message.class); + + JmsCheckpointMark.Preparer preparer = + JmsCheckpointMark.newPreparer(JmsIO.AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE); + preparer.add(msg1); + preparer.add(msg2); + preparer.add(msg3); + + AtomicInteger activeCheckpoints = new AtomicInteger(0); + JmsCheckpointMark mark = + preparer.newCheckpoint( + null, null, JmsIO.AcknowledgeMode.INDIVIDUAL_ACKNOWLEDGE, activeCheckpoints); + assertNotNull(mark.getMessages()); + assertEquals(3, mark.getMessages().size()); + assertNull(mark.getConsumer()); + assertNull(mark.getSession()); + assertEquals(1, activeCheckpoints.get()); + + mark.finalizeCheckpoint(); + + Mockito.verify(msg1, Mockito.times(1)).acknowledge(); + Mockito.verify(msg2, Mockito.times(1)).acknowledge(); + Mockito.verify(msg3, Mockito.times(1)).acknowledge(); + assertEquals(0, activeCheckpoints.get()); + } + + @Test + public void testJmsCheckpointMarkClientAcknowledgeUnsafeNoSessionRecreation() throws Exception { + Message msg1 = Mockito.mock(Message.class); + Message msg2 = Mockito.mock(Message.class); + + JmsCheckpointMark.Preparer preparer = + JmsCheckpointMark.newPreparer(JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE_UNSAFE); + preparer.add(msg1); + preparer.add(msg2); + + AtomicInteger activeCheckpoints = new AtomicInteger(0); + JmsCheckpointMark mark = + preparer.newCheckpoint( + null, null, JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE_UNSAFE, activeCheckpoints); + assertNotNull(mark.getMessages()); + assertEquals(1, mark.getMessages().size()); + assertNull(mark.getConsumer()); + assertNull(mark.getSession()); + assertEquals(1, activeCheckpoints.get()); + + mark.finalizeCheckpoint(); + + Mockito.verify(msg2, Mockito.times(1)).acknowledge(); + Mockito.verify(msg1, Mockito.never()).acknowledge(); + assertEquals(0, activeCheckpoints.get()); + } + + /** Test the checkpoint mark default coder, which is actually AvroCoder. */ + @Test + public void testCheckpointMarkDefaultCoder() throws Exception { + JmsCheckpointMark jmsCheckpointMark = + JmsCheckpointMark.newPreparer(JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE) + .newCheckpoint(null, null, JmsIO.AcknowledgeMode.CLIENT_ACKNOWLEDGE, null); + Coder coder = + new JmsIO.UnboundedJmsSource(null).getCheckpointMarkCoder(); + CoderProperties.coderSerializable(coder); + CoderProperties.coderDecodeEncodeEqual(coder, jmsCheckpointMark); + } + + @Test + public void testCloseWithTimeout() throws IOException, JMSException { + ConnectionFactory connectionFactory = Mockito.mock(ConnectionFactory.class); + Connection connection = Mockito.mock(Connection.class); + Session session = Mockito.mock(Session.class); + MessageConsumer consumer = Mockito.mock(MessageConsumer.class); + Queue queue = Mockito.mock(Queue.class); + + Mockito.when(connectionFactory.createConnection(Mockito.any(), Mockito.any())) + .thenReturn(connection); + Mockito.when(connection.createSession(Mockito.anyBoolean(), Mockito.anyInt())) + .thenReturn(session); + Mockito.when(session.createQueue(Mockito.anyString())).thenReturn(queue); + Mockito.when(session.createConsumer(Mockito.any())).thenReturn(consumer); + + Duration closeTimeout = Duration.millis(2000L); + JmsIO.Read spec = + JmsIO.read() + .withConnectionFactory(connectionFactory) + .withUsername("user") + .withPassword("password") + .withQueue(QUEUE) + .withCloseTimeout(closeTimeout); + + JmsIO.UnboundedJmsSource source = new JmsIO.UnboundedJmsSource<>(spec); + + ScheduledExecutorService mockScheduledExecutorService = + Mockito.mock(ScheduledExecutorService.class); + ExecutorOptions options = PipelineOptionsFactory.as(ExecutorOptions.class); + options.setScheduledExecutorService(mockScheduledExecutorService); + ArgumentCaptor runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + Mockito.when( + mockScheduledExecutorService.schedule( + runnableArgumentCaptor.capture(), Mockito.anyLong(), Mockito.any(TimeUnit.class))) + .thenReturn(null /* unused */); + + JmsIO.UnboundedJmsReader reader = source.createReader(options, null); + reader.start(); + assertFalse(getDiscardedValue(reader)); + reader.checkpointMarkPreparer.add(Mockito.mock(Message.class)); + org.apache.beam.sdk.io.UnboundedSource.CheckpointMark mark = reader.getCheckpointMark(); + reader.close(); + assertTrue(getDiscardedValue(reader)); + Mockito.verify(mockScheduledExecutorService) + .schedule(Mockito.any(Runnable.class), Mockito.eq(1L), Mockito.eq(TimeUnit.SECONDS)); + mark.finalizeCheckpoint(); + runnableArgumentCaptor.getValue().run(); + assertTrue(getDiscardedValue(reader)); + Mockito.verifyNoMoreInteractions(mockScheduledExecutorService); + } + + private boolean getDiscardedValue(JmsIO.UnboundedJmsReader reader) { + JmsCheckpointMark.Preparer preparer = reader.checkpointMarkPreparer; + preparer.lock.readLock().lock(); + try { + return preparer.discarded; + } finally { + preparer.lock.readLock().unlock(); + } + } +} diff --git a/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsSchemaTransformProviderTest.java b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsSchemaTransformProviderTest.java new file mode 100644 index 000000000000..da2d1b5cf93d --- /dev/null +++ b/sdks/java/io/jms/src/test/java/org/apache/beam/sdk/io/jms/JmsSchemaTransformProviderTest.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.jms; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.apache.beam.sdk.io.jms.JmsReadSchemaTransformProvider.ReadConfiguration; +import org.apache.beam.sdk.io.jms.JmsWriteSchemaTransformProvider.WriteConfiguration; +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.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; + +/** Tests for {@link JmsReadSchemaTransformProvider} and {@link JmsWriteSchemaTransformProvider}. */ +@RunWith(JUnit4.class) +public class JmsSchemaTransformProviderTest { + + @Rule + public final transient TestPipeline pipeline = + TestPipeline.create().enableAbandonedNodeEnforcement(false); + + @Test + public void testReadFindTransform() { + ServiceLoader serviceLoader = + ServiceLoader.load(SchemaTransformProvider.class); + List providers = + StreamSupport.stream(serviceLoader.spliterator(), false) + .filter(provider -> provider.getClass() == JmsReadSchemaTransformProvider.class) + .collect(Collectors.toList()); + SchemaTransformProvider jmsProvider = providers.get(0); + + assertEquals(Lists.newArrayList("output"), jmsProvider.outputCollectionNames()); + assertEquals(Lists.newArrayList(), jmsProvider.inputCollectionNames()); + assertEquals("beam:schematransform:org.apache.beam:jms_read:v1", jmsProvider.identifier()); + assertNotNull(jmsProvider.description()); + + assertEquals( + Sets.newHashSet( + "connection_configuration", + "queue", + "topic", + "max_num_records", + "max_read_time_seconds", + "close_timeout_seconds", + "acknowledge_mode", + "individual_acknowledge_mode_code"), + jmsProvider.configurationSchema().getFields().stream() + .map(Schema.Field::getName) + .collect(Collectors.toSet())); + } + + @Test + public void testReadBuildTransformWithQueue() { + ReadConfiguration readConfig = + ReadConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .setQueue("TEST_QUEUE") + .setMaxNumRecords(100L) + .setMaxReadTimeSeconds(5L) + .setCloseTimeoutSeconds(10L) + .setAcknowledgeMode("CLIENT_ACKNOWLEDGE") + .setIndividualAcknowledgeModeCode(101) + .build(); + + SchemaTransform transform = new JmsReadSchemaTransformProvider().from(readConfig); + PCollectionRowTuple output = transform.expand(PCollectionRowTuple.empty(pipeline)); + + assertEquals(1, output.getAll().size()); + assertTrue(output.has("output")); + assertEquals( + Schema.builder().addStringField("payload").build(), output.get("output").getSchema()); + } + + @Test + public void testReadBuildTransformWithTopic() { + ReadConfiguration readConfig = + ReadConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .setTopic("TEST_TOPIC") + .build(); + + SchemaTransform transform = new JmsReadSchemaTransformProvider().from(readConfig); + PCollectionRowTuple output = transform.expand(PCollectionRowTuple.empty(pipeline)); + + assertEquals(1, output.getAll().size()); + assertTrue(output.has("output")); + assertEquals( + Schema.builder().addStringField("payload").build(), output.get("output").getSchema()); + } + + @Test + public void testReadInvalidConfigurations() { + ReadConfiguration bothConfig = + ReadConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .setQueue("TEST_QUEUE") + .setTopic("TEST_TOPIC") + .build(); + SchemaTransform bothTransform = new JmsReadSchemaTransformProvider().from(bothConfig); + assertThrows( + IllegalArgumentException.class, + () -> bothTransform.expand(PCollectionRowTuple.empty(pipeline))); + + ReadConfiguration neitherConfig = + ReadConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .build(); + SchemaTransform neitherTransform = new JmsReadSchemaTransformProvider().from(neitherConfig); + assertThrows( + IllegalArgumentException.class, + () -> neitherTransform.expand(PCollectionRowTuple.empty(pipeline))); + } + + @Test + public void testReadWithNonEmptyInputThrows() { + ReadConfiguration readConfig = + ReadConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .setQueue("TEST_QUEUE") + .build(); + SchemaTransform transform = new JmsReadSchemaTransformProvider().from(readConfig); + + PCollection dummyInput = + pipeline.apply( + "CreateDummy", Create.empty(Schema.builder().addStringField("dummy").build())); + assertThrows( + IllegalStateException.class, + () -> transform.expand(PCollectionRowTuple.of("input", dummyInput))); + } + + @Test + public void testWriteFindTransform() { + ServiceLoader serviceLoader = + ServiceLoader.load(SchemaTransformProvider.class); + List providers = + StreamSupport.stream(serviceLoader.spliterator(), false) + .filter(provider -> provider.getClass() == JmsWriteSchemaTransformProvider.class) + .collect(Collectors.toList()); + SchemaTransformProvider jmsProvider = providers.get(0); + + assertEquals(Lists.newArrayList(), jmsProvider.outputCollectionNames()); + assertEquals(Lists.newArrayList("input"), jmsProvider.inputCollectionNames()); + assertEquals("beam:schematransform:org.apache.beam:jms_write:v1", jmsProvider.identifier()); + assertNotNull(jmsProvider.description()); + + assertEquals( + Sets.newHashSet("connection_configuration", "queue", "topic"), + jmsProvider.configurationSchema().getFields().stream() + .map(Schema.Field::getName) + .collect(Collectors.toSet())); + } + + @Test + public void testWriteBuildTransformWithQueueAndTopic() { + WriteConfiguration queueConfig = + WriteConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .setQueue("TEST_QUEUE") + .build(); + SchemaTransform queueTransform = new JmsWriteSchemaTransformProvider().from(queueConfig); + Schema schema = Schema.builder().addStringField("payload").build(); + PCollection inputRows = pipeline.apply("CreateQueueRows", Create.empty(schema)); + PCollectionRowTuple output = queueTransform.expand(PCollectionRowTuple.of("input", inputRows)); + assertTrue(output.getAll().isEmpty()); + + WriteConfiguration topicConfig = + WriteConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .setTopic("TEST_TOPIC") + .build(); + SchemaTransform topicTransform = new JmsWriteSchemaTransformProvider().from(topicConfig); + Schema bytesSchema = Schema.builder().addByteArrayField("bytes").build(); + PCollection bytesRows = pipeline.apply("CreateTopicRows", Create.empty(bytesSchema)); + PCollectionRowTuple topicOutput = + topicTransform.expand(PCollectionRowTuple.of("input", bytesRows)); + assertTrue(topicOutput.getAll().isEmpty()); + } + + @Test + public void testWriteInvalidConfigurations() { + WriteConfiguration bothConfig = + WriteConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .setQueue("TEST_QUEUE") + .setTopic("TEST_TOPIC") + .build(); + SchemaTransform bothTransform = new JmsWriteSchemaTransformProvider().from(bothConfig); + Schema schema = Schema.builder().addStringField("payload").build(); + PCollection inputRows = pipeline.apply("CreateBothRows", Create.empty(schema)); + assertThrows( + IllegalArgumentException.class, + () -> bothTransform.expand(PCollectionRowTuple.of("input", inputRows))); + + WriteConfiguration neitherConfig = + WriteConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .build(); + SchemaTransform neitherTransform = new JmsWriteSchemaTransformProvider().from(neitherConfig); + PCollection inputRows2 = pipeline.apply("CreateNeitherRows", Create.empty(schema)); + assertThrows( + IllegalArgumentException.class, + () -> neitherTransform.expand(PCollectionRowTuple.of("input", inputRows2))); + } + + @Test + public void testWriteInvalidInputSchema() { + WriteConfiguration config = + WriteConfiguration.builder() + .setConnectionConfiguration(ConnectionConfiguration.create("tcp://localhost:61616")) + .setQueue("TEST_QUEUE") + .build(); + SchemaTransform transform = new JmsWriteSchemaTransformProvider().from(config); + + Schema invalidSchema = Schema.builder().addInt32Field("id").addStringField("name").build(); + PCollection inputRows = + pipeline.apply("CreateInvalidSchemaRows", Create.empty(invalidSchema)); + assertThrows( + IllegalStateException.class, + () -> transform.expand(PCollectionRowTuple.of("input", inputRows))); + } +} diff --git a/sdks/java/io/kafka/build.gradle b/sdks/java/io/kafka/build.gradle index ba25078b64e3..80d2fe43099b 100644 --- a/sdks/java/io/kafka/build.gradle +++ b/sdks/java/io/kafka/build.gradle @@ -36,18 +36,13 @@ ext { } def kafkaVersions = [ - '201': "2.0.1", - '231': "2.3.1", - '241': "2.4.1", - '251': "2.5.1", - '282': "2.8.2", - '312': "3.1.2", - '390': "3.9.0", + '392': "3.9.2", ] kafkaVersions.each{k,v -> configurations.create("kafkaVersion$k")} dependencies { + implementation platform(library.java.opentelemetry_bom) implementation library.java.vendored_guava_32_1_2_jre provided library.java.jackson_dataformat_csv permitUnusedDeclared library.java.jackson_dataformat_csv @@ -63,10 +58,13 @@ dependencies { if (JavaVersion.current().compareTo(JavaVersion.VERSION_21) >= 0) { // this dependency is a provided dependency for kafka-avro-serializer. It is not needed to compile with Java<=17 // but needed for compile only under Java21, specifically, required for extending from AbstractKafkaAvroDeserializer - compileOnly library.java.kafka + compileOnly library.java.kafka_scala_2_12 + permitUnusedDeclared library.java.kafka_scala_2_12 } testImplementation library.java.kafka_clients testImplementation project(path: ":runners:core-java") + implementation library.java.opentelemetry_api + implementation library.java.opentelemetry_context implementation library.java.slf4j_api implementation library.java.joda_time implementation library.java.jackson_annotations diff --git a/sdks/java/io/kafka/kafka-201/build.gradle b/sdks/java/io/kafka/kafka-201/build.gradle deleted file mode 100644 index a26ca4ac19cf..000000000000 --- a/sdks/java/io/kafka/kafka-201/build.gradle +++ /dev/null @@ -1,24 +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 { - delimited="2.0.1" - undelimited="201" - sdfCompatible=true -} - -apply from: "../kafka-integration-test.gradle" \ No newline at end of file diff --git a/sdks/java/io/kafka/kafka-231/build.gradle b/sdks/java/io/kafka/kafka-231/build.gradle deleted file mode 100644 index 712158dcd3ae..000000000000 --- a/sdks/java/io/kafka/kafka-231/build.gradle +++ /dev/null @@ -1,24 +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 { - delimited="2.3.1" - undelimited="231" - sdfCompatible=true -} - -apply from: "../kafka-integration-test.gradle" \ No newline at end of file diff --git a/sdks/java/io/kafka/kafka-251/build.gradle b/sdks/java/io/kafka/kafka-251/build.gradle deleted file mode 100644 index 4de9f97a738a..000000000000 --- a/sdks/java/io/kafka/kafka-251/build.gradle +++ /dev/null @@ -1,24 +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 { - delimited="2.5.1" - undelimited="251" - sdfCompatible=true -} - -apply from: "../kafka-integration-test.gradle" \ No newline at end of file diff --git a/sdks/java/io/kafka/kafka-282/build.gradle b/sdks/java/io/kafka/kafka-282/build.gradle deleted file mode 100644 index b754d93077e5..000000000000 --- a/sdks/java/io/kafka/kafka-282/build.gradle +++ /dev/null @@ -1,24 +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 { - delimited="2.8.2" - undelimited="282" - sdfCompatible=true -} - -apply from: "../kafka-integration-test.gradle" \ No newline at end of file diff --git a/sdks/java/io/kafka/kafka-312/build.gradle b/sdks/java/io/kafka/kafka-312/build.gradle deleted file mode 100644 index af2ad3717b65..000000000000 --- a/sdks/java/io/kafka/kafka-312/build.gradle +++ /dev/null @@ -1,24 +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 { - delimited="3.1.2" - undelimited="312" - sdfCompatible=true -} - -apply from: "../kafka-integration-test.gradle" \ No newline at end of file diff --git a/sdks/java/io/kafka/kafka-390/build.gradle b/sdks/java/io/kafka/kafka-390/build.gradle deleted file mode 100644 index 8c8821386265..000000000000 --- a/sdks/java/io/kafka/kafka-390/build.gradle +++ /dev/null @@ -1,24 +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 { - delimited="3.9.0" - undelimited="390" - sdfCompatible=true -} - -apply from: "../kafka-integration-test.gradle" \ No newline at end of file diff --git a/sdks/java/io/kafka/kafka-241/build.gradle b/sdks/java/io/kafka/kafka-392/build.gradle similarity index 90% rename from sdks/java/io/kafka/kafka-241/build.gradle rename to sdks/java/io/kafka/kafka-392/build.gradle index c0ac7df674b5..3df1ebeb69ad 100644 --- a/sdks/java/io/kafka/kafka-241/build.gradle +++ b/sdks/java/io/kafka/kafka-392/build.gradle @@ -16,9 +16,9 @@ * limitations under the License. */ project.ext { - delimited="2.4.1" - undelimited="241" + delimited="3.9.2" + undelimited="392" sdfCompatible=true } -apply from: "../kafka-integration-test.gradle" \ No newline at end of file +apply from: "../kafka-integration-test.gradle" diff --git a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaCommitOffset.java b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaCommitOffset.java index ac6650c354d4..281a5b9a47c6 100644 --- a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaCommitOffset.java +++ b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaCommitOffset.java @@ -25,6 +25,8 @@ import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.VarLongCoder; import org.apache.beam.sdk.coders.VoidCoder; +import org.apache.beam.sdk.metrics.Counter; +import org.apache.beam.sdk.metrics.Metrics; import org.apache.beam.sdk.schemas.NoSuchSchemaException; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.MapElements; @@ -62,6 +64,9 @@ public class KafkaCommitOffset static class CommitOffsetDoFn extends DoFn, Void> { private static final Logger LOG = LoggerFactory.getLogger(CommitOffsetDoFn.class); + private static final Counter COMMIT_FAILURES = + Metrics.counter(CommitOffsetDoFn.class, "commit-failures"); + private final Map consumerConfig; private final SerializableFunction, Consumer> consumerFactoryFn; @@ -85,6 +90,7 @@ public void processElement(@Element KV element) { element.getKey().getTopicPartition(), new OffsetAndMetadata(element.getValue() + 1))); } catch (Exception e) { + COMMIT_FAILURES.inc(); // TODO: consider retrying. LOG.warn("Getting exception when committing offset: {}", e.getMessage()); } 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..33fdc76ca58b 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 @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.io.kafka; +import static java.nio.charset.StandardCharsets.UTF_8; 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 static org.apache.kafka.clients.consumer.ConsumerConfig.AUTO_OFFSET_RESET_CONFIG; @@ -25,6 +26,13 @@ import com.google.auto.value.AutoValue; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.confluent.kafka.serializers.KafkaAvroDeserializer; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; @@ -40,6 +48,7 @@ import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.sdk.coders.AtomicCoder; import org.apache.beam.sdk.coders.ByteArrayCoder; @@ -61,6 +70,7 @@ import org.apache.beam.sdk.options.Default; import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.SdkHarnessOptions; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.options.ValueProvider; import org.apache.beam.sdk.runners.AppliedPTransform; @@ -125,6 +135,7 @@ import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.config.SaslConfigs; import org.apache.kafka.common.header.Header; +import org.apache.kafka.common.header.Headers; import org.apache.kafka.common.header.internals.RecordHeader; import org.apache.kafka.common.serialization.ByteArrayDeserializer; import org.apache.kafka.common.serialization.Deserializer; @@ -614,6 +625,7 @@ public static Read read() { .setTimestampPolicyFactory(TimestampPolicyFactory.withProcessingTime()) .setConsumerPollingTimeout(2L) .setRedistributed(false) + .setEnableOpenTelemetryTracing(false) .setAllowDuplicates(false) .setRedistributeNumKeys(0) .build(); @@ -653,6 +665,7 @@ public static WriteRecords writeRecords() { .setEosTriggerNumElements(1) // keep default numElements .setEosTriggerTimeout(null) // keep default trigger (timeout) .setNumShards(0) + .setEnableOpenTelemetryTracing(false) .setConsumerFactoryFn(KafkaIOUtils.KAFKA_CONSUMER_FACTORY_FN) .setBadRecordRouter(BadRecordRouter.THROWING_ROUTER) .setBadRecordErrorHandler(new DefaultErrorHandler<>()) @@ -742,6 +755,9 @@ public abstract static class Read @Pure public abstract @Nullable Duration getWatchTopicPartitionDuration(); + @Pure + public abstract boolean isEnableOpenTelemetryTracing(); + @Pure public abstract TimestampPolicyFactory getTimestampPolicyFactory(); @@ -832,6 +848,8 @@ Builder setCheckStopReadingFn( return setCheckStopReadingFn(CheckStopReadingFnWrapper.of(checkStopReadingFn)); } + abstract Builder setEnableOpenTelemetryTracing(boolean enableOpenTelemetryTracing); + abstract Builder setConsumerPollingTimeout(long consumerPollingTimeout); abstract Builder setLogTopicVerification(@Nullable Boolean logTopicVerification); @@ -865,6 +883,7 @@ static void setupExternalBuilder( // Set required defaults builder.setTopicPartitions(Collections.emptyList()); + builder.setEnableOpenTelemetryTracing(false); builder.setConsumerFactoryFn(KafkaIOUtils.KAFKA_CONSUMER_FACTORY_FN); if (config.maxReadTime != null) { builder.setMaxReadTime(Duration.standardSeconds(config.maxReadTime)); @@ -1302,6 +1321,10 @@ public Read withValueDeserializer(DeserializerProvider deserializerProv return toBuilder().setValueDeserializerProvider(deserializerProvider).build(); } + public Read withEnableOpenTelemetryTracing() { + return toBuilder().setEnableOpenTelemetryTracing(true).build(); + } + public Read withValueDeserializerProviderAndCoder( DeserializerProvider deserializerProvider, Coder valueCoder) { return toBuilder() @@ -1628,18 +1651,13 @@ public PCollection> expand(PBegin input) { checkArgument( getConsumerConfig().get(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG) != null, "withBootstrapServers() is required"); - // With dynamic read, we no longer require providing topic/partition during pipeline - // construction time. But it requires enabling beam_fn_api. + // With dynamic read, topics and partitions are discovered during pipeline execution. if (!isDynamicRead()) { checkArgument( (getTopics() != null && getTopics().size() > 0) || (getTopicPartitions() != null && getTopicPartitions().size() > 0) || getTopicPattern() != null, "Either withTopic(), withTopics(), withTopicPartitions() or withTopicPattern() is required"); - } else { - checkArgument( - ExperimentalOptions.hasExperiment(input.getPipeline().getOptions(), "beam_fn_api"), - "Kafka Dynamic Read requires enabling experiment beam_fn_api."); } checkArgument(getKeyDeserializerProvider() != null, "withKeyDeserializer() is required"); checkArgument(getValueDeserializerProvider() != null, "withValueDeserializer() is required"); @@ -1920,6 +1938,14 @@ public PCollection> expand(PBegin input) { .withMaxNumRecords(kafkaRead.getMaxNumRecords()); } PCollection> output = input.getPipeline().apply(transform); + + if (kafkaRead.isEnableOpenTelemetryTracing()) { + output = + output.apply( + "Extract OpenTelemetry context from Header", + ParDo.of(new OpenTelemetryHeaderConsumer<>())); + } + if (kafkaRead.getOffsetDeduplication() != null && kafkaRead.getOffsetDeduplication()) { output = output.apply( @@ -2041,9 +2067,15 @@ public PCollection> expand(PBegin input) { .apply(ParDo.of(new GenerateKafkaSourceDescriptor(kafkaRead))); } } + PCollection> pcol = + output.apply(readTransform).setCoder(KafkaRecordCoder.of(keyCoder, valueCoder)); + if (kafkaRead.isEnableOpenTelemetryTracing()) { + pcol = + pcol.apply( + "Extract OpenTelemetry context from Header", + ParDo.of(new OpenTelemetryHeaderConsumer<>())); + } if (kafkaRead.isRedistributed()) { - PCollection> pcol = - output.apply(readTransform).setCoder(KafkaRecordCoder.of(keyCoder, valueCoder)); if (kafkaRead.getRedistributeNumKeys() == 0) { return pcol.apply( "Insert Redistribute", @@ -2057,7 +2089,7 @@ public PCollection> expand(PBegin input) { .withNumBuckets((int) kafkaRead.getRedistributeNumKeys())); } } - return output.apply(readTransform).setCoder(KafkaRecordCoder.of(keyCoder, valueCoder)); + return pcol; } } @@ -2218,6 +2250,101 @@ public void populateDisplayData(DisplayData.Builder builder) { } } + static class OpenTelemetryHeaderConsumer + extends DoFn, KafkaRecord> { + @Nullable Tracer tracer = null; + + @Setup + public void setup(PipelineOptions options) { + // inject tracer via options + io.opentelemetry.api.OpenTelemetry openTelemetry = + options.as(SdkHarnessOptions.class).getOpenTelemetry(); + if (openTelemetry != null) { + tracer = openTelemetry.getTracer("KafkaIO"); + } + } + + Context extractSpanContext(KafkaRecord message) { + TextMapGetter> extractMessageAttributes = + new TextMapGetter>() { + + @Override + public @Nullable String get(@Nullable KafkaRecord carrier, String key) { + if (carrier == null) { + return null; + } + Headers headers = carrier.getHeaders(); + if (headers == null) { + return null; + } + Header header = headers.lastHeader(key); + if (header == null) { + return null; + } + return new String(header.value(), UTF_8); + } + + @Override + public Iterable keys(@Nullable KafkaRecord carrier) { + if (carrier == null || carrier.getHeaders() == null) { + return ImmutableList.of(); + } + return StreamSupport.stream(carrier.getHeaders().spliterator(), false) + .map(Header::key) + .collect(Collectors.toList()); + } + }; + return W3CTraceContextPropagator.getInstance() + .extract(Context.current(), message, extractMessageAttributes); + } + + @ProcessElement + public void processElement( + @Element KafkaRecord element, OutputReceiver> receiver) { + Context context = extractSpanContext(element); + Span span = + Preconditions.checkArgumentNotNull(tracer) + .spanBuilder("KafkaIO.Read") + .setParent(context) + .startSpan(); + try (Scope ignored = span.makeCurrent()) { + receiver.output(element); + } finally { + span.end(); + } + } + } + + static class OpenTelemetryHeaderPropagator + extends DoFn, ProducerRecord> { + ProducerRecord injectTraceContext(ProducerRecord message) { + org.apache.kafka.common.header.internals.RecordHeaders headers = + new org.apache.kafka.common.header.internals.RecordHeaders(message.headers()); + TextMapSetter + injectMessageAttributes = + (carrier, key, value) -> { + if (carrier != null) { + carrier.add(key, value.getBytes(UTF_8)); + } + }; + W3CTraceContextPropagator.getInstance() + .inject(Context.current(), headers, injectMessageAttributes); + return new ProducerRecord<>( + message.topic(), + message.partition(), + message.timestamp(), + message.key(), + message.value(), + headers); + } + + @ProcessElement + public void processElement( + @Element ProducerRecord element, OutputReceiver> receiver) { + receiver.output(injectTraceContext(element)); + } + } + /** * A {@link PTransform} to read from Kafka topics. Similar to {@link KafkaIO.Read}, but removes * Kafka metatdata and returns a {@link PCollection} of {@link KV}. See {@link KafkaIO} for more @@ -3162,6 +3289,8 @@ public abstract static class WriteRecords // we shouldn't have to duplicate the same API for similar transforms like {@link Write} and // {@link WriteRecords}. See example at {@link PubsubIO.Write}. + public abstract boolean isEnableOpenTelemetryTracing(); + @Pure public abstract @Nullable String getTopic(); @@ -3212,6 +3341,8 @@ public abstract static class WriteRecords abstract static class Builder { abstract Builder setTopic(String topic); + abstract Builder setEnableOpenTelemetryTracing(boolean enableOpenTelemetryTracing); + abstract Builder setProducerConfig(Map producerConfig); abstract Builder setProducerFactoryFn( @@ -3277,6 +3408,10 @@ public WriteRecords withValueSerializer(Class> val return toBuilder().setValueSerializer(valueSerializer).build(); } + public WriteRecords withEnableOpenTelemetryTracing() { + return toBuilder().setEnableOpenTelemetryTracing(true).build(); + } + /** * Adds the given producer properties, overriding old values of properties with the same key. * @@ -3413,7 +3548,11 @@ public PDone expand(PCollection> input) { checkArgument(getKeySerializer() != null, "withKeySerializer() is required"); checkArgument(getValueSerializer() != null, "withValueSerializer() is required"); - + if (this.isEnableOpenTelemetryTracing()) { + input = + input.apply( + "Propagate OpenTelemetry Tracing", ParDo.of(new OpenTelemetryHeaderPropagator<>())); + } if (isEOS()) { checkArgument(getTopic() != null, "withTopic() is required when isEOS() is true"); checkArgument( @@ -3653,6 +3792,10 @@ public Write withInputTimestamp() { return withWriteRecordsTransform(getWriteRecordsTransform().withInputTimestamp()); } + public Write withEnableOpenTelemetryTracing() { + return withWriteRecordsTransform(getWriteRecordsTransform().withEnableOpenTelemetryTracing()); + } + /** * Wrapper method over {@link * WriteRecords#withPublishTimestampFunction(KafkaPublishTimestampFunction)}, used to keep the diff --git a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIOReadImplementationCompatibility.java b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIOReadImplementationCompatibility.java index 95709135d809..053f45c846aa 100644 --- a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIOReadImplementationCompatibility.java +++ b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaIOReadImplementationCompatibility.java @@ -139,6 +139,12 @@ Object getDefaultValue() { }, OFFSET_DEDUPLICATION(LEGACY), LOG_TOPIC_VERIFICATION, + ENABLE_OPEN_TELEMETRY_TRACING { + @Override + Object getDefaultValue() { + return false; + } + }, REDISTRIBUTE_BY_RECORD_KEY { @Override Object getDefaultValue() { diff --git a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaUnboundedReader.java b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaUnboundedReader.java index c5dc5c408fe2..c3aa6f5418bc 100644 --- a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaUnboundedReader.java +++ b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/KafkaUnboundedReader.java @@ -30,6 +30,7 @@ import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -114,17 +115,22 @@ public boolean start() throws IOException { try { Duration timeout = resolveDefaultApiTimeout(spec); future.get(timeout.getMillis(), TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - consumer.wakeup(); // This unblocks consumer stuck on network I/O. - // Likely reason : Kafka servers are configured to advertise internal ips, but - // those ips are not accessible from workers outside. - String msg = - String.format( - "%s: Timeout while initializing partition '%s'. " - + "Kafka client may not be able to connect to servers.", - this, pState.topicPartition); - LOG.error("{}", msg); - throw new IOException(msg); + } catch (TimeoutException | ExecutionException e) { + if (e instanceof TimeoutException + || e.getCause() instanceof org.apache.kafka.common.errors.TimeoutException) { + // TODO: Find out if manually waking up was only relevant for legacy Kafka clients. + consumer.wakeup(); // This unblocks consumer stuck on network I/O. + // Likely reason : Kafka servers are configured to advertise internal ips, but + // those ips are not accessible from workers outside. + String msg = + String.format( + "%s: Timeout while initializing partition '%s'. " + + "Kafka client may not be able to connect to servers.", + this, pState.topicPartition); + LOG.error("{}", msg); + throw new IOException(msg); + } + throw new IOException(e); } catch (Exception e) { throw new IOException(e); } diff --git a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/ReadFromKafkaDoFn.java b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/ReadFromKafkaDoFn.java index 27af3b67930f..684534434751 100644 --- a/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/ReadFromKafkaDoFn.java +++ b/sdks/java/io/kafka/src/main/java/org/apache/beam/sdk/io/kafka/ReadFromKafkaDoFn.java @@ -19,7 +19,6 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; -import java.io.Closeable; import java.math.BigDecimal; import java.math.MathContext; import java.time.Duration; @@ -27,7 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicLong; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.io.kafka.KafkaIO.ReadSourceDescriptors; import org.apache.beam.sdk.io.kafka.KafkaIOUtils.MovingAvg; @@ -49,7 +48,6 @@ import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimator; import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimators.MonotonicallyIncreasing; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.util.ExpiringMemoizingSerializableSupplier; import org.apache.beam.sdk.util.MemoizingPerInstantiationSerializableSupplier; import org.apache.beam.sdk.util.Preconditions; import org.apache.beam.sdk.util.SerializableSupplier; @@ -71,6 +69,8 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.InvalidOffsetException; +import org.apache.kafka.common.KafkaException; import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.config.ConfigDef; @@ -235,30 +235,12 @@ public MovingAvg load(KafkaSourceDescriptor kafkaSourceDescriptor) CacheBuilder.newBuilder() .concurrencyLevel(Runtime.getRuntime().availableProcessors()) .weakValues() - .removalListener( - (RemovalNotification - notification) -> { - final @Nullable KafkaLatestOffsetEstimator value; - if (notification.getCause() == RemovalCause.COLLECTED - && (value = notification.getValue()) != null) { - value.close(); - } - }) .build( - new CacheLoader() { + new CacheLoader() { @Override - public KafkaLatestOffsetEstimator load( - final KafkaSourceDescriptor sourceDescriptor) { - LOG.info( - "Creating Kafka consumer for offset estimation for {}", - sourceDescriptor); - final Map config = - KafkaIOUtils.overrideBootstrapServersConfig( - consumerConfig, sourceDescriptor); - final Consumer consumer = - consumerFactoryFn.apply(config); - return new KafkaLatestOffsetEstimator( - consumer, sourceDescriptor.getTopicPartition()); + public AtomicLong load(final KafkaSourceDescriptor sourceDescriptor) { + LOG.info("Creating end offset estimator for {}", sourceDescriptor); + return new AtomicLong(Long.MIN_VALUE); } })); this.pollConsumerCacheSupplier = @@ -319,8 +301,7 @@ public Consumer load( private final SerializableSupplier> avgRecordSizeCacheSupplier; - private final SerializableSupplier< - LoadingCache> + private final SerializableSupplier> latestOffsetEstimatorCacheSupplier; private final SerializableSupplier>> @@ -329,8 +310,7 @@ public Consumer load( private transient @MonotonicNonNull LoadingCache avgRecordSizeCache; - private transient @MonotonicNonNull LoadingCache< - KafkaSourceDescriptor, KafkaLatestOffsetEstimator> + private transient @MonotonicNonNull LoadingCache latestOffsetEstimatorCache; private transient @MonotonicNonNull LoadingCache> @@ -349,46 +329,6 @@ public Consumer load( @VisibleForTesting static final String RAW_SIZE_METRIC_PREFIX = KafkaUnboundedReader.RAW_SIZE_METRIC_PREFIX; - /** - * A {@link GrowableOffsetRangeTracker.RangeEndEstimator} which uses a Kafka {@link Consumer} to - * fetch backlog. - */ - private static class KafkaLatestOffsetEstimator - implements GrowableOffsetRangeTracker.RangeEndEstimator, Closeable { - private final Consumer offsetConsumer; - private final Supplier offsetSupplier; - - KafkaLatestOffsetEstimator( - final Consumer offsetConsumer, final TopicPartition topicPartition) { - this.offsetConsumer = offsetConsumer; - this.offsetSupplier = - new ExpiringMemoizingSerializableSupplier<>( - () -> { - try { - return offsetConsumer - .endOffsets(Collections.singleton(topicPartition)) - .getOrDefault(topicPartition, Long.MIN_VALUE); - } catch (Throwable t) { - LOG.error("Failed to get end offset for {}", topicPartition, t); - return Long.MIN_VALUE; - } - }, - Duration.ofSeconds(1), - Long.MIN_VALUE, - Duration.ZERO); - } - - @Override - public long estimate() { - return offsetSupplier.get(); - } - - @Override - public void close() { - offsetConsumer.close(); - } - } - @GetInitialRestriction @RequiresNonNull({"pollConsumerCache"}) public OffsetRange initialRestriction(@Element KafkaSourceDescriptor kafkaSourceDescriptor) { @@ -500,8 +440,8 @@ public double getSize( @RequiresNonNull({"latestOffsetEstimatorCache"}) public UnsplittableRestrictionTracker restrictionTracker( @Element KafkaSourceDescriptor kafkaSourceDescriptor, @Restriction OffsetRange restriction) { - final LoadingCache - latestOffsetEstimatorCache = this.latestOffsetEstimatorCache; + final LoadingCache latestOffsetEstimatorCache = + this.latestOffsetEstimatorCache; if (restriction.getTo() < Long.MAX_VALUE) { return new UnsplittableRestrictionTracker<>(new OffsetRangeTracker(restriction)); @@ -510,9 +450,10 @@ public UnsplittableRestrictionTracker restrictionTracker( // OffsetEstimators are cached for each topic-partition because they hold a stateful connection, // so we want to minimize the amount of connections that we start and track with Kafka. Another // point is that it has a memoized backlog, and this should make that more reusable estimations. + final AtomicLong latestOffsetEstimator = + latestOffsetEstimatorCache.getUnchecked(kafkaSourceDescriptor); return new UnsplittableRestrictionTracker<>( - new GrowableOffsetRangeTracker( - restriction.getFrom(), latestOffsetEstimatorCache.getUnchecked(kafkaSourceDescriptor))); + new GrowableOffsetRangeTracker(restriction.getFrom(), latestOffsetEstimator::get)); } @ProcessElement @@ -525,14 +466,13 @@ public ProcessContinuation processElement( throws Exception { final LoadingCache avgRecordSizeCache = this.avgRecordSizeCache; - final LoadingCache - latestOffsetEstimatorCache = this.latestOffsetEstimatorCache; + final LoadingCache latestOffsetEstimatorCache = + this.latestOffsetEstimatorCache; final LoadingCache> pollConsumerCache = this.pollConsumerCache; final MovingAvg avgRecordSize = avgRecordSizeCache.get(kafkaSourceDescriptor); - final KafkaLatestOffsetEstimator latestOffsetEstimator = - latestOffsetEstimatorCache.get(kafkaSourceDescriptor); + final AtomicLong latestOffsetEstimator = latestOffsetEstimatorCache.get(kafkaSourceDescriptor); final Consumer consumer = pollConsumerCache.get(kafkaSourceDescriptor); final Deserializer keyDeserializerInstance = Preconditions.checkStateNotNull(this.keyDeserializerInstance); @@ -580,6 +520,20 @@ public ProcessContinuation processElement( // Fetch the next records. final ConsumerRecords rawRecords = consumer.poll(remainingTimeout); final Duration elapsed = pollTimer.elapsed(); + try { + final long position = consumer.position(topicPartition); + consumer + .currentLag(topicPartition) + .ifPresent(lag -> latestOffsetEstimator.lazySet(position + lag)); + } catch (InvalidOffsetException e) { + // The position is undefined or out of range. + latestOffsetEstimator.lazySet(Long.MIN_VALUE); + } catch (KafkaException e) { + // Wakeups, interrupts, authentication failures, authorization failures, timeouts and + // unrecoverable errors can be ignored. This routine is reattempted on every iteration + // and fallback methods would likely trigger the same unrecoverable errors. + } + try { remainingTimeout = remainingTimeout.minus(elapsed); } catch (ArithmeticException e) { @@ -687,7 +641,7 @@ public ProcessContinuation processElement( final long estimatedBacklogBytes = (long) - (BigDecimal.valueOf(latestOffsetEstimator.estimate()) + (BigDecimal.valueOf(Math.max(expectedOffset, latestOffsetEstimator.get())) .subtract(BigDecimal.valueOf(expectedOffset), MathContext.DECIMAL128) .doubleValue() * avgRecordSize.get()); @@ -752,8 +706,8 @@ public void setup() throws Exception { public void teardown() throws Exception { final LoadingCache avgRecordSizeCache = this.avgRecordSizeCache; - final LoadingCache - latestOffsetEstimatorCache = this.latestOffsetEstimatorCache; + final LoadingCache latestOffsetEstimatorCache = + this.latestOffsetEstimatorCache; final LoadingCache> pollConsumerCache = this.pollConsumerCache; diff --git a/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaCommitOffsetTest.java b/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaCommitOffsetTest.java index c16e25510ab8..b64f5edabe76 100644 --- a/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaCommitOffsetTest.java +++ b/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaCommitOffsetTest.java @@ -17,11 +17,11 @@ */ package org.apache.beam.sdk.io.kafka; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import org.apache.beam.sdk.coders.CannotProvideCoderException; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.StringUtf8Coder; @@ -236,7 +236,7 @@ public synchronized void commitSync(Map offse } @Override - public synchronized void close(long timeout, TimeUnit unit) { + public synchronized void close(Duration timeout) { // Ignore closing since we're using a single consumer. } } diff --git a/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaIOReadImplementationCompatibilityTest.java b/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaIOReadImplementationCompatibilityTest.java index 33870794e776..55ae55ceca84 100644 --- a/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaIOReadImplementationCompatibilityTest.java +++ b/sdks/java/io/kafka/src/test/java/org/apache/beam/sdk/io/kafka/KafkaIOReadImplementationCompatibilityTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.io.kafka; +import static org.apache.beam.sdk.io.kafka.KafkaIOReadImplementationCompatibility.KafkaIOReadImplementation.SDF; import static org.apache.beam.sdk.io.kafka.KafkaIOTest.mkKafkaReadTransformWithOffsetDedup; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -30,6 +31,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.PipelineResult; import org.apache.beam.sdk.io.kafka.KafkaIOReadImplementationCompatibility.KafkaIOReadProperties; import org.apache.beam.sdk.io.kafka.KafkaIOTest.ValueAsTimestampFn; @@ -162,6 +164,26 @@ public void testReadTransformCreationWithSdfImplementationBoundProperty() { assertThat(Lineage.query(r.metrics(), Lineage.Type.SOURCE), containsInAnyOrder(expect)); } + @Test + public void testDynamicReadUsesSdfWithoutBeamFnApiExperiment() { + KafkaIO.Read read = + KafkaIOTest.mkKafkaReadTransform( + 1000, + null, + new ValueAsTimestampFn(), + false, /* redistribute */ + false, /* allowDuplicates */ + 0, /* numKeys */ + null, /* offsetDeduplication */ + null, /* topics */ + null /* redistributeByRecordKey */) + .withDynamicRead(Duration.standardMinutes(1)); + + assertThat( + KafkaIOReadImplementationCompatibility.getCompatibility(read).supportsOnly(SDF), is(true)); + Pipeline.create().apply(read); + } + @Test public void testReadTransformCreationWithBothImplementationBoundProperties() { thrown.expect(IllegalStateException.class); 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..8693f4597bc1 --- /dev/null +++ b/sdks/java/io/messaging-expansion-service/build.gradle @@ -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. + */ + +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 + implementation project(":sdks:java:io:jms") + permitUnusedDeclared project(":sdks:java:io:jms") // BEAM-11761 + runtimeOnly library.java.activemq_client + 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/mongodb/build.gradle b/sdks/java/io/mongodb/build.gradle index d4a172f01f8b..d56d969f30f0 100644 --- a/sdks/java/io/mongodb/build.gradle +++ b/sdks/java/io/mongodb/build.gradle @@ -32,6 +32,8 @@ dependencies { implementation library.java.mongodb_driver_core implementation library.java.slf4j_api implementation library.java.vendored_guava_32_1_2_jre + provided library.java.everit_json_schema + permitUnusedDeclared library.java.everit_json_schema testImplementation library.java.junit testImplementation project(path: ":sdks:java:io:common") testImplementation project(path: ":sdks:java:testing:test-utils") diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/AggregationQuery.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/AggregationQuery.java index 1b5a49436431..a96641123ac2 100644 --- a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/AggregationQuery.java +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/AggregationQuery.java @@ -66,13 +66,10 @@ public AggregationQuery withMongoDbPipeline(List mongoDbPipeline) @Override public MongoCursor apply(MongoCollection collection) { + List pipeline = new ArrayList<>(mongoDbPipeline()); if (bucket() != null) { - if (mongoDbPipeline().size() == 1) { - mongoDbPipeline().add(bucket()); - } else { - mongoDbPipeline().set(mongoDbPipeline().size() - 1, bucket()); - } + pipeline.add(bucket()); } - return collection.aggregate(mongoDbPipeline()).iterator(); + return collection.aggregate(pipeline).iterator(); } } diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbGridFSIO.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbGridFSIO.java index 05468a28863b..86e5554f2740 100644 --- a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbGridFSIO.java +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbGridFSIO.java @@ -23,6 +23,7 @@ import com.google.auto.value.AutoValue; import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; +import com.mongodb.MongoDriverInformation; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoCursor; @@ -119,6 +120,9 @@ */ public class MongoDbGridFSIO { + private static final MongoDriverInformation DRIVER_INFO = + MongoDriverInformation.builder().driverName("Apache Beam").build(); + /** Callback for the parser to use to submit data. */ public interface ParserCallback extends Serializable { /** Output the object. The default timestamp will be the GridFSFile creation timestamp. */ @@ -203,13 +207,13 @@ static ConnectionConfiguration create( MongoClient setupMongo() { if (uri() == null) { - return MongoClients.create(); + return MongoClients.create(MongoClientSettings.builder().build(), DRIVER_INFO); } MongoClientSettings settings = MongoClientSettings.builder() .applyConnectionString(new ConnectionString(Preconditions.checkStateNotNull(uri()))) .build(); - return MongoClients.create(settings); + return MongoClients.create(settings, DRIVER_INFO); } GridFSBucket setupGridFS(MongoClient mongo) { diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbIO.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbIO.java index fc2a761b99a8..46c3f8fcd58a 100644 --- a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbIO.java +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbIO.java @@ -27,6 +27,7 @@ import com.mongodb.MongoClientSettings; import com.mongodb.MongoClientSettings.Builder; import com.mongodb.MongoCommandException; +import com.mongodb.MongoDriverInformation; import com.mongodb.client.AggregateIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; @@ -145,6 +146,9 @@ public class MongoDbIO { private static final Logger LOG = LoggerFactory.getLogger(MongoDbIO.class); + private static final MongoDriverInformation DRIVER_INFO = + MongoDriverInformation.builder().driverName("Apache Beam").build(); + public static final String ERROR_MSG_QUERY_FN = " class is not supported. " + "Please provide one of the predefined classes in the MongoDbIO package " @@ -427,7 +431,7 @@ long getDocumentCount() { spec.ignoreSSLCertificate()) .applyConnectionString(new ConnectionString(uri)) .build(); - try (MongoClient mongoClient = MongoClients.create(settings)) { + try (MongoClient mongoClient = MongoClients.create(settings, DRIVER_INFO)) { return getDocumentCount(mongoClient, database, collection); } catch (Exception e) { return -1; @@ -459,7 +463,7 @@ public long getEstimatedSizeBytes(PipelineOptions pipelineOptions) { spec.ignoreSSLCertificate()) .applyConnectionString(new ConnectionString(uri)) .build(); - try (MongoClient mongoClient = MongoClients.create(settings)) { + try (MongoClient mongoClient = MongoClients.create(settings, DRIVER_INFO)) { try { return getEstimatedSizeBytes(mongoClient, database, collection); } catch (MongoCommandException exception) { @@ -496,7 +500,7 @@ public List> split( spec.ignoreSSLCertificate()) .applyConnectionString(new ConnectionString(uri)) .build(); - try (MongoClient mongoClient = MongoClients.create(settings)) { + try (MongoClient mongoClient = MongoClients.create(settings, DRIVER_INFO)) { MongoDatabase mongoDatabase = mongoClient.getDatabase(database); List splitKeys; @@ -812,7 +816,7 @@ private MongoClient createClient(Read spec) { spec.ignoreSSLCertificate()) .applyConnectionString(new ConnectionString(uri)) .build(); - return MongoClients.create(settings); + return MongoClients.create(settings, DRIVER_INFO); } } @@ -1012,7 +1016,7 @@ public void createMongoClient() { spec.ignoreSSLCertificate()) .applyConnectionString(new ConnectionString(uri)) .build(); - client = MongoClients.create(settings); + client = MongoClients.create(settings, DRIVER_INFO); } @StartBundle diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbReadSchemaTransformConfiguration.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbReadSchemaTransformConfiguration.java new file mode 100644 index 000000000000..e72fe8df8079 --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbReadSchemaTransformConfiguration.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 Read transform. */ +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class MongoDbReadSchemaTransformConfiguration implements Serializable { + + @SchemaFieldDescription("The connection URI for the MongoDB server.") + public abstract String getUri(); + + @SchemaFieldDescription("The MongoDB database to read from.") + public abstract String getDatabase(); + + @SchemaFieldDescription("The MongoDB collection to read from.") + public abstract String getCollection(); + + @SchemaFieldDescription( + "The schema in which the data is encoded, defined with JSON-schema syntax (https://json-schema.org/).") + public abstract String getSchema(); + + @SchemaFieldDescription( + "An optional BSON filter to apply to the read. This should be a valid JSON string.") + @Nullable + public abstract String getFilter(); + + @SchemaFieldDescription( + "This option specifies whether and where to output rows that failed to be read.") + @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."); + checkArgument( + getSchema() != null && !getSchema().isEmpty(), "MongoDB schema must be specified."); + } + + public static Builder builder() { + return new AutoValue_MongoDbReadSchemaTransformConfiguration.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 setSchema(String schema); + + public abstract Builder setFilter(String filter); + + public abstract Builder setErrorHandling(ErrorHandling errorHandling); + + public abstract MongoDbReadSchemaTransformConfiguration build(); + } +} diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbReadSchemaTransformProvider.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbReadSchemaTransformProvider.java new file mode 100644 index 000000000000..b9fe50816914 --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbReadSchemaTransformProvider.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.util.Collections; +import java.util.List; +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.schemas.utils.JsonUtils; +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 reading from MongoDB. */ +@AutoService(SchemaTransformProvider.class) +public class MongoDbReadSchemaTransformProvider + extends TypedSchemaTransformProvider { + + private static final String OUTPUT_TAG_NAME = "output"; + 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( + MongoDbReadSchemaTransformProvider.class, "MongoDB-read-error-counter"); + + @Override + protected SchemaTransform from(MongoDbReadSchemaTransformConfiguration configuration) { + return new MongoDbReadSchemaTransform(configuration); + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:mongodb_read:v1"; + } + + @Override + public List inputCollectionNames() { + return Collections.emptyList(); + } + + @Override + public List outputCollectionNames() { + return Collections.singletonList(OUTPUT_TAG_NAME); + } + + /** The {@link SchemaTransform} that performs the read operation. */ + private static class MongoDbReadSchemaTransform extends SchemaTransform { + private final MongoDbReadSchemaTransformConfiguration configuration; + + MongoDbReadSchemaTransform(MongoDbReadSchemaTransformConfiguration configuration) { + configuration.validate(); + this.configuration = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + Schema schema = JsonUtils.beamSchemaFromJsonSchema(configuration.getSchema()); + + MongoDbIO.Read read = + MongoDbIO.read() + .withUri(configuration.getUri()) + .withDatabase(configuration.getDatabase()) + .withCollection(configuration.getCollection()); + + final String filterStr = configuration.getFilter(); + if (filterStr != null) { + read = read.withQueryFn(FindQuery.create().withFilters(Document.parse(filterStr))); + } + + PCollection mongoDocs = input.getPipeline().apply("ReadFromMongoDb", read); + + boolean handleErrors = ErrorHandling.hasOutput(configuration.getErrorHandling()); + Schema errorSchema = ErrorHandling.errorSchemaBytes(); + + PCollectionTuple outputTuple = + mongoDocs.apply( + "ConvertToBeamRows", + ParDo.of(new DocumentToRowFn(schema, handleErrors, errorSchema)) + .withOutputTags(OUTPUT_TAG, TupleTagList.of(ERROR_TAG))); + + PCollection beamRows = outputTuple.get(OUTPUT_TAG).setRowSchema(schema); + PCollection errorOutput = outputTuple.get(ERROR_TAG).setRowSchema(errorSchema); + + PCollectionRowTuple output = PCollectionRowTuple.of(OUTPUT_TAG_NAME, beamRows); + ErrorHandling errorHandling = configuration.getErrorHandling(); + if (handleErrors && errorHandling != null) { + output = output.and(errorHandling.getOutput(), errorOutput); + } + return output; + } + } + + /** Converts a MongoDB BSON {@link Document} to a Beam {@link Row}. */ + static class DocumentToRowFn extends DoFn { + private final Schema schema; + private final boolean handleErrors; + private final Schema errorSchema; + + DocumentToRowFn(Schema schema, boolean handleErrors, Schema errorSchema) { + this.schema = schema; + this.handleErrors = handleErrors; + this.errorSchema = errorSchema; + } + + @ProcessElement + public void processElement(@Element Document doc, MultiOutputReceiver receiver) { + try { + receiver.get(OUTPUT_TAG).output(MongoDbUtils.toRow(doc, schema)); + } catch (Exception e) { + if (!handleErrors) { + throw new RuntimeException( + "Failed to convert BSON Document to Beam Row: " + doc.toJson(), e); + } + errorCounter.inc(); + byte[] docBytes; + try { + docBytes = doc.toJson().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } catch (Exception jsonEx) { + docBytes = doc.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + receiver.get(ERROR_TAG).output(ErrorHandling.errorRecord(errorSchema, docBytes, e)); + } + } + } +} 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..a8c286f36ecf --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbUtils.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.sdk.io.mongodb; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.values.Row; +import org.bson.BsonNull; +import org.bson.Document; +import org.bson.types.Binary; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Instant; + +/** 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; + } + + /** + * Converts a BSON {@link Document} (or any Map representing fields) to a Beam {@link Row} + * matching the given {@link Schema}. + */ + public static Row toRow(Map doc, Schema schema) { + Row.Builder rowBuilder = Row.withSchema(schema); + for (Field field : schema.getFields()) { + Object value = doc.get(field.getName()); + rowBuilder.addValue(convertFromBsonValue(value, field.getType())); + } + return rowBuilder.build(); + } + + @SuppressWarnings("JavaUtilDate") + private static @Nullable Object convertFromBsonValue( + @Nullable Object value, FieldType fieldType) { + if (value == null || value instanceof BsonNull) { + return null; + } + + switch (fieldType.getTypeName()) { + case BYTE: + return (value instanceof Number) + ? ((Number) value).byteValue() + : Byte.parseByte(value.toString()); + case INT16: + return (value instanceof Number) + ? ((Number) value).shortValue() + : Short.parseShort(value.toString()); + case INT32: + return (value instanceof Number) + ? ((Number) value).intValue() + : Integer.parseInt(value.toString()); + case INT64: + return (value instanceof Number) + ? ((Number) value).longValue() + : Long.parseLong(value.toString()); + case FLOAT: + return (value instanceof Number) + ? ((Number) value).floatValue() + : Float.parseFloat(value.toString()); + case DOUBLE: + return (value instanceof Number) + ? ((Number) value).doubleValue() + : Double.parseDouble(value.toString()); + case DECIMAL: + return (value instanceof Number) + ? java.math.BigDecimal.valueOf(((Number) value).doubleValue()) + : new java.math.BigDecimal(value.toString()); + case STRING: + return value.toString(); + case BOOLEAN: + return (value instanceof Boolean) + ? (Boolean) value + : Boolean.parseBoolean(value.toString()); + case DATETIME: + if (value instanceof java.util.Date) { + return new Instant(((java.util.Date) value).getTime()); + } else if (value instanceof Number) { + return new Instant(((Number) value).longValue()); + } else { + return Instant.parse(value.toString()); + } + case BYTES: + if (value instanceof Binary) { + return ((Binary) value).getData(); + } else if (value instanceof byte[]) { + return (byte[]) value; + } else { + return value.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + case ARRAY: + case ITERABLE: + if (!(value instanceof Iterable)) { + throw new IllegalArgumentException( + "Expected Iterable for type " + + fieldType + + ", but got: " + + value.getClass().getName()); + } + Iterable iterable = (Iterable) value; + List<@Nullable Object> rowList = new ArrayList<>(); + FieldType elementType = fieldType.getCollectionElementType(); + if (elementType == null) { + throw new IllegalArgumentException( + "Collection element type cannot be null for type: " + fieldType); + } + for (Object item : iterable) { + rowList.add(convertFromBsonValue(item, elementType)); + } + return rowList; + case MAP: + if (!(value instanceof Map)) { + throw new IllegalArgumentException( + "Expected Map for type " + fieldType + ", but got: " + value.getClass().getName()); + } + Map map = (Map) value; + Map rowMap = new HashMap<>(); + FieldType valueType = fieldType.getMapValueType(); + if (valueType == null) { + throw new IllegalArgumentException( + "Map value type cannot be null for type: " + fieldType); + } + for (Map.Entry entry : map.entrySet()) { + rowMap.put( + String.valueOf(entry.getKey()), convertFromBsonValue(entry.getValue(), valueType)); + } + return rowMap; + case ROW: + Schema rowSchema = fieldType.getRowSchema(); + if (rowSchema == null) { + throw new IllegalArgumentException("Row schema cannot be null for type: " + fieldType); + } + if (value instanceof Map) { + return toRow((Map) value, rowSchema); + } else { + throw new IllegalArgumentException( + "Cannot convert value of type " + + (value != null ? value.getClass().getName() : "null") + + " to Row"); + } + default: + throw new IllegalArgumentException("Unsupported field type: " + fieldType); + } + } +} 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/MongoDbReadSchemaTransformProviderTest.java b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbReadSchemaTransformProviderTest.java new file mode 100644 index 000000000000..3b226efdb316 --- /dev/null +++ b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbReadSchemaTransformProviderTest.java @@ -0,0 +1,221 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.nio.charset.StandardCharsets; +import java.util.Collections; +import org.apache.beam.sdk.Pipeline; +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.PCollectionRowTuple; +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 MongoDbReadSchemaTransformProvider}. */ +@RunWith(JUnit4.class) +public class MongoDbReadSchemaTransformProviderTest { + + @Rule public transient TestPipeline p = TestPipeline.create(); + + @Test + public void testInvalidConfigMissingUri() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbReadSchemaTransformConfiguration.builder() + .setDatabase("db") + .setCollection("col") + .setSchema("{}") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigMissingDatabase() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbReadSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setCollection("col") + .setSchema("{}") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigMissingCollection() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbReadSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setDatabase("db") + .setSchema("{}") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigMissingSchema() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbReadSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setDatabase("db") + .setCollection("col") + .build() + .validate(); + }); + } + + @Test + public void testConfigurationSchema() throws Exception { + Schema schema = + SchemaRegistry.createDefault().getSchema(MongoDbReadSchemaTransformConfiguration.class); + + // We expect 6 fields: uri, database, collection, schema, filter, errorHandling + assertEquals(6, schema.getFieldCount()); + assertNotNull(schema.getField("uri")); + assertNotNull(schema.getField("database")); + assertNotNull(schema.getField("collection")); + assertNotNull(schema.getField("schema")); + assertNotNull(schema.getField("filter")); + assertNotNull(schema.getField("errorHandling")); + } + + @Test + public void testExpandWithFilter() { + MongoDbReadSchemaTransformConfiguration config = + MongoDbReadSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setDatabase("db") + .setCollection("col") + .setSchema("{\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}}") + .setFilter("{\"name\": \"John\"}") + .build(); + + MongoDbReadSchemaTransformProvider provider = new MongoDbReadSchemaTransformProvider(); + PCollectionRowTuple output = + provider.from(config).expand(PCollectionRowTuple.empty(Pipeline.create())); + + assertNotNull(output.get("output")); + } + + @Test + public void testDocumentToRowFn() { + Schema beamSchema = Schema.builder().addStringField("name").addInt32Field("age").build(); + + Document doc = new Document().append("name", "John").append("age", 30); + + PCollection inputDocs = + p.apply( + Create.of(Collections.singletonList(doc)) + .withCoder(MongoDbWriteSchemaTransformProvider.DocumentCoder.of())); + + Schema errorSchema = ErrorHandling.errorSchemaBytes(); + PCollectionTuple outputTuple = + inputDocs.apply( + "ConvertToRows", + ParDo.of( + new MongoDbReadSchemaTransformProvider.DocumentToRowFn( + beamSchema, false, errorSchema)) + .withOutputTags( + MongoDbReadSchemaTransformProvider.OUTPUT_TAG, + TupleTagList.of(MongoDbReadSchemaTransformProvider.ERROR_TAG))); + + PCollection outputRows = + outputTuple.get(MongoDbReadSchemaTransformProvider.OUTPUT_TAG).setRowSchema(beamSchema); + outputTuple.get(MongoDbReadSchemaTransformProvider.ERROR_TAG).setRowSchema(errorSchema); + + PAssert.that(outputRows) + .satisfies( + rows -> { + Row row = rows.iterator().next(); + assertEquals("John", row.getString("name")); + assertEquals(Integer.valueOf(30), row.getInt32("age")); + return null; + }); + + p.run().waitUntilFinish(); + } + + @Test + public void testDocumentToRowFnWithErrors() { + Schema beamSchema = Schema.builder().addInt32Field("age").build(); + + // Invalid document: age value is a string "not_an_int" which cannot be converted to INT32 + Document invalidDoc = new Document().append("age", "not_an_int"); + + PCollection inputDocs = + p.apply( + Create.of(Collections.singletonList(invalidDoc)) + .withCoder(MongoDbWriteSchemaTransformProvider.DocumentCoder.of())); + + Schema errorSchema = ErrorHandling.errorSchemaBytes(); + PCollectionTuple outputTuple = + inputDocs.apply( + "ConvertToRowsWithErrors", + ParDo.of( + new MongoDbReadSchemaTransformProvider.DocumentToRowFn( + beamSchema, true, errorSchema)) + .withOutputTags( + MongoDbReadSchemaTransformProvider.OUTPUT_TAG, + TupleTagList.of(MongoDbReadSchemaTransformProvider.ERROR_TAG))); + + PCollection errorRows = + outputTuple.get(MongoDbReadSchemaTransformProvider.ERROR_TAG).setRowSchema(errorSchema); + PCollection outputRows = + outputTuple.get(MongoDbReadSchemaTransformProvider.OUTPUT_TAG).setRowSchema(beamSchema); + + PAssert.that(outputRows).empty(); + PAssert.that(errorRows) + .satisfies( + rows -> { + Row errorRow = rows.iterator().next(); + byte[] failedRowBytes = errorRow.getBytes("failed_row"); + String failedJson = new String(failedRowBytes, StandardCharsets.UTF_8); + String errMsg = errorRow.getString("error_message"); + assertNotNull(failedJson); + assertNotNull(errMsg); + return null; + }); + + p.run().waitUntilFinish(); + } +} 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..f454d100f0a6 --- /dev/null +++ b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbUtilsTest.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +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.bson.types.Binary; +import org.joda.time.Instant; +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); + } + + @Test + public void testToRowWithSimplePrimitives() { + Schema schema = + Schema.builder() + .addStringField("stringField") + .addInt32Field("intField") + .addInt64Field("longField") + .addBooleanField("booleanField") + .addDoubleField("doubleField") + .build(); + + Document doc = + new Document() + .append("stringField", "hello") + .append("intField", 42) + .append("longField", 123456789L) + .append("booleanField", true) + .append("doubleField", 3.14); + + Row row = MongoDbUtils.toRow(doc, schema); + + assertNotNull(row); + assertEquals("hello", row.getString("stringField")); + assertEquals(Integer.valueOf(42), row.getInt32("intField")); + assertEquals(Long.valueOf(123456789L), row.getInt64("longField")); + assertEquals(Boolean.TRUE, row.getBoolean("booleanField")); + assertEquals(Double.valueOf(3.14), row.getDouble("doubleField")); + } + + @Test + public void testToRowWithNestedRow() { + Schema nestedSchema = + Schema.builder().addStringField("nestedString").addInt32Field("nestedInt").build(); + + Schema parentSchema = + Schema.builder() + .addStringField("parentString") + .addRowField("nestedRow", nestedSchema) + .build(); + + Document nestedDoc = + new Document().append("nestedString", "nestedValue").append("nestedInt", 100); + Document parentDoc = + new Document().append("parentString", "parentValue").append("nestedRow", nestedDoc); + + Row row = MongoDbUtils.toRow(parentDoc, parentSchema); + + assertNotNull(row); + assertEquals("parentValue", row.getString("parentString")); + + Row nestedRow = row.getRow("nestedRow"); + assertNotNull(nestedRow); + assertEquals("nestedValue", nestedRow.getString("nestedString")); + assertEquals(Integer.valueOf(100), nestedRow.getInt32("nestedInt")); + } + + @Test + public void testToRowWithIterable() { + Schema schema = Schema.builder().addArrayField("listField", FieldType.STRING).build(); + + Document doc = new Document().append("listField", Arrays.asList("a", "b", "c")); + + Row row = MongoDbUtils.toRow(doc, schema); + + assertNotNull(row); + List list = (List) row.getArray("listField"); + assertEquals(3, list.size()); + assertEquals("a", list.get(0)); + assertEquals("b", list.get(1)); + assertEquals("c", list.get(2)); + } + + @Test + public void testToRowWithMap() { + Schema schema = + Schema.builder().addMapField("mapField", FieldType.STRING, FieldType.INT32).build(); + + Document nestedMap = new Document().append("key", 42); + Document doc = new Document().append("mapField", nestedMap); + + Row row = MongoDbUtils.toRow(doc, schema); + + assertNotNull(row); + Map map = row.getMap("mapField"); + assertEquals(1, map.size()); + assertEquals(Integer.valueOf(42), map.get("key")); + } + + @Test + public void testToRowWithNullValues() { + Schema schema = Schema.builder().addNullableField("nullableString", FieldType.STRING).build(); + + Document doc = new Document().append("nullableString", new BsonNull()); + + Row row = MongoDbUtils.toRow(doc, schema); + + assertNotNull(row); + assertNull(row.getString("nullableString")); + } + + @Test + @SuppressWarnings("JavaUtilDate") + public void testToRowWithDateAndBinary() { + Schema schema = + Schema.builder().addDateTimeField("dateField").addByteArrayField("binaryField").build(); + + Date now = new Date(); + byte[] bytes = "hello binary".getBytes(StandardCharsets.UTF_8); + Document doc = new Document().append("dateField", now).append("binaryField", new Binary(bytes)); + + Row row = MongoDbUtils.toRow(doc, schema); + + assertNotNull(row); + assertEquals(new Instant(now.getTime()), row.getDateTime("dateField")); + assertArrayEquals(bytes, row.getBytes("binaryField")); + } +} 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/java/io/mqtt/build.gradle b/sdks/java/io/mqtt/build.gradle index 85d6c6c16f25..1f2d808ed7e0 100644 --- a/sdks/java/io/mqtt/build.gradle +++ b/sdks/java/io/mqtt/build.gradle @@ -27,8 +27,7 @@ dependencies { implementation project(path: ":sdks:java:core", configuration: "shadow") implementation library.java.slf4j_api implementation library.java.joda_time - implementation "org.fusesource.mqtt-client:mqtt-client:1.15" - implementation "org.fusesource.hawtbuf:hawtbuf:1.11" + implementation "com.hivemq:hivemq-mqtt-client:1.3.15" testImplementation project(path: ":sdks:java:io:common") testImplementation library.java.activemq_broker testImplementation library.java.activemq_mqtt 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..b40703db44a0 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 @@ -21,23 +21,33 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; +import com.hivemq.client.mqtt.MqttGlobalPublishFilter; +import com.hivemq.client.mqtt.datatypes.MqttQos; +import com.hivemq.client.mqtt.mqtt3.Mqtt3BlockingClient; +import com.hivemq.client.mqtt.mqtt3.Mqtt3Client; +import com.hivemq.client.mqtt.mqtt3.Mqtt3ClientBuilder; +import com.hivemq.client.mqtt.mqtt3.message.publish.Mqtt3Publish; import java.io.IOException; import java.io.Serializable; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.apache.beam.sdk.coders.ByteArrayCoder; 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.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; @@ -49,12 +59,6 @@ import org.apache.beam.sdk.values.PDone; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.checkerframework.checker.nullness.qual.Nullable; -import org.fusesource.mqtt.client.BlockingConnection; -import org.fusesource.mqtt.client.FutureConnection; -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.joda.time.Duration; import org.joda.time.Instant; import org.slf4j.Logger; @@ -205,13 +209,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(); @@ -292,29 +300,40 @@ private void populateDisplayData(DisplayData.Builder builder) { builder.addIfNotNull(DisplayData.item("username", getUsername())); } - private MQTT createClient() throws Exception { + private Mqtt3BlockingClient createClient() throws Exception { LOG.debug("Creating MQTT client to {}", getServerUri()); - MQTT client = new MQTT(); - client.setHost(getServerUri()); - if (getUsername() != null) { - LOG.debug("MQTT client uses username {}", getUsername()); - client.setUserName(getUsername()); - client.setPassword(getPassword()); + URI uri = new URI(getServerUri()); + String host = uri.getHost(); + int port = uri.getPort(); + if (port == -1) { + port = "ssl".equals(uri.getScheme()) || "tls".equals(uri.getScheme()) ? 8883 : 1883; + } + + Mqtt3ClientBuilder builder = Mqtt3Client.builder().serverHost(host).serverPort(port); + + if ("ssl".equals(uri.getScheme()) || "tls".equals(uri.getScheme())) { + builder = builder.sslWithDefaultConfig(); } - if (getClientId() != null) { - String clientId = getClientId() + "-" + UUID.randomUUID().toString(); - clientId = - clientId.substring(0, Math.min(clientId.length(), MQTT_3_1_MAX_CLIENT_ID_LENGTH)); - LOG.debug("MQTT client id set to {}", clientId); - client.setClientId(clientId); + + String clientId = getClientId(); + if (clientId == null) { + clientId = UUID.randomUUID().toString(); } else { - String clientId = UUID.randomUUID().toString(); - clientId = - clientId.substring(0, Math.min(clientId.length(), MQTT_3_1_MAX_CLIENT_ID_LENGTH)); - LOG.debug("MQTT client id set to random value {}", clientId); - client.setClientId(clientId); + clientId = clientId + "-" + UUID.randomUUID().toString(); } - return client; + clientId = clientId.substring(0, Math.min(clientId.length(), MQTT_3_1_MAX_CLIENT_ID_LENGTH)); + LOG.debug("MQTT client id set to {}", clientId); + builder = builder.identifier(clientId); + + if (getUsername() != null) { + LOG.debug("MQTT client uses username {}", getUsername()); + var auth = builder.simpleAuth().username(getUsername()); + if (getPassword() != null) { + auth = auth.password(getPassword().getBytes(StandardCharsets.UTF_8)); + } + builder = auth.applySimpleAuth(); + } + return builder.buildBlocking(); } } @@ -422,9 +441,9 @@ public void populateDisplayData(DisplayData.Builder builder) { static class MqttCheckpointMark implements UnboundedSource.CheckpointMark, Serializable { @VisibleForTesting String clientId; - @VisibleForTesting transient List messages = new ArrayList<>(); + @VisibleForTesting transient List messages = new ArrayList<>(); - public MqttCheckpointMark(String id, List messages) { + public MqttCheckpointMark(String id, List messages) { this.clientId = id; this.messages = messages; } @@ -437,9 +456,9 @@ public MqttCheckpointMark(String id, List messages) { @Override public void finalizeCheckpoint() { LOG.debug("Finalizing checkpoint acknowledging pending messages for client ID {}", clientId); - for (Message message : messages) { + for (Mqtt3Publish message : messages) { try { - message.ack(); + message.acknowledge(); } catch (Exception e) { LOG.warn("Can't ack message for client ID {}", clientId, e); } @@ -473,7 +492,7 @@ public int hashCode() { static class Preparer { @VisibleForTesting String clientId; @VisibleForTesting Instant oldestMessageTimestamp = Instant.now(); - @VisibleForTesting transient List messages = new ArrayList<>(); + @VisibleForTesting transient List messages = new ArrayList<>(); public Preparer(MqttCheckpointMark checkpointMark) { clientId = checkpointMark.clientId; @@ -486,7 +505,7 @@ public Preparer(String id) { public Preparer() {} - public void add(Message message, Instant timestamp) { + public void add(Mqtt3Publish message, Instant timestamp) { if (timestamp.isBefore(oldestMessageTimestamp)) { oldestMessageTimestamp = timestamp; } @@ -494,7 +513,7 @@ public void add(Message message, Instant timestamp) { } MqttCheckpointMark newCheckpoint() { - List currentMessages = messages; + List currentMessages = messages; messages = new ArrayList<>(); oldestMessageTimestamp = Instant.now(); return new MqttCheckpointMark(clientId, currentMessages); @@ -525,7 +544,8 @@ public UnboundedReader createReader( new UnboundedMqttReader<>( this, preparer, - message -> (T) MqttRecord.of(message.getTopic(), message.getPayload())); + message -> + (T) MqttRecord.of(message.getTopic().toString(), message.getPayloadAsBytes())); } else { unboundedMqttReader = new UnboundedMqttReader<>(this, preparer); } @@ -563,12 +583,13 @@ static class UnboundedMqttReader extends UnboundedSource.UnboundedReader { private final UnboundedMqttSource source; - private MQTT client; - private BlockingConnection connection; + private Mqtt3BlockingClient client; + private Mqtt3BlockingClient.Mqtt3Publishes publishes; + private String clientId = ""; private T current; private Instant currentTimestamp; private final MqttCheckpointMark.Preparer checkpointPreparer; - private SerializableFunction extractFn; + private SerializableFunction extractFn; public UnboundedMqttReader( UnboundedMqttSource source, MqttCheckpointMark.Preparer checkpointPreparer) { @@ -579,13 +600,13 @@ public UnboundedMqttReader( } else { this.checkpointPreparer = new MqttCheckpointMark.Preparer(); } - this.extractFn = message -> (T) message.getPayload(); + this.extractFn = message -> (T) message.getPayloadAsBytes(); } public UnboundedMqttReader( UnboundedMqttSource source, MqttCheckpointMark.Preparer checkpointPreparer, - SerializableFunction extractFn) { + SerializableFunction extractFn) { this(source, checkpointPreparer); this.extractFn = extractFn; } @@ -596,11 +617,20 @@ public boolean start() throws IOException { Read spec = source.spec; try { client = spec.connectionConfiguration().createClient(); - LOG.debug("Reader client ID is {}", client.getClientId()); - checkpointPreparer.clientId = client.getClientId().toString(); - connection = createConnection(client); - connection.subscribe( - new Topic[] {new Topic(spec.connectionConfiguration().getTopic(), QoS.AT_LEAST_ONCE)}); + this.clientId = client.getConfig().getClientIdentifier().map(Object::toString).orElse(""); + LOG.debug("Reader client ID is {}", clientId); + checkpointPreparer.clientId = clientId; + client.connect(); + + // Subscribe and get the publishes stream with manual acks enabled + publishes = client.publishes(MqttGlobalPublishFilter.ALL, true); + + client + .subscribeWith() + .topicFilter(spec.connectionConfiguration().getTopic()) + .qos(MqttQos.AT_LEAST_ONCE) + .send(); + return advance(); } catch (Exception e) { throw new IOException(e); @@ -610,11 +640,12 @@ public boolean start() throws IOException { @Override public boolean advance() throws IOException { try { - LOG.trace("MQTT reader (client ID {}) waiting message ...", client.getClientId()); - Message message = connection.receive(1, TimeUnit.SECONDS); - if (message == null) { + LOG.trace("MQTT reader (client ID {}) waiting message ...", clientId); + Optional messageOpt = publishes.receive(1, TimeUnit.SECONDS); + if (!messageOpt.isPresent()) { return false; } + Mqtt3Publish message = messageOpt.get(); current = this.extractFn.apply(message); currentTimestamp = Instant.now(); checkpointPreparer.add(message, currentTimestamp); @@ -626,13 +657,24 @@ public boolean advance() throws IOException { @Override public void close() throws IOException { - LOG.debug("Closing MQTT reader (client ID {})", client.getClientId()); - try { - if (connection != null) { - connection.disconnect(); + LOG.debug("Closing MQTT reader (client ID {})", clientId); + if (publishes != null) { + try { + publishes.close(); + } catch (Exception e) { + LOG.warn("Error closing publishes stream", e); + } finally { + publishes = null; + } + } + if (client != null) { + try { + client.disconnect(); + } catch (Exception e) { + throw new IOException(e); + } finally { + client = null; } - } catch (Exception e) { - throw new IOException(e); } } @@ -757,8 +799,7 @@ private static class WriteFn extends DoFn { private final SerializableFunction payloadFn; private final boolean retained; - private transient MQTT client; - private transient BlockingConnection connection; + private transient Mqtt3BlockingClient client; public WriteFn(Write spec) { this.spec = spec; @@ -776,8 +817,9 @@ public WriteFn(Write spec) { public void createMqttClient() throws Exception { LOG.debug("Starting MQTT writer"); this.client = this.spec.connectionConfiguration().createClient(); - LOG.debug("MQTT writer client ID is {}", client.getClientId()); - this.connection = createConnection(client); + String clientId = client.getConfig().getClientIdentifier().map(Object::toString).orElse(""); + LOG.debug("MQTT writer client ID is {}", clientId); + this.client.connect(); } @ProcessElement @@ -786,32 +828,25 @@ public void processElement(ProcessContext context) throws Exception { byte[] payload = this.payloadFn.apply(element); String topic = this.topicFn.apply(element); LOG.debug("Sending message {}", new String(payload, StandardCharsets.UTF_8)); - this.connection.publish(topic, payload, QoS.AT_LEAST_ONCE, this.retained); + + client + .publishWith() + .topic(topic) + .payload(payload) + .qos(MqttQos.AT_LEAST_ONCE) + .retain(this.retained) + .send(); } @Teardown public void closeMqttClient() throws Exception { - if (this.connection != null) { - LOG.debug("Disconnecting MQTT connection (client ID {})", client.getClientId()); - this.connection.disconnect(); + if (this.client != null) { + String clientId = + client.getConfig().getClientIdentifier().map(Object::toString).orElse(""); + LOG.debug("Disconnecting MQTT connection (client ID {})", clientId); + this.client.disconnect(); } } } } - - /** Create a connected MQTT BlockingConnection from given client, aware of connection timeout. */ - static BlockingConnection createConnection(MQTT client) throws Exception { - FutureConnection futureConnection = client.futureConnection(); - org.fusesource.mqtt.client.Future connecting = futureConnection.connect(); - while (true) { - try { - connecting.await(1, TimeUnit.MINUTES); - } catch (TimeoutException e) { - LOG.warn("Connection to {} pending after waiting for 1 minute", client.getHost()); - continue; - } - break; - } - return new BlockingConnection(futureConnection); - } } 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/MqttIOTest.java b/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttIOTest.java index 71e24a6859c1..dfadc818a469 100644 --- a/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttIOTest.java +++ b/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttIOTest.java @@ -21,16 +21,24 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import com.hivemq.client.mqtt.MqttGlobalPublishFilter; +import com.hivemq.client.mqtt.datatypes.MqttQos; +import com.hivemq.client.mqtt.datatypes.MqttTopic; +import com.hivemq.client.mqtt.mqtt3.Mqtt3BlockingClient; +import com.hivemq.client.mqtt.mqtt3.Mqtt3Client; +import com.hivemq.client.mqtt.mqtt3.message.publish.Mqtt3Publish; +import com.hivemq.client.mqtt.mqtt3.message.publish.Mqtt3PublishBuilder; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentSkipListMap; @@ -49,13 +57,6 @@ import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PBegin; import org.apache.beam.sdk.values.PCollection; -import org.fusesource.hawtbuf.Buffer; -import org.fusesource.mqtt.client.BlockingConnection; -import org.fusesource.mqtt.client.Callback; -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.joda.time.Duration; import org.joda.time.Instant; import org.junit.After; @@ -130,21 +131,21 @@ public void testReadNoClientId() throws Exception { // produce messages on the brokerService in another thread // This thread prevents to block the pipeline waiting for new messages - MQTT client = new MQTT(); - client.setHost("tcp://localhost:" + port); - final BlockingConnection publishConnection = client.blockingConnection(); - publishConnection.connect(); + Mqtt3BlockingClient publishClient = + Mqtt3Client.builder().serverHost("localhost").serverPort(port).buildBlocking(); + publishClient.connect(); Thread publisherThread = new Thread( () -> { try { doConnect(connection -> !connection.getConnectionId().isEmpty()); for (int i = 0; i < 10; i++) { - publishConnection.publish( - topicName, - ("This is test " + i).getBytes(StandardCharsets.UTF_8), - QoS.EXACTLY_ONCE, - false); + publishClient + .publishWith() + .topic(topicName) + .payload(("This is test " + i).getBytes(StandardCharsets.UTF_8)) + .qos(MqttQos.EXACTLY_ONCE) + .send(); } } catch (Exception e) { // nothing to do @@ -153,7 +154,7 @@ public void testReadNoClientId() throws Exception { publisherThread.start(); pipeline.run(); - publishConnection.disconnect(); + publishClient.disconnect(); publisherThread.join(); } @@ -181,21 +182,21 @@ public void testRead() throws Exception { // produce messages on the brokerService in another thread // This thread prevents to block the pipeline waiting for new messages - MQTT client = new MQTT(); - client.setHost("tcp://localhost:" + port); - final BlockingConnection publishConnection = client.blockingConnection(); - publishConnection.connect(); + Mqtt3BlockingClient publishClient = + Mqtt3Client.builder().serverHost("localhost").serverPort(port).buildBlocking(); + publishClient.connect(); Thread publisherThread = new Thread( () -> { try { doConnect(connection -> connection.getConnectionId().startsWith("READ_PIPELINE")); for (int i = 0; i < 10; i++) { - publishConnection.publish( - "READ_TOPIC", - ("This is test " + i).getBytes(StandardCharsets.UTF_8), - QoS.EXACTLY_ONCE, - false); + publishClient + .publishWith() + .topic("READ_TOPIC") + .payload(("This is test " + i).getBytes(StandardCharsets.UTF_8)) + .qos(MqttQos.EXACTLY_ONCE) + .send(); } } catch (Exception e) { // nothing to do @@ -205,7 +206,7 @@ public void testRead() throws Exception { pipeline.run(); publisherThread.join(); - publishConnection.disconnect(); + publishClient.disconnect(); } @Test(timeout = 60 * 1000) @@ -237,10 +238,9 @@ public void testReadWithMetadata() throws Exception { // produce messages on the brokerService in another thread // This thread prevents to block the pipeline waiting for new messages - MQTT client = new MQTT(); - client.setHost("tcp://localhost:" + port); - final BlockingConnection publishConnection = client.blockingConnection(); - publishConnection.connect(); + Mqtt3BlockingClient publishClient = + Mqtt3Client.builder().serverHost("localhost").serverPort(port).buildBlocking(); + publishClient.connect(); Thread publisherThread = new Thread( () -> { @@ -249,18 +249,20 @@ public void testReadWithMetadata() throws Exception { // Sleep two seconds, to give enough time for client to be ready to accept messages Thread.sleep(2 * 1000); for (int i = 0; i < 5; i++) { - publishConnection.publish( - topic1, - ("This is test " + i).getBytes(StandardCharsets.UTF_8), - QoS.EXACTLY_ONCE, - false); + publishClient + .publishWith() + .topic(topic1) + .payload(("This is test " + i).getBytes(StandardCharsets.UTF_8)) + .qos(MqttQos.EXACTLY_ONCE) + .send(); } for (int i = 5; i < 10; i++) { - publishConnection.publish( - topic2, - ("This is test " + i).getBytes(StandardCharsets.UTF_8), - QoS.EXACTLY_ONCE, - false); + publishClient + .publishWith() + .topic(topic2) + .payload(("This is test " + i).getBytes(StandardCharsets.UTF_8)) + .qos(MqttQos.EXACTLY_ONCE) + .send(); } } catch (Exception e) { @@ -271,7 +273,7 @@ public void testReadWithMetadata() throws Exception { publisherThread.start(); pipeline.run(); - publishConnection.disconnect(); + publishClient.disconnect(); publisherThread.join(); } @@ -289,34 +291,53 @@ public void testReceiveWithTimeoutAndNoData() throws Exception { pipeline.run(); } - private static class FakeMessage extends Message { + private static class FakeMessage implements Mqtt3Publish { + private int ackCount = 0; - private int ackCount; + @Override + public void acknowledge() { + ackCount++; + } - public FakeMessage() { - super(null, null, null, null); - this.ackCount = 0; + public int getAckCount() { + return ackCount; } @Override - public void ack() { - ++ackCount; + public Optional getPayload() { + return Optional.empty(); } @Override - public void ack(final Callback unused) { - ++ackCount; + public byte[] getPayloadAsBytes() { + return new byte[0]; } - public int getAckCount() { - return ackCount; + @Override + public MqttTopic getTopic() { + return MqttTopic.of("temp"); + } + + @Override + public MqttQos getQos() { + return MqttQos.AT_LEAST_ONCE; + } + + @Override + public boolean isRetain() { + return false; + } + + @Override + public Mqtt3PublishBuilder.Complete extend() { + return null; } } @Test public void testReadCheckpoint() { MqttIO.MqttCheckpointMark.Preparer preparer = new MqttIO.MqttCheckpointMark.Preparer("id"); - ArrayList messages = new ArrayList<>(); + ArrayList messages = new ArrayList<>(); for (int i = 0; i < 5; ++i) { messages.add(new FakeMessage()); } @@ -328,30 +349,35 @@ public void testReadCheckpoint() { preparer.add(messages.get(3), Instant.ofEpochMilli(40)); preparer.add(messages.get(4), Instant.ofEpochMilli(50)); MqttIO.MqttCheckpointMark checkpointB = preparer.newCheckpoint(); - assertTrue( - Arrays.stream(messages.toArray()).allMatch(m -> ((FakeMessage) m).getAckCount() == 0)); + + // Check that ackCount is 0 for all + assertTrue(messages.stream().allMatch(m -> m.getAckCount() == 0)); + checkpointA.finalizeCheckpoint(); // only messages in finalized checkpoint acked - assertTrue( - Arrays.stream(messages.subList(0, 3).toArray()) - .allMatch(m -> ((FakeMessage) m).getAckCount() == 1)); - assertTrue( - Arrays.stream(messages.subList(3, 5).toArray()) - .allMatch(m -> ((FakeMessage) m).getAckCount() == 0)); + for (int i = 0; i < 3; ++i) { + assertEquals(1, messages.get(i).getAckCount()); + } + for (int i = 3; i < 5; ++i) { + assertEquals(0, messages.get(i).getAckCount()); + } + checkpointB.finalizeCheckpoint(); - // all messaged acked once - assertTrue( - Arrays.stream(messages.toArray()).allMatch(m -> ((FakeMessage) m).getAckCount() == 1)); + // all messages acked once + for (int i = 0; i < 5; ++i) { + assertEquals(1, messages.get(i).getAckCount()); + } } @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)}); + Mqtt3BlockingClient client = + Mqtt3Client.builder().serverHost("localhost").serverPort(port).buildBlocking(); + client.connect(); + Mqtt3BlockingClient.Mqtt3Publishes publishes = + client.publishes(MqttGlobalPublishFilter.ALL, true); + client.subscribeWith().topicFilter("WRITE_TOPIC").qos(MqttQos.EXACTLY_ONCE).send(); final Set messages = new ConcurrentSkipListSet<>(); @@ -360,9 +386,9 @@ public void testWrite() throws Exception { () -> { try { for (int i = 0; i < numberOfTestMessages; i++) { - Message message = connection.receive(); - messages.add(new String(message.getPayload(), StandardCharsets.UTF_8)); - message.ack(); + Mqtt3Publish message = publishes.receive(); + messages.add(new String(message.getPayloadAsBytes(), StandardCharsets.UTF_8)); + message.acknowledge(); } } catch (Exception e) { LOG.error("Can't receive message", e); @@ -384,7 +410,7 @@ public void testWrite() throws Exception { pipeline.run(); subscriber.join(); - connection.disconnect(); + client.disconnect(); assertEquals(numberOfTestMessages, messages.size()); for (int i = 0; i < numberOfTestMessages; i++) { @@ -398,17 +424,15 @@ public void testDynamicWrite() throws Exception { final int numberOfTopic2Count = 100; final int numberOfTestMessages = numberOfTopic1Count + numberOfTopic2Count; - MQTT client = new MQTT(); - client.setHost("tcp://localhost:" + port); - final BlockingConnection connection = client.blockingConnection(); - connection.connect(); + Mqtt3BlockingClient client = + Mqtt3Client.builder().serverHost("localhost").serverPort(port).buildBlocking(); + client.connect(); + Mqtt3BlockingClient.Mqtt3Publishes publishes = + client.publishes(MqttGlobalPublishFilter.ALL, true); final String writeTopic1 = "WRITE_TOPIC_1"; final String writeTopic2 = "WRITE_TOPIC_2"; - connection.subscribe( - new Topic[] { - new Topic(Buffer.utf8(writeTopic1), QoS.EXACTLY_ONCE), - new Topic(Buffer.utf8(writeTopic2), QoS.EXACTLY_ONCE) - }); + client.subscribeWith().topicFilter(writeTopic1).qos(MqttQos.EXACTLY_ONCE).send(); + client.subscribeWith().topicFilter(writeTopic2).qos(MqttQos.EXACTLY_ONCE).send(); final Map> messageMap = new ConcurrentSkipListMap<>(); final Thread subscriber = @@ -416,14 +440,14 @@ public void testDynamicWrite() throws Exception { () -> { try { for (int i = 0; i < numberOfTestMessages; i++) { - Message message = connection.receive(); - List messages = messageMap.get(message.getTopic()); + Mqtt3Publish message = publishes.receive(); + List messages = messageMap.get(message.getTopic().toString()); if (messages == null) { messages = new ArrayList<>(); } - messages.add(new String(message.getPayload(), StandardCharsets.UTF_8)); - messageMap.put(message.getTopic(), messages); - message.ack(); + messages.add(new String(message.getPayloadAsBytes(), StandardCharsets.UTF_8)); + messageMap.put(message.getTopic().toString(), messages); + message.acknowledge(); } } catch (Exception e) { LOG.error("Can't receive message", e); @@ -455,7 +479,7 @@ public void testDynamicWrite() throws Exception { pipeline.run(); subscriber.join(); - connection.disconnect(); + client.disconnect(); assertEquals( numberOfTestMessages, messageMap.values().stream().mapToLong(Collection::size).sum()); 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/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)); 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..5b9fa4968429 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); @@ -595,13 +596,10 @@ private void executeAsync(Callable callable) throws UserCodeExecutionExcep private static void parseAndThrow(Future future, ExecutionException e) throws UserCodeExecutionException { future.cancel(true); - if (e.getCause() == null) { - throw new UserCodeExecutionException(e); - } - Throwable cause = checkStateNotNull(e.getCause()); - if (cause instanceof UserCodeQuotaException) { - throw new UserCodeQuotaException(cause); + Throwable cause = e.getCause(); + if (cause instanceof UserCodeExecutionException) { + throw (UserCodeExecutionException) cause; } - throw new UserCodeExecutionException(cause); + throw new UserCodeExecutionException(cause == null ? e : cause); } } 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..5fb20bf38b8b 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 @@ -104,6 +104,20 @@ public void givenCallerThrowsUserCodeExecutionException_emitsIntoFailurePCollect pipeline.run(); } + @Test + public void givenCallerThrowsNonUserCodeException_emitsWrappedUserCodeExecutionException() { + Result result = + pipeline + .apply(Create.of(new Request("a"))) + .apply(Call.of(new CallerThrowsRuntimeException(), NON_DETERMINISTIC_RESPONSE_CODER)); + + PCollection failures = result.getFailures(); + PAssert.thatSingleton(countStackTracesOf(failures, UserCodeExecutionException.class)) + .isEqualTo(1L); + + pipeline.run(); + } + @Test public void givenCallerThrowsQuotaException_emitsIntoFailurePCollection() { Result result = @@ -123,7 +137,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"))) @@ -142,7 +156,7 @@ public void givenCallerTimeout_emitsFailurePCollection() { } @Test - public void givenCallerThrowsTimeoutException_emitsFailurePCollection() { + public void givenCallerThrowsTimeoutException_thenPreservesExceptionType() { Result result = pipeline .apply(Create.of(new Request("a"))) @@ -150,7 +164,7 @@ public void givenCallerThrowsTimeoutException_emitsFailurePCollection() { PCollection failures = result.getFailures(); PAssert.thatSingleton(countStackTracesOf(failures, UserCodeExecutionException.class)) - .isEqualTo(1L); + .isEqualTo(0L); PAssert.thatSingleton(countStackTracesOf(failures, UserCodeQuotaException.class)).isEqualTo(0L); PAssert.thatSingleton(countStackTracesOf(failures, UserCodeTimeoutException.class)) .isEqualTo(1L); @@ -158,6 +172,23 @@ public void givenCallerThrowsTimeoutException_emitsFailurePCollection() { pipeline.run(); } + @Test + public void givenCallerThrowsRemoteSystemException_thenPreservesExceptionType() { + Result result = + pipeline + .apply(Create.of(new Request("a"))) + .apply( + Call.of(new CallerThrowsRemoteSystemException(), NON_DETERMINISTIC_RESPONSE_CODER)); + + PCollection failures = result.getFailures(); + PAssert.thatSingleton(countStackTracesOf(failures, UserCodeRemoteSystemException.class)) + .isEqualTo(1L); + PAssert.thatSingleton(countStackTracesOf(failures, UserCodeExecutionException.class)) + .isEqualTo(0L); + + pipeline.run(); + } + @Test public void givenSetupThrowsUserCodeExecutionException_throwsError() { pipeline @@ -182,8 +213,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 +261,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 +301,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 +312,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 +321,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 +388,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); } @@ -376,6 +406,14 @@ public Response call(Request request) throws UserCodeExecutionException { } } + private static class CallerThrowsRuntimeException implements Caller { + + @Override + public Response call(Request request) { + throw new RuntimeException("unexpected error"); + } + } + private static class CallerThrowsTimeout implements Caller { @Override @@ -384,6 +422,14 @@ public Response call(Request request) throws UserCodeExecutionException { } } + private static class CallerThrowsRemoteSystemException implements Caller { + + @Override + public Response call(Request request) throws UserCodeExecutionException { + throw new UserCodeRemoteSystemException(""); + } + } + private static class CallerInvokesQuotaException implements Caller { @Override @@ -397,16 +443,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 +462,7 @@ public void setup() throws UserCodeExecutionException { } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class SetupThrowsUserCodeQuotaException implements SetupTeardown { @@ -426,7 +472,7 @@ public void setup() throws UserCodeExecutionException { } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class SetupThrowsUserCodeTimeoutException implements SetupTeardown { @@ -436,28 +482,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 +513,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 +523,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 +565,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 +586,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 +595,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); diff --git a/sdks/java/io/singlestore/src/main/java/org/apache/beam/sdk/io/singlestore/SingleStoreDefaultRowMapper.java b/sdks/java/io/singlestore/src/main/java/org/apache/beam/sdk/io/singlestore/SingleStoreDefaultRowMapper.java index 6837f1893d06..dcfb1813c494 100644 --- a/sdks/java/io/singlestore/src/main/java/org/apache/beam/sdk/io/singlestore/SingleStoreDefaultRowMapper.java +++ b/sdks/java/io/singlestore/src/main/java/org/apache/beam/sdk/io/singlestore/SingleStoreDefaultRowMapper.java @@ -64,6 +64,7 @@ import org.apache.beam.sdk.values.Row; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; import org.joda.time.chrono.ISOChronology; @@ -256,7 +257,7 @@ Object getValue(ResultSet rs, Integer index) throws SQLException { return null; } return new DateTime(time.getTime(), ISOChronology.getInstanceUTC()) - .withDate(new LocalDate(0L)); + .withDate(new LocalDate(0L, DateTimeZone.UTC)); } @Override diff --git a/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/crosslanguage/package-info.java b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/crosslanguage/package-info.java index 0c0c8aa0e53a..32af28b9dde5 100644 --- a/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/crosslanguage/package-info.java +++ b/sdks/java/io/snowflake/src/main/java/org/apache/beam/sdk/io/snowflake/crosslanguage/package-info.java @@ -17,8 +17,4 @@ */ /** Cross-language for SnowflakeIO. */ -@DefaultAnnotation(NonNull.class) package org.apache.beam.sdk.io.snowflake.crosslanguage; - -import edu.umd.cs.findbugs.annotations.DefaultAnnotation; -import edu.umd.cs.findbugs.annotations.NonNull; diff --git a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/BrokerResponse.java b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/BrokerResponse.java index 1a47f8012285..6f3f4248b7ca 100644 --- a/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/BrokerResponse.java +++ b/sdks/java/io/solace/src/main/java/org/apache/beam/sdk/io/solace/broker/BrokerResponse.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.stream.Collectors; import org.checkerframework.checker.nullness.qual.Nullable; @@ -35,10 +36,14 @@ public BrokerResponse(int responseCode, String message, @Nullable InputStream co this.code = responseCode; this.message = message; if (content != null) { - this.content = - new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8)) - .lines() - .collect(Collectors.joining("\n")); + // Use try-with-resources so the underlying InputStream is always closed once the + // response body has been read; otherwise the HTTP connection stream leaks. + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))) { + this.content = reader.lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read broker response content", e); + } } } diff --git a/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/broker/BrokerResponseTest.java b/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/broker/BrokerResponseTest.java new file mode 100644 index 000000000000..086142194236 --- /dev/null +++ b/sdks/java/io/solace/src/test/java/org/apache/beam/sdk/io/solace/broker/BrokerResponseTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.solace.broker; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.junit.Test; + +public class BrokerResponseTest { + + /** An {@link java.io.InputStream} that records whether {@code close()} was invoked. */ + private static class CloseTrackingInputStream extends ByteArrayInputStream { + private boolean closed = false; + + CloseTrackingInputStream(String data) { + super(data.getBytes(StandardCharsets.UTF_8)); + } + + boolean wasClosed() { + return closed; + } + + @Override + public void close() { + closed = true; + } + } + + @Test + public void testContentIsReadAndStreamIsClosed() { + CloseTrackingInputStream stream = new CloseTrackingInputStream("line1\nline2"); + + BrokerResponse response = new BrokerResponse(200, "OK", stream); + + assertEquals("line1\nline2", response.content); + assertTrue( + "the content InputStream must be closed after the response body has been read", + stream.wasClosed()); + } + + @Test + public void testNullContentIsHandled() { + BrokerResponse response = new BrokerResponse(204, "No Content", null); + + assertNull(response.content); + } +} diff --git a/sdks/java/io/sparkreceiver/3/build.gradle b/sdks/java/io/sparkreceiver/3/build.gradle index c57cbac2fd3d..a35a7f3d1fda 100644 --- a/sdks/java/io/sparkreceiver/3/build.gradle +++ b/sdks/java/io/sparkreceiver/3/build.gradle @@ -39,6 +39,12 @@ configurations.all { exclude group: "org.slf4j", module: "jul-to-slf4j" } +configurations.configureEach { + resolutionStrategy.capabilitiesResolution.withCapability("org.lz4:lz4-java") { + select("at.yawk.lz4:lz4-java:1.10.2") + } +} + dependencies { implementation library.java.commons_lang3 implementation library.java.joda_time 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..27c647478e17 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,7 +93,9 @@ 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 DELTA_LAKE_CDC = "delta_cdc"; public static final String KAFKA = "kafka"; public static final String BIGQUERY = "bigquery"; public static final String POSTGRES = "postgres"; @@ -104,7 +106,10 @@ 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( + DELTA_LAKE_CDC, getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_CDC_READ)) .put(KAFKA, getUrn(ExternalTransforms.ManagedTransforms.Urns.KAFKA_READ)) .put(BIGQUERY, getUrn(ExternalTransforms.ManagedTransforms.Urns.BIGQUERY_READ)) .put(POSTGRES, getUrn(ExternalTransforms.ManagedTransforms.Urns.POSTGRES_READ)) @@ -128,8 +133,12 @@ public class Managed { *

+ @@ -809,7 +812,11 @@ This table provides a consolidated, at-a-glance overview of the available built- native - + @@ -878,6 +885,22 @@ This table provides a consolidated, at-a-glance overview of the available built- + + + + + + + + + + + + + @@ -1249,10 +1272,17 @@ This table provides a consolidated, at-a-glance overview of the available built- ✔ native + - - + @@ -1289,6 +1319,22 @@ This table provides a consolidated, at-a-glance overview of the available built- + + + + + + + + + + + + +
Not available + ✔ + via X-language + Not available Not available Not available Not availableNot available + ✔ +
+ read / write +
ArrowFlightIO + ✔ + native + Not availableNot availableNot availableNot available
DatabaseIO + ✔ + via X-language + Not available Not availableNot availableNot available + ✔ +
+ write +
Delta Lake + ✔ + native + Not availableNot availableNot availableNot available
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..70b5efe9ed17 100644 --- a/website/www/site/content/en/documentation/io/managed-io.md +++ b/website/www/site/content/en/documentation/io/managed-io.md @@ -59,7 +59,34 @@ and Beam SQL is invoked via the Managed API under the hood. Write Configuration - ICEBERG_CDC + DELTA + + table (str)
+ hadoop_config (map[str, str])
+ timestamp (str)
+ version (int64)
+ + + Unavailable + + + + DELTA_CDC + + table (str)
+ start_version (int64)
+ start_timestamp (str)
+ end_version (int64)
+ end_timestamp (str)
+ hadoop_config (map[str, str])
+ include_metadata_columns (list[str])
+ + + Unavailable + + + + ICEBERG table (str)
catalog_name (str)
@@ -67,17 +94,23 @@ and Beam SQL is invoked via the Managed API under the hood. config_properties (map[str, str])
drop (list[str])
filter (str)
- from_snapshot (int64)
- from_timestamp (int64)
keep (list[str])
- poll_interval_seconds (int32)
- starting_strategy (str)
- streaming (boolean)
- to_snapshot (int64)
- to_timestamp (int64)
- Unavailable + 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)
@@ -109,7 +142,7 @@ and Beam SQL is invoked via the Managed API under the hood. - ICEBERG + ICEBERG_CDC table (str)
catalog_name (str)
@@ -117,29 +150,24 @@ and Beam SQL is invoked via the Managed API under the hood. config_properties (map[str, str])
drop (list[str])
filter (str)
+ from_snapshot (int64)
+ from_timestamp (int64)
keep (list[str])
+ poll_interval_seconds (int32)
+ starting_strategy (str)
+ streaming (boolean)
+ to_snapshot (int64)
+ to_timestamp (int64)
- 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)
+ Unavailable - MYSQL + POSTGRES jdbc_url (str)
- connection_init_sql (list[str])
connection_properties (str)
- disable_auto_commit (boolean)
fetch_size (int32)
location (str)
num_partitions (int32)
@@ -153,7 +181,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)
@@ -162,10 +189,11 @@ and Beam SQL is invoked via the Managed API under the hood. - POSTGRES + SQLSERVER jdbc_url (str)
connection_properties (str)
+ disable_auto_commit (boolean)
fetch_size (int32)
location (str)
num_partitions (int32)
@@ -187,9 +215,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 +252,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,30 +260,157 @@ and Beam SQL is invoked via the Managed API under the hood. write_statement (str)
+ + + +## Configuration Details + +### `DELTA` Read + +

+ + + + + + - + + + + + + + + + + + + + + + +
ConfigurationTypeDescription
BIGQUERY - kms_key (str)
- query (str)
- row_restriction (str)
- fields (list[str])
- table (str)
+ table
- table (str)
- drop (list[str])
- keep (list[str])
- kms_key (str)
- only (str)
- triggering_frequency_seconds (int64)
+ str +
+ Identifier of the Delta Lake table. +
+ hadoop_config + + map[str, str] + + Properties passed to the Hadoop Configuration. +
+ timestamp + + str + + Timestamp of the Delta Lake table to read. +
+ version + + int64 + + Version of the Delta Lake table to read.
-## Configuration Details +### `DELTA_CDC` Read -### `ICEBERG_CDC` Read +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConfigurationTypeDescription
+ table + + str + + Identifier of the Delta Lake table. +
+ start_version + + int64 + + Start version of the Delta Lake table to read changes from. Either start_version or start_timestamp must be set. +
+ start_timestamp + + str + + Start timestamp of the Delta Lake table to read changes from. Either start_version or start_timestamp must be set. +
+ end_version + + int64 + + End version of the Delta Lake table to read changes up to. +
+ end_timestamp + + str + + End timestamp of the Delta Lake table to read changes up to. +
+ hadoop_config + + map[str, str] + + Properties passed to the Hadoop Configuration. +
+ include_metadata_columns + + list[str] + + Metadata columns to include in the output rows. Supported columns are: _change_type, _commit_version, and _commit_timestamp. +
+
+ +### `ICEBERG` Read
@@ -312,179 +487,194 @@ and Beam SQL is invoked via the Managed API under the hood. +
- from_snapshot + keep - int64 + list[str] - Starts reading from this snapshot ID (inclusive). + A subset of column names to read exclusively. If null or empty, all columns will be read.
+
+ +### `ICEBERG` Write + +
+ + + + + + -
ConfigurationTypeDescription
- from_timestamp + table - int64 + str - Starts reading from the first snapshot (inclusive) that was created after this timestamp (in milliseconds). + 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`.
- keep + autosharding - list[str] + boolean - A subset of column names to read exclusively. If null or empty, all columns will be read. + 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.
- poll_interval_seconds + catalog_name - int32 + str - The interval at which to poll for new snapshots. Defaults to 60 seconds. + Name of the catalog containing the table.
- starting_strategy + catalog_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 used to set up the Iceberg catalog.
- streaming + config_properties - boolean + map[str, str] - Enables streaming reads, where source continuously polls for snapshots forever. + Properties passed to the Hadoop Configuration.
- to_snapshot + direct_write_byte_limit - int64 + int32 - Reads up to this snapshot ID (inclusive). + For a streaming pipeline, sets the limit for lifting bundles into the direct write path.
- to_timestamp + distribution_mode - int64 + str - Reads up to the latest snapshot (inclusive) created before this timestamp (in milliseconds). + Defines distribution of write data. Supported distributions: +- none: don't shuffle rows (default) +- hash: shuffle rows by partition key before writing data
-
- -### `KAFKA` Write - -
- - - - - -
ConfigurationTypeDescription
- bootstrap_servers + drop - str + list[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 field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'.
- format + keep - str + list[str] - The encoding format for the data stored in Kafka. Valid options are: RAW,JSON,AVRO,PROTO + 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'.
- topic + only str - n/a + The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'.
- file_descriptor_path + partition_fields - str + list[str] - The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization. + 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.
- message_name + sort_fields - str + list[str] - The name of the Protocol Buffer message to be used for schema extraction and data conversion. + 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.
- producer_config_updates + table_properties 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 + 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.
- schema + triggering_frequency_seconds - str + int32 - n/a + For a streaming pipeline, sets the frequency at which snapshots are produced.
@@ -656,7 +846,7 @@ and Beam SQL is invoked via the Managed API under the hood.
-### `ICEBERG` Read +### `KAFKA` Write
@@ -667,85 +857,85 @@ and Beam SQL is invoked via the Managed API under the hood.
- 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. | Format: host1:port1,host2:port2,...
- catalog_name + format str - Name of the catalog containing the table. + The encoding format for the data stored in Kafka. Valid options are: RAW,JSON,AVRO,PROTO
- catalog_properties + topic - map[str, str] + str - Properties used to set up the Iceberg catalog. + n/a
- config_properties + file_descriptor_path - map[str, str] + str - Properties passed to the Hadoop Configuration. + The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization.
- drop + message_name - list[str] + str - A subset of column names to exclude from reading. If null or empty, all columns will be read. + The name of the Protocol Buffer message to be used for schema extraction and data conversion.
- filter + producer_config_updates - str + map[str, 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 + 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
- keep + schema - list[str] + str - A subset of column names to read exclusively. If null or empty, all columns will be read. + n/a
-### `ICEBERG` Write +### `ICEBERG_CDC` Read
@@ -762,7 +952,7 @@ and Beam SQL is invoked via the Managed API under the hood. str @@ -800,147 +990,146 @@ 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`. + Identifier of the Iceberg table.
- direct_write_byte_limit + drop - int32 + list[str] - For a streaming pipeline, sets the limit for lifting bundles into the direct write path. + A subset of column names to exclude from reading. If null or empty, all columns will be read.
- drop + filter - list[str] + str - A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'. + 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
- keep + from_snapshot - list[str] + int64 - 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'. + Starts reading from this snapshot ID (inclusive).
- only + from_timestamp - str + int64 - The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'. + Starts reading from the first snapshot (inclusive) that was created after this timestamp (in milliseconds).
- partition_fields + keep 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: - -- `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. + A subset of column names to read exclusively. If null or empty, all columns will be read.
- table_properties + poll_interval_seconds - map[str, str] + int32 - 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. + The interval at which to poll for new snapshots. Defaults to 60 seconds.
- triggering_frequency_seconds + starting_strategy - int32 + str - For a streaming pipeline, sets the frequency at which snapshots are produced. + 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.
-
- -### `MYSQL` Read - -
- - - - + + + +
ConfigurationTypeDescription + streaming + + boolean + + Enables streaming reads, where source continuously polls for snapshots forever. +
- jdbc_url + to_snapshot - str + int64 - Connection URL for the JDBC source. + Reads up to this snapshot ID (inclusive).
- connection_init_sql + to_timestamp - list[str] + int64 - Sets the connection init sql statements used by the Driver. Only MySQL and MariaDB support this. + Reads up to the latest snapshot (inclusive) created before this timestamp (in milliseconds).
+
+ +### `POSTGRES` Read + +
+ + + + + + @@ -1034,7 +1223,7 @@ For more information on table properties, please visit https://iceberg.apache.or
ConfigurationTypeDescription
- connection_properties + jdbc_url 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;". + Connection URL for the JDBC source.
- disable_auto_commit + connection_properties - boolean + 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. + 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;".
-### `MYSQL` Write +### `POSTGRES` Write
@@ -1076,17 +1265,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 +1323,7 @@ For more information on table properties, please visit https://iceberg.apache.or
-### `POSTGRES` Write +### `SQLSERVER` Write
@@ -1245,7 +1423,7 @@ For more information on table properties, please visit https://iceberg.apache.or
-### `POSTGRES` Read +### `SQLSERVER` Read
@@ -1276,6 +1454,17 @@ For more information on table properties, please visit https://iceberg.apache.or 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 @@ -1367,7 +1556,7 @@ For more information on table properties, please visit https://iceberg.apache.or
-### `SQLSERVER` Read +### `BIGQUERY` Read
@@ -1378,129 +1567,141 @@ For more information on table properties, please visit https://iceberg.apache.or +
- jdbc_url + kms_key str - Connection URL for the JDBC source. + Use this Cloud KMS key to encrypt your data
- connection_properties + query 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;". + The SQL query to be executed to read from the BigQuery table.
- disable_auto_commit + row_restriction - boolean + 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. + Read only rows that match this filter, which must be compatible with Google standard SQL. This is not supported when reading via query.
- fetch_size + fields - int32 + list[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. + 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"
- location + table str - Name of the table to read from. + The fully-qualified name of the BigQuery table to read from. Format: [${PROJECT}:]${DATASET}.${TABLE}
+
+ +### `BIGQUERY` Write + +
+ + + + + +
ConfigurationTypeDescription
- num_partitions + table - int32 + str - The number of partitions + The bigquery table to write to. Format: [${PROJECT}:]${DATASET}.${TABLE}
- output_parallelization + drop - boolean + list[str] - Whether to reshuffle the resulting PCollection so results are distributed to all workers. + A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'.
- partition_column + keep - str + list[str] - Name of a column of numeric type that will be used for partitioning. + 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
- read_query + only str - SQL query used to query the JDBC source. + The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'.
- username + triggering_frequency_seconds - str + int64 - Username for the JDBC source. + Determines how often to 'commit' progress into BigQuery. Default is every 5 seconds.
-### `SQLSERVER` Write +### `MYSQL` Read
@@ -1517,29 +1718,18 @@ For more information on table properties, please visit https://iceberg.apache.or str - - - - - @@ -1555,119 +1745,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 +1856,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.
diff --git a/website/www/site/content/en/documentation/runners/flink.md b/website/www/site/content/en/documentation/runners/flink.md index 1aafa41ecb9c..5f0eb482c53b 100644 --- a/website/www/site/content/en/documentation/runners/flink.md +++ b/website/www/site/content/en/documentation/runners/flink.md @@ -93,7 +93,7 @@ from the [compatibility table](#flink-version-compatibility) below. For example: {{< highlight java >}} org.apache.beam - beam-runners-flink-1.18 + beam-runners-flink-1.20 {{< param release_latest >}} {{< /highlight >}} @@ -166,7 +166,7 @@ If you have a Flink `JobManager` running on your local machine you can provide ` To run a pipeline on Flink, set the runner to `FlinkRunner` and `flink_master` to the master URL of a Flink cluster. In addition, optionally set `environment_type` set to `LOOPBACK`. For example, -after starting up a [local flink cluster](https://ci.apache.org/projects/flink/flink-docs-release-1.18/getting-started/tutorials/local_setup.html), +after starting up a [local flink cluster](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/try-flink/local_installation/), one could run: {{< /paragraph >}} @@ -196,9 +196,10 @@ The optional `flink_version` option may be required as well for older versions o {{< paragraph class="language-portable" >}} Starting with Beam 2.18.0, pre-built Flink Job Service Docker images are available at Docker Hub: -[Flink 1.16](https://hub.docker.com/r/apache/beam_flink1.16_job_server). -[Flink 1.17](https://hub.docker.com/r/apache/beam_flink1.17_job_server). -[Flink 1.18](https://hub.docker.com/r/apache/beam_flink1.18_job_server). +[Flink 1.19](https://hub.docker.com/r/apache/beam_flink1.19_job_server). +[Flink 1.20](https://hub.docker.com/r/apache/beam_flink1.20_job_server). +For Flink 2 and later the images share one repository, with the Beam and Flink versions in the tag, +for example [beam_flink_job_server:2.75.0-flink2.0](https://hub.docker.com/layers/apache/beam_flink_job_server/2.75.0-flink2.0). {{< /paragraph >}} @@ -207,7 +208,7 @@ To run a pipeline on an embedded Flink cluster: {{< /paragraph >}} {{< paragraph class="language-portable" >}} -(1) Start the JobService endpoint: `docker run --net=host apache/beam_flink1.18_job_server:latest` +(1) Start the JobService endpoint: `docker run --net=host apache/beam_flink1.20_job_server:latest` {{< /paragraph >}} {{< paragraph class="language-portable" >}} @@ -217,7 +218,7 @@ You might encounter an error message like `Caused by: java.io.IOException: Insuf This can be resolved by providing a Flink configuration file to override the default settings. You can find an example configuration file [here](https://github.com/apache/beam/blob/master/runners/flink/src/test/resources/flink-conf.yaml). To start the Job Service endpoint with your custom configuration, mount a local directory containing your Flink configuration to the `/flink-conf` path in the Docker container and pass this as `--flink-conf-dir`: -`docker run --net=host -v :/flink-conf beam-flink-runner apache/beam_flink1.18_job_server:latest --flink-conf-dir /flink-conf` +`docker run --net=host -v :/flink-conf beam-flink-runner apache/beam_flink1.20_job_server:latest --flink-conf-dir /flink-conf` {{< /paragraph >}} {{< paragraph class="language-portable" >}} @@ -240,7 +241,7 @@ with beam.Pipeline(options) as p: {{< paragraph class="language-portable" >}} -To run on a separate [Flink cluster](https://ci.apache.org/projects/flink/flink-docs-release-1.18/getting-started/tutorials/local_setup.html): +To run on a separate [Flink cluster](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/try-flink/local_installation/): {{< /paragraph >}} {{< paragraph class="language-portable" >}} @@ -248,7 +249,7 @@ To run on a separate [Flink cluster](https://ci.apache.org/projects/flink/flink- {{< /paragraph >}} {{< paragraph class="language-portable" >}} -(2) Start JobService with Flink Rest endpoint: `docker run --net=host apache/beam_flink1.18_job_server:latest --flink-master=localhost:8081`. +(2) Start JobService with Flink Rest endpoint: `docker run --net=host apache/beam_flink1.20_job_server:latest --flink-master=localhost:8081`. {{< /paragraph >}} {{< paragraph class="language-portable" >}} @@ -316,8 +317,8 @@ reference. ## Flink Version Compatibility The Flink cluster version has to match the minor version used by the FlinkRunner. -The minor version is the first two numbers in the version string, e.g. in `1.18.0` the -minor version is `1.18`. +The minor version is the first two numbers in the version string, e.g. in `1.20.0` the +minor version is `1.20`. We try to track the latest version of Apache Flink at the time of the Beam release. A Flink version is supported by Beam for the time it is supported by the Flink community. @@ -330,6 +331,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 @@ -348,12 +359,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 diff --git a/website/www/site/content/en/documentation/runners/spark.md b/website/www/site/content/en/documentation/runners/spark.md index 29ef5c28102a..fce4927a82c9 100644 --- a/website/www/site/content/en/documentation/runners/spark.md +++ b/website/www/site/content/en/documentation/runners/spark.md @@ -271,7 +271,7 @@ gcloud dataproc clusters create CLUSTER_NAME \ 2. Create a Cloud Storage bucket.
-gsutil mb BUCKET_NAME
+gcloud storage buckets create gs://BUCKET_NAME
 
3. Install the necessary Python libraries for the job in your local environment. @@ -313,7 +313,7 @@ gcloud dataproc jobs submit spark \ 6. Check that the results were written to your bucket.
-gsutil cat gs://BUCKET_NAME/python-wordcount-out-SHARD_ID
+gcloud storage cat gs://BUCKET_NAME/python-wordcount-out-SHARD_ID
 
diff --git a/website/www/site/content/en/documentation/sdks/python-multi-language-pipelines.md b/website/www/site/content/en/documentation/sdks/python-multi-language-pipelines.md index 332a62901a23..2f4bfd7299fc 100644 --- a/website/www/site/content/en/documentation/sdks/python-multi-language-pipelines.md +++ b/website/www/site/content/en/documentation/sdks/python-multi-language-pipelines.md @@ -203,7 +203,7 @@ export NUM_WORKERS="1" # other commands, e.g. changing into the appropriate directory -gsutil rm gs://$GCS_BUCKET/javaprefix/* +gcloud storage rm gs://$GCS_BUCKET/javaprefix/* python addprefix.py \ --runner DataflowRunner \ diff --git a/website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md b/website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md index 26dc1db1bc06..7d27f91d322d 100644 --- a/website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md +++ b/website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md @@ -91,9 +91,48 @@ If your pipeline uses packages that are not available publicly (e.g. packages th See the [build documentation](https://pypa-build.readthedocs.io/en/latest/index.html) for more details on this command. +## Staging Individual Files {#staging-files} + +If your pipeline relies on one or more individual Python files or non-Python data files that do not need to be packaged as a full Python package, you can stage them individually to the remote workers. + +To stage individual files, run your pipeline with the `--files_to_stage` (or `--file_to_stage`) pipeline option. This option accepts a list of local file paths: + + --files_to_stage="/path/to/my_module.py,/path/to/data_config.json" + +When the pipeline runs, the runner uploads these files and makes them available on the workers in the worker's staged files directory. + +### Accessing Staged Files on the Workers + +Staged files are downloaded onto the worker: + +* **Python modules**: Starting with Apache Beam 2.76.0, to make it easy to import staged Python files as modules, Beam automatically appends the worker's staged files directory to the Python interpreter's search path (`sys.path`) during startup. This means you can import them directly in your code: + + import my_module + +* **Data or configuration files**: If you staged non-Python files (such as a JSON config), they are downloaded to the staged files directory. You can locate this directory on the worker by reading the `SEMI_PERSISTENT_DIRECTORY` environment variable, or look for `/tmp/staged` which is the default location for staged files on containerized runners: + + import os + staged_dir = os.environ.get('SEMI_PERSISTENT_DIRECTORY', '/tmp/staged') + config_path = os.path.join(staged_dir, 'data_config.json') + +### Importing Plugins on Worker Startup + +You can use staged files in combination with the `--beam_plugins` pipeline option (supported starting with Apache Beam 2.76.0) to run initialization code on the workers before any processing starts. + +To use a staged file as a plugin: +1. Stage the plugin file (e.g. `my_custom_plugin.py`) using `--file_to_stage`. +2. Reference the module name in `--beam_plugins`. + +For example, run your pipeline with: + + --file_to_stage="/path/to/my_custom_plugin.py" \ + --beam_plugins="my_custom_plugin" + +This instructs the SDK worker to import `my_custom_plugin` immediately on startup, triggering any initialization logic defined in the module. + ## Multiple File Dependencies {#multiple-file-dependencies} -Often, your pipeline code spans multiple files. To run your project remotely, you must group these files as a Python package and specify the package when you run your pipeline. When the remote workers start, they will install your package. To group your files as a Python package and make it available remotely, perform the following steps: +Often, your pipeline code spans multiple files. To run your project remotely, it is best to group these files as a Python package and specify the package when you run your pipeline. When the remote workers start, they will install your package. To group your files as a Python package and make it available remotely, perform the following steps: 1. Create a [setup.py](https://pythonhosted.org/an_example_pypi_project/setuptools.html) file for your project. The following is a very basic `setup.py` file. 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 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). 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/content/en/get-started/downloads.md b/website/www/site/content/en/get-started/downloads.md index c3a3eff7f533..c9250d1e7f3c 100644 --- a/website/www/site/content/en/get-started/downloads.md +++ b/website/www/site/content/en/get-started/downloads.md @@ -95,23 +95,39 @@ versions denoted `0.x.y`. ### Current release -#### 2.73.0 (2026-04-29) +#### 2.75.0 (2026-07-08) -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.75.0/apache-beam-2.75.0-source-release.zip). +[SHA-512](https://downloads.apache.org/beam/2.75.0/apache-beam-2.75.0-source-release.zip.sha512). +[signature](https://downloads.apache.org/beam/2.75.0/apache-beam-2.75.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.75.0) ### Archived releases +#### 2.74.0 (2026-06-02) + +Official [source code download](https://archive.apache.org/dist/beam/2.74.0/apache-beam-2.74.0-source-release.zip). +[SHA-512](https://archive.apache.org/dist/beam/2.74.0/apache-beam-2.74.0-source-release.zip.sha512). +[signature](https://archive.apache.org/dist/beam/2.74.0/apache-beam-2.74.0-source-release.zip.asc). + +[Release notes](https://github.com/apache/beam/releases/tag/v2.74.0) + +#### 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 +135,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) 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 d911918b9d2b..b808f834ae49 100644 --- a/website/www/site/content/en/get-started/quickstart-java.md +++ b/website/www/site/content/en/get-started/quickstart-java.md @@ -344,7 +344,7 @@ ls /tmp/counts* ls counts* {{< /runner >}} {{< runner dataflow >}} -gsutil ls gs:///counts* +gcloud storage ls gs:///counts* {{< /runner >}} {{< runner nemo >}} ls counts* @@ -368,7 +368,7 @@ more /tmp/counts* more counts* {{< /runner >}} {{< runner dataflow >}} -gsutil cat gs:///counts* +gcloud storage cat gs:///counts* {{< /runner >}} {{< runner nemo >}} more counts* 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..e544a1fcc692 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 @@ -97,6 +97,23 @@ The above installation will not install all the extra dependencies for using fea - **Beam YAML Dataframe API** - Installation Command: `pip install 'apache-beam[dataframe]'` - Required for using [Beam Dataframe API](/documentation/dsls/dataframes/overview/) +- **[Machine Learning (Beam ML)](/documentation/ml/about-ml)** + - Installation Command: `pip install 'apache-beam[]'` + - Required for machine learning frameworks and utilities: + - `ml_cpu` ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.html)) + - `milvus` ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.rag.enrichment.milvus_search.html)) + - `onnx` ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.onnx_inference.html)) + - [**tensorflow**](/documentation/ml/about-ml#tensorflow) ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.tensorflow_inference.html)) + - `tensorflow-hub` ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.transforms.embeddings.tensorflow_hub.html)) + - [**tft**](/documentation/ml/preprocess-data#data-processing-transforms-that-use-tft) ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.transforms.tft.html)) + - [**torch**](/documentation/ml/about-ml#pytorch) ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.pytorch_inference.html)) + - `transformers` ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.huggingface_inference.html)) + - [**vllm**](/documentation/ml/large-language-modeling) ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.vllm_inference.html)) + - `xgboost` ([pydoc](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.xgboost_inference.html)) +- **Interactive Beam** + - Installation Command: `pip install 'apache-beam[interactive]'` + - Required for using [Beam Interactive Runner](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/runners/interactive/README.md) in Jupyter notebooks + - To also install testing interactive notebook features, use `apache-beam[interactive,interactive_test]`. - **Tests** - Installation Command: `pip install 'apache-beam[test]'` - Required for developing Beam and running unit tests diff --git a/website/www/site/content/en/performance/_index.md b/website/www/site/content/en/performance/_index.md index 1624c58efe2e..08fd368809c8 100644 --- a/website/www/site/content/en/performance/_index.md +++ b/website/www/site/content/en/performance/_index.md @@ -38,6 +38,7 @@ writing to various Beam IOs. - [BigQuery](/performance/bigquery) - [BigTable](/performance/bigtable) - [TextIO](/performance/textio) +- [IcebergIO](/performance/icebergio) # Measured Beam Python ML Pipelines @@ -46,6 +47,14 @@ See the following pages for performance measures recorded when running various B ## Streaming - [PyTorch Sentiment Analysis Streaming DistilBERT base](/performance/pytorchbertsentimentstreaming) +- [PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) CPU](/performance/pytorchimagenetrightfitcpu) +- [PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting Exactly-once) CPU](/performance/pytorchimagenetrightfitoncecpu) +- [PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) GPU](/performance/pytorchimagenetrightfitgpu) +- [PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting Exactly-once)](/performance/pytorchimagenetrightfitoncegpu) +- [PyTorch Image Captioning BLIP + CLIP Streaming CPU](/performance/pytorchimagecaptioningstreamingcpu) +- [PyTorch Image Captioning BLIP + CLIP Streaming GPU](/performance/pytorchimagecaptioningstreaminggpu) +- [PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming CPU](/performance/pytorchimageobjectdetectionstreamingcpu) +- [PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming GPU](/performance/pytorchimageobjectdetectionstreaminggpu) - [Table Row Inference Sklearn Streaming](/performance/tablerowinferencestreaming) ## Batch @@ -59,3 +68,12 @@ 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) +- [PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch CPU](/performance/pytorchimageobjectdetectionbatchcpu) +- [PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch GPU](/performance/pytorchimageobjectdetectionbatchgpu) +- [PyTorch Image Captioning BLIP + CLIP Batch CPU](/performance/pytorchimagecaptioningbatchcpu) +- [PyTorch Image Captioning BLIP + CLIP Batch GPU](/performance/pytorchimagecaptioningbatchgpu) +- [MLTransform Generate Vocab (batch)](/performance/mltransformvocab) +- [MLTransform One-Hot Encoding](/performance/mltransformonehot) +- [MLTransform Text Embedding](/performance/mltransform-text-embedding) +- [MLTransform Image Embedding GPU](/performance/mltransform-image-embedding-gpu) +- [MLTransform Image Embedding CPU](/performance/mltransform-image-embedding-cpu) diff --git a/website/www/site/content/en/performance/icebergio/_index.md b/website/www/site/content/en/performance/icebergio/_index.md new file mode 100644 index 000000000000..e618865097b4 --- /dev/null +++ b/website/www/site/content/en/performance/icebergio/_index.md @@ -0,0 +1,50 @@ +--- +title: "IcebergIO Performance" +--- + + + +# IcebergIO Performance + +The following graphs show various metrics when reading from or writing to Apache Iceberg tables using +IcebergIO. See the [glossary](/performance/glossary) for definitions. + +## Read + +### What is the estimated cost of reading from Apache Iceberg tables using IcebergIO? + +{{< performance_looks io="icebergio" read_or_write="read" section="test_name" >}} + +### How has various metrics changed when reading from Apache Iceberg tables using IcebergIO for different Beam SDK versions? + +{{< performance_looks io="icebergio" read_or_write="read" section="version" >}} + +### How has various metrics changed over time when reading from Apache Iceberg tables using IcebergIO? + +{{< performance_looks io="icebergio" read_or_write="read" section="date" >}} + +## Write + +### What is the estimated cost of writing to Apache Iceberg tables using IcebergIO? + +{{< performance_looks io="icebergio" read_or_write="write" section="test_name" >}} + +### How has various metrics changed when writing to Apache Iceberg tables using IcebergIO for different Beam SDK versions? + +{{< performance_looks io="icebergio" read_or_write="write" section="version" >}} + +### How has various metrics changed over time when writing to Apache Iceberg tables using IcebergIO? + +{{< performance_looks io="icebergio" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/mltransform-image-embedding-cpu/_index.md b/website/www/site/content/en/performance/mltransform-image-embedding-cpu/_index.md new file mode 100644 index 000000000000..6780093b244a --- /dev/null +++ b/website/www/site/content/en/performance/mltransform-image-embedding-cpu/_index.md @@ -0,0 +1,48 @@ +--- +title: "MLTransform Image Embedding CPU Performance" +--- + + + +# MLTransform Image Embedding CPU Performance + +**Model**: Sentence Transformers — clip-ViT-B-32 (image) +**Accelerator**: CPU with Dataflow Prime right-fitting (16 GB min RAM) +**Host**: Dataflow Prime with throughput-based autoscaling + +This batch pipeline reads image URIs from GCS, decodes images with Pillow, +generates image embeddings through `MLTransform` with +`SentenceTransformerEmbeddings(image_model=True)`, and writes results to +BigQuery using batch file loads. + +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_image_embedding.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="mltransform-image-embedding-cpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="mltransform-image-embedding-cpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="mltransform-image-embedding-cpu" read_or_write="write" section="date" >}} + +See also [MLTransform Image Embedding GPU](/performance/mltransform-image-embedding-gpu) +for the Tesla T4 GPU variant of this pipeline. diff --git a/website/www/site/content/en/performance/mltransform-image-embedding-gpu/_index.md b/website/www/site/content/en/performance/mltransform-image-embedding-gpu/_index.md new file mode 100644 index 000000000000..f5f5f1fee50c --- /dev/null +++ b/website/www/site/content/en/performance/mltransform-image-embedding-gpu/_index.md @@ -0,0 +1,48 @@ +--- +title: "MLTransform Image Embedding GPU Performance" +--- + + + +# MLTransform Image Embedding GPU Performance + +**Model**: Sentence Transformers — clip-ViT-B-32 (image) +**Accelerator**: NVIDIA Tesla T4 GPU +**Host**: Dataflow Prime with throughput-based autoscaling + +This batch pipeline reads image URIs from GCS, decodes images with Pillow, +generates image embeddings through `MLTransform` with +`SentenceTransformerEmbeddings(image_model=True)`, and writes results to +BigQuery using batch file loads. + +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_image_embedding.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="mltransform-image-embedding-gpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="mltransform-image-embedding-gpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="mltransform-image-embedding-gpu" read_or_write="write" section="date" >}} + +See also [MLTransform Image Embedding CPU](/performance/mltransform-image-embedding-cpu) +for the CPU right-fitting variant of this pipeline. diff --git a/website/www/site/content/en/performance/mltransform-text-embedding/_index.md b/website/www/site/content/en/performance/mltransform-text-embedding/_index.md new file mode 100644 index 000000000000..051fb88b6c08 --- /dev/null +++ b/website/www/site/content/en/performance/mltransform-text-embedding/_index.md @@ -0,0 +1,44 @@ +--- +title: "MLTransform Text Embedding Performance" +--- + + + +# MLTransform Text Embedding Performance + +**Model**: Sentence Transformers — all-MiniLM-L6-v2 +**Accelerator**: CPU only +**Host**: 10 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This batch pipeline reads text lines from GCS, generates sentence embeddings +through `MLTransform` with `SentenceTransformerEmbeddings`, and writes JSONL +results to GCS. + +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_text_embedding.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="mltransform-text-embedding" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="mltransform-text-embedding" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="mltransform-text-embedding" read_or_write="write" section="date" >}} 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/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/content/en/performance/pytorchimagecaptioningbatchcpu/_index.md b/website/www/site/content/en/performance/pytorchimagecaptioningbatchcpu/_index.md new file mode 100644 index 000000000000..d5d5f094e553 --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimagecaptioningbatchcpu/_index.md @@ -0,0 +1,41 @@ +--- +title: "PyTorch Image Captioning BLIP + CLIP Batch CPU Performance" +--- + + + +# PyTorch Image Captioning BLIP + CLIP Batch CPU + +**Model**: PyTorch Image Captioning — BLIP (candidate generation) + CLIP (ranking) +**Accelerator**: CPU only +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This batch pipeline performs image captioning using a multi-model open-source PyTorch approach. +It first generates multiple candidate captions per image using a BLIP model, then ranks these candidates with a CLIP model based on image-text similarity. + +The following graphs show various metrics when running PyTorch Image Captioning BLIP + CLIP Batch CPU pipeline. +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/inference/pytorch_image_captioning.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimagecaptioningbatchcpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimagecaptioningbatchcpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimagecaptioningbatchcpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimagecaptioningbatchgpu/_index.md b/website/www/site/content/en/performance/pytorchimagecaptioningbatchgpu/_index.md new file mode 100644 index 000000000000..d0d12bf3f540 --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimagecaptioningbatchgpu/_index.md @@ -0,0 +1,41 @@ +--- +title: "PyTorch Image Captioning BLIP + CLIP Batch GPU Performance" +--- + + + +# PyTorch Image Captioning BLIP + CLIP Batch GPU + +**Model**: PyTorch Image Captioning — BLIP (candidate generation) + CLIP (ranking) +**Accelerator**: Tesla T4 GPU +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This batch pipeline performs image captioning using a multi-model open-source PyTorch approach. +It first generates multiple candidate captions per image using a BLIP model, then ranks these candidates with a CLIP model based on image-text similarity. + +The following graphs show various metrics when running PyTorch Image Captioning BLIP + CLIP Batch GPU pipeline. +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/inference/pytorch_image_captioning.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimagecaptioningbatchgpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimagecaptioningbatchgpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimagecaptioningbatchgpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimagecaptioningstreamingcpu/_index.md b/website/www/site/content/en/performance/pytorchimagecaptioningstreamingcpu/_index.md new file mode 100644 index 000000000000..41223f1bf0b0 --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimagecaptioningstreamingcpu/_index.md @@ -0,0 +1,41 @@ +--- +title: "PyTorch Image Captioning BLIP + CLIP Streaming CPU Performance" +--- + + + +# PyTorch Image Captioning BLIP + CLIP Streaming CPU + +**Model**: PyTorch Image Captioning — BLIP (candidate generation) + CLIP (ranking) +**Accelerator**: CPU only +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This streaming pipeline performs image captioning using a multi-model open-source PyTorch approach. +It first generates multiple candidate captions per image using a BLIP model, then ranks these candidates with a CLIP model based on image-text similarity. + +The following graphs show various metrics when running PyTorch Image Captioning BLIP + CLIP Streaming CPU pipeline. +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/inference/pytorch_image_captioning.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimagecaptioningstreamingcpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimagecaptioningstreamingcpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimagecaptioningstreamingcpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimagecaptioningstreaminggpu/_index.md b/website/www/site/content/en/performance/pytorchimagecaptioningstreaminggpu/_index.md new file mode 100644 index 000000000000..b7694baa82e2 --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimagecaptioningstreaminggpu/_index.md @@ -0,0 +1,41 @@ +--- +title: "PyTorch Image Captioning BLIP + CLIP Streaming GPU Performance" +--- + + + +# PyTorch Image Captioning BLIP + CLIP Streaming GPU + +**Model**: PyTorch Image Captioning — BLIP (candidate generation) + CLIP (ranking) +**Accelerator**: Tesla T4 GPU +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This streaming pipeline performs image captioning using a multi-model open-source PyTorch approach. +It first generates multiple candidate captions per image using a BLIP model, then ranks these candidates with a CLIP model based on image-text similarity. + +The following graphs show various metrics when running PyTorch Image Captioning BLIP + CLIP Streaming GPU pipeline. +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/inference/pytorch_image_captioning.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimagecaptioningstreaminggpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimagecaptioningstreaminggpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimagecaptioningstreaminggpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimagenetrightfitcpu/_index.md b/website/www/site/content/en/performance/pytorchimagenetrightfitcpu/_index.md new file mode 100644 index 000000000000..9b7b26d9b3ef --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimagenetrightfitcpu/_index.md @@ -0,0 +1,43 @@ +--- +title: "PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) CPU Performance" +--- + + + +# PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) + +**Model**: PyTorch Image Classification — EfficientNet-B0 (pretrained on ImageNet) +**Accelerator**: CPU only +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This streaming pipeline performs image classification using an open-source PyTorch EfficientNet-B0 model. +It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel, and runs inference with adaptive batch sizing. + +The following graphs show various metrics when running the PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) CPU pipeline. +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/inference/pytorch_imagenet_rightfit.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimagenetrightfitcpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimagenetrightfitcpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimagenetrightfitcpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimagenetrightfitgpu/_index.md b/website/www/site/content/en/performance/pytorchimagenetrightfitgpu/_index.md new file mode 100644 index 000000000000..fefd149b8416 --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimagenetrightfitgpu/_index.md @@ -0,0 +1,43 @@ +--- +title: "PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) GPU Performance" +--- + + + +# PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) + +**Model**: PyTorch Image Classification — EfficientNet-B0 (pretrained on ImageNet) +**Accelerator**: Tesla T4 GPU (right-fitted batch size 64 → 32 → 16 → 8) +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This streaming pipeline performs image classification using an open-source PyTorch EfficientNet-B0 model optimized for T4 GPUs. +It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel, and runs inference with adaptive batch sizing for optimal GPU utilization. + +The following graphs show various metrics when running the PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) GPU pipeline. +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/inference/pytorch_imagenet_rightfit.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimagenetrightfitgpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimagenetrightfitgpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimagenetrightfitgpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimagenetrightfitoncecpu/_index.md b/website/www/site/content/en/performance/pytorchimagenetrightfitoncecpu/_index.md new file mode 100644 index 000000000000..8e0cf82c349e --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimagenetrightfitoncecpu/_index.md @@ -0,0 +1,44 @@ +--- +title: "PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting, Exactly-once) CPU Performance" +--- + + + +# PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) + +**Model**: PyTorch Image Classification — EfficientNet-B0 (pretrained on ImageNet) +**Accelerator**: CPU only +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This streaming pipeline performs image classification using an open-source PyTorch EfficientNet-B0 model. +It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel, and runs inference with adaptive batch sizing. +The pipeline ensures exactly-once semantics via stateful deduplication and idempotent BigQuery writes, allowing stable and reproducible performance measurements under continuous load. + +The following graphs show various metrics when running the PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting, Exactly-once) CPU pipeline. +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/inference/pytorch_imagenet_rightfit.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimagenetrightfitoncecpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimagenetrightfitoncecpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimagenetrightfitoncecpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimagenetrightfitoncegpu/_index.md b/website/www/site/content/en/performance/pytorchimagenetrightfitoncegpu/_index.md new file mode 100644 index 000000000000..85a55b27955b --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimagenetrightfitoncegpu/_index.md @@ -0,0 +1,44 @@ +--- +title: "PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting, Exactly-once) GPU Performance" +--- + + + +# PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting) + +**Model**: PyTorch Image Classification — EfficientNet-B0 (pretrained on ImageNet) +**Accelerator**: Tesla T4 GPU (right-fitted batch size 64 → 32 → 16 → 8) +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This streaming pipeline performs image classification using an open-source PyTorch EfficientNet-B0 model optimized for T4 GPUs. +It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel, and runs inference with adaptive batch sizing for optimal GPU utilization. +The pipeline ensures exactly-once semantics via stateful deduplication and idempotent BigQuery writes, allowing stable and reproducible performance measurements under continuous load. + +The following graphs show various metrics when running the PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting, Exactly-once) GPU pipeline. +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/inference/pytorch_imagenet_rightfit.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimagenetrightfitoncegpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimagenetrightfitoncegpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimagenetrightfitoncegpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchcpu/_index.md b/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchcpu/_index.md new file mode 100644 index 000000000000..a2ef39a2cf32 --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchcpu/_index.md @@ -0,0 +1,41 @@ +--- +title: "PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch CPU Performance" +--- + + + +# PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch CPU + +**Model**: PyTorch Image Object Detection — Faster R-CNN ResNet-50 FPN (pretrained on COCO) +**Accelerator**: CPU only +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This batch pipeline performs object detection using an open-source PyTorch Faster R-CNN ResNet-50 FPN model on CPU. +It reads image URIs from GCS, decodes and preprocesses images, and runs batched inference with a fixed batch size. + +The following graphs show various metrics when running PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch CPU pipeline. +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/inference/pytorch_image_object_detection.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimageobjectdetectionbatchcpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimageobjectdetectionbatchcpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimageobjectdetectionbatchcpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchgpu/_index.md b/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchgpu/_index.md new file mode 100644 index 000000000000..f6d6107d19f9 --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchgpu/_index.md @@ -0,0 +1,41 @@ +--- +title: "PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch GPU Performance" +--- + + + +# PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch GPU + +**Model**: PyTorch Image Object Detection — Faster R-CNN ResNet-50 FPN (pretrained on COCO) +**Accelerator**: Tesla T4 GPU (fixed batch size) +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This batch pipeline performs object detection using an open-source PyTorch Faster R-CNN ResNet-50 FPN model on GPU. +It reads image URIs from GCS, decodes and preprocesses images, and runs batched inference with a fixed batch size to measure stable GPU performance. + +The following graphs show various metrics when running PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch GPU pipeline. +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/inference/pytorch_image_object_detection.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimageobjectdetectionbatchgpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimageobjectdetectionbatchgpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimageobjectdetectionbatchgpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimageobjectdetectionstreamingcpu/_index.md b/website/www/site/content/en/performance/pytorchimageobjectdetectionstreamingcpu/_index.md new file mode 100644 index 000000000000..b866d2893cd7 --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimageobjectdetectionstreamingcpu/_index.md @@ -0,0 +1,41 @@ +--- +title: "PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming CPU Performance" +--- + + + +# PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming CPU + +**Model**: PyTorch Image Object Detection — Faster R-CNN ResNet-50 FPN (pretrained on COCO) +**Accelerator**: CPU only +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This streaming pipeline performs object detection using an open-source PyTorch Faster R-CNN ResNet-50 FPN model on CPU. +It reads image URIs from GCS, decodes and preprocesses images, and runs inference. + +The following graphs show various metrics when running PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming CPU pipeline. +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/inference/pytorch_image_object_detection.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimageobjectdetectionstreamingcpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimageobjectdetectionstreamingcpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimageobjectdetectionstreamingcpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/content/en/performance/pytorchimageobjectdetectionstreaminggpu/_index.md b/website/www/site/content/en/performance/pytorchimageobjectdetectionstreaminggpu/_index.md new file mode 100644 index 000000000000..443f5e99e76f --- /dev/null +++ b/website/www/site/content/en/performance/pytorchimageobjectdetectionstreaminggpu/_index.md @@ -0,0 +1,41 @@ +--- +title: "PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming GPU Performance" +--- + + + +# PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming GPU + +**Model**: PyTorch Image Object Detection — Faster R-CNN ResNet-50 FPN (pretrained on COCO) +**Accelerator**: Tesla T4 GPU +**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM) + +This streaming pipeline performs object detection using an open-source PyTorch Faster R-CNN ResNet-50 FPN model on GPU. +It reads image URIs from GCS, decodes and preprocesses images, and runs inference. + +The following graphs show various metrics when running PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming GPU pipeline. +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/inference/pytorch_image_object_detection.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="pytorchimageobjectdetectionstreaminggpu" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="pytorchimageobjectdetectionstreaminggpu" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="pytorchimageobjectdetectionstreaminggpu" read_or_write="write" section="date" >}} diff --git a/website/www/site/data/authors.yml b/website/www/site/data/authors.yml index b74a6456d609..d98f2281bddc 100644 --- a/website/www/site/data/authors.yml +++ b/website/www/site/data/authors.yml @@ -87,6 +87,9 @@ jamesmalone: name: James Malone email: jamesmalone@apache.org twitter: chimerasaurus +janaom: + name: Jana Polianskaja + email: jana.polianskaja@gmail.com jesseanderson: name: Jesse Anderson twitter: jessetanderson diff --git a/website/www/site/data/performance.yaml b/website/www/site/data/performance.yaml index 8841bbb58ecb..03eaad774b0b 100644 --- a/website/www/site/data/performance.yaml +++ b/website/www/site/data/performance.yaml @@ -107,6 +107,37 @@ looks: title: AvgInputThroughputBytesPerSec by Version - id: fVVHhXCrHNgBG52TJsTjR8VbmWCCQnVN title: AvgInputThroughputElementsPerSec by Version + icebergio: + read: + folder: 113 + test_name: + - id: x5R7w8VnzkccgzrBTWQ82J7JBNtkGxH2 + title: Read IcebergIO RunTime and EstimatedCost + date: + - id: HQsKkmn7cS7m5bCYzPBYqnnFM6MXvJgV + title: AvgOutputThroughputBytesPerSec by Date + - id: MNYqD2ZCwV2XfYjwDg4NDVT7vr4Ytpz2 + title: AvgOutputThroughputElementsPerSec by Date + version: + - id: Cf7wGQfrM4hn8Wdf64ZjwshrHHdZKmKs + title: AvgOutputThroughputBytesPerSec by Version + - id: MGSX85MhCnCFFWYzSCGS36JRWxcBccqd + title: AvgOutputThroughputElementsPerSec by Version + write: + folder: 114 + test_name: + - id: yvvWB2JbbbFQ4R5MTwF2hbVW2X4bDxnY + title: Write IcebergIO RunTime and EstimatedCost + date: + - id: Gcw6jfhvvkkHKDwqqTkhZtYYNNH2Z8DZ + title: AvgInputThroughputBytesPerSec by Date + - id: RYtRGKsxBRkyRgWk6RGjshFnZPk76vhM + title: AvgInputThroughputElementsPerSec by Date + version: + - id: B5q25ktnXNhHxpYrGmqzgcGMgX26zvM3 + title: AvgInputThroughputBytesPerSec by Version + - id: VfMWk6rk26JQNr8wRpsK5Gk6XQPfd7vW + title: AvgInputThroughputElementsPerSec by Version pytorchbertbase: write: folder: 76 @@ -283,3 +314,275 @@ looks: title: AvgThroughputBytesPerSec by Version - id: P7wKZy6tQFWbbDfm4HzfCJnsQrVgfGsJ title: AvgThroughputElementsPerSec by Version + pytorchimagenetrightfitcpu: + write: + folder: 92 + cost: + - id: zJhxrMmxJ3zVHH5WZnDQqcBHdFDrBhxK + title: RunTime and EstimatedCost + date: + - id: RybzxZdkXJg6PzQZkBcfxTJkByTf3ZV5 + title: AvgThroughputBytesPerSec by Date + - id: xTVYPytQVH7zXYz7SvphnRV4nQcxCddp + title: AvgThroughputElementsPerSec by Date + version: + - id: dGN6Zr6rh7DfnRtTDCN6GHcNfhSkrbCq + title: AvgThroughputBytesPerSec by Version + - id: VJMWrZh3jXk2mqCZk4NQn3tBrHgGWqnC + title: AvgThroughputElementsPerSec by Version + pytorchimagenetrightfitoncecpu: + write: + folder: 97 + cost: + - id: DhsvrRxpby2F25jbj8NPVR6FhzKgsHBP + title: RunTime and EstimatedCost + date: + - id: sntwpmZjMVT57kvG8KM8rzDjBKsrRTF7 + title: AvgThroughputBytesPerSec by Date + - id: 5GJHCVH65y2zmnrZ3QqmYqk8vKfPD4D6 + title: AvgThroughputElementsPerSec by Date + version: + - id: rz24FQ24rz3HtFmpzK2rKD4zFT5kpG46 + title: AvgThroughputBytesPerSec by Version + - id: jZkcSdHbvmGXy8f37RShNvgGv6HVN7Dy + title: AvgThroughputElementsPerSec by Version + pytorchimagenetrightfitgpu: + write: + folder: 98 + cost: + - id: sMgzq4N8kJfjSjhSzVDzmXSHbXKwCy4r + title: RunTime and EstimatedCost + date: + - id: t3D6hqq7BywmvjBmxtWCvRMsFFMgPBg7 + title: AvgThroughputBytesPerSec by Date + - id: MRptKCFsnTKcxPgVKKVvs7c8XVTf6MSN + title: AvgThroughputElementsPerSec by Date + version: + - id: v6jHqBc8WvCPspgdxQdjSZ6bCgqqPvZ3 + title: AvgThroughputBytesPerSec by Version + - id: DhGNQDmRDhYSCDgXDsx6BtCcDfwKCWhb + title: AvgThroughputElementsPerSec by Version + pytorchimagenetrightfitoncegpu: + write: + folder: 99 + cost: + - id: YbwBRmMVBCxPzjm22vWvJ2fDG5mmM3BR + title: RunTime and EstimatedCost + date: + - id: WtCZn2CrRrDdygbxQhfVhNM8F7NRRpsW + title: AvgThroughputBytesPerSec by Date + - id: RBMNkqksbcN3WcwCxz4B5mxVq5Y8yk2x + title: AvgThroughputElementsPerSec by Date + version: + - id: 48tzWvydG73Y3QwPJwQzYCkzdDnpF2T7 + title: AvgThroughputBytesPerSec by Version + - id: 3VDCrNY6q8FyS28YRrxjJtMBYkxKRdBw + title: AvgThroughputElementsPerSec by Version + pytorchimageobjectdetectionbatchgpu: + write: + folder: 93 + cost: + - id: XGPVSYhVbZGJHQCtMPW4nRGynxXpdzdh + title: RunTime and EstimatedCost + date: + - id: ZW48KBPBxShGgWx53vjfvgqp6cVmHCNB + title: AvgThroughputBytesPerSec by Date + - id: BjMWg26F3gNHQyZSsg8HjhTg3mCh6jFJ + title: AvgThroughputElementsPerSec by Date + version: + - id: kbF7Gnqjsjnvh3MKvYszSgWFbgDYYTWR + title: AvgThroughputBytesPerSec by Version + - id: cTxrpY3KGrCb35dq7fnjvdsDnd7t85pJ + title: AvgThroughputElementsPerSec by Version + pytorchimageobjectdetectionbatchcpu: + write: + folder: 100 + cost: + - id: QbMVTdnxzmZzfymSrKsGTvyqFTMpwqy7 + title: RunTime and EstimatedCost + date: + - id: v6H8sqn7X66j5g6qpgC6B57z624cCfDM + title: AvgThroughputBytesPerSec by Date + - id: KBMSpWzB5RgdYsTdZxGCXDYbWpskVqGS + title: AvgThroughputElementsPerSec by Date + version: + - id: GWz5VDrMTxSCrFqQwk2MyPHMwBHC253c + title: AvgThroughputBytesPerSec by Version + - id: NBWHNG8GmGHp8Bmg25wqg3tP8jWdP5PW + title: AvgThroughputElementsPerSec by Version + pytorchimageobjectdetectionstreaminggpu: + write: + folder: 101 + cost: + - id: SfgfDsbRVk67XMnKZShq3xVBFghN7GSv + title: RunTime and EstimatedCost + date: + - id: MtMJZTTrkgXZz24zbQbpzwYNjWwcfhqw + title: AvgThroughputBytesPerSec by Date + - id: kKncXDm5MKvfwVzFyXqQ4TpfR5VdT5z7 + title: AvgThroughputElementsPerSec by Date + version: + - id: ZWQ7vnsXYb5PDr5yDFjM3QP8gkkmdgdQ + title: AvgThroughputBytesPerSec by Version + - id: cTmDCbdCQyrsK3fS333vmR3BwCwrGZgp + title: AvgThroughputElementsPerSec by Version + pytorchimageobjectdetectionstreamingcpu: + write: + folder: 102 + cost: + - id: FHgY6JfxRc23jJ6THkb7sPVw2HSw4kgs + title: RunTime and EstimatedCost + date: + - id: G5DNH6vXpGVnYP8ZGMwppdGvgvndM8kn + title: AvgThroughputBytesPerSec by Date + - id: CGR43NrksWDscGNWCKsVGCwc3RxmvGhD + title: AvgThroughputElementsPerSec by Date + version: + - id: wGGVRGxBjXxQf3yvynSFtcDCVCqKzByy + title: AvgThroughputBytesPerSec by Version + - id: M5nWYF76fWPnbTH6QkzWqzRV8fSWxtWD + title: AvgThroughputElementsPerSec by Version + pytorchimagecaptioningbatchgpu: + write: + folder: 94 + cost: + - id: nhYggnCYJzfgDyTXZRGJwpSyYKvBpfhV + title: RunTime and EstimatedCost + date: + - id: qzjkYQJCrxjc8GPcBF7dFJSFdTXQKxmR + title: AvgThroughputBytesPerSec by Date + - id: kgkdbBCbkhDHpb2rHNGQDxcwGqzykwqD + title: AvgThroughputElementsPerSec by Date + version: + - id: 4p7b4HWVMRC8HXDZYMXbVYf2VPzYQ2kz + title: AvgThroughputBytesPerSec by Version + - id: xQZRDYJRvmV7qMhb3j2XTrPmC8TP4DTr + title: AvgThroughputElementsPerSec by Version + pytorchimagecaptioningbatchcpu: + write: + folder: 103 + cost: + - id: M4vTdbrK6BxJXdXcnR7WPwW7VF4Fmhbb + title: RunTime and EstimatedCost + date: + - id: T23Fty8X6dF6DFhxSy32ymSwqz64fPXT + title: AvgThroughputBytesPerSec by Date + - id: Wm2qdmKgHbzfJbW7vNVfrGtQWV4xGCx3 + title: AvgThroughputElementsPerSec by Date + version: + - id: wnQqxHgwJstnV4fPCkNpvMxJpyPG4326 + title: AvgThroughputBytesPerSec by Version + - id: kPMV7d8Htpp6CT8q5hC6NF5DbTRwBQcJ + title: AvgThroughputElementsPerSec by Version + pytorchimagecaptioningstreaminggpu: + write: + folder: 104 + cost: + - id: rgrPw4ZdZjCD8cvKqdvdPGGtPJgWfNK5 + title: RunTime and EstimatedCost + date: + - id: jwpm4S36mGXPrY2nfpWrCvDHN6Nsrnpq + title: AvgThroughputBytesPerSec by Date + - id: QmY5QVTpndNTBJdqWjMxxD4zQkZqn6Xs + title: AvgThroughputElementsPerSec by Date + version: + - id: htD3YwWcTPYcRk4wqtqHshnt7hrgyrfk + title: AvgThroughputBytesPerSec by Version + - id: rv3QbsbxBV68MWG8hdjhnH84zJsPgJQh + title: AvgThroughputElementsPerSec by Version + pytorchimagecaptioningstreamingcpu: + write: + folder: 105 + cost: + - id: YkFKPzV2Rvgy5hmmx2KPyJCHhbxhSjzj + title: RunTime and EstimatedCost + date: + - id: 5WStCthVw8gtCdKrY5mhQf8T6N45GGqH + title: AvgThroughputBytesPerSec by Date + - id: ZnJj4QFrd3dwmsmv3xdNKytPfbW7j88C + title: AvgThroughputElementsPerSec by Date + version: + - id: VHqjNCVNScFqHF8d65khMDGxfSPrBrMh + title: AvgThroughputBytesPerSec by Version + - id: ykZJhrhHDzyPBSTxyNcdYG7yw5qs3dYj + 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 + 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 + mltransform-text-embedding: + write: + folder: 109 + cost: + - id: wm4mr82Q6MGcJZbvQF32MQB6vYMh3pBM + title: RunTime and EstimatedCost + date: + - id: dcCym4PGpqg3mTXsTXS5tfmTcJCVNTBD + title: AvgThroughputBytesPerSec by Date + - id: Q5sKJqB53Jkhy5TKCybfQgXjJnhp7pRQ + title: AvgThroughputElementsPerSec by Date + version: + - id: qdpHqnkzRPb2zqJr8sjCpW84KSQkhDHJ + title: AvgThroughputBytesPerSec by Version + - id: HcWgFVVct3xq2WjCMmY7Q8KcHGms7KDW + title: AvgThroughputElementsPerSec by Version + mltransform-image-embedding-gpu: + write: + folder: 110 + cost: + - id: zQZzvdcy3srprGq5qxqmRRN4GRHpdRDX + title: RunTime and EstimatedCost + date: + - id: X2wdRXcgsFG7sxfSQykhypFDkYtRjN2J + title: AvgThroughputBytesPerSec by Date + - id: zppHCwdN5qrDpyn2WQpJd8YmfQCPw4dk + title: AvgThroughputElementsPerSec by Date + version: + - id: FZXH7JKyFNWmrkgcFQZYxH7wywZgQpYF + title: AvgThroughputBytesPerSec by Version + - id: FQ3QYHgZTxvh3zPp3BcsGnmJx7yxfVzc + title: AvgThroughputElementsPerSec by Version + mltransform-image-embedding-cpu: + write: + folder: 111 + cost: + - id: dxnGs2xD2vNH4Y6HbFMDd5Hc2d5cqVWc + title: RunTime and EstimatedCost + date: + - id: tkwdB46WT8YDkR4Q37jJ2p7y5fGchmTC + title: AvgThroughputBytesPerSec by Date + - id: x3f5pRDvjKxVPy5Y6GjghhqSddYxprn3 + title: AvgThroughputElementsPerSec by Date + version: + - id: ZPHYbRdBMBSvd7RGS8wF8C8d8hysQmbH + title: AvgThroughputBytesPerSec by Version + - id: zjMRJssC6tNrtKPBgs8yPW8jHkbk8wjk + title: AvgThroughputElementsPerSec by Version 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
  • diff --git a/website/www/site/layouts/partials/section-menu/en/sdks.html b/website/www/site/layouts/partials/section-menu/en/sdks.html index 45fc937ac1f0..633d14c23288 100644 --- a/website/www/site/layouts/partials/section-menu/en/sdks.html +++ b/website/www/site/layouts/partials/section-menu/en/sdks.html @@ -112,6 +112,7 @@
  • Overview
  • Walkthrough
  • Shell
  • +
  • DDL
  • Apache Calcite dialect diff --git a/website/www/site/layouts/shortcodes/documentation/capability-matrix-big.html b/website/www/site/layouts/shortcodes/documentation/capability-matrix-big.html index 95166f0e04d9..e42caddb7a72 100644 --- a/website/www/site/layouts/shortcodes/documentation/capability-matrix-big.html +++ b/website/www/site/layouts/shortcodes/documentation/capability-matrix-big.html @@ -24,32 +24,20 @@

    {{ $category.description }}

    -
    +
    - - - - {{ range $row := $category.rows }} - - - - {{ end }} -
    {{ $row.name }}
    -
    -
    -
    - + + {{ range $column := $cap_data.columns }} {{ end }} -
    {{ $column.name }}
    -
    -
    - + + {{ range $row := $category.rows }} + {{ range $val := $row.values }} {{ end }} -
    {{ $row.name }} {{ partial "documentation/capability-matrix-row" (dict "type" $cap_view "val" $val) }} @@ -57,18 +45,10 @@

    {{ $category.description }}

    {{ end }}
    -
    + +
    {{ end }} {{ end }} - diff --git a/website/www/site/layouts/shortcodes/documentation/capability-matrix-single.html b/website/www/site/layouts/shortcodes/documentation/capability-matrix-single.html index da436257fe7a..8c63b5c49bd4 100644 --- a/website/www/site/layouts/shortcodes/documentation/capability-matrix-single.html +++ b/website/www/site/layouts/shortcodes/documentation/capability-matrix-single.html @@ -55,32 +55,20 @@
    What do those signs mean?
    {{ range $category := $cap_data.categories }}

    {{ $category.description }}

    -
    +
    - - - - {{ range $row := $category.rows }} - - - - {{ end }} -
    {{ $row.name }}
    -
    -
    -
    - + + {{ range $column := $cap_data.columns }} {{ end }} -
    {{ $column.name }}
    -
    -
    - + + {{ range $row := $category.rows }} + {{ range $val := $row.values }} {{ end }} -
    {{ $row.name }} {{ partial "documentation/capability-matrix-row" (dict "type" $cap_view "val" $val) }} @@ -88,19 +76,10 @@

    {{ $category.description }}

    {{ end }}
    -
    + +
    {{ T "capability-go-big" }} {{ end }} - diff --git a/website/www/site/layouts/shortcodes/tab.html b/website/www/site/layouts/shortcodes/tab.html index a5d6ecd607a4..4f329832c672 100644 --- a/website/www/site/layouts/shortcodes/tab.html +++ b/website/www/site/layouts/shortcodes/tab.html @@ -10,6 +10,28 @@ limitations under the License. See accompanying LICENSE file. */}} +{{ if eq .Ordinal 0 }} + +{{ end }} + +{{/* --- 2. Your Normal Content Box --- */}} {{ $content := (trim .Inner "\n\r") | htmlUnescape | safeHTML }} {{ $ctx := . }} {{ $language := .Get 0 }} 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 diff --git a/website/www/yarn.lock b/website/www/yarn.lock index 04f6526dfd56..ffadae8a73ff 100644 --- a/website/www/yarn.lock +++ b/website/www/yarn.lock @@ -442,9 +442,9 @@ isexe@^2.0.0: integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= js-yaml@^3.13.1: - version "3.14.2" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" - integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== + version "3.15.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.1.tgz#24bc95028f361cdaaa84745b06a109c4486773c0" + integrity sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag== dependencies: argparse "^1.0.7" esprima "^4.0.0"