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/adding-new-metadata/SKILL.md b/.agent/skills/adding-new-metadata/SKILL.md index 72b061f1df82..fd050d8c7fdb 100644 --- a/.agent/skills/adding-new-metadata/SKILL.md +++ b/.agent/skills/adding-new-metadata/SKILL.md @@ -88,7 +88,6 @@ You must ensure that when a DoFn processes an element and outputs a new element, ### Timers If metadata needs to survive timer firings (e.g., knowing an `@OnTimer` fired because of a system drain), it must be added to Timer data structures. This is a bit of uncharted area which was only implemented for CausedByDrain metadata that comes from backend, not from persisted metadata. In order to persist all WindowedValue metadata across timer, more work has to be done, below are some pointers: * `runners/core-java/src/main/java/org/apache/beam/runners/core/TimerInternals.java` and implementations (e.g., `WindmillTimerInternals.java` in Dataflow). -* `runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerData.java` (or generic `TimerData`). * **Action:** Add the field to `TimerData`, next to `CausedByDrain`. Propagate it when setting the timer and expose it when the timer fires so it bubbles up. * Eventually, metadata from Timer lands in WindowedValue, so it can be exposed to users. Keep field names, types, and getters similar to WindowedValue as much as possible, as common interface may be introduced eventually. @@ -116,4 +115,4 @@ User needs to access the metadata in their `DoFn` (e.g., `@ProcessElement public 9. [ ] Update `ReduceFnRunner` and `OutputAndTimeBoundedSplittableProcessElementInvoker` for complex transform propagation. 10. [ ] If required by timers, update `TimerData` and `TimerInternals`. 11. [ ] If exposed to the user, update `DoFnSignatures` and `ByteBuddyDoFnInvokerFactory`. -12. [ ] Update other runners (Flink, Spark, Samza) to ensure they propagate the new `WindowedValue` fields correctly in their specific operators/runners. +12. [ ] Update other runners (Flink, Spark) to ensure they propagate the new `WindowedValue` fields correctly in their specific operators/runners. diff --git a/.agent/skills/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/runners/SKILL.md b/.agent/skills/runners/SKILL.md index f92943ab097c..b5e25f9898ee 100644 --- a/.agent/skills/runners/SKILL.md +++ b/.agent/skills/runners/SKILL.md @@ -34,7 +34,6 @@ Runners execute Beam pipelines on distributed processing backends. Each runner t | Dataflow | `runners/google-cloud-dataflow-java/` | Google Cloud Dataflow | | Flink | `runners/flink/` | Apache Flink | | Spark | `runners/spark/` | Apache Spark | -| Samza | `runners/samza/` | Apache Samza | | Jet | `runners/jet/` | Hazelcast Jet | | Twister2 | `runners/twister2/` | Twister2 | diff --git a/.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 79f1d932b8da..7adaf351c8f5 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -51,6 +51,10 @@ github: protected_branches: master: {} + release-2.75: {} + release-2.74.0-postrelease: {} + release-2.74: {} + release-2.73.0-postrelease: {} release-2.73: {} release-2.72.0-postrelease: {} release-2.72: {} diff --git a/.gemini/config.yaml b/.gemini/config.yaml index 9fe8d66315e2..b590c9d230dc 100644 --- a/.gemini/config.yaml +++ b/.gemini/config.yaml @@ -46,7 +46,11 @@ code_review: # Post code review on PR open. # Type boolean, default: true. - code_review: false + code_review: true + + # Enables agent functionality on draft pull requests. + # Type: boolean, default: true. + include_drafts: false # List of glob patterns to ignore (files and directories). # Type: array of string, default: []. diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 081c266e43c7..594dbbc59b22 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -72,7 +72,7 @@ body: - label: "Component: Infrastructure" - label: "Component: Spark Runner" - label: "Component: Flink Runner" - - label: "Component: Samza Runner" + - label: "Component: Prism Runner" - label: "Component: Twister2 Runner" - label: "Component: Hazelcast Jet Runner" - label: "Component: Google Cloud Dataflow Runner" diff --git a/.github/ISSUE_TEMPLATE/failing_test.yml b/.github/ISSUE_TEMPLATE/failing_test.yml index 0699621e2b16..bc461771d4dc 100644 --- a/.github/ISSUE_TEMPLATE/failing_test.yml +++ b/.github/ISSUE_TEMPLATE/failing_test.yml @@ -78,7 +78,7 @@ body: - label: "Component: Infrastructure" - label: "Component: Spark Runner" - label: "Component: Flink Runner" - - label: "Component: Samza Runner" + - label: "Component: Prism Runner" - label: "Component: Twister2 Runner" - label: "Component: Hazelcast Jet Runner" - label: "Component: Google Cloud Dataflow Runner" diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 049c0049d29a..abde43ea7302 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -66,7 +66,7 @@ body: - label: "Component: Infrastructure" - label: "Component: Spark Runner" - label: "Component: Flink Runner" - - label: "Component: Samza Runner" + - label: "Component: Prism Runner" - label: "Component: Twister2 Runner" - label: "Component: Hazelcast Jet Runner" - label: "Component: Google Cloud Dataflow Runner" diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml index 25d7e55e4e90..c02ff781ad44 100644 --- a/.github/ISSUE_TEMPLATE/task.yml +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -67,7 +67,7 @@ body: - label: "Component: Infrastructure" - label: "Component: Spark Runner" - label: "Component: Flink Runner" - - label: "Component: Samza Runner" + - label: "Component: Prism Runner" - label: "Component: Twister2 Runner" - label: "Component: Hazelcast Jet Runner" - label: "Component: Google Cloud Dataflow Runner" diff --git a/.github/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..c82ad087fe46 100644 --- a/.github/actions/setup-environment-action/action.yml +++ b/.github/actions/setup-environment-action/action.yml @@ -71,12 +71,12 @@ runs: - name: Install Java if: ${{ inputs.java-version != '' }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: ${{ inputs.java-version == 'default' && '11' || inputs.java-version }} - 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/autolabeler.yml b/.github/autolabeler.yml index 6cd7516e7440..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/**/*"] @@ -89,6 +90,5 @@ io: ["sdks/go/pkg/beam/io/**/*", "sdks/java/io/**/*", "sdks/python/apache_beam/ "local": ["runners/local-java/**/*"] "portability": ["runners/portability/**/*"] "prism": ["runners/prism/**/*", "sdks/go/pkg/beam/runners/prism/**/*", "sdks/go/cmd/prism/**/*", "sdks/python/apache_beam/runners/portability/prism_runner.py","sdks/python/apache_beam/runners/portability/prism_runner_test.py"] -"samza": ["runners/samza/**/*"] "spark": ["runners/spark/**/*", "sdks/go/pkg/beam/runners/spark/**/*"] "twister2": ["runners/twister2/**/*"] diff --git a/.github/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/codecov.yml b/.github/codecov.yml index 0936f392ccef..5d0eaccf22da 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -73,6 +73,7 @@ ignore: - "**/*_microbenchmark.py" - "sdks/go/pkg/beam/register/register.go" - "sdks/python/apache_beam/testing/benchmarks/nexmark/**" + - "**/*_benchmark.py" - "sdks/python/apache_beam/examples/**" # See https://docs.codecov.com/docs/flags for options. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e7a40726ed9b..0d6860fd1680 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -23,6 +23,8 @@ updates: directory: "/sdks/python" # Location of package manifests schedule: interval: "daily" + exclude-paths: + - "container/**" - package-ecosystem: "gradle" directory: "/" # Location of package manifests schedule: diff --git a/.github/gh-actions-self-hosted-runners/arc/environments/beam.env b/.github/gh-actions-self-hosted-runners/arc/environments/beam.env index 0c817c9989ee..90dadae8b470 100644 --- a/.github/gh-actions-self-hosted-runners/arc/environments/beam.env +++ b/.github/gh-actions-self-hosted-runners/arc/environments/beam.env @@ -34,9 +34,9 @@ subnetwork_cidr_range = "10.119.0.0/20" service_account_id = "beam-github-actions@apache-beam-testing.iam.gserviceaccount.com" runner_group = "beam" main_runner = { - name = "main-runner" - runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:d7cd81a1649bc665581951d2330c4b8acd19ed72" + name = "main-runner-2404" machine_type = "e2-standard-16" + runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:latest" min_node_count = "1" max_node_count = "30" min_replicas = "1" @@ -47,43 +47,9 @@ main_runner = { cpu = "2" memory = "3Gi" } - labels = ["self-hosted", "ubuntu-20.04", "main"] + labels = ["self-hosted", "ubuntu-24.04", "main"] } additional_runner_pools = [{ - name = "small-runner" - machine_type = "e2-standard-2" - runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:d7cd81a1649bc665581951d2330c4b8acd19ed72" - min_node_count = "1" - max_node_count = "15" - min_replicas = "1" - max_replicas = "15" - webhook_scaling = false - requests = { - cpu = "1500m" - memory = "5Gi" - } - labels = ["self-hosted", "ubuntu-20.04", "small"] - enable_selector = true - enable_taint = true -}, -{ - name = "highmem-runner" - machine_type = "c3-highmem-8" - runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:d7cd81a1649bc665581951d2330c4b8acd19ed72" - min_node_count = "1" - max_node_count = "15" - min_replicas = "1" - max_replicas = "15" - webhook_scaling = false - requests = { - cpu = "7.5" - memory = "5Gi" - } - labels = ["self-hosted", "ubuntu-20.04", "highmem"] - enable_selector = true - enable_taint = true -}, -{ name = "highmem-runner-22" machine_type = "c3-highmem-22" runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:latest" @@ -100,24 +66,6 @@ additional_runner_pools = [{ enable_selector = true enable_taint = true }, -{ - name = "main-runner-2404" - machine_type = "e2-standard-16" - runner_image = "us-central1-docker.pkg.dev/apache-beam-testing/beam-github-actions/beam-arc-runner:latest" - min_node_count = "1" - max_node_count = "30" - min_replicas = "1" - max_replicas = "240" - webhook_scaling = false - disk_size_gb = 200 - requests = { - cpu = "2" - memory = "3Gi" - } - labels = ["self-hosted", "ubuntu-24.04", "main"] - enable_selector = true - enable_taint = true -}, { name = "small-runner-2404" machine_type = "e2-standard-2" diff --git a/.github/gh-actions-self-hosted-runners/arc/variables.tf b/.github/gh-actions-self-hosted-runners/arc/variables.tf index 3caeffe5a523..6dd1120f83a5 100644 --- a/.github/gh-actions-self-hosted-runners/arc/variables.tf +++ b/.github/gh-actions-self-hosted-runners/arc/variables.tf @@ -90,8 +90,8 @@ variable "main_runner" { max_replicas = optional(number, 1) disk_size_gb = optional(number, 100) webhook_scaling = optional(bool, false) - runner_image = optional(string, "summerwind/actions-runner:v2.304.0-ubuntu-20.04-30355f7") - labels = optional(list(string), ["self-hosted", "ubuntu-20.04","main"]) + runner_image = optional(string, "summerwind/actions-runner:v2.334.0-ubuntu-24.04-012f1a5") + labels = optional(list(string), ["self-hosted", "ubuntu-24.04","main"]) enable_selector = optional(bool, false) enable_taint = optional(bool, false) requests = optional(object({ @@ -119,8 +119,8 @@ variable "additional_runner_pools" { max_replicas = optional(number, 1) disk_size_gb = optional(number, 100) webhook_scaling = optional(bool, false) - runner_image = optional(string, "summerwind/actions-runner:v2.304.0-ubuntu-20.04-30355f7") - labels = optional(list(string), ["self-hosted", "ubuntu-20.04","changeme"]) + runner_image = optional(string, "summerwind/actions-runner:v2.334.0-ubuntu-24.04-012f1a5") + labels = optional(list(string), ["self-hosted", "ubuntu-24.04","changeme"]) enable_selector = optional(bool, true) enable_taint = optional(bool, true) requests = optional(object({ diff --git a/.github/issue-rules.yml b/.github/issue-rules.yml index c4acb2945575..4fafb1c87f78 100644 --- a/.github/issue-rules.yml +++ b/.github/issue-rules.yml @@ -52,8 +52,8 @@ rules: addLabels: ['spark'] - contains: '[x] Component: Flink' addLabels: ['flink'] -- contains: '[x] Component: Samza' - addLabels: ['samza'] +- contains: '[x] Component: Prism' + addLabels: ['prism'] - contains: '[x] Component: Twister2' addLabels: ['twister2'] - contains: '[x] Component: Hazelcast' diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json b/.github/trigger_files/IO_Iceberg_Integration_Tests.json index 5d04b2c0a8c7..b73af5e61a43 100644 --- a/.github/trigger_files/IO_Iceberg_Integration_Tests.json +++ b/.github/trigger_files/IO_Iceberg_Integration_Tests.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 5 + "modification": 1 } diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json b/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json index 5abe02fc09c7..3a009261f4f9 100644 --- a/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json +++ b/.github/trigger_files/IO_Iceberg_Integration_Tests_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 1 + "modification": 2 } diff --git a/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json b/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json index b73af5e61a43..5d04b2c0a8c7 100644 --- a/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json +++ b/.github/trigger_files/beam_CloudML_Benchmarks_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "modification": 1 + "modification": 5 } 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 ed3d846bc7b0..504c32974c7c 100644 --- a/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json +++ b/.github/trigger_files/beam_PostCommit_Go_VR_Flink.json @@ -1,5 +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_Java_DataflowV1.json b/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json index 5e7fbb916f4b..ae6cb268ff68 100644 --- a/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json +++ b/.github/trigger_files/beam_PostCommit_Java_DataflowV1.json @@ -3,6 +3,6 @@ "https://github.com/apache/beam/pull/34902": "Introducing OutputBuilder", "https://github.com/apache/beam/pull/35177": "Introducing WindowedValueReceiver to runners", "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 4, + "modification": 9, "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface" } diff --git a/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json b/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json index 73012c45df18..d0d502569834 100644 --- a/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json +++ b/.github/trigger_files/beam_PostCommit_Java_DataflowV2.json @@ -1,4 +1,4 @@ { - "modification": 6, + "modification": 7, "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface" } diff --git a/.github/trigger_files/beam_PostCommit_Java_Nexmark_Flink.json b/.github/trigger_files/beam_PostCommit_Java_Nexmark_Flink.json index e3d6056a5de9..b26833333238 100644 --- a/.github/trigger_files/beam_PostCommit_Java_Nexmark_Flink.json +++ b/.github/trigger_files/beam_PostCommit_Java_Nexmark_Flink.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 2 } diff --git a/.github/trigger_files/beam_PostCommit_Java_Tpcds_Flink.json b/.github/trigger_files/beam_PostCommit_Java_Tpcds_Flink.json new file mode 100644 index 000000000000..e3d6056a5de9 --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Java_Tpcds_Flink.json @@ -0,0 +1,4 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run", + "modification": 1 +} diff --git a/.github/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 090751435f20..e623d3373a93 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run!", - "modification": 7, + "modification": 1, } diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json index 50d17c108f2e..e623d3373a93 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run!", - "modification": 2, + "modification": 1, } diff --git a/.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_Samza.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Samza.json deleted file mode 100644 index 6acc30e8280f..000000000000 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Samza.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "https://github.com/apache/beam/pull/34902": "Introducing OutputBuilder", - "https://github.com/apache/beam/pull/35177": "Introducing WindowedValueReceiver to runners", - "comment": "Modify this file in a trivial way to cause this test suite to run", - "https://github.com/apache/beam/pull/31156": "noting that PR #31156 should run this test", - "https://github.com/apache/beam/pull/31270": "re-add specialized Samza translation of Redistribute", - "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface", - "modification": 1 -} diff --git a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json index f4e3f8b8dbe2..1efc8e9e4405 100644 --- a/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json +++ b/.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark.json @@ -1,18 +1,4 @@ { - "https://github.com/apache/beam/pull/34902": "Introducing OutputBuilder", - "https://github.com/apache/beam/pull/35213": "Eliminating getPane() in favor of getPaneInfo()", - "https://github.com/apache/beam/pull/35177": "Introducing WindowedValueReceiver to runners", - "comment": "Modify this file in a trivial way to cause this test suite to run", - "https://github.com/apache/beam/pull/31156": "noting that PR #31156 should run this test", - "https://github.com/apache/beam/pull/31798": "noting that PR #31798 should run this test", - "https://github.com/apache/beam/pull/32546": "noting that PR #32546 should run this test", - "https://github.com/apache/beam/pull/33267": "noting that PR #33267 should run this test", - "https://github.com/apache/beam/pull/33322": "noting that PR #33322 should run this test", - "https://github.com/apache/beam/pull/34123": "noting that PR #34123 should run this test", - "https://github.com/apache/beam/pull/34080": "noting that PR #34080 should run this test", - "https://github.com/apache/beam/pull/34155": "noting that PR #34155 should run this test", - "https://github.com/apache/beam/pull/34560": "noting that PR #34560 should run this test", - "https://github.com/apache/beam/pull/35159": "moving WindowedValue and making an interface", - "https://github.com/apache/beam/pull/35316": "noting that PR #35316 should run this test", - "https://github.com/apache/beam/pull/38011": "noting that PR #38011 should run this test" + "comment": "Modify this file in a trivial way to cause this test suite to run", + "modification": 1 } diff --git a/.github/trigger_files/beam_PostCommit_Python.json b/.github/trigger_files/beam_PostCommit_Python.json index 69f759d8463d..2e255c8f3cf6 100644 --- a/.github/trigger_files/beam_PostCommit_Python.json +++ b/.github/trigger_files/beam_PostCommit_Python.json @@ -1,5 +1,5 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run.", - "pr": "38069", - "modification": 40 + "pr": "37345", + "modification": 52 } 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_ValidatesRunner_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json index b26833333238..f1ba03a243ee 100644 --- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 2 + "modification": 5 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Versions.json b/.github/trigger_files/beam_PostCommit_Python_Versions.json new file mode 100644 index 000000000000..8b2c8c445c1f --- /dev/null +++ b/.github/trigger_files/beam_PostCommit_Python_Versions.json @@ -0,0 +1,4 @@ +{ + "comment": "Modify this file in a trivial way to cause this test suite to run", + "revision": 5 +} diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Dataflow.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Dataflow.json index e3d6056a5de9..83346d34aee0 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Dataflow.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Dataflow.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 16 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Direct.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Direct.json index e3d6056a5de9..c5309eebb070 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_Gcp_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 17 } diff --git a/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json index e3d6056a5de9..455144f02a35 100644 --- a/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_IO_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "modification": 1 + "modification": 6 } 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..c537844dc84a --- /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": 3 +} diff --git a/.github/trigger_files/beam_PostCommit_SQL.json b/.github/trigger_files/beam_PostCommit_SQL.json index 833fd9b0d174..5df3841d2363 100644 --- a/.github/trigger_files/beam_PostCommit_SQL.json +++ b/.github/trigger_files/beam_PostCommit_SQL.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run ", - "modification": 2 + "modification": 3 } diff --git a/.github/trigger_files/beam_PostCommit_XVR_Direct.json b/.github/trigger_files/beam_PostCommit_XVR_Direct.json index 73867c483554..702328d16d4b 100644 --- a/.github/trigger_files/beam_PostCommit_XVR_Direct.json +++ b/.github/trigger_files/beam_PostCommit_XVR_Direct.json @@ -1,3 +1,3 @@ { - "modification": 5 + "modification": 1 } diff --git a/.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_Yaml_Xlang_Direct.json b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json index 541dc4ea8e87..b5704c67ef1c 100644 --- a/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json +++ b/.github/trigger_files/beam_PostCommit_Yaml_Xlang_Direct.json @@ -1,4 +1,4 @@ { "comment": "Modify this file in a trivial way to cause this test suite to run", - "revision": 2 + "revision": 6 } 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_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 5de07f97eb0b..f0daaf7966df 100644 --- a/.github/workflows/IO_Iceberg_Integration_Tests.yml +++ b/.github/workflows/IO_Iceberg_Integration_Tests.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Integration_Tests"] job_phrase: ["Run IcebergIO Integration Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml b/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml index 8f9a7770bfdc..e0bb9a06f2d5 100644 --- a/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml +++ b/.github/workflows/IO_Iceberg_Integration_Tests_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Integration_Tests_Dataflow"] job_phrase: ["Run IcebergIO Integration Tests on Dataflow"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml b/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml index 138d9b499310..60aa67a7081f 100644 --- a/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml +++ b/.github/workflows/IO_Iceberg_Managed_Integration_Tests_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Managed_Integration_Tests_Dataflow"] job_phrase: ["Run IcebergIO Managed Integration Tests on Dataflow"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Performance_Tests.yml b/.github/workflows/IO_Iceberg_Performance_Tests.yml index 1fa9ab39eac8..f35923193937 100644 --- a/.github/workflows/IO_Iceberg_Performance_Tests.yml +++ b/.github/workflows/IO_Iceberg_Performance_Tests.yml @@ -63,7 +63,7 @@ jobs: job_name: ["IO_Iceberg_Performance_Tests"] job_phrase: ["Run IcebergIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/IO_Iceberg_Unit_Tests.yml b/.github/workflows/IO_Iceberg_Unit_Tests.yml index 21c993d7406f..6df57f1399a3 100644 --- a/.github/workflows/IO_Iceberg_Unit_Tests.yml +++ b/.github/workflows/IO_Iceberg_Unit_Tests.yml @@ -82,7 +82,7 @@ jobs: github.event.comment.body == 'Run IcebergIO Unit Tests' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableCheckStyle=true \ --info - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c1fc56355461..f6b1a440e175 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -237,6 +237,7 @@ PreCommit Jobs run in a schedule and also get triggered in a PR if relevant sour | [ PreCommit Java Clickhouse IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml) | N/A |`Run Java_Clickhouse_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Csv IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml) | N/A |`Run Java_Csv_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Datadog IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml) | N/A |`Run Java_Datadog_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml?query=event%3Aschedule) | +| [ PreCommit Java Delta IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml) | N/A |`Run Java_Delta_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Debezium IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml) | N/A |`Run Java_Debezium_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java ElasticSearch IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml) | N/A |`Run Java_ElasticSearch_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Examples Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow.yml) | N/A |`Run Java_Examples_Dataflow PreCommit`| [![.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Examples_Dataflow.yml?query=event%3Aschedule) | @@ -267,7 +268,7 @@ PreCommit Jobs run in a schedule and also get triggered in a PR if relevant sour | [ PreCommit Java SingleStore IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml) | N/A |`Run Java_SingleStore_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Snowflake IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml) | N/A |`Run Java_Snowflake_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Solr IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml) | N/A |`Run Java_Solr_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml?query=event%3Aschedule) | -| [ PreCommit Java Spark3 Versions ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark3_Versions.yml) | N/A | `Run Java_Spark3_Versions PreCommit` | [![.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark3_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark3_Versions.yml?query=event%3Aschedule) | +| [ PreCommit Java Spark Versions ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark_Versions.yml) | N/A | `Run Java_Spark_Versions PreCommit` | [![.github/workflows/beam_PreCommit_Java_Spark_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Spark_Versions.yml?query=event%3Aschedule) | | [ PreCommit Java Splunk IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml) | N/A |`Run Java_Splunk_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Thrift IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml) | N/A |`Run Java_Thrift_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml?query=event%3Aschedule) | | [ PreCommit Java Tika IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml) | N/A |`Run Java_Tika_IO_Direct PreCommit`| [![.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml?query=event%3Aschedule) | @@ -290,7 +291,6 @@ PreCommit Jobs run in a schedule and also get triggered in a PR if relevant sour | [ PreCommit RAT ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_RAT.yml) | N/A | `Run RAT PreCommit` | [![.github/workflows/beam_PreCommit_RAT.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_RAT.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_RAT.yml?query=event%3Aschedule) | | [ PreCommit Spotless ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Spotless.yml) | N/A | `Run Spotless PreCommit` | [![.github/workflows/beam_PreCommit_Spotless.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Spotless.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Spotless.yml?query=event%3Aschedule) | | [ PreCommit SQL ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL.yml) | N/A |`Run SQL PreCommit`| [![.github/workflows/beam_PreCommit_SQL.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL.yml?query=event%3Aschedule) | -| [ PreCommit SQL Java8 ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java8.yml) | N/A |`Run SQL_Java8 PreCommit`| [![.github/workflows/beam_PreCommit_SQL_Java8.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java8.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java8.yml?query=event%3Aschedule) | | [ PreCommit SQL Java17 ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java17.yml) | N/A |`Run SQL_Java17 PreCommit`| [![.github/workflows/beam_PreCommit_SQL_Java17.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java17.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_SQL_Java17.yml?query=event%3Aschedule) | | [ PreCommit Typescript ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Typescript.yml) | N/A |`Run Typescript PreCommit`| [![.github/workflows/beam_PreCommit_Typescript.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Typescript.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Typescript.yml?query=event%3Aschedule) | | [ PreCommit Website ](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Website.yml) | N/A |`Run Website PreCommit`| [![.github/workflows/beam_PreCommit_Website.yml](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Website.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PreCommit_Website.yml?query=event%3Aschedule) | @@ -325,7 +325,6 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Go ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go.yml) | N/A |`beam_PostCommit_Go.json`| [![.github/workflows/beam_PostCommit_Go.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go.yml?query=event%3Aschedule) | | [ PostCommit Go Dataflow ARM](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_Dataflow_ARM.yml) | N/A |`beam_PostCommit_Go_Dataflow_ARM.json`| [![.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_Dataflow_ARM.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_Dataflow_ARM.yml?query=event%3Aschedule) | | [ PostCommit Go VR Flink](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Flink.yml) | N/A |`beam_PostCommit_Go_VR_Flink.json`| [![.github/workflows/beam_PostCommit_Go_VR_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Flink.yml?query=event%3Aschedule) | -| [ PostCommit Go VR Samza](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Samza.yml) | N/A |`beam_PostCommit_Go_VR_Samza.json`| [![.github/workflows/beam_PostCommit_Go_VR_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Samza.yml?query=event%3Aschedule) | | [ PostCommit Go VR Spark](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Spark.yml) | N/A |`beam_PostCommit_Go_VR_Spark.json`| [![.github/workflows/beam_PostCommit_Go_VR_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Go_VR_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java Avro Versions ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Avro_Versions.yml) | N/A |`beam_PostCommit_Java_Avro_Versions.json`| [![.github/workflows/beam_PostCommit_Java_Avro_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Avro_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Avro_Versions.yml?query=event%3Aschedule) | | [ PostCommit Java BigQueryEarlyRollout ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml) | N/A |`beam_PostCommit_Java_BigQueryEarlyRollout.json`| [![.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml?query=event%3Aschedule) | @@ -356,10 +355,11 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java Nexmark Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Nexmark_Spark.yml) | N/A |`beam_PostCommit_Java_Nexmark_Spark.json`| [![.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Nexmark_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Nexmark_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Flink Batch ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml) | N/A |`beam_PostCommit_Java_PVR_Flink_Batch.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Flink Streaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml) | N/A |`beam_PostCommit_Java_PVR_Flink_Streaming.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml?query=event%3Aschedule) | -| [ PostCommit Java PVR Samza ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Samza.yml) | N/A |`beam_PostCommit_Java_PVR_Samza.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Samza.yml?query=event%3Aschedule) | | [ PostCommit Java SingleStoreIO IT ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml) | N/A |`beam_PostCommit_Java_SingleStoreIO_IT.json`| [![.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Spark3 Streaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml) | N/A |`beam_PostCommit_Java_PVR_Spark3_Streaming.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml?query=event%3Aschedule) | | [ PostCommit Java PVR Spark Batch ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml) | N/A |`beam_PostCommit_Java_PVR_Spark_Batch.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml?query=event%3Aschedule) | +| [ PostCommit Java PVR Spark4 Batch ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml) | N/A |`beam_PostCommit_Java_PVR_Spark4_Batch.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml?query=event%3Aschedule) | +| [ PostCommit Java PVR Spark4 Streaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml) | N/A |`beam_PostCommit_Java_PVR_Spark4_Streaming.json`| [![.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml?query=event%3Aschedule) | | [ PostCommit Java Tpcds Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml) | N/A |`beam_PostCommit_Java_Tpcds_Dataflow.json`| [![.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Java Tpcds Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Flink.yml) | N/A |`beam_PostCommit_Java_Tpcds_Flink.json`| [![.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Flink.yml?query=event%3Aschedule) | | [ PostCommit Java Tpcds Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Spark.yml) | N/A |`beam_PostCommit_Java_Tpcds_Spark.json`| [![.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_Tpcds_Spark.yml?query=event%3Aschedule) | @@ -370,12 +370,10 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java ValidatesRunner Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Dataflow.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Direct JavaVersions ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml) | ['8','25'] |`beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Direct.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml?query=event%3Aschedule) | -| [ PostCommit Java ValidatesRunner Flink Java8 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink_Java8.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Flink.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml?query=event%3Aschedule) | -| [ PostCommit Java ValidatesRunner Samza ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Samza.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml?query=event%3Aschedule) | -| [ PostCommit Java ValidatesRunner Spark Java8 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark_Java8.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner SparkStructuredStreaming ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml?query=event%3Aschedule) | +| [ PostCommit Java ValidatesRunner Spark4 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Spark4.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner Twister2 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Twister2.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml?query=event%3Aschedule) | | [ PostCommit Java ValidatesRunner ULR ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_ULR.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml?query=event%3Aschedule) | | [ PostCommit Java ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml) | N/A |`beam_PostCommit_Java.json`| [![.github/workflows/beam_PostCommit_Java.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java.yml?query=event%3Aschedule) | @@ -395,11 +393,11 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Python ValidatesContainer Dataflow With RC ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml) | ['3.9','3.10','3.11','3.12'] |`beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml?query=event%3Aschedule) | | [ PostCommit Python ValidatesRunner Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml) | ['3.9','3.11','3.12'] |`beam_PostCommit_Python_ValidatesRunner_Dataflow.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Python ValidatesRunner Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml) | ['3.9','3.11','3.12'] |`beam_PostCommit_Python_ValidatesRunner_Flink.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml?query=event%3Aschedule) | -| [ PostCommit Python ValidatesRunner Samza ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml) | ['3.9','3.11','3.12'] |`beam_PostCommit_Python_ValidatesRunner_Samza.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml?query=event%3Aschedule) | | [ PostCommit Python ValidatesRunner Spark ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml) | ['3.9','3.10','3.11','3.12'] |`beam_PostCommit_Python_ValidatesRunner_Spark.json`| [![.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang Gcp Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml) | N/A |`beam_PostCommit_Python_Xlang_Gcp_Dataflow.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang Gcp Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml) | N/A |`beam_PostCommit_Python_Xlang_Gcp_Direct.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang IO Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml) | N/A |`beam_PostCommit_Python_Xlang_IO_Dataflow.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml?query=event%3Aschedule) | +| [ PostCommit Python Versions ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Versions.yml) | N/A |`beam_PostCommit_Python_Versions.json`| [![.github/workflows/beam_PostCommit_Python_Versions.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Versions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Versions.yml?query=event%3Aschedule) | | [ PostCommit SQL ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_SQL.yml) | N/A |`beam_PostCommit_SQL.json`| [![.github/workflows/beam_PostCommit_SQL.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_SQL.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_SQL.yml?query=event%3Aschedule) | | [ PostCommit TransformService Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_TransformService_Direct.yml) | N/A |`beam_PostCommit_TransformService_Direct.json`| [![.github/workflows/beam_PostCommit_TransformService_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_TransformService_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_TransformService_Direct.yml?query=event%3Aschedule) | [ PostCommit Website Test](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Website_Test.yml) | N/A |`beam_PostCommit_Website_Test.json`| [![.github/workflows/beam_PostCommit_Website_Test.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Website_Test.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Website_Test.yml?query=event%3Aschedule) | @@ -409,7 +407,6 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit XVR JavaUsingPython Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml) | N/A |`beam_PostCommit_XVR_JavaUsingPython_Dataflow.json`| [![.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit XVR PythonUsingJava Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml) | N/A |`beam_PostCommit_XVR_PythonUsingJava_Dataflow.json`| [![.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml?query=event%3Aschedule) | | [ PostCommit XVR PythonUsingJavaSQL Dataflow ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml) | N/A |`beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.json`| [![.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml?query=event%3Aschedule) | -| [ PostCommit XVR Samza ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Samza.yml) | N/A |`beam_PostCommit_XVR_Samza.json`| [![.github/workflows/beam_PostCommit_XVR_Samza.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Samza.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Samza.yml?query=event%3Aschedule) | | [ PostCommit XVR Spark3 ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Spark3.yml) | N/A |`beam_PostCommit_XVR_Spark3.json`| [![.github/workflows/beam_PostCommit_XVR_Spark3.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Spark3.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_XVR_Spark3.yml?query=event%3Aschedule) | | [ PostCommit YAML Xlang Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml) | N/A |`beam_PostCommit_Yaml_Xlang_Direct.json`| [![.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml?query=event%3Aschedule) | | [ Python Validates Container Dataflow ARM ](https://github.com/apache/beam/actions/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml) | ['3.9','3.10','3.11','3.12'] |`beam_Python_ValidatesContainer_Dataflow_ARM.json`|[![.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml](https://github.com/apache/beam/actions/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml?query=event%3Aschedule) | @@ -417,6 +414,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ PostCommit Java ValidatesRunner Dataflow Streaming Engine ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml) | N/A |`beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.json`| [![.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml?query=event%3Aschedule) | | [ PostCommit Python Portable Flink ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Portable_Flink.yml) | N/A |`beam_PostCommit_Python_Portable_Flink.json`| [![.github/workflows/beam_PostCommit_Python_Portable_Flink.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Portable_Flink.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Portable_Flink.yml?query=event%3Aschedule) | | [ PostCommit Python Xlang IO Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml) | N/A |`beam_PostCommit_Python_Xlang_IO_Direct.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml?query=event%3Aschedule) | +| [ PostCommit Python Xlang Messaging Direct ](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml) | N/A |`beam_PostCommit_Python_Xlang_Messaging_Direct.json`| [![.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml?query=event%3Aschedule) | ### PerformanceTests and Benchmark Jobs @@ -536,6 +534,7 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ Publish Beam SDK Snapshots ](https://github.com/apache/beam/actions/workflows/beam_Publish_Beam_SDK_Snapshots.yml) | N/A | [![.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_Beam_SDK_Snapshots.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_Beam_SDK_Snapshots.yml?query=event%3Aschedule) | | [ Publish BeamMetrics ](https://github.com/apache/beam/actions/workflows/beam_Publish_BeamMetrics.yml) | N/A | [![.github/workflows/beam_Publish_BeamMetrics.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_BeamMetrics.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_BeamMetrics.yml?query=event%3Aschedule) | [ Publish Docker Snapshots ](https://github.com/apache/beam/actions/workflows/beam_Publish_Docker_Snapshots.yml) | N/A | [![.github/workflows/beam_Publish_Docker_Snapshots.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_Docker_Snapshots.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_Docker_Snapshots.yml?query=event%3Aschedule) | +| [ Publish Python VLLM Image ](https://github.com/apache/beam/actions/workflows/beam_Publish_Python_VLLM_Image.yml) | N/A | [![.github/workflows/beam_Publish_Python_VLLM_Image.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_Python_VLLM_Image.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_Python_VLLM_Image.yml?query=event%3Aschedule) | | [ Publish Website ](https://github.com/apache/beam/actions/workflows/beam_Publish_Website.yml) | N/A | [![.github/workflows/beam_Publish_Website.yml](https://github.com/apache/beam/actions/workflows/beam_Publish_Website.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Publish_Website.yml?query=event%3Aschedule) | | [ Release Nightly Snapshot ](https://github.com/apache/beam/actions/workflows/beam_Release_NightlySnapshot.yml) | N/A | [![.github/workflows/beam_Release_NightlySnapshot.yml](https://github.com/apache/beam/actions/workflows/beam_Release_NightlySnapshot.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Release_NightlySnapshot.yml?query=event%3Aschedule) | | [ Release Nightly Snapshot Python ](https://github.com/apache/beam/actions/workflows/beam_Release_Python_NightlySnapshot.yml) | N/A | [![.github/workflows/beam_Release_Python_NightlySnapshot.yml](https://github.com/apache/beam/actions/workflows/beam_Release_Python_NightlySnapshot.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Release_Python_NightlySnapshot.yml?query=event%3Aschedule) | @@ -546,3 +545,4 @@ PostCommit Jobs run in a schedule against master branch and generally do not get | [ Infrastructure Policy Enforcer ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_PolicyEnforcer.yml) | N/A | [![.github/workflows/beam_Infrastructure_PolicyEnforcer.yml](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_PolicyEnforcer.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_PolicyEnforcer.yml?query=event%3Aschedule) | | [ Modify the GCP User Roles according to the infra/users.yml file ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_UsersPermissions.yml) | N/A | [![.github/workflows/beam_Infrastructure_UsersPermissions.yml](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_UsersPermissions.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_UsersPermissions.yml?query=event%3Aschedule) | | [ Service Account Keys Management ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_ServiceAccountKeys.yml) | N/A | [![.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_ServiceAccountKeys.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_ServiceAccountKeys.yml?query=event%3Aschedule) | +| [ Unmanaged Service Accounts Keys Audit ](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml) | N/A | [![.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml/badge.svg?event=schedule)](https://github.com/apache/beam/actions/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml?query=event%3Aschedule) | diff --git a/.github/workflows/assign_milestone.yml b/.github/workflows/assign_milestone.yml index 1f4ce3073ec2..3cc35ba342d2 100644 --- a/.github/workflows/assign_milestone.yml +++ b/.github/workflows/assign_milestone.yml @@ -31,11 +31,11 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 2 - - uses: actions/github-script@v8 + - uses: actions/github-script@v9 with: script: | const fs = require('fs') diff --git a/.github/workflows/beam_CancelStaleDataflowJobs.yml b/.github/workflows/beam_CancelStaleDataflowJobs.yml index 59f55af17db5..62659a7b4238 100644 --- a/.github/workflows/beam_CancelStaleDataflowJobs.yml +++ b/.github/workflows/beam_CancelStaleDataflowJobs.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Cancel Stale Dataflow Jobs' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CleanUpDataprocResources.yml b/.github/workflows/beam_CleanUpDataprocResources.yml index c508dcdd8517..57ce2229fbbe 100644 --- a/.github/workflows/beam_CleanUpDataprocResources.yml +++ b/.github/workflows/beam_CleanUpDataprocResources.yml @@ -55,7 +55,7 @@ jobs: timeout-minutes: 100 name: "beam_CleanUpDataprocResources" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Delete leaked resources for all the jobs that generates flink clusters run: | cd ${{ github.workspace }}/.test-infra/dataproc; ./cleanup.sh -xe \ No newline at end of file diff --git a/.github/workflows/beam_CleanUpGCPResources.yml b/.github/workflows/beam_CleanUpGCPResources.yml index e3527a970839..480e48c1c210 100644 --- a/.github/workflows/beam_CleanUpGCPResources.yml +++ b/.github/workflows/beam_CleanUpGCPResources.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Clean GCP Resources' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml b/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml index c0723ed6dfec..b2a686fb6e9b 100644 --- a/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml +++ b/.github/workflows/beam_CleanUpPrebuiltSDKImages.yml @@ -62,7 +62,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Clean Prebuilt Images' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml b/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml index 700470ab9793..85ba3f01e47c 100644 --- a/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_CloudML_Benchmarks_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_CloudML_Benchmarks_Dataflow"] job_phrase: ["Run TFT Criteo Benchmarks"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_IODatastoresCredentialsRotation.yml b/.github/workflows/beam_IODatastoresCredentialsRotation.yml index 4ba622055c9a..fdb570c4b61a 100644 --- a/.github/workflows/beam_IODatastoresCredentialsRotation.yml +++ b/.github/workflows/beam_IODatastoresCredentialsRotation.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_IODatastoresCredentialsRotation"] job_phrase: ["N/A"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml index bc60b8bde351..6bda0379bc7d 100644 --- a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml +++ b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Inference_Python_Benchmarks_Dataflow"] job_phrase: ["Run Inference Benchmarks"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,6 +94,8 @@ jobs: ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_VLLM_Gemma_Batch.txt ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Table_Row_Inference_Batch.txt ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Table_Row_Inference_Stream.txt + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Generate_Vocab_Batch.txt + ${{ github.workspace }}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt # The env variables are created and populated in the test-arguments-action as "_test_arguments_" - name: get current time run: echo "NOW_UTC=$(date '+%m%d%H%M%S' --utc)" >> $GITHUB_ENV @@ -214,3 +216,26 @@ jobs: -PpythonVersion=3.10 \ -PloadTest.requirementsTxtFile=apache_beam/ml/inference/table_row_inference_requirements.txt \ '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_10 }} --autoscaling_algorithm=THROUGHPUT_BASED --max_num_workers=20 --metrics_table=result_table_row_inference_stream --influx_measurement=result_table_row_inference_stream --mode=streaming --input_subscription=projects/apache-beam-testing/subscriptions/table_row_inference_benchmark --window_size_sec=60 --trigger_interval_sec=30 --timeout_ms=900000 --output_table=apache-beam-testing:beam_run_inference.result_table_row_inference_stream_outputs --job_name=benchmark-tests-table-row-inference-stream-${{env.NOW_UTC}}' + - name: run MLTransform Generate Vocab Batch + uses: ./.github/actions/gradle-command-self-hosted-action + timeout-minutes: 180 + with: + gradle-command: :sdks:python:apache_beam:testing:load_tests:run + arguments: | + -PloadTest.mainClass=apache_beam.testing.benchmarks.inference.mltransform_generate_vocab_benchmark \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PloadTest.requirementsTxtFile=apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --job_name=benchmark-tests-mltransform-generate-vocab-batch-${{env.NOW_UTC}}' + - name: run MLTransform One-Hot Encoding Batch + uses: ./.github/actions/gradle-command-self-hosted-action + timeout-minutes: 180 + with: + gradle-command: :sdks:python:apache_beam:testing:load_tests:run + arguments: | + -PloadTest.mainClass=apache_beam.testing.benchmarks.inference.mltransform_one_hot_encoding_benchmark \ + -Prunner=DataflowRunner \ + -PpythonVersion=3.10 \ + -PbeamPythonExtra=ml_test \ + -PloadTest.requirementsTxtFile=apache_beam/ml/transforms/mltransform_tests_requirements.txt \ + '-PloadTest.args=${{ env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --autoscaling_algorithm=NONE --metrics_table=mltransform_one_hot_encoding_batch --influx_measurement=mltransform_one_hot_encoding_batch --job_name=benchmark-tests-mltransform-one-hot-encoding-batch-${{env.NOW_UTC}} --output_file=gs://temp-storage-for-end-to-end-tests/mltransform/one_hot_output_${{env.NOW_UTC}} --artifact_location=gs://temp-storage-for-end-to-end-tests/mltransform/artifacts_${{env.NOW_UTC}}' \ No newline at end of file diff --git a/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml b/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml new file mode 100644 index 000000000000..6d34bd1d156f --- /dev/null +++ b/.github/workflows/beam_Infrastructure_AuditUnmanagedKeys.yml @@ -0,0 +1,69 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# This workflow works with the GCP security log analyzer to +# generate weekly security reports and initialize log sinks + +name: Unmanaged Service Accounts Keys Audit + +on: + workflow_dispatch: + schedule: + # Every day at 00:00 UTC + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + issues: write + id-token: write + +jobs: + beam_UnmanagedKeysAudit: + name: Audit Unmanaged Service Account Keys + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.13' + + - name: Install Python dependencies + working-directory: ./infra/enforcement + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Unmanaged Service Account Keys Audit + working-directory: ./infra/enforcement + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: python account_keys.py --action announce + + + + diff --git a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml index baadd47751bf..cfef684c6f9f 100644 --- a/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml +++ b/.github/workflows/beam_Infrastructure_PolicyEnforcer.yml @@ -42,7 +42,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Python uses: actions/setup-python@v4 diff --git a/.github/workflows/beam_Infrastructure_SecurityLogging.yml b/.github/workflows/beam_Infrastructure_SecurityLogging.yml index c59eb7173eac..0af0cc81dffc 100644 --- a/.github/workflows/beam_Infrastructure_SecurityLogging.yml +++ b/.github/workflows/beam_Infrastructure_SecurityLogging.yml @@ -44,7 +44,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Python uses: actions/setup-python@v4 diff --git a/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml b/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml index fc3293a49a51..918116232b49 100644 --- a/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml +++ b/.github/workflows/beam_Infrastructure_ServiceAccountKeys.yml @@ -48,7 +48,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup gcloud uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db diff --git a/.github/workflows/beam_Infrastructure_UsersPermissions.yml b/.github/workflows/beam_Infrastructure_UsersPermissions.yml index 8d25db729064..12d78b1925c3 100644 --- a/.github/workflows/beam_Infrastructure_UsersPermissions.yml +++ b/.github/workflows/beam_Infrastructure_UsersPermissions.yml @@ -45,13 +45,13 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.merged == true && github.base_ref || github.event.pull_request.head.sha }} - name: Setup gcloud uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Install Terraform - uses: hashicorp/setup-terraform@v3 + uses: hashicorp/setup-terraform@v4 with: terraform_version: 1.12.2 - name: Initialize Terraform diff --git a/.github/workflows/beam_Java_JMH.yml b/.github/workflows/beam_Java_JMH.yml index e879392d211b..e2d88a89b40d 100644 --- a/.github/workflows/beam_Java_JMH.yml +++ b/.github/workflows/beam_Java_JMH.yml @@ -62,7 +62,7 @@ jobs: timeout-minutes: 900 name: "beam_Java_JMH" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action - name: run the Java JMH micro-benchmark harness suite diff --git a/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml b/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml index 5a749ba32d83..7fb9abb9a599 100644 --- a/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml +++ b/.github/workflows/beam_Java_LoadTests_Combine_Smoke.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Java_LoadTests_Combine_Smoke"] job_phrase: ["Run Java Load Tests Combine Smoke"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml index b9886a5d08ac..0ec35e14536e 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Go CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml index 61831c4c140f..75cdee0a5e87 100644 --- a/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml +++ b/.github/workflows/beam_LoadTests_Go_CoGBK_Flink_batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-cogbk-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 5 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-cogbk-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_CoGBK_Flink_Batch"] job_phrase: ["Run Load Tests Go CoGBK Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml index 211d045e7163..49e9a51d8967 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Go Combine Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml index 2fb2eb65ae10..4e023f141da6 100644 --- a/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_Combine_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-combine-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 5 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-combine-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_Combine_Flink_Batch"] job_phrase: ["Run Load Tests Go Combine Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml index b13248da10f0..c346e4ef1139 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Go GBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml index 17e8f050581e..4d7b955c2ad2 100644 --- a/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_GBK_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-gbk-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 5 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-gbk-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_GBK_Flink_Batch"] job_phrase: ["Run Load Tests Go GBK Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml index fbef4c9a4f50..4cfd1a8b21bf 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Go ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml index 065f0258a849..745432a4a296 100644 --- a/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_ParDo_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-pardo-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-pardo-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_ParDo_Flink_Batch"] job_phrase: ["Run Load Tests Go ParDo Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml index b910d0374b98..ccb73f51f9f5 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Go_SideInput_Dataflow_Batch"] job_phrase: ["Run Load Tests Go SideInput Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml index 31e4cf668e8c..21109c0c89f2 100644 --- a/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Go_SideInput_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-go-sideinput-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 5 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-go-sideinput-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Go_SideInput_Flink_Batch"] job_phrase: ["Run Load Tests Go SideInput Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml index d1f5fdcbcea7..d0ae3d337a4f 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Java CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml index 77e5e981f346..ccb77b6fcb8b 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java CoGBK Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -115,7 +115,7 @@ jobs: -Prunner=:runners:google-cloud-dataflow-java \ '-PloadTest.args=${{ env.beam_LoadTests_Java_CoGBK_Dataflow_Streaming_test_arguments_4 }}' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml index e935d3e590ec..c7798c6267f3 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Batch_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["CoGBK Dataflow V2 Batch"] java_version: ['11','17'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml index 2cb60eb34289..e20c26dbcd2e 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_Dataflow_V2_Streaming_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["CoGBK Dataflow V2 Streaming"] java_version: ['11','17'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml index 8273aa2bb8d4..9769622b2cde 100644 --- a/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_CoGBK_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java CoGBK SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml index 0c2ab42471bd..e1169fdeb2f9 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Java Combine Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml index 515151328a0e..c2c6c3ab80bd 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java Combine Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml index b3a4128d3974..5d4113a35d29 100644 --- a/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_Combine_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java Combine SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml index 8108d3a6d5b5..91ad51daf0e0 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Java GBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml index e99c5e25137d..4295085a4f8a 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java GBK Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml index 2ca560ab9951..6237622e3959 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Batch"] job_phrase: ["Run Load Tests GBK Dataflow V2 Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml index 67542fdf3f96..beb74f56d559 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Batch_Java17"] job_phrase: ["Run Load Tests Java 17 GBK Dataflow V2 Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml index 06a8d326a3ac..cb6dc88da539 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Streaming"] job_phrase: ["Run Load Tests GBK Dataflow V2 Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml index 1180b1889e44..8bfe24be9a9d 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Dataflow_V2_Streaming_Java17"] job_phrase: ["Run Load Tests Java 17 GBK Dataflow V2 Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml b/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml index f13ee1d2c6c9..fc143008eac4 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_Smoke.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_Smoke"] job_phrase: ["Run Java Load Tests GBK Smoke"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml index 01b81108f1eb..40b0af0e8635 100644 --- a/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_GBK_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java GBK SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml index f28a5a63b68a..411c8fec77ab 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Java ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml index d38728ef4f86..0a490623daf5 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_Dataflow_Streaming"] job_phrase: ["Run Load Tests Java ParDo Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml index 7ecc3af73b16..cc80be262670 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Batch_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["ParDo Dataflow V2 Batch"] java_version: ['11','17'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml index e10990b553d0..848632818c45 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_Dataflow_V2_Streaming_JavaVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: ["ParDo Dataflow V2 Streaming"] java_version: ['11','17'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml b/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml index b80adb5750b6..20540be6a0d3 100644 --- a/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml +++ b/.github/workflows/beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_ParDo_SparkStructuredStreaming_Batch"] job_phrase: ["Run Load Tests Java ParDo SparkStructuredStreaming Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Java_PubsubIO.yml b/.github/workflows/beam_LoadTests_Java_PubsubIO.yml index 0f8cca3fc095..915de7440603 100644 --- a/.github/workflows/beam_LoadTests_Java_PubsubIO.yml +++ b/.github/workflows/beam_LoadTests_Java_PubsubIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Java_PubsubIO"] job_phrase: ["Run Load Tests Java PubsubIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml index fe7d6dd3f01c..cb12ef2bc4f9 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Python CoGBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml index 66e4a9e3368d..5f1774a0b6c8 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python CoGBK Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml index ea9fab2b6d41..646a705eb85d 100644 --- a/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_CoGBK_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-cogbk-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-cogbk-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_CoGBK_Flink_Batch"] job_phrase: ["Run Load Tests Python CoGBK Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml index 32049892fe17..8096fbeb5497 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Dataflow_Batch"] job_phrase: ["Run Load Tests Python Combine Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml index 510b000312ba..97682ce161c4 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python Combine Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml index c8e05c2e1738..a72f2566cbba 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-cmb-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-cmb-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Flink_Batch"] job_phrase: ["Run Load Tests Python Combine Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml index 61b4e239b125..cb2d45ffffc9 100644 --- a/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_Combine_Flink_Streaming.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-cmb-flink-streaming-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-cmb-flink-streaming-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_Combine_Flink_Streaming"] job_phrase: ["Run Load Tests Python Combine Flink Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml b/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml index ea007e8ddb1e..b240daab30da 100644 --- a/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml +++ b/.github/workflows/beam_LoadTests_Python_FnApiRunner_Microbenchmark.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_FnApiRunner_Microbenchmark"] job_phrase: ["Run Python Load Tests FnApiRunner Microbenchmark"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml index 8d3fcde2c2be..1933569df097 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Dataflow_Batch"] job_phrase: ["Run Load Tests Python GBK Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml index 87abf4bf2f11..c9a75e714d34 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python GBK Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml index 794ea092c766..2a8163a4a098 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-gbk-flk-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-py-gbk-flk-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_Flink_Batch"] job_phrase: ["Run Load Tests Python GBK Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml index 674286178b3a..544db35178cd 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_reiterate_Dataflow_Batch"] job_phrase: ["Run Load Tests Python GBK reiterate Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml index 81cf0e0d7f9c..fe7d19b8e758 100644 --- a/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_GBK_reiterate_Dataflow_Streaming"] job_phrase: ["Run Load Tests Python GBK reiterate Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml index f5e47b1fbe54..f73ea2c3cf46 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Dataflow_Batch"] job_phrase: ["Run Load Tests Python ParDo Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml index 2512ee32fdb5..ea041470903f 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Dataflow_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Dataflow_Streaming"] job_phrase: ["Run Python Load Tests ParDo Dataflow Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml index 88ff32847b6d..f72ee9e98f2a 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Batch.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-pardo-flink-batch-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-pardo-flink-batch-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Flink_Batch"] job_phrase: ["Run Load Tests Python ParDo Flink Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml index bf034f7c2398..345dc43386bc 100644 --- a/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml +++ b/.github/workflows/beam_LoadTests_Python_ParDo_Flink_Streaming.yml @@ -50,12 +50,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-loadtests-py-pardo-flink-stream-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-loadtests-python-pardo-flink-stream-${{ github.run_id }} jobs: @@ -72,7 +72,7 @@ jobs: job_name: ["beam_LoadTests_Python_ParDo_Flink_Streaming"] job_phrase: ["Run Load Tests Python ParDo Flink Streaming"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml b/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml index 95b38bce1502..4ca3f7ef0276 100644 --- a/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml +++ b/.github/workflows/beam_LoadTests_Python_SideInput_Dataflow_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_LoadTests_Python_SideInput_Dataflow_Batch"] job_phrase: ["Run Load Tests Python SideInput Dataflow Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_LoadTests_Python_Smoke.yml b/.github/workflows/beam_LoadTests_Python_Smoke.yml index 511e7ac3227a..9f70b0e98c57 100644 --- a/.github/workflows/beam_LoadTests_Python_Smoke.yml +++ b/.github/workflows/beam_LoadTests_Python_Smoke.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_LoadTests_Python_Smoke"] job_phrase: ["Run Python Load Tests Smoke"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_MetricsCredentialsRotation.yml b/.github/workflows/beam_MetricsCredentialsRotation.yml index d5eceea47a3b..4498940783ba 100644 --- a/.github/workflows/beam_MetricsCredentialsRotation.yml +++ b/.github/workflows/beam_MetricsCredentialsRotation.yml @@ -59,7 +59,7 @@ jobs: job_name: ["beam_MetricsCredentialsRotation"] job_phrase: ["N/A"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Metrics_Report.yml b/.github/workflows/beam_Metrics_Report.yml index d4b9c914a0d5..75c2ff4c01cc 100644 --- a/.github/workflows/beam_Metrics_Report.yml +++ b/.github/workflows/beam_Metrics_Report.yml @@ -61,7 +61,7 @@ jobs: ) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -75,7 +75,7 @@ jobs: INFLUXDB_USER: ${{ secrets.INFLUXDB_USER }} INFLUXDB_USER_PASSWORD: ${{ secrets.INFLUXDB_USER_PASSWORD }} - name: Archive Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Metrics Report path: "${{ github.workspace }}/.test-infra/jenkins/metrics_report/beam-metrics_report.html" diff --git a/.github/workflows/beam_PerformanceTests_AvroIOIT.yml b/.github/workflows/beam_PerformanceTests_AvroIOIT.yml index 00417593e194..7cbffd8e8bf0 100644 --- a/.github/workflows/beam_PerformanceTests_AvroIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_AvroIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_AvroIOIT"] job_phrase: ["Run Java AvroIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml index 7988d99569e4..a7ec4307593d 100644 --- a/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_AvroIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_AvroIOIT_HDFS"] job_phrase: ["Run Java AvroIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml index 26ee6315b25d..78584e824f27 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Avro.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Batch_Java_Avro"] job_phrase: ["Run BigQueryIO Batch Performance Test Java Avro"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -DintegrationTestRunner=dataflow \ -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_BigQueryIO_Batch_Java_Avro_test_arguments_1 }}]' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml index a9cc6387e597..c71d8a7f17ee 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Batch_Java_Json.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Batch_Java_Json"] job_phrase: ["Run BigQueryIO Batch Performance Test Java Json"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -DintegrationTestRunner=dataflow \ -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_BigQueryIO_Batch_Java_Json_test_arguments_1 }}]' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml b/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml index 1539c641db9d..4fe06419e821 100644 --- a/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml +++ b/.github/workflows/beam_PerformanceTests_BigQueryIO_Streaming_Java.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BigQueryIO_Streaming_Java"] job_phrase: ["Run BigQueryIO Streaming Performance Test Java"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -DintegrationTestRunner=dataflow \ -DintegrationTestPipelineOptions='[${{ env.beam_PerformanceTests_BigQueryIO_Streaming_Java_test_arguments_1 }}]' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml index 5dcb712abf11..97ba3bbd13d4 100644 --- a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml +++ b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Read_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BiqQueryIO_Read_Python"] job_phrase: ["Run BigQueryIO Read Performance Test Python"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml index 391c24ee8668..298fe697dfda 100644 --- a/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml +++ b/.github/workflows/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_BiqQueryIO_Write_Python_Batch"] job_phrase: ["Run BigQueryIO Write Performance Test Python"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Cdap.yml b/.github/workflows/beam_PerformanceTests_Cdap.yml index f5d3758e4997..698c7924c23a 100644 --- a/.github/workflows/beam_PerformanceTests_Cdap.yml +++ b/.github/workflows/beam_PerformanceTests_Cdap.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Cdap"] job_phrase: ["Run Java CdapIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml index 0504ed52a5b5..996984f4d085 100644 --- a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Compressed_TextIOIT"] job_phrase: ["Run Java CompressedTextIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml index 0b10c2e70e68..d0b1e0f0bd2b 100644 --- a/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_Compressed_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_Compressed_TextIOIT_HDFS"] job_phrase: ["Run Java CompressedTextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_HadoopFormat.yml b/.github/workflows/beam_PerformanceTests_HadoopFormat.yml index 9375d3336df8..2f93007c8420 100644 --- a/.github/workflows/beam_PerformanceTests_HadoopFormat.yml +++ b/.github/workflows/beam_PerformanceTests_HadoopFormat.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_HadoopFormat"] job_phrase: ["Run Java HadoopFormatIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_JDBC.yml b/.github/workflows/beam_PerformanceTests_JDBC.yml index eb3d1e9893e4..dd5e3752c2d3 100644 --- a/.github/workflows/beam_PerformanceTests_JDBC.yml +++ b/.github/workflows/beam_PerformanceTests_JDBC.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_JDBC"] job_phrase: ["Run Java JdbcIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_Kafka_IO.yml b/.github/workflows/beam_PerformanceTests_Kafka_IO.yml index d2bd6fbe4cb1..a90fb2f17228 100644 --- a/.github/workflows/beam_PerformanceTests_Kafka_IO.yml +++ b/.github/workflows/beam_PerformanceTests_Kafka_IO.yml @@ -64,7 +64,7 @@ jobs: env: KAFKA_SERVICE_PORT: 32400 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml index 4996a1eaa849..f7a932545c22 100644 --- a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ManyFiles_TextIOIT"] job_phrase: ["Run Java ManyFilesTextIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml index 5afbb928beeb..f2ad58c4e6e3 100644 --- a/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_ManyFiles_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ManyFiles_TextIOIT_HDFS"] job_phrase: ["Run Java ManyFilesTextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml b/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml index da4d043e2985..5bd3d6a271f2 100644 --- a/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml +++ b/.github/workflows/beam_PerformanceTests_MongoDBIO_IT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_MongoDBIO_IT"] job_phrase: ["Run Java MongoDBIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml b/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml index 46c038d09e84..7b187a636f54 100644 --- a/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_ParquetIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ParquetIOIT"] job_phrase: ["Run Java ParquetIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml index 36346d389d08..7496a7e89e56 100644 --- a/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_ParquetIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_ParquetIOIT_HDFS"] job_phrase: ["Run Java ParquetIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml b/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml index 9a23cd1f7283..4d23c0a832d2 100644 --- a/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml +++ b/.github/workflows/beam_PerformanceTests_PubsubIOIT_Python_Streaming.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_PubsubIOIT_Python_Streaming"] job_phrase: ["Run PubsubIO Performance Test Python"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml b/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml index 493ee1577e36..07b7c30c5aed 100644 --- a/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml +++ b/.github/workflows/beam_PerformanceTests_SQLBigQueryIO_Batch_Java.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SQLBigQueryIO_Batch_Java"] job_phrase: ["Run SQLBigQueryIO Batch Performance Test Java"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: -DintegrationTestRunner=dataflow \ '-DintegrationTestPipelineOptions=[${{env.beam_PerformanceTests_SQLBigQueryIO_Batch_Java_test_arguments_1}}]' \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml index ae1e13134f4b..91063b956765 100644 --- a/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml +++ b/.github/workflows/beam_PerformanceTests_SingleStoreIO.yml @@ -65,7 +65,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java SingleStoreIO Performance Test' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-rbac.yaml kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-cluster-crd.yaml kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-operator.yaml - kubectl wait --for=condition=Ready pod -l name=sdb-operator --timeout=300s + kubectl rollout status deployment/sdb-operator --timeout=300s - name: Install Singlestore cluster id: install_singlestore run: | diff --git a/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml b/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml index 32e3ca8634de..053aaff33cc4 100644 --- a/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml +++ b/.github/workflows/beam_PerformanceTests_SpannerIO_Read_2GB_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SpannerIO_Read_2GB_Python"] job_phrase: ["Run SpannerIO Read 2GB Performance Test Python"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml b/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml index 932fa8c5fc25..beeaf2d67078 100644 --- a/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml +++ b/.github/workflows/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch"] job_phrase: ["Run SpannerIO Write 2GB Performance Test Python Batch"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml b/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml index 779404783487..7ac0cedb9cb6 100644 --- a/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml +++ b/.github/workflows/beam_PerformanceTests_SparkReceiver_IO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_SparkReceiver_IO"] job_phrase: ["Run Java SparkReceiverIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml b/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml index 3a51dfd208af..6103e6180eee 100644 --- a/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_TFRecordIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TFRecordIOIT"] job_phrase: ["Run Java TFRecordIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml index 9fc0de240f88..5a3f22f858fd 100644 --- a/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_TFRecordIOIT_HDFS.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PerformanceTests_TFRecordIOIT_HDFS"] job_phrase: ["Run Java TFRecordIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT.yml b/.github/workflows/beam_PerformanceTests_TextIOIT.yml index d832032e086e..a3cf7033f7eb 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT"] job_phrase: ["Run Java TextIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml index 9b1c934e46fc..01820ac6bb70 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT_HDFS"] job_phrase: ["Run Java TextIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml b/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml index 56c062e20637..c25afec144c7 100644 --- a/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml +++ b/.github/workflows/beam_PerformanceTests_TextIOIT_Python.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_TextIOIT_Python"] job_phrase: ["Run Python TextIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml b/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml index 82d2e060195c..927bc7960442 100644 --- a/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml +++ b/.github/workflows/beam_PerformanceTests_WordCountIT_PythonVersions.yml @@ -66,7 +66,7 @@ jobs: job_phrase_2: [WordCountIT Performance Test] python_version: ['3.10'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,7 +104,7 @@ jobs: -Ptest=apache_beam/examples/wordcount_it_test.py::WordCountIT::test_wordcount_it \ "-Ptest-pipeline-options=${{ env.beam_PerformanceTests_WordCountIT_PythonVersions_test_arguments_1 }}" - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PerformanceTests_XmlIOIT.yml b/.github/workflows/beam_PerformanceTests_XmlIOIT.yml index 5ce099f5b004..135a80a8aef6 100644 --- a/.github/workflows/beam_PerformanceTests_XmlIOIT.yml +++ b/.github/workflows/beam_PerformanceTests_XmlIOIT.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_XmlIOIT"] job_phrase: ["Run Java XmlIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml b/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml index 8a8f944c254a..c11a2f34479f 100644 --- a/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml +++ b/.github/workflows/beam_PerformanceTests_XmlIOIT_HDFS.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PerformanceTests_XmlIOIT_HDFS"] job_phrase: ["Run Java XmlIO Performance Test HDFS"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml index 17e1ac271268..94c556e4c475 100644 --- a/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml +++ b/.github/workflows/beam_PerformanceTests_xlang_KafkaIO_Python.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PerfTests_xlang_KafkaIO_Python"] job_phrase: ["Run Python xlang KafkaIO Performance Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,6 +73,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: default - name: Set k8s access uses: ./.github/actions/setup-k8s-access @@ -179,6 +182,8 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:java:io:expansion-service:shadowJar + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 # The env variable is created and populated in the test-arguments-action as "_test_arguments_" - name: run integrationTest uses: ./.github/actions/gradle-command-self-hosted-action diff --git a/.github/workflows/beam_Playground_CI_Nightly.yml b/.github/workflows/beam_Playground_CI_Nightly.yml index 0241512f994d..539cdeddc2b8 100644 --- a/.github/workflows/beam_Playground_CI_Nightly.yml +++ b/.github/workflows/beam_Playground_CI_Nightly.yml @@ -61,12 +61,12 @@ jobs: sdk: ["python", "java", "go"] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: python-version: default - go-version: '1.25' + go-version: '1.26' - name: Install requirements run: | cd $BEAM_ROOT_DIR/playground/infrastructure diff --git a/.github/workflows/beam_Playground_Precommit.yml b/.github/workflows/beam_Playground_Precommit.yml index 8c40a08f7ac2..056118e7170d 100644 --- a/.github/workflows/beam_Playground_Precommit.yml +++ b/.github/workflows/beam_Playground_Precommit.yml @@ -34,6 +34,7 @@ jobs: if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Playground PreCommit' name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] @@ -45,9 +46,9 @@ jobs: env: DATASTORE_EMULATOR_VERSION: '423.0.0' PYTHON_VERSION: '3.10' - JAVA_VERSION: '11' + JAVA_VERSION: '17' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -62,7 +63,10 @@ jobs: - name: Add GOPATH/bin to PATH run: echo "PATH=$PATH:$(go env GOPATH)/bin" >> $GITHUB_ENV - + - name: Install Dependencies + run: | + sudo apt-get update --yes + sudo apt-get install -y wget - name: Install sbt for running SCIO tests run: | sudo apt-get update --yes diff --git a/.github/workflows/beam_PostCommit_Go.yml b/.github/workflows/beam_PostCommit_Go.yml index 1f5a1569a20c..3da9821cefad 100644 --- a/.github/workflows/beam_PostCommit_Go.yml +++ b/.github/workflows/beam_PostCommit_Go.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go"] job_phrase: ["Run Go PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,7 +73,7 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml index 5ec840e200ba..64667f10736c 100644 --- a/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Go_Dataflow_ARM.yml @@ -65,7 +65,7 @@ jobs: job_name: ["beam_PostCommit_Go_Dataflow_ARM"] job_phrase: ["Run Go PostCommit Dataflow ARM"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -78,7 +78,7 @@ jobs: java-version: default go-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io diff --git a/.github/workflows/beam_PostCommit_Go_VR_Flink.yml b/.github/workflows/beam_PostCommit_Go_VR_Flink.yml index abbd2b83c4ed..d476e36a4cc2 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Flink.yml +++ b/.github/workflows/beam_PostCommit_Go_VR_Flink.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go_VR_Flink"] job_phrase: ["Run Go Flink ValidatesRunner"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Go_VR_Spark.yml b/.github/workflows/beam_PostCommit_Go_VR_Spark.yml index 7eb5f66650a2..34df660a2b55 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Spark.yml +++ b/.github/workflows/beam_PostCommit_Go_VR_Spark.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Go_VR_Spark"] job_phrase: ["Run Go Spark ValidatesRunner"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java.yml b/.github/workflows/beam_PostCommit_Java.yml index 876fdf0b505a..74f1c0a13b69 100644 --- a/.github/workflows/beam_PostCommit_Java.yml +++ b/.github/workflows/beam_PostCommit_Java.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :javaPostCommit - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml b/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml index 891854d013fc..f702e0a15690 100644 --- a/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Avro_Versions.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Avro Versions PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :javaAvroVersionsTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml b/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml index 8ed0345ec7c5..89201eadcd5f 100644 --- a/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml +++ b/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java BigQueryEarlyRollout PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: # body: | # PostCommit Java BigQueryEarlyRollout failed on ${{ env.date }}. This test monitors BigQuery rollouts impacting Beam and should be escalated immediately if a real issue is encountered to pause further rollouts. For further details refer to the following links:\n * Failing job: https://github.com/apache/beam/actions/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml \n * Job configuration: https://github.com/apache/beam/blob/master/.github/workflows/beam_PostCommit_Java_BigQueryEarlyRollout.yml - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_DataflowV1.yml b/.github/workflows/beam_PostCommit_Java_DataflowV1.yml index 8aa8851921c8..35eb3d331c0f 100644 --- a/.github/workflows/beam_PostCommit_Java_DataflowV1.yml +++ b/.github/workflows/beam_PostCommit_Java_DataflowV1.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_Dataflow' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:postCommit - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_DataflowV2.yml b/.github/workflows/beam_PostCommit_Java_DataflowV2.yml index cb1581a404b4..5c38e94ed7dd 100644 --- a/.github/workflows/beam_PostCommit_Java_DataflowV2.yml +++ b/.github/workflows/beam_PostCommit_Java_DataflowV2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_DataflowV2' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:postCommitRunnerV2 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml index 77561b23a4de..4acbb637a6d7 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples on Dataflow' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:examples:javaPostCommit max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml index 29bfe65ff271..fb6b6df52768 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_ARM.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'pull_request_target' || startswith(github.event.comment.body, 'Run Java_Examples_Dataflow_ARM PostCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -84,7 +84,7 @@ jobs: ${{ matrix.java_version != '11' && matrix.java_version || '' }} 11 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io @@ -107,7 +107,7 @@ jobs: -PdisableCheckStyle=true \ -PskipCheckerFramework \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml index 007ca1a600c0..a33513af45ac 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_Java.yml @@ -60,14 +60,14 @@ jobs: matrix: job_name: [beam_PostCommit_Java_Examples_Dataflow_Java] job_phrase: [Run Java examples on Dataflow Java] - java_version: ['8','17','21', '25'] + java_version: ['17', '21', '25'] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Java examples on Dataflow Java') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,7 +86,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:examples:java${{ matrix.java_version }}PostCommit max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml index 03484e90df52..4b95f1674664 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples on Dataflow Runner V2' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,7 +103,7 @@ jobs: -Pjava17Home=$JAVA_HOME_17_X64 \ -PdockerTag=${{ env.DOCKER_TAG }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml index 373e49524a65..a2ef615b8c5f 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Dataflow_V2_Java.yml @@ -61,7 +61,7 @@ jobs: job_name: [beam_PostCommit_Java_Examples_Dataflow_V2_Java] job_phrase_1: [Run Java ] job_phrase_2: [Examples on Dataflow Runner V2] - java_version: ['8', '17', '21', '25'] + java_version: ['17', '21', '25'] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || @@ -69,7 +69,7 @@ jobs: (contains(github.event.comment.body, 'Run Java') && contains(github.event.comment.body, 'Examples on Dataflow Runner V2')) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PtestJavaVersion=${{ matrix.java_version }} \ -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml b/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml index 4606803f3aec..27fbbdd82bbc 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Direct.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Direct' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: with: gradle-command: :runners:direct:examplesIntegrationTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml index d82b89fe44b2..765953d23d51 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Flink' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,9 +80,9 @@ 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@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml b/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml index 06840e305fbf..caa22f10e0ff 100644 --- a/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Examples_Spark.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Examples_Spark' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: with: gradle-command: :runners:spark:3:examplesIntegrationTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml index 8c0d35e87f07..37102fa1918e 100644 --- a/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Hadoop_Versions.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run PostCommit_Java_Hadoop_Versions' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -72,24 +72,12 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - with: - java-version: | - 8 - 11 - name: run javaHadoopVersionsTest script uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :javaHadoopVersionsTest - # TODO(https://github.com/apache/beam/issues/32189) remove when embedded hive supports Java11 - - name: run java8HadoopVersionsTest script - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :sdks:java:io:hcatalog:hadoopVersionsTest - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml b/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml index 71ac34483c47..db43441f2cda 100644 --- a/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml +++ b/.github/workflows/beam_PostCommit_Java_IO_Performance_Tests.yml @@ -66,7 +66,7 @@ jobs: job_phrase: ["Run Java PostCommit IO Performance Tests"] test_case: ["GCSPerformanceTest", "BigTablePerformanceTest", "BigQueryStorageApiStreamingPerformanceTest"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: echo "BEAM_VERSION=${BEAM_VERSION}" >> $GITHUB_ENV - name: Checkout release branch if: github.event_name == 'schedule' #This has scheduled runs run against the latest release - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ env.BEAM_VERSION }} repository: apache/beam @@ -105,7 +105,7 @@ jobs: exportDataset: performance_tests exportTable: io_performance_metrics_test - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml b/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml index 3e6242b5fb81..91f79a68c56c 100644 --- a/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_InfluxDbIO_IT.yml @@ -65,7 +65,7 @@ jobs: github.event_name == 'pull_request_target' || github.event.comment.body == 'Run Java InfluxDbIO_IT' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml index 2ab3f7dc306e..b3b1e54ae9ea 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Dataflow"] job_phrase: ["Run Jpms Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: gradle-command: :sdks:java:testing:jpms-tests:dataflowRunnerIntegrationTest arguments: -Dorg.gradle.java.home=$JAVA_HOME_11_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml index 0fd15f1ec004..de1b6417f8e9 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Dataflow_Versions.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run Jpms Dataflow Versions PostCommit"] java_version: ["17", "21", "25"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,7 +86,7 @@ jobs: -PtestJavaVersion=${{ matrix.java_version }} -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml index 904382039b06..dd627b7bd235 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Direct.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Direct"] job_phrase: ["Run Jpms Direct PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :sdks:java:testing:jpms-tests:directRunnerIntegrationTest - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml index ea58b3591a33..fa2f0c5753b4 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Direct_Versions.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run Jpms Direct Versions PostCommit"] java_version: ["17", "21", "25"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -86,7 +86,7 @@ jobs: -PtestJavaVersion=${{ matrix.java_version }} -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml index ed62695a1382..1501eae26b61 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Flink_Java11.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Flink_Java11"] job_phrase: ["Run Jpms Flink Java 11 PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: gradle-command: :sdks:java:testing:jpms-tests:flinkRunnerIntegrationTest arguments: -Dorg.gradle.java.home=$JAVA_HOME_11_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml b/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml index 9d4a76f1fb74..21ea989f2758 100644 --- a/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml +++ b/.github/workflows/beam_PostCommit_Java_Jpms_Spark_Java11.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Java_Jpms_Spark_Java11"] job_phrase: ["Run Jpms Spark Java 11 PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -80,7 +80,7 @@ jobs: gradle-command: :sdks:java:testing:jpms-tests:sparkRunnerIntegrationTest arguments: -Dorg.gradle.java.home=$JAVA_HOME_11_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml index d0218f6f093e..ff94638922fa 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow.yml @@ -93,7 +93,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml index 60d5e4c7c385..81916d8fe7a5 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2.yml @@ -93,7 +93,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Runner V2 Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml index 972aab081dba..a58e814c5567 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Dataflow_V2_Java.yml @@ -95,7 +95,7 @@ jobs: (contains(github.event.comment.body, 'Run Dataflow Runner V2 Java') && contains(github.event.comment.body, 'Nexmark Tests')) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml index e23337440e42..5175f8db2176 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Direct.yml @@ -88,7 +88,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Direct Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml index 1c908fceefdc..f64a9bd41fb6 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml @@ -87,7 +87,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Flink Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,13 +102,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 d36fb5bdd26b..99ed344def91 100644 --- a/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Spark.yml @@ -87,7 +87,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml index 35f6baa42bde..10138c8425e4 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,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Flink_Batch PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -103,7 +103,7 @@ jobs: env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH }} - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml index ffa2dc24bad5..45d9a95ac9b1 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml @@ -56,18 +56,18 @@ 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@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: with: gradle-command: runners:flink:${{ matrix.flink_version }}:job-server:validatesPortableRunnerStreaming - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml index eb3ae9edd3f0..ce51a09ada02 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark3_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Spark v3 PortableValidatesRunner Streaming' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:spark:3:job-server:validatesPortableRunnerStreaming - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Samza.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml similarity index 77% rename from .github/workflows/beam_PostCommit_Java_PVR_Samza.yml rename to .github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml index dd28b4555e5f..4442e8c55ff1 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Samza.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Batch.yml @@ -15,13 +15,13 @@ # specific language governing permissions and limitations # under the License. -name: PostCommit Java PVR Samza +name: PostCommit Java PVR Spark4 Batch on: schedule: - - cron: '15 4/6 * * *' + - cron: '45 5/6 * * *' pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_PVR_Samza.json'] + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_PVR_Spark4_Batch.json'] workflow_dispatch: # This allows a subsequently queued workflow run to interrupt previous runs @@ -51,21 +51,20 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PostCommit_Java_PVR_Samza: + beam_PostCommit_Java_PVR_Spark4_Batch: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 120 + timeout-minutes: 240 strategy: - matrix: - job_name: [beam_PostCommit_Java_PVR_Samza] - job_phrase: [Run Java Samza PortableValidatesRunner] + matrix: + job_name: [beam_PostCommit_Java_PVR_Spark4_Batch] + job_phrase: [Run Java Spark4 PortableValidatesRunner Batch] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event.comment.body == 'Run Java Samza PortableValidatesRunner' + (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,22 +73,18 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 with: - java-version: | - 8 - 11 - - name: run PostCommit Java Samza script + java-version: 17 + - name: run PostCommit Java PortableValidatesRunner Spark4 Batch script env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:samza:job-server:validatesPortableRunner - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ + gradle-command: | + :runners:spark:4:job-server:validatesPortableRunnerBatch \ + :runners:spark:4:job-server:validatesPortableRunnerDocker \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -98,7 +93,12 @@ jobs: uses: EnricoMi/publish-unit-test-result-action@v2 if: always() with: + large_files: true commit: '${{ env.prsha || env.GITHUB_SHA }}' comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} files: '**/build/test-results/**/*.xml' - large_files: true \ No newline at end of file + - name: Archive SpotBugs Results + uses: actions/upload-artifact@v7 + with: + name: SpotBugs Results + path: "**/build/reports/spotbugs/*.html" diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml similarity index 79% rename from .github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml rename to .github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml index fab48ec60019..7a66f317c974 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark4_Streaming.yml @@ -15,13 +15,13 @@ # specific language governing permissions and limitations # under the License. -name: PostCommit Java ValidatesRunner Spark Java8 +name: PostCommit Java PVR Spark4 Streaming on: schedule: - - cron: '45 4/6 * * *' + - cron: '45 5/6 * * *' pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark_Java8.json'] + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_PVR_Spark4_Streaming.json'] workflow_dispatch: # This allows a subsequently queued workflow run to interrupt previous runs @@ -51,21 +51,21 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PostCommit_Java_ValidatesRunner_Spark_Java8: + beam_PostCommit_Java_PVR_Spark4_Streaming: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 270 + timeout-minutes: 180 strategy: - matrix: - job_name: [beam_PostCommit_Java_ValidatesRunner_Spark_Java8] - job_phrase: [Run Spark ValidatesRunner Java 8] + matrix: + job_name: [beam_PostCommit_Java_PVR_Spark4_Streaming] + job_phrase: [Run Java Spark v4 PortableValidatesRunner Streaming] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') || - startswith(github.event.comment.body, 'Run Spark ValidatesRunner Java 8') + github.event.comment.body == 'Run Java Spark v4 PortableValidatesRunner Streaming' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -75,19 +75,13 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: | - 8 - 11 - - name: run validatesRunner Java8 script + java-version: 17 + - name: run PostCommit Java PortableValidatesRunner Spark4 Streaming script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:spark:3:validatesRunner - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - max-workers: 12 + gradle-command: :runners:spark:4:job-server:validatesPortableRunnerStreaming - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml b/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml index 81ed0f588825..f14e32ad2901 100644 --- a/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml +++ b/.github/workflows/beam_PostCommit_Java_PVR_Spark_Batch.yml @@ -64,7 +64,7 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: :runners:spark:3:job-server:validatesPortableRunnerBatch \ :runners:spark:3:job-server:validatesPortableRunnerDocker \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -96,7 +96,7 @@ jobs: comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} files: '**/build/test-results/**/*.xml' - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: SpotBugs Results path: "**/build/reports/spotbugs/*.html" diff --git a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml index ad309a25a42f..3fb73d6c1311 100644 --- a/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Java_SingleStoreIO_IT.yml @@ -67,7 +67,7 @@ jobs: github.event_name == 'pull_request_target' || github.event.comment.body == 'Run Java SingleStoreIO_IT' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,7 +87,7 @@ jobs: kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-rbac.yaml kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-cluster-crd.yaml kubectl apply -f ${{github.workspace}}/.test-infra/kubernetes/singlestore/sdb-operator.yaml - kubectl wait --for=condition=Ready pod -l name=sdb-operator --timeout=300s + kubectl rollout status deployment/sdb-operator --timeout=300s - name: Install SingleStore cluster id: install_singlestore run: | diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml index fc8abb083626..ea822f0895e0 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Dataflow.yml @@ -90,7 +90,7 @@ jobs: job_name: ["beam_PostCommit_Java_Tpcds_Dataflow"] job_phrase: ["Run Dataflow Runner Tpcds Tests"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml index 7cbcc5c314e7..a96dc9416e24 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml @@ -87,7 +87,7 @@ jobs: job_name: ["beam_PostCommit_Java_Tpcds_Flink"] job_phrase: ["Run Flink Runner Tpcds Tests"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -101,5 +101,5 @@ jobs: with: gradle-command: :sdks:java:testing:tpcds:run arguments: | - -Ptpcds.runner=:runners:flink:1.20 \ + -Ptpcds.runner=:runners:flink:2.2 \ "-Ptpcds.args=${{env.tpcdsBigQueryArgs}} ${{env.tpcdsInfluxDBArgs}} ${{ env.GRADLE_COMMAND_ARGUMENTS }} --queries=${{env.tpcdsQueriesArg}}" \ diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml b/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml index 7a34da19b9c7..2204670a6caf 100644 --- a/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Spark.yml @@ -86,7 +86,7 @@ jobs: job_phrase: ["Run Spark Runner Tpcds Tests"] runner: [SparkRunner, SparkStructuredStreamingRunner] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml index dddae544717e..1bdf330ffbb5 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunner max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml index 879996baac4a..750621a7c43b 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_JavaVersions.yml @@ -60,14 +60,14 @@ 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@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -89,7 +89,7 @@ jobs: -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml index 34ed632b0ee2..7a4133d7c425 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Streaming ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerStreaming max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml index e4c138065719..d1501a8696e6 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_Engine.yml @@ -64,7 +64,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Dataflow Streaming Engine ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,7 +81,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerStreamingEngine max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml index 3e8f121bcb9e..204fd44ed9d1 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_TagEncodingV2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow Streaming TagEncodingV2 ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerStreamingTagEncodingV2 max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml index b3ef916ba4ed..c85d7c4cac63 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow V2 ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerV2 max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml index 9c565182d83c..679cc01cc0ea 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Dataflow_V2_Streaming.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java Dataflow V2 ValidatesRunner Streaming' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -89,7 +89,7 @@ jobs: gradle-command: :runners:google-cloud-dataflow-java:validatesRunnerV2Streaming max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml index 179c46d4046f..c69a5ce9704a 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Direct ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: - name: run validatesRunner script run: ./gradlew :runners:direct-java:validatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml index cc7affe0b84d..ec7f63b011f5 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Direct_JavaVersions.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Direct ValidatesRunner Java') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -88,7 +88,7 @@ jobs: -PtestJavaVersion=${{ matrix.java_version }} \ -Pjava${{ matrix.java_version }}Home=$JAVA_HOME_${{ matrix.java_version }}_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml index 1cc77a5da43b..d33b3b412498 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,14 @@ 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@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: with: gradle-command: :runners:flink:${{ matrix.flink_version }}:validatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml deleted file mode 100644 index ac4093a361c4..000000000000 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.yml +++ /dev/null @@ -1,102 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -name: PostCommit Java ValidatesRunner Flink Java8 - -on: - schedule: - - cron: '45 5/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Flink_Java8.json'] - workflow_dispatch: - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: write - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_Java_ValidatesRunner_Flink_Java8: - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 270 - strategy: - matrix: - job_name: [beam_PostCommit_Java_ValidatesRunner_Flink_Java8] - job_phrase: [Run Flink ValidatesRunner Java 8] - if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - startswith(github.event.comment.body, 'Run Flink ValidatesRunner Java 8') - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - with: - java-version: | - 8 - 11 - - name: run validatesRunner Java8 script - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :runners:flink:1.20:validatesRunner - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - max-workers: 12 - - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 - if: ${{ !success() }} - with: - name: JUnit Test Results - path: "**/build/reports/tests/" - - name: Publish JUnit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - large_files: true - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/build/test-results/**/*.xml' diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml index c40527bad150..9f8af5d6699e 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:spark:3:validatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml similarity index 80% rename from .github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml rename to .github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml index 7f65b2ccee33..704539ac92de 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Samza.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Spark4.yml @@ -13,13 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: PostCommit Java ValidatesRunner Samza +name: PostCommit Java ValidatesRunner Spark4 on: schedule: - cron: '45 4/6 * * *' pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Samza.json'] + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Java_ValidatesRunner_Spark4.json'] workflow_dispatch: #Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event @@ -49,21 +49,21 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PostCommit_Java_ValidatesRunner_Samza: + beam_PostCommit_Java_ValidatesRunner_Spark4: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 + timeout-minutes: 120 strategy: matrix: - job_name: [beam_PostCommit_Java_ValidatesRunner_Samza] - job_phrase: [Run Samza ValidatesRunner] + job_name: [beam_PostCommit_Java_ValidatesRunner_Spark4] + job_phrase: [Run Spark4 ValidatesRunner] if: | github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event.comment.body == 'Run Samza ValidatesRunner' + github.event.comment.body == 'Run Spark4 ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -72,20 +72,15 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 with: - java-version: | - 8 - 11 + java-version: '17' - name: run validatesRunner script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:samza:validatesRunner - arguments: | - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ + gradle-command: :runners:spark:4:validatesRunner + arguments: -PdisableSpotlessCheck=true - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -97,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_Java_ValidatesRunner_SparkStructuredStreaming.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml index 637e96d8e623..75bc7a1ddf94 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Spark StructuredStreaming ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:spark:3:validatesStructuredStreamingRunnerBatch - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml index 1fc0b4c375bd..83e0bef0c0f5 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Twister2.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Twister2 ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :runners:twister2:validatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml index dfd70fb0fc70..d84409f839e8 100644 --- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml +++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_ULR.yml @@ -63,7 +63,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run ULR Loopback ValidatesRunner' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -78,7 +78,7 @@ jobs: - name: run ulrLoopbackValidatesRunner script run: ./gradlew :runners:portability:java:ulrLoopbackValidatesRunner - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Javadoc.yml b/.github/workflows/beam_PostCommit_Javadoc.yml index 142a2b830da8..53a967406f07 100644 --- a/.github/workflows/beam_PostCommit_Javadoc.yml +++ b/.github/workflows/beam_PostCommit_Javadoc.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Javadoc PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,7 +87,7 @@ jobs: path-to-root: sdks/java/javadoc/build/docs/javadoc base-url-path: https://beam.apache.org/releases/javadoc/current/ - name: Upload Javadoc Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Javadoc Results path: '**/sdks/java/javadoc/build/docs/javadoc/**' \ No newline at end of file diff --git a/.github/workflows/beam_PostCommit_PortableJar_Flink.yml b/.github/workflows/beam_PostCommit_PortableJar_Flink.yml index d4ac112e1abd..0764e7d89fb4 100644 --- a/.github/workflows/beam_PostCommit_PortableJar_Flink.yml +++ b/.github/workflows/beam_PostCommit_PortableJar_Flink.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_PortableJar_Flink"] job_phrase: ["Run PortableJar_Flink PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: arguments: | -PpythonVersion=3.10 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_PortableJar_Spark.yml b/.github/workflows/beam_PostCommit_PortableJar_Spark.yml index 4919ba941911..5840f3d9b6d4 100644 --- a/.github/workflows/beam_PostCommit_PortableJar_Spark.yml +++ b/.github/workflows/beam_PostCommit_PortableJar_Spark.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_PortableJar_Spark"] job_phrase: ["Run PortableJar_Spark PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: arguments: | -PpythonVersion=3.10 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python.yml b/.github/workflows/beam_PostCommit_Python.yml index 1fe437fc0edb..5d9d45b54bd3 100644 --- a/.github/workflows/beam_PostCommit_Python.yml +++ b/.github/workflows/beam_PostCommit_Python.yml @@ -69,7 +69,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startswith(github.event.comment.body, 'Run Python PostCommit 3.') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -105,7 +105,7 @@ jobs: env: CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results (${{ join(matrix.os, ', ') }}) diff --git a/.github/workflows/beam_PostCommit_Python_Arm.yml b/.github/workflows/beam_PostCommit_Python_Arm.yml index 43b33e840fc0..cb4b3c7cf5cf 100644 --- a/.github/workflows/beam_PostCommit_Python_Arm.yml +++ b/.github/workflows/beam_PostCommit_Python_Arm.yml @@ -53,7 +53,7 @@ env: jobs: beam_PostCommit_Python_Arm: name: ${{ matrix.job_name }} ${{ matrix.python_version }} - runs-on: ubuntu-22.04 + runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 480 strategy: fail-fast: false @@ -66,7 +66,7 @@ jobs: github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository @@ -91,7 +91,7 @@ jobs: - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io @@ -119,7 +119,7 @@ jobs: MULTIARCH_TAG: ${{ steps.set_tag.outputs.TAG }} USER: github-actions - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Dependency.yml b/.github/workflows/beam_PostCommit_Python_Dependency.yml index 0652671d732b..1bd982860afd 100644 --- a/.github/workflows/beam_PostCommit_Python_Dependency.yml +++ b/.github/workflows/beam_PostCommit_Python_Dependency.yml @@ -67,7 +67,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Python PostCommit Dependency') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PuseWheelDistribution \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml index 14dbfc5be501..752036143e8e 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Examples_Dataflow"] job_phrase: ["Run Python Examples_Dataflow"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: -PuseWheelDistribution \ -PpythonVersion=3.14 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml b/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml index 4645e191efaf..c5f02f1eed2b 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Direct.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Direct"] python_version: ['3.10','3.11','3.12', '3.13','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml b/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml index 466d275e0aaf..22607406cafb 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Flink.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Flink"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml b/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml index 308f8442203b..4a2cb5663195 100644 --- a/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml +++ b/.github/workflows/beam_PostCommit_Python_Examples_Spark.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Examples_Spark"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml b/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml index f44c1c117385..e01b765ff5e7 100644 --- a/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml +++ b/.github/workflows/beam_PostCommit_Python_MongoDBIO_IT.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_MongoDBIO_IT"] job_phrase: ["Run Python MongoDBIO_IT"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -82,7 +82,7 @@ jobs: arguments: | -PpythonVersion=3.14 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml b/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml index f942a30b5261..71038c3c8941 100644 --- a/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Nexmark_Direct.yml @@ -108,7 +108,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Python Direct Runner Nexmark Tests' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml b/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml index 40c881a3f220..adb4923d7092 100644 --- a/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_Portable_Flink.yml @@ -67,7 +67,7 @@ jobs: # Run modes not covered by PreCommit_Python_PVR_Flink (i.e. other than 'LOOPBACK') environment_type: ['DOCKER'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -88,7 +88,7 @@ jobs: arguments: | -PpythonVersion=3.10 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results ${{ matrix.environment_type }} diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml index cac0f9c0a50b..c03e8999d805 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow.yml @@ -67,7 +67,7 @@ jobs: job_phrase: ["Run Python Dataflow ValidatesContainer"] python_version: ['3.10','3.11','3.12','3.13','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -117,7 +117,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results ${{ matrix.python_version }} diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml index aca5894e123b..069836fabea3 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesContainer_Dataflow_With_RC.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python RC Dataflow ValidatesContainer"] python_version: ['3.10','3.11','3.12','3.13','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -113,7 +113,7 @@ jobs: -PtestRCDependencies=true \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml index 3e840f579614..4b57013dc4f0 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Dataflow.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Dataflow ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: -PuseWheelDistribution \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml index 88037f344a0d..d7fec110ea52 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Flink.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Flink ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -92,7 +92,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml index 7d70e893a281..7d3faaa844ad 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml +++ b/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Spark.yml @@ -65,7 +65,7 @@ jobs: job_phrase: ["Run Python Spark ValidatesRunner"] python_version: ['3.10', '3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: arguments: | -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml b/.github/workflows/beam_PostCommit_Python_Versions.yml similarity index 51% rename from .github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml rename to .github/workflows/beam_PostCommit_Python_Versions.yml index 144de32d2be6..b7dd2f500ebd 100644 --- a/.github/workflows/beam_PostCommit_Python_ValidatesRunner_Samza.yml +++ b/.github/workflows/beam_PostCommit_Python_Versions.yml @@ -13,13 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: PostCommit Python ValidatesRunner Samza - +name: PostCommit Python Versions on: - schedule: - - cron: '15 5/6 * * *' pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Samza.json'] + branches: [ "master", "release-*" ] + paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Python_Versions.json'] + push: + tags: ['v*'] + branches: ['master', 'release-*'] + paths: [ "model/**","sdks/python/**",".github/workflows/beam_PostCommit_Python_Versions.yml"] + schedule: + - cron: '0 3/6 * * *' workflow_dispatch: #Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event @@ -40,32 +44,51 @@ permissions: # 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 }}' + group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.event.pull_request.head.label || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' cancel-in-progress: true env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} + # Aggressive stability settings for flaky CI environment + PYTHONHASHSEED: "0" + OMP_NUM_THREADS: "1" + OPENBLAS_NUM_THREADS: "1" + # TODO(https://github.com/grpc/grpc/issues/37710): Remove once fixed. + GRPC_ENABLE_FORK_SUPPORT: "0" + # gRPC stability - more conservative for unstable networks + GRPC_ARG_KEEPALIVE_TIME_MS: "10000" + GRPC_ARG_KEEPALIVE_TIMEOUT_MS: "15000" + GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS: "1" + GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA: "0" + GRPC_ARG_MAX_RECONNECT_BACKOFF_MS: "30000" + # Beam-specific - very generous timeouts + BEAM_RETRY_MAX_ATTEMPTS: "5" + BEAM_RETRY_INITIAL_DELAY_MS: "5000" + BEAM_RETRY_MAX_DELAY_MS: "120000" + # Force stable execution + BEAM_TESTING_FORCE_SINGLE_BUNDLE: "true" + BEAM_TESTING_DETERMINISTIC_ORDER: "true" jobs: - beam_PostCommit_Python_ValidatesRunner_Samza: - if: | - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - startsWith(github.event.comment.body, 'Run Python Samza ValidatesRunner') - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 + beam_PostCommit_Python_Versions: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 180 strategy: fail-fast: false matrix: - job_name: ["beam_PostCommit_Python_ValidatesRunner_Samza"] - job_phrase: ["Run Python Samza ValidatesRunner"] - python_version: ['3.10', '3.14'] + job_name: ['beam_PostCommit_Python_Versions'] + job_phrase: ['Run Python Version PostCommit'] + python_version: ['3.11','3.12','3.13'] # Run Python PreCommit tests on "middle" versions + if: | + github.event_name == 'push' || + github.event_name == 'pull_request_target' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') || + github.event_name == 'workflow_dispatch' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,11 +97,8 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 with: - java-version: | - 8 - 11 + java-version: default python-version: ${{ matrix.python_version }} - name: Set PY_VER_CLEAN id: set_py_ver_clean @@ -86,19 +106,38 @@ jobs: PY_VER=${{ matrix.python_version }} PY_VER_CLEAN=${PY_VER//.} echo "py_ver_clean=$PY_VER_CLEAN" >> $GITHUB_OUTPUT - - name: Run samzaValidatesRunner script + - name: Run pythonPreCommit + env: + TOX_TESTENV_PASSENV: "DOCKER_*,TESTCONTAINERS_*,TC_*,BEAM_*,GRPC_*,OMP_*,OPENBLAS_*,PYTHONHASHSEED,PYTEST_*" + # Aggressive retry and timeout settings for flaky CI + PYTEST_ADDOPTS: "-v --tb=short --maxfail=5 --durations=30 --reruns=5 --reruns-delay=15 --timeout=600 --disable-warnings" + # Container stability - much more generous timeouts + TC_TIMEOUT: "300" + TC_MAX_TRIES: "15" + TC_SLEEP_TIME: "5" + # Additional gRPC stability for flaky environment + GRPC_ARG_KEEPALIVE_TIME_MS: "60000" + GRPC_ARG_KEEPALIVE_TIMEOUT_MS: "60000" + GRPC_ARG_MAX_CONNECTION_IDLE_MS: "60000" + GRPC_ARG_HTTP2_BDP_PROBE: "1" + GRPC_ARG_SO_REUSEPORT: "1" + # Force sequential execution to reduce load + PYTEST_XDIST_WORKER_COUNT: "1" + # Additional gRPC settings + GRPC_ARG_MAX_RECONNECT_BACKOFF_MS: "120000" + GRPC_ARG_INITIAL_RECONNECT_BACKOFF_MS: "2000" + BEAM_RUNNER_BUNDLE_TIMEOUT_MS: "600000" uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:test-suites:portable:py${{steps.set_py_ver_clean.outputs.py_ver_clean}}:samzaValidatesRunner + gradle-command: :sdks:python:test-suites:tox:py${{steps.set_py_ver_clean.outputs.py_ver_clean}}:preCommitPy${{steps.set_py_ver_clean.outputs.py_ver_clean}} arguments: | -PpythonVersion=${{ matrix.python_version }} \ - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ + -Pposargs="--ignore=apache_beam/ml/" \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: - name: Python Test Results + name: Python ${{ matrix.python_version }} Test Results path: '**/pytest*.xml' - name: Publish Python Test Results uses: EnricoMi/publish-unit-test-result-action@v2 @@ -107,4 +146,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_Gcp_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml index f4be27554dad..3202c29a89ab 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_Gcp_Dataflow"] job_phrase: ["Run Python_Xlang_Gcp_Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 3.14 @@ -84,8 +87,9 @@ jobs: arguments: | -PpythonVersion=3.14 \ -PuseWheelDistribution \ + -Pjava17Home=$JAVA_HOME_17_X64 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml index bfbb8e04889b..1862512a83db 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Gcp_Direct.yml @@ -64,7 +64,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_Gcp_Direct"] job_phrase: ["Run Python_Xlang_Gcp_Direct PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 3.14 @@ -85,8 +88,10 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:test-suites:direct:gcpCrossLanguagePostCommit + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml index 130db04ca203..036ba9e25f39 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_IO_Dataflow"] job_phrase: ["Run Python_Xlang_IO_Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,6 +73,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 3.14 @@ -84,8 +87,9 @@ jobs: -PpythonVersion=3.14 \ -PuseWheelDistribution \ -PkafkaBootstrapServer=10.128.0.40:9094,10.128.0.28:9094,10.128.0.165:9094 \ + -Pjava17Home=$JAVA_HOME_17_X64 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml index 7335346a2fb9..d091a6bd6060 100644 --- a/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_IO_Direct.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Python_Xlang_IO_Direct"] job_phrase: ["Run Python_Xlang_IO_Direct PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,6 +73,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 3.14 @@ -80,9 +83,11 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:test-suites:direct:ioCrossLanguagePostCommit - arguments: -PuseWheelDistribution + arguments: | + -PuseWheelDistribution \ + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_Go_VR_Samza.yml b/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml similarity index 66% rename from .github/workflows/beam_PostCommit_Go_VR_Samza.yml rename to .github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml index ac6f2dd5ed85..ef4c35c189ee 100644 --- a/.github/workflows/beam_PostCommit_Go_VR_Samza.yml +++ b/.github/workflows/beam_PostCommit_Python_Xlang_Messaging_Direct.yml @@ -13,24 +13,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: PostCommit Go VR Samza +name: PostCommit Python Xlang Messaging Direct on: schedule: - - cron: '30 3/6 * * *' + - cron: '45 5/6 * * *' pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_Go_VR_Samza.json'] + 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: read - checks: read + pull-requests: write + checks: write contents: read deployments: read id-token: none - issues: read + issues: write discussions: read packages: read pages: read @@ -49,21 +49,21 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PostCommit_Go_VR_Samza: + 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 Go Samza ValidatesRunner' + 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_Go_VR_Samza"] - job_phrase: ["Run Go Samza ValidatesRunner"] + job_name: ["beam_PostCommit_Python_Xlang_Messaging_Direct"] + job_phrase: ["Run Python_Xlang_Messaging_Direct PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -72,14 +72,26 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 with: - java-version: | - 8 - 11 - - name: run Go Samza ValidatesRunner script - env: - CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} + 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:go:test:samzaValidatesRunner -Pjava8Home=$JAVA_HOME_8_X64 -PtestJavaVersion=8 + 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 d082981d626a..8c198aa6f38a 100644 --- a/.github/workflows/beam_PostCommit_SQL.yml +++ b/.github/workflows/beam_PostCommit_SQL.yml @@ -65,7 +65,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run SQL PostCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -79,7 +79,7 @@ jobs: with: gradle-command: :sqlPostCommit - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_TransformService_Direct.yml b/.github/workflows/beam_PostCommit_TransformService_Direct.yml index e6badfb9d31a..0da5ed93b202 100644 --- a/.github/workflows/beam_PostCommit_TransformService_Direct.yml +++ b/.github/workflows/beam_PostCommit_TransformService_Direct.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run TransformService_Direct PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,7 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: 11 + java-version: | + 17 + 11 python-version: | 3.10 ${{ matrix.python_version }} @@ -83,11 +85,12 @@ jobs: with: gradle-command: :sdks:python:test-suites:direct:xlang:transformServicePythonUsingJava arguments: | - -PtestJavaVersion=11 \ + -PtestJavaVersion=17 \ + -Pjava17Home=$JAVA_HOME_17_X64 \ -Pjava11Home=$JAVA_HOME_11_X64 \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PostCommit_Website_Test.yml b/.github/workflows/beam_PostCommit_Website_Test.yml index b6fe904a6ae7..a0bfce216996 100644 --- a/.github/workflows/beam_PostCommit_Website_Test.yml +++ b/.github/workflows/beam_PostCommit_Website_Test.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_Website_Test"] job_phrase: ["Run Full Website Test"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PostCommit_XVR_Direct.yml b/.github/workflows/beam_PostCommit_XVR_Direct.yml index 014a7bf9176c..c8e03769bd04 100644 --- a/.github/workflows/beam_PostCommit_XVR_Direct.yml +++ b/.github/workflows/beam_PostCommit_XVR_Direct.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_Direct PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Direct script env: @@ -83,9 +86,10 @@ jobs: gradle-command: :sdks:python:test-suites:direct:xlang:validatesCrossLanguageRunner arguments: | -PpythonVersion=${{ matrix.python_version }} \ + -Pjava17Home=$JAVA_HOME_17_X64 \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_Flink.yml b/.github/workflows/beam_PostCommit_XVR_Flink.yml index bb20b06efc41..273cf5c994cc 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,7 @@ jobs: job_phrase: ["Run XVR_Flink PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -75,6 +75,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Flink script env: @@ -83,10 +86,11 @@ jobs: with: gradle-command: :runners:flink:${{ env.FlinkVersion }}:job-server:validatesCrossLanguageRunner arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=${{ matrix.python_version }} \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml index b4edf563fbbb..d9f58f649e0a 100644 --- a/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_GoUsingJava_Dataflow.yml @@ -66,7 +66,7 @@ jobs: job_name: ["beam_PostCommit_XVR_GoUsingJava_Dataflow"] job_phrase: ["Run XVR_GoUsingJava_Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -76,9 +76,12 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: run XVR GoUsingJava Dataflow script env: USER: github-actions @@ -86,9 +89,10 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerGoUsingJava arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PuseDockerBuildx - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml index e9b03a5bcaac..68fa1e646311 100644 --- a/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_JavaUsingPython_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_JavaUsingPython_Dataflow PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 ${{ matrix.python_version }} @@ -82,9 +85,10 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerJavaUsingPython arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml index 1967f8438e19..c180e43cc8cc 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow.yml @@ -63,7 +63,7 @@ jobs: job_name: ["beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow"] job_phrase: ["Run XVR_PythonUsingJavaSQL_Dataflow PostCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -73,15 +73,19 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: 3.14 - name: run PostCommit XVR PythonUsingJavaSQL Dataflow script uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerPythonUsingSql arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=3.14 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml index 5a9e6a5574fb..1c7bc806c035 100644 --- a/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml +++ b/.github/workflows/beam_PostCommit_XVR_PythonUsingJava_Dataflow.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_PythonUsingJava_Dataflow PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: | 3.10 ${{ matrix.python_version }} @@ -82,9 +85,10 @@ jobs: with: gradle-command: :runners:google-cloud-dataflow-java:validatesCrossLanguageRunnerPythonUsingJava arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostCommit_XVR_Samza.yml b/.github/workflows/beam_PostCommit_XVR_Samza.yml deleted file mode 100644 index 6b6905f6ce72..000000000000 --- a/.github/workflows/beam_PostCommit_XVR_Samza.yml +++ /dev/null @@ -1,107 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -name: PostCommit XVR Samza - -on: - schedule: - - cron: '45 5/6 * * *' - pull_request_target: - paths: ['release/trigger_all_tests.json', '.github/trigger_files/beam_PostCommit_XVR_Samza.json'] - workflow_dispatch: - -#Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event -permissions: - actions: write - pull-requests: write - checks: write - contents: read - deployments: read - id-token: none - issues: read - discussions: read - packages: read - pages: read - repository-projects: read - security-events: read - statuses: read - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -jobs: - beam_PostCommit_XVR_Samza: - if: | - github.event_name == 'workflow_dispatch' || - github.event_name == 'pull_request_target' || - (github.event_name == 'schedule' && github.repository == 'apache/beam') || - github.event.comment.body == 'Run XVR_Samza PostCommit' - runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 100 - name: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) - strategy: - matrix: - job_name: ["beam_PostCommit_XVR_Samza"] - job_phrase: ["Run XVR_Samza PostCommit"] - python_version: ['3.10','3.14'] - steps: - - uses: actions/checkout@v4 - - name: Setup repository - uses: ./.github/actions/setup-action - with: - comment_phrase: ${{ matrix.job_phrase }} ${{ matrix.python_version }} - github_token: ${{ secrets.GITHUB_TOKEN }} - github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) - - name: Setup environment - uses: ./.github/actions/setup-environment-action - # TODO(https://github.com/apache/beam/issues/32208) move to Java11 after bump to Samza 1.8 - with: - java-version: | - 8 - 11 - python-version: | - ${{ matrix.python_version }} - - name: run PostCommit XVR Samza script - env: - CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}} - uses: ./.github/actions/gradle-command-self-hosted-action - with: - gradle-command: :runners:samza:job-server:validatesCrossLanguageRunner - arguments: | - -PpythonVersion=${{ matrix.python_version }} \ - -PtestJavaVersion=8 \ - -Pjava8Home=$JAVA_HOME_8_X64 \ - -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 - if: ${{ !success() }} - with: - name: JUnit Test Results - path: "**/build/reports/tests/" - - name: Publish JUnit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - commit: '${{ env.prsha || env.GITHUB_SHA }}' - comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} - files: '**/build/test-results/**/*.xml' - large_files: true \ No newline at end of file diff --git a/.github/workflows/beam_PostCommit_XVR_Spark3.yml b/.github/workflows/beam_PostCommit_XVR_Spark3.yml index adfce4346d4f..e303e80a7ea3 100644 --- a/.github/workflows/beam_PostCommit_XVR_Spark3.yml +++ b/.github/workflows/beam_PostCommit_XVR_Spark3.yml @@ -64,7 +64,7 @@ jobs: job_phrase: ["Run XVR_Spark3 PostCommit"] python_version: ['3.10','3.14'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,6 +74,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: ${{ matrix.python_version }} - name: run PostCommit XVR Spark3 script env: @@ -82,10 +85,11 @@ jobs: with: gradle-command: :runners:spark:3:job-server:validatesCrossLanguageRunner arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -PpythonVersion=${{ matrix.python_version }} \ -PskipNonPythonTask=${{ (matrix.python_version == '3.10' && true) || false }} \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PostCommit_Yaml_Xlang_Direct.yml index 1d9eb3eb78a2..740c901a2d7b 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,7 @@ jobs: job_phrase: ["Run Yaml_Xlang_Direct PostCommit"] test_set: ["data", "databases", "messaging"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -74,15 +74,19 @@ jobs: uses: ./.github/actions/setup-environment-action with: python-version: default - java-version: '11' + java-version: | + 17 + 11 - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: run PostCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=ml_test,yaml + gradle-command: :sdks:python:postCommitYamlIntegrationTests -PyamlTestSet=${{ matrix.test_set }} -PbeamPythonExtra=p310_ml_test,yaml + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PostRelease_NightlySnapshot.yml b/.github/workflows/beam_PostRelease_NightlySnapshot.yml index 11cd459c5a5d..f83b6499e582 100644 --- a/.github/workflows/beam_PostRelease_NightlySnapshot.yml +++ b/.github/workflows/beam_PostRelease_NightlySnapshot.yml @@ -54,7 +54,7 @@ jobs: name: beam_PostRelease_NightlySnapshot runs-on: [self-hosted, ubuntu-24.04, highmem] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/beam_PreCommit_CommunityMetrics.yml b/.github/workflows/beam_PreCommit_CommunityMetrics.yml index 0901351be1c8..97fd004c2937 100644 --- a/.github/workflows/beam_PreCommit_CommunityMetrics.yml +++ b/.github/workflows/beam_PreCommit_CommunityMetrics.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run CommunityMetrics PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: with: java-version: default - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Remove default github maven configuration run: rm ~/.m2/settings.xml - name: Install docker compose diff --git a/.github/workflows/beam_PreCommit_Flink_Container.yml b/.github/workflows/beam_PreCommit_Flink_Container.yml index 0d23cf14a8ea..7e9571dca6f3 100644 --- a/.github/workflows/beam_PreCommit_Flink_Container.yml +++ b/.github/workflows/beam_PreCommit_Flink_Container.yml @@ -70,12 +70,12 @@ env: GCLOUD_ZONE: us-central1-a CLUSTER_NAME: beam-precommit-flink-container-${{ github.run_id }} GCS_BUCKET: gs://beam-flink-cluster - FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz + FLINK_DOWNLOAD_URL: https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz HADOOP_DOWNLOAD_URL: https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar FLINK_TASKMANAGER_SLOTS: 1 DETACHED_MODE: true HARNESS_IMAGES_TO_PULL: gcr.io/apache-beam-testing/beam-sdk/beam_go_sdk:latest - JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink1.17_job_server:latest + JOB_SERVER_IMAGE: gcr.io/apache-beam-testing/beam_portability/beam_flink_job_server:latest-flink2.0 ARTIFACTS_DIR: gs://beam-flink-cluster/beam-precommit-flink-container-${{ github.run_id }} DOCKER_REGISTRY: gcr.io DOCKER_REPOSITORY_ROOT: ${{ github.event_name == 'pull_request_target' && 'gcr.io/apache-beam-testing/beam-sdk-pr' || 'gcr.io/apache-beam-testing/beam-sdk' }} @@ -98,7 +98,7 @@ jobs: job_name: ["beam_PreCommit_Flink_Container"] job_phrase: ["Run Flink Container PreCommit"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - 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 diff --git a/.github/workflows/beam_PreCommit_GHA.yml b/.github/workflows/beam_PreCommit_GHA.yml index 89afbca79172..af5513a2835e 100644 --- a/.github/workflows/beam_PreCommit_GHA.yml +++ b/.github/workflows/beam_PreCommit_GHA.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GHA PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Go.yml b/.github/workflows/beam_PreCommit_Go.yml index 18dff488ae3d..4d64cba3675c 100644 --- a/.github/workflows/beam_PreCommit_Go.yml +++ b/.github/workflows/beam_PreCommit_Go.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Go PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_GoPortable.yml b/.github/workflows/beam_PreCommit_GoPortable.yml index 8dbff2750603..1d64bb3c23c6 100644 --- a/.github/workflows/beam_PreCommit_GoPortable.yml +++ b/.github/workflows/beam_PreCommit_GoPortable.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GoPortable PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_GoPrism.yml b/.github/workflows/beam_PreCommit_GoPrism.yml index b833f5cf078e..6ff29459a047 100644 --- a/.github/workflows/beam_PreCommit_GoPrism.yml +++ b/.github/workflows/beam_PreCommit_GoPrism.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run GoPrism PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_ItFramework.yml b/.github/workflows/beam_PreCommit_ItFramework.yml index 57a5b53d64bb..dc00bea0ee7c 100644 --- a/.github/workflows/beam_PreCommit_ItFramework.yml +++ b/.github/workflows/beam_PreCommit_ItFramework.yml @@ -76,7 +76,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run It_Framework PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -90,7 +90,7 @@ jobs: - name: run ItFrameworkPrecommit script run: ./gradlew -p it build - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java.yml b/.github/workflows/beam_PreCommit_Java.yml index a11538c38a82..b3e89b46218e 100644 --- a/.github/workflows/beam_PreCommit_Java.yml +++ b/.github/workflows/beam_PreCommit_Java.yml @@ -170,7 +170,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -191,7 +191,7 @@ jobs: -PdisableCheckStyle=true \ -PenableJacocoReport \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -205,7 +205,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -217,7 +217,7 @@ jobs: name: Publish SpotBugs path: '**/build/reports/spotbugs/*.html' - name: Archive Jacoco Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Jacoco Results path: '**/build/jacoco/report/**' diff --git a/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml index 49fda8ae9023..e3fdb9717313 100644 --- a/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_Amazon-Web-Services2_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -118,7 +118,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -132,7 +132,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml index d68a58c4e0f0..884f1f64667e 100644 --- a/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Amqp_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Amqp_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -106,7 +106,7 @@ jobs: comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} files: '**/build/test-results/**/*.xml' - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml index ae48e88c24dc..f06c3e6db095 100644 --- a/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Azure_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_Azure_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,7 +111,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -125,7 +125,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml index afb2fb67f3f5..c81e712e20af 100644 --- a/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Cassandra_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Cassandra_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml index 5e7c60821646..0367a70b4a3c 100644 --- a/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Cdap_IO_Direct.yml @@ -80,7 +80,7 @@ jobs: github.event.comment.body == 'Run Java_Cdap_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -97,7 +97,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -111,7 +111,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml index 54cc2cacf80a..8671235d587a 100644 --- a/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Clickhouse_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Clickhouse_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml index e7ca1a5cde05..f409ae559582 100644 --- a/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Csv_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Csv_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml index d9aad3f96d68..48e563c905fc 100644 --- a/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Datadog_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Datadog_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Dataflow.yml b/.github/workflows/beam_PreCommit_Java_Dataflow.yml index 542672a64a69..3e477197f824 100644 --- a/.github/workflows/beam_PreCommit_Java_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Dataflow.yml @@ -77,7 +77,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java Dataflow Non-portable Worker PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,7 +99,7 @@ jobs: -PdisableCheckStyle=true \ -PenableJacocoReport - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -113,7 +113,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -125,7 +125,7 @@ jobs: name: Publish SpotBugs path: '**/build/reports/spotbugs/*.html' - name: Archive Jacoco Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Jacoco Results path: '**/build/jacoco/report/**' diff --git a/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml index 05dcefa1df70..7b839616efac 100644 --- a/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Debezium_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Debezium_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -110,7 +110,7 @@ jobs: -PtestJavaVersion=17 \ -Pjava17Home=$JAVA_HOME_17_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -124,7 +124,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_SQL_Java8.yml b/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml similarity index 72% rename from .github/workflows/beam_PreCommit_SQL_Java8.yml rename to .github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml index 1e69b3e3ca4c..63439b38bc55 100644 --- a/.github/workflows/beam_PreCommit_SQL_Java8.yml +++ b/.github/workflows/beam_PreCommit_Java_Delta_IO_Direct.yml @@ -13,33 +13,34 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: PreCommit SQL Java8 +name: PreCommit Java Delta IO Direct on: push: tags: ['v*'] branches: ['master', 'release-*'] - paths: ['sdks/java/extensions/sql/**','.github/workflows/beam_PreCommit_SQL_Java8.yml'] + 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/extensions/sql/**', 'release/trigger_all_tests.json', '.github/trigger_files/beam_PreCommit_SQL_Java8.json'] + 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 3/6 * * *' + - cron: '15 2/6 * * *' workflow_dispatch: -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.number || github.event.pull_request.head.label || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' - cancel-in-progress: true - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} - -# Setting explicit permissions for the action to avoid the default permissions which are `write-all` in case of pull_request_target event +#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 @@ -55,23 +56,33 @@ permissions: 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_SQL_Java8: + beam_PreCommit_Java_Delta_IO_Direct: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - runs-on: [self-hosted, ubuntu-24.04, main] strategy: matrix: - job_name: [beam_PreCommit_SQL_Java8] - job_phrase: [Run SQL_Java8 PreCommit] - timeout-minutes: 120 + 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 SQL_Java8 PreCommit' + github.event.comment.body == 'Run Java_Delta_IO_Direct PreCommit' + runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -81,28 +92,19 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: | - 8 - 11 - python-version: default - go-version: default - - name: Install Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.10.4' - channel: 'stable' - - name: Build and Test + java-version: 17 + - name: run Delta IO build script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:java:extensions:sql:preCommit + gradle-command: :sdks:java:io:delta:build arguments: | -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - -PtestJavaVersion=8 \ - -PskipCheckerFramework \ - -Pjava8Home=$JAVA_HOME_8_X64 \ + -PtestJavaVersion=17 \ + -Pjava17Home=$JAVA_HOME_17_X64 \ + --info - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -116,7 +118,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -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_ElasticSearch_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml index f31a0f883f57..67eb433e4db5 100644 --- a/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_ElasticSearch_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_ElasticSearch_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -106,7 +106,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -120,7 +120,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml index 1dafd6a29878..dd58e8d474f9 100644 --- a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml +++ b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow.yml @@ -87,7 +87,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_Examples_Dataflow PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -107,7 +107,7 @@ jobs: -PdisableCheckStyle=true \ -Pdocker-pull-licenses \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml index 0259dc4dfadd..2c4b607a1a5e 100644 --- a/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml +++ b/.github/workflows/beam_PreCommit_Java_Examples_Dataflow_Java21.yml @@ -86,7 +86,7 @@ jobs: github.event.comment.body == 'Run Java_Examples_Dataflow_Java21 PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -122,7 +122,7 @@ jobs: -Pdocker-pull-licenses \ max-workers: 12 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -136,7 +136,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: SpotBugs Results path: '**/build/reports/spotbugs/*.html' \ No newline at end of file diff --git a/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml index dd435fcf21eb..00f42519fd11 100644 --- a/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_File-schema-transform_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_File-schema-transform_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,7 +94,7 @@ jobs: -PdisableCheckStyle=true \ -Dfile.encoding=UTF-8 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -108,7 +108,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml b/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml index 809ed57324f1..7308963dc6d3 100644 --- a/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Flink_Versions.yml @@ -75,7 +75,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event.comment.body == 'Run Java_Flink_Versions PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: arguments: | -PdisableSpotlessCheck=true -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml index 161bc77649f3..eb9739c33b59 100644 --- a/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_GCP_IO_Direct.yml @@ -94,7 +94,7 @@ jobs: github.event.comment.body == 'Run Java_GCP_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -115,7 +115,7 @@ jobs: -PdisableCheckStyle=true \ -PenableJacocoReport \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -129,7 +129,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -145,7 +145,7 @@ jobs: run: | echo "path=sdks/java/io/google-cloud-platform/build/reports/jacoco/test/jacocoTestReport.xml" >> $GITHUB_OUTPUT - name: Archive Jacoco Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Jacoco Results path: '**/build/jacoco/report/**' diff --git a/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml index debcbba36daf..41e857a0ab42 100644 --- a/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Google-ads_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Google-ads_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -105,7 +105,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml index 65e36210f7a5..35d00af9aafd 100644 --- a/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_HBase_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_HBase_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -95,7 +95,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -109,7 +109,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml index 62e37b4d1718..f8561eb7c856 100644 --- a/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_HCatalog_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_HCatalog_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -95,7 +95,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -109,7 +109,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml index e0eb08a8b7b0..6c0e93f37e3d 100644 --- a/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Hadoop_IO_Direct.yml @@ -102,7 +102,7 @@ jobs: github.event.comment.body == 'Run Java_Hadoop_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -133,7 +133,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -147,7 +147,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml b/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml index 67eba0170590..a3af909a9ad6 100644 --- a/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_IOs_Direct.yml @@ -79,7 +79,7 @@ jobs: github.event.comment.body == 'Run Java_IOs_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -97,7 +97,7 @@ jobs: -PdisableCheckStyle=true \ -Dfile.encoding=UTF-8 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -111,7 +111,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml index 7f20a70c0628..84177c33b5af 100644 --- a/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_InfluxDb_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_InfluxDb_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml index c7658f75d6ed..2ac937a34548 100644 --- a/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_JDBC_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_JDBC_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml index 7d34ae42c140..309a316a99a0 100644 --- a/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Jms_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Jms_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml index 71945c923c73..39c4b064c863 100644 --- a/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Kafka_IO_Direct.yml @@ -84,7 +84,7 @@ jobs: github.event.comment.body == 'Run Java_Kafka_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,7 +102,7 @@ jobs: -PdisableCheckStyle=true \ max-workers: 4 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -116,7 +116,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml index 98220eabdf54..a4531c47aede 100644 --- a/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Kudu_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Kudu_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml index a665ecad4b82..54a512170cf6 100644 --- a/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_MongoDb_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_MongoDb_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml index 5ffd65a1c106..0131ea6d7fe0 100644 --- a/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Mqtt_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Mqtt_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml index fdec1c5bf53d..224718385639 100644 --- a/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Neo4j_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_Neo4j_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,7 +102,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -116,7 +116,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml index 98eb7c2befff..f4b1b4545e32 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml @@ -82,7 +82,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Flink_Batch PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -94,17 +94,17 @@ 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 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results path: "**/build/reports/tests/" - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: java-code-coverage-report path: "**/build/test-results/**/*.xml" diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml index f7b0fceae0f3..a173e38eb47d 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml @@ -87,7 +87,7 @@ jobs: github.event.comment.body == 'Run Java_PVR_Flink_Docker PreCommit' timeout-minutes: 240 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,11 +99,11 @@ 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 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml b/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml index 42ac08bb1848..19be2efc087b 100644 --- a/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml +++ b/.github/workflows/beam_PreCommit_Java_PVR_Prism_Loopback.yml @@ -88,7 +88,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Java_PVR_Prism_Loopback PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,13 +102,13 @@ jobs: with: gradle-command: :runners:prism:java:prismLoopbackValidatesRunnerTests - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results path: "**/build/reports/tests/" - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: java-code-coverage-report path: "**/build/test-results/**/*.xml" diff --git a/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml index 26f0f5816ebf..cf2d3f6ab203 100644 --- a/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Parquet_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Parquet_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml index f8557ef4c10e..03a037c87306 100644 --- a/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Pulsar_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Pulsar_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml index 12363d0dc535..3faf5a1a2daf 100644 --- a/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_RabbitMq_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_RabbitMq_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml index 50a0df0ef664..e38dd57fefda 100644 --- a/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Redis_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Redis_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml index faadc9dc91f6..15f83fc9dc87 100644 --- a/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_RequestResponse_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_RequestResponse_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -105,7 +105,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml index 24cbe7fcb619..9f552343232a 100644 --- a/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_SingleStore_IO_Direct.yml @@ -78,7 +78,7 @@ jobs: github.event.comment.body == 'Run Java_SingleStore_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -95,7 +95,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -109,7 +109,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml index 88d531df6730..b78854779e51 100644 --- a/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Snowflake_IO_Direct.yml @@ -80,7 +80,7 @@ jobs: github.event.comment.body == 'Run Java_Snowflake_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -104,7 +104,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -118,7 +118,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml index 9bd8ba4dff56..e72aa00bdd15 100644 --- a/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Solace_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Solace_IO_Direct PreCommit' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -100,7 +100,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -114,7 +114,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml index 2ca3dd3e9d8b..ee2ea171dd4a 100644 --- a/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Solr_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Solr_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml b/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml similarity index 84% rename from .github/workflows/beam_PreCommit_Java_Spark3_Versions.yml rename to .github/workflows/beam_PreCommit_Java_Spark_Versions.yml index 9a35688f4807..9821419da139 100644 --- a/.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml +++ b/.github/workflows/beam_PreCommit_Java_Spark_Versions.yml @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -name: PreCommit Java Spark3 Versions +name: PreCommit Java Spark Versions on: push: @@ -23,13 +23,13 @@ on: branches: ['master', 'release-*'] paths: - 'runners/spark/**' - - '.github/workflows/beam_PreCommit_Java_Spark3_Versions.yml' + - '.github/workflows/beam_PreCommit_Java_Spark_Versions.yml' pull_request_target: branches: ['master', 'release-*'] paths: - 'runners/spark/**' - 'release/trigger_all_tests.json' - - '.github/trigger_files/beam_PreCommit_Java_Spark3_Versions.json' + - '.github/trigger_files/beam_PreCommit_Java_Spark_Versions.json' issue_comment: types: [created] schedule: @@ -63,22 +63,22 @@ env: GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }} jobs: - beam_PreCommit_Java_Spark3_Versions: + beam_PreCommit_Java_Spark_Versions: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) runs-on: [self-hosted, ubuntu-24.04, main] strategy: matrix: - job_name: [beam_PreCommit_Java_Spark3_Versions] - job_phrase: [Run Java_Spark3_Versions PreCommit] + job_name: [beam_PreCommit_Java_Spark_Versions] + job_phrase: [Run Java_Spark_Versions PreCommit] timeout-minutes: 120 if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || (github.event_name == 'schedule' && github.repository == 'apache/beam') || github.event_name == 'workflow_dispatch' || - github.event.comment.body == 'Run Java_Spark3_Versions PreCommit' + github.event.comment.body == 'Run Java_Spark_Versions PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,14 +87,19 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action + with: + java-version: | + 17 + 11 - name: run sparkVersionsTest script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:spark:3:sparkVersionsTest + gradle-command: :runners:spark:3:sparkVersionsTest :runners:spark:4:build arguments: | -PdisableSpotlessCheck=true \ + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results diff --git a/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml index 5fd66a536085..405e82f4517d 100644 --- a/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Splunk_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Splunk_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml index 5ec0f6651873..1286ffcc7816 100644 --- a/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Thrift_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Thrift_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml b/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml index 2954bd4fb548..477397c1e30a 100644 --- a/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml +++ b/.github/workflows/beam_PreCommit_Java_Tika_IO_Direct.yml @@ -76,7 +76,7 @@ jobs: github.event.comment.body == 'Run Java_Tika_IO_Direct PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -93,7 +93,7 @@ jobs: -PdisableSpotlessCheck=true \ -PdisableCheckStyle=true \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -107,7 +107,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Kotlin_Examples.yml b/.github/workflows/beam_PreCommit_Kotlin_Examples.yml index cd567d84686c..35c3786c00e3 100644 --- a/.github/workflows/beam_PreCommit_Kotlin_Examples.yml +++ b/.github/workflows/beam_PreCommit_Kotlin_Examples.yml @@ -88,7 +88,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Kotlin_Examples PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Portable_Python.yml b/.github/workflows/beam_PreCommit_Portable_Python.yml index fb3cf5446b63..2a616e7dd944 100644 --- a/.github/workflows/beam_PreCommit_Portable_Python.yml +++ b/.github/workflows/beam_PreCommit_Portable_Python.yml @@ -90,7 +90,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Portable_Python PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Prism_Python.yml b/.github/workflows/beam_PreCommit_Prism_Python.yml index 0e894453de97..4571b5b3c8c8 100644 --- a/.github/workflows/beam_PreCommit_Prism_Python.yml +++ b/.github/workflows/beam_PreCommit_Prism_Python.yml @@ -84,7 +84,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Prism_Python PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python.yml b/.github/workflows/beam_PreCommit_Python.yml index 279c3c64ebab..d856f011a64a 100644 --- a/.github/workflows/beam_PreCommit_Python.yml +++ b/.github/workflows/beam_PreCommit_Python.yml @@ -83,7 +83,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python'] job_phrase: ['Run Python PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || @@ -91,7 +91,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -137,7 +137,7 @@ jobs: -Pposargs="--ignore=apache_beam/dataframe/ --ignore=apache_beam/ml/ --ignore=apache_beam/examples/ --ignore=apache_beam/runners/ --ignore=apache_beam/transforms/" \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results @@ -150,13 +150,3 @@ jobs: comment_mode: ${{ github.event_name == 'issue_comment' && 'always' || 'off' }} files: '**/pytest*.xml' large_files: true - - name: Cleanup - if: always() - run: | - # Kill any remaining processes - sudo pkill -f "gradle" || true - sudo pkill -f "java" || true - sudo pkill -f "python.*pytest" || true - # Clean up temp files - sudo rm -rf /tmp/beam-* || true - sudo rm -rf /tmp/gradle-* || true \ No newline at end of file diff --git a/.github/workflows/beam_PreCommit_PythonDocker.yml b/.github/workflows/beam_PreCommit_PythonDocker.yml index c40190201d04..ca950aac2116 100644 --- a/.github/workflows/beam_PreCommit_PythonDocker.yml +++ b/.github/workflows/beam_PreCommit_PythonDocker.yml @@ -56,7 +56,7 @@ jobs: beam_PreCommit_PythonDocker: name: ${{ matrix.job_name }} (${{ matrix.job_phrase }} ${{ matrix.python_version }}) runs-on: [self-hosted, ubuntu-24.04, main] - timeout-minutes: 30 + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -69,7 +69,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -83,7 +83,7 @@ jobs: python-version: ${{ matrix.python_version }} go-version: default - name: Setup Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 with: install: true driver: 'docker' diff --git a/.github/workflows/beam_PreCommit_PythonDocs.yml b/.github/workflows/beam_PreCommit_PythonDocs.yml index b1628a90667e..9d7caa15f858 100644 --- a/.github/workflows/beam_PreCommit_PythonDocs.yml +++ b/.github/workflows/beam_PreCommit_PythonDocs.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonDocs PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonFormatter.yml b/.github/workflows/beam_PreCommit_PythonFormatter.yml index b9804d7f3114..bbc04512c80d 100644 --- a/.github/workflows/beam_PreCommit_PythonFormatter.yml +++ b/.github/workflows/beam_PreCommit_PythonFormatter.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonFormatter PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_PythonLint.yml b/.github/workflows/beam_PreCommit_PythonLint.yml index 43a0ae4a88e2..f4c15b5a0115 100644 --- a/.github/workflows/beam_PreCommit_PythonLint.yml +++ b/.github/workflows/beam_PreCommit_PythonLint.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run PythonLint PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Python_Coverage.yml b/.github/workflows/beam_PreCommit_Python_Coverage.yml index 794a28b77374..4799c1e7b09e 100644 --- a/.github/workflows/beam_PreCommit_Python_Coverage.yml +++ b/.github/workflows/beam_PreCommit_Python_Coverage.yml @@ -74,7 +74,7 @@ jobs: github.event_name == 'workflow_dispatch' || startswith(github.event.comment.body, 'Run Python_Coverage PreCommit 3.') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -101,7 +101,7 @@ jobs: flags: python directory: sdks/python - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results (${{ join(matrix.os, ', ') }}) diff --git a/.github/workflows/beam_PreCommit_Python_Dataframes.yml b/.github/workflows/beam_PreCommit_Python_Dataframes.yml index 2842317090cd..f80b8c0d298a 100644 --- a/.github/workflows/beam_PreCommit_Python_Dataframes.yml +++ b/.github/workflows/beam_PreCommit_Python_Dataframes.yml @@ -64,7 +64,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python_Dataframes'] job_phrase: ['Run Python_Dataframes PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Dataframes PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: -Pposargs=apache_beam/dataframe/ \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Dill.yml b/.github/workflows/beam_PreCommit_Python_Dill.yml index 63ef67c3db34..6a807bda69c1 100644 --- a/.github/workflows/beam_PreCommit_Python_Dill.yml +++ b/.github/workflows/beam_PreCommit_Python_Dill.yml @@ -81,7 +81,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Dill PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,7 +111,7 @@ jobs: }}" \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Examples.yml b/.github/workflows/beam_PreCommit_Python_Examples.yml index b4cd132184c1..c8608a1909ff 100644 --- a/.github/workflows/beam_PreCommit_Python_Examples.yml +++ b/.github/workflows/beam_PreCommit_Python_Examples.yml @@ -65,7 +65,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python_Examples'] job_phrase: ['Run Python_Examples PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Examples PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,7 +99,7 @@ jobs: -Pposargs=apache_beam/examples/ \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Integration.yml b/.github/workflows/beam_PreCommit_Python_Integration.yml index a8cac23844b4..22a334ee442d 100644 --- a/.github/workflows/beam_PreCommit_Python_Integration.yml +++ b/.github/workflows/beam_PreCommit_Python_Integration.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Integration PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -105,7 +105,7 @@ jobs: -PuseWheelDistribution \ -PpythonVersion=${{ matrix.python_version }} \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_ML.yml b/.github/workflows/beam_PreCommit_Python_ML.yml index 34aacce81b73..568ae36c1bef 100644 --- a/.github/workflows/beam_PreCommit_Python_ML.yml +++ b/.github/workflows/beam_PreCommit_Python_ML.yml @@ -87,7 +87,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_ML PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Free Disk Space (Ubuntu) if: contains(matrix.os, 'ubuntu-latest') uses: jlumbroso/free-disk-space@v1.3.1 @@ -129,7 +129,7 @@ jobs: }}" \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results ${{ matrix.os }} diff --git a/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml b/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml index 2e79a2806b30..2125eb41bfb3 100644 --- a/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml +++ b/.github/workflows/beam_PreCommit_Python_PVR_Flink.yml @@ -90,7 +90,7 @@ jobs: github.event.comment.body == 'Run Python_PVR_Flink PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -111,7 +111,7 @@ jobs: arguments: | -PpythonVersion=3.14 \ - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Runners.yml b/.github/workflows/beam_PreCommit_Python_Runners.yml index 4637102c50e0..70d9ed4cd02f 100644 --- a/.github/workflows/beam_PreCommit_Python_Runners.yml +++ b/.github/workflows/beam_PreCommit_Python_Runners.yml @@ -64,7 +64,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python_Runners'] job_phrase: ['Run Python_Runners PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Runners PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: -Pposargs=apache_beam/runners/ \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_Python_Transforms.yml b/.github/workflows/beam_PreCommit_Python_Transforms.yml index b4e56ed80bc4..24c86947abce 100644 --- a/.github/workflows/beam_PreCommit_Python_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Python_Transforms.yml @@ -65,7 +65,7 @@ jobs: matrix: job_name: ['beam_PreCommit_Python_Transforms'] job_phrase: ['Run Python_Transforms PreCommit'] - python_version: ['3.10','3.11','3.12','3.13','3.14'] + python_version: ['3.10','3.14'] if: | github.event_name == 'push' || github.event_name == 'pull_request_target' || @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || startsWith(github.event.comment.body, 'Run Python_Transforms PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -99,7 +99,7 @@ jobs: -Pposargs=apache_beam/transforms/ \ -PpythonVersion=${{ matrix.python_version }} - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_PreCommit_RAT.yml b/.github/workflows/beam_PreCommit_RAT.yml index 3faa442ca0dc..a8fc9ce32b3c 100644 --- a/.github/workflows/beam_PreCommit_RAT.yml +++ b/.github/workflows/beam_PreCommit_RAT.yml @@ -69,7 +69,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run RAT PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_SQL.yml b/.github/workflows/beam_PreCommit_SQL.yml index a9248b08b902..7f4cd810ffd7 100644 --- a/.github/workflows/beam_PreCommit_SQL.yml +++ b/.github/workflows/beam_PreCommit_SQL.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run SQL PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -91,7 +91,7 @@ jobs: -PdisableCheckStyle=true \ -PenableJacocoReport \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -105,7 +105,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results @@ -121,7 +121,7 @@ jobs: run: | echo "path=sdks/java/extensions/sql/build/reports/jacoco/test/jacocoTestReport.xml" >> $GITHUB_OUTPUT - name: Archive Jacoco Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Jacoco Results path: '**/build/jacoco/report/**' diff --git a/.github/workflows/beam_PreCommit_SQL_Java17.yml b/.github/workflows/beam_PreCommit_SQL_Java17.yml index ba7a1996d8ab..ddd508e79e29 100644 --- a/.github/workflows/beam_PreCommit_SQL_Java17.yml +++ b/.github/workflows/beam_PreCommit_SQL_Java17.yml @@ -71,7 +71,7 @@ jobs: github.event.comment.body == 'Run SQL_Java17 PreCommit' runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -98,7 +98,7 @@ jobs: -PskipCheckerFramework \ -Pjava17Home=$JAVA_HOME_17_X64 \ - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: ${{ !success() }} with: name: JUnit Test Results @@ -112,7 +112,7 @@ jobs: files: '**/build/test-results/**/*.xml' large_files: true - name: Archive SpotBugs Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: SpotBugs Results diff --git a/.github/workflows/beam_PreCommit_Spotless.yml b/.github/workflows/beam_PreCommit_Spotless.yml index 06619d83f142..64701930e856 100644 --- a/.github/workflows/beam_PreCommit_Spotless.yml +++ b/.github/workflows/beam_PreCommit_Spotless.yml @@ -76,7 +76,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Spotless PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -85,15 +85,13 @@ jobs: github_job: ${{ matrix.job_name }} (${{ matrix.job_phrase }}) - name: Setup environment uses: ./.github/actions/setup-environment-action - - name: Install rsync - run: sudo apt-get update && sudo apt-get install -y rsync - name: run Spotless PreCommit script uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: spotlessCheck checkStyleMain checkStyleTest :buildSrc:spotlessCheck arguments: -PdisableSpotlessApply - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: java-code-coverage-report path: "**/build/reports/checkstyle/*.xml" diff --git a/.github/workflows/beam_PreCommit_Typescript.yml b/.github/workflows/beam_PreCommit_Typescript.yml index 5cfd7a0b076f..a20f96f79105 100644 --- a/.github/workflows/beam_PreCommit_Typescript.yml +++ b/.github/workflows/beam_PreCommit_Typescript.yml @@ -72,7 +72,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Typescript PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Website.yml b/.github/workflows/beam_PreCommit_Website.yml index bd50ca8cbb04..59718f02c7b1 100644 --- a/.github/workflows/beam_PreCommit_Website.yml +++ b/.github/workflows/beam_PreCommit_Website.yml @@ -71,7 +71,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Website PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml b/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml index 09054e6d0e90..0fdfe3974bb4 100644 --- a/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml +++ b/.github/workflows/beam_PreCommit_Website_Stage_GCS.yml @@ -73,7 +73,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Website_Stage_GCS PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Whitespace.yml b/.github/workflows/beam_PreCommit_Whitespace.yml index 866686500c47..a1f6beb7a623 100644 --- a/.github/workflows/beam_PreCommit_Whitespace.yml +++ b/.github/workflows/beam_PreCommit_Whitespace.yml @@ -70,7 +70,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.comment.body == 'Run Whitespace PreCommit' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml index 40c0933f97f6..df4162ec984c 100644 --- a/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml +++ b/.github/workflows/beam_PreCommit_Xlang_Generated_Transforms.yml @@ -92,7 +92,7 @@ jobs: (github.event_name == 'schedule' && github.repository == 'apache/beam') || startsWith(github.event.comment.body, 'Run Xlang_Generated_Transforms PreCommit') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -102,7 +102,9 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: - java-version: default + java-version: | + 17 + 11 python-version: ${{ matrix.python_version }} - name: Set PY_VER_CLEAN id: set_py_ver_clean @@ -114,3 +116,5 @@ jobs: uses: ./.github/actions/gradle-command-self-hosted-action with: gradle-command: :sdks:python:test-suites:direct:crossLanguageWrapperValidationPreCommit --info + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 diff --git a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml index 779b984cd02c..e3dd78460734 100644 --- a/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml +++ b/.github/workflows/beam_PreCommit_Yaml_Xlang_Direct.yml @@ -65,19 +65,20 @@ 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@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -87,13 +88,18 @@ jobs: - name: Setup environment uses: ./.github/actions/setup-environment-action with: + java-version: | + 17 + 11 python-version: default - name: run PreCommit Yaml Xlang Direct script uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=ml_test,yaml + gradle-command: :sdks:python:yamlIntegrationTests -PbeamPythonExtra=p310_ml_test,yaml + arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python Test Results diff --git a/.github/workflows/beam_Prober_CommunityMetrics.yml b/.github/workflows/beam_Prober_CommunityMetrics.yml index 0513caeed2e6..abe9e0e4e974 100644 --- a/.github/workflows/beam_Prober_CommunityMetrics.yml +++ b/.github/workflows/beam_Prober_CommunityMetrics.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_Prober_CommunityMetrics"] job_phrase: ["Run Community Metrics Prober"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Publish_BeamMetrics.yml b/.github/workflows/beam_Publish_BeamMetrics.yml index 60a9298b17ef..111913692feb 100644 --- a/.github/workflows/beam_Publish_BeamMetrics.yml +++ b/.github/workflows/beam_Publish_BeamMetrics.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_PostCommit_BeamMetrics_Publish"] job_phrase: ["Run Beam Metrics Deployment"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml b/.github/workflows/beam_Publish_Beam_SDK_Snapshots.yml index 3a7f57cba08b..8ffd35c06baa 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 }} @@ -52,7 +47,7 @@ jobs: if: | github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') - runs-on: ubuntu-22.04 + runs-on: [self-hosted, ubuntu-24.04, main] timeout-minutes: 300 name: ${{ matrix.job_name }} (${{ matrix.container_task }}) strategy: @@ -79,7 +74,7 @@ jobs: - "python:container:ml:py313:docker" - "java:expansion-service:container:docker" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository @@ -99,7 +94,7 @@ jobs: if: github.ref == 'refs/heads/master' run: echo "LATEST_TAG=,latest" >> $GITHUB_ENV - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Authenticate on GCP uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 with: @@ -114,7 +109,9 @@ jobs: if: ${{ startsWith(matrix.container_task, 'java') }} uses: ./.github/actions/setup-environment-action with: - java-version: 11 + java-version: | + 17 + 11 - name: Setup Python environment if: ${{ startsWith(matrix.container_task, 'python') }} uses: ./.github/actions/setup-environment-action @@ -125,6 +122,7 @@ jobs: with: gradle-command: :sdks:${{ matrix.container_task }} arguments: | + -Pjava17Home=$JAVA_HOME_17_X64 \ -Pdocker-repository-root=gcr.io/apache-beam-testing/beam-sdk \ -Pdocker-tag-list=${{ github.sha }},${BEAM_VERSION}${LATEST_TAG} \ -Pcontainer-architecture-list=arm64,amd64 \ diff --git a/.github/workflows/beam_Publish_Docker_Snapshots.yml b/.github/workflows/beam_Publish_Docker_Snapshots.yml index fe113121890e..c8251c3d913e 100644 --- a/.github/workflows/beam_Publish_Docker_Snapshots.yml +++ b/.github/workflows/beam_Publish_Docker_Snapshots.yml @@ -61,7 +61,7 @@ jobs: job_name: ["beam_Publish_Docker_Snapshots"] job_phrase: ["Publish Docker Snapshots"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: @@ -84,10 +84,10 @@ jobs: -Pdocker-repository-root=gcr.io/apache-beam-testing/beam_portability \ -Pdocker-tag-list=${{ github.sha }}${LATEST_TAG} \ -Pdocker-pull-licenses - - name: run Publish Docker Snapshots script for Flink 1.17 + - name: run Publish Docker Snapshots script for Flink 2.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..21b6dc8d53c8 --- /dev/null +++ b/.github/workflows/beam_Publish_Python_VLLM_Image.yml @@ -0,0 +1,75 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Publish Python VLLM Image + +on: + schedule: + - cron: '0 0 * * 0' # Run weekly (Sunday at 00:00) + workflow_dispatch: + +# Setting explicit permissions for the action to avoid the default permissions +permissions: + actions: write + pull-requests: read + checks: read + contents: read + deployments: read + id-token: none + issues: read + discussions: read + packages: read + pages: read + repository-projects: read + security-events: read + statuses: read + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.issue.number || github.sha || github.head_ref || github.ref }}-${{ github.event.schedule || github.event.comment.id || github.event.sender.login }}' + cancel-in-progress: true + +jobs: + build_and_push_image: + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'schedule' && github.repository == 'apache/beam') + runs-on: [self-hosted, ubuntu-24.04, main] + timeout-minutes: 60 + steps: + - name: Checkout code + uses: actions/checkout@v7 + - name: Authenticate on GCP + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 + with: + service_account: ${{ secrets.GCP_SA_EMAIL }} + credentials_json: ${{ secrets.GCP_SA_KEY }} + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 + - name: GCloud Docker credential helper + run: | + gcloud auth configure-docker us.gcr.io + - name: Build and Push Multi-Arch Image + run: | + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t us.gcr.io/apache-beam-testing/python-postcommit-it/vllm:latest \ + -f sdks/python/apache_beam/ml/inference/test_resources/vllm.dockerfile.old \ + --push \ + . diff --git a/.github/workflows/beam_Publish_Website.yml b/.github/workflows/beam_Publish_Website.yml index a2966319c985..4420878bd280 100644 --- a/.github/workflows/beam_Publish_Website.yml +++ b/.github/workflows/beam_Publish_Website.yml @@ -53,7 +53,7 @@ jobs: timeout-minutes: 30 name: beam_Publish_Website steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -69,4 +69,4 @@ jobs: with: gradle-command: :website:clean :website:publishWebsite arguments: -PgitPublishRemote="https://github.com/apache/beam.git" -PgcpCredsFile="${{ steps.auth-gcp.outputs.credentials_file_path }}" - - uses: actions/checkout@v4 # Extra checkout to make sure we're on master for post steps. + - uses: actions/checkout@v7 # Extra checkout to make sure we're on master for post steps. diff --git a/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml b/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml index 0d70538d175d..3a904db7d32e 100644 --- a/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml +++ b/.github/workflows/beam_Python_CostBenchmarks_Dataflow.yml @@ -61,7 +61,7 @@ jobs: job_name: ["beam_Python_CostBenchmark_Dataflow"] job_phrase: ["Run Python Dataflow Cost Benchmarks"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml index 1837fbc3e565..19c506c93ebe 100644 --- a/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml +++ b/.github/workflows/beam_Python_ValidatesContainer_Dataflow_ARM.yml @@ -63,7 +63,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - name: Setup repository @@ -84,7 +84,7 @@ jobs: - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: GCloud Docker credential helper run: | gcloud auth configure-docker us.gcr.io @@ -113,7 +113,7 @@ jobs: MULTIARCH_TAG: ${{ steps.set_tag.outputs.TAG }} USER: github-actions - name: Archive Python Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: Python ${{ matrix.python_version }} Test Results diff --git a/.github/workflows/beam_Release_NightlySnapshot.yml b/.github/workflows/beam_Release_NightlySnapshot.yml index 6c011675b9ef..012d46907f4d 100644 --- a/.github/workflows/beam_Release_NightlySnapshot.yml +++ b/.github/workflows/beam_Release_NightlySnapshot.yml @@ -51,7 +51,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_Release_Python_NightlySnapshot.yml b/.github/workflows/beam_Release_Python_NightlySnapshot.yml index 7cb8bf27951a..e0c3ffd93011 100644 --- a/.github/workflows/beam_Release_Python_NightlySnapshot.yml +++ b/.github/workflows/beam_Release_Python_NightlySnapshot.yml @@ -53,7 +53,7 @@ jobs: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'apache/beam') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_BigQueryIO.yml b/.github/workflows/beam_StressTests_Java_BigQueryIO.yml index e0cdfbcdd5be..c2bdbd465568 100644 --- a/.github/workflows/beam_StressTests_Java_BigQueryIO.yml +++ b/.github/workflows/beam_StressTests_Java_BigQueryIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_BigQueryIO"] job_phrase: ["Run Stress Tests Java BigQueryIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_BigTableIO.yml b/.github/workflows/beam_StressTests_Java_BigTableIO.yml index e6025a91b639..2e5eb3a4ebdc 100644 --- a/.github/workflows/beam_StressTests_Java_BigTableIO.yml +++ b/.github/workflows/beam_StressTests_Java_BigTableIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_BigTableIO"] job_phrase: ["Run Stress Tests Java BigTableIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_KafkaIO.yml b/.github/workflows/beam_StressTests_Java_KafkaIO.yml index f59665e7e8ae..b1ed4c8d5801 100644 --- a/.github/workflows/beam_StressTests_Java_KafkaIO.yml +++ b/.github/workflows/beam_StressTests_Java_KafkaIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_KafkaIO"] job_phrase: ["Run Stress Tests Java KafkaIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_PubSubIO.yml b/.github/workflows/beam_StressTests_Java_PubSubIO.yml index 2c6415bbf0ca..ed911e128092 100644 --- a/.github/workflows/beam_StressTests_Java_PubSubIO.yml +++ b/.github/workflows/beam_StressTests_Java_PubSubIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_PubSubIO"] job_phrase: ["Run Stress Tests Java PubSubIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/beam_StressTests_Java_SpannerIO.yml b/.github/workflows/beam_StressTests_Java_SpannerIO.yml index 605a4c84eec0..3f56af490d77 100644 --- a/.github/workflows/beam_StressTests_Java_SpannerIO.yml +++ b/.github/workflows/beam_StressTests_Java_SpannerIO.yml @@ -62,7 +62,7 @@ jobs: job_name: ["beam_StressTests_Java_SpannerIO"] job_phrase: ["Run Stress Tests Java SpannerIO"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup repository uses: ./.github/actions/setup-action with: diff --git a/.github/workflows/build_release_candidate.yml b/.github/workflows/build_release_candidate.yml index cabe2b4c8ced..3f9aa95ede15 100644 --- a/.github/workflows/build_release_candidate.yml +++ b/.github/workflows/build_release_candidate.yml @@ -61,7 +61,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -74,7 +74,7 @@ jobs: 11 - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Auth for nexus @@ -126,7 +126,7 @@ jobs: java-version: '11' - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: stage source @@ -170,7 +170,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Mask Apache Password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -193,7 +193,7 @@ jobs: disable-cache: true - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Install dependencies @@ -270,30 +270,32 @@ jobs: ] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@v1.3.1 - - name: Install Java 11 + - name: Install Java uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: | + 17 + 11 - name: Install Python 3.10 uses: actions/setup-python@v5 with: python-version: '3.10' - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Remove default github maven configuration # This step is a workaround to avoid a decryption issue of Beam's # net.linguica.gradle.maven.settings plugin and github's provided maven # settings.xml file run: rm ~/.m2/settings.xml || true - name: Login to Docker Hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -315,13 +317,13 @@ jobs: with: docker-images: false - name: Checkout Beam Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam path: beam - name: Checkout Beam Site Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: apache/beam-site path: beam-site @@ -431,7 +433,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -455,7 +457,7 @@ jobs: go-version: '1.26' - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Build prism artifacts @@ -559,7 +561,7 @@ jobs: MANAGED_IO_DOCS_PATH: website/www/site/content/en/documentation/io/managed-io.md steps: - name: Checkout Beam Repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: "v${{ github.event.inputs.RELEASE }}-RC${{ github.event.inputs.RC }}" repository: apache/beam @@ -570,11 +572,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.10' - - name: Install Java 11 + - name: Install Java 17 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - name: Remove default github maven configuration # This step is a workaround to avoid a decryption issue of Beam's # net.linguica.gradle.maven.settings plugin and github's provided maven diff --git a/.github/workflows/build_runner_image.yml b/.github/workflows/build_runner_image.yml index 9e7092008b5a..4b556628e6c8 100644 --- a/.github/workflows/build_runner_image.yml +++ b/.github/workflows/build_runner_image.yml @@ -38,16 +38,16 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} - 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@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Build and Load to docker - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf with: context: ${{ env.working-directory }} load: true @@ -57,7 +57,7 @@ jobs: - name: Push Docker image if: github.ref == 'refs/heads/master' id: docker_build - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf with: context: ${{ env.working-directory }} push: true diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 93f1419e4aba..e4fc279d0b34 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -55,7 +55,7 @@ jobs: py-versions-full: ${{ steps.set-py-versions.outputs.py-versions-full }} py-versions-test: ${{ steps.set-py-versions.outputs.py-versions-test }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -88,7 +88,7 @@ jobs: rc_num: ${{ steps.get_rc_version.outputs.RC_NUM }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install python uses: actions/setup-python@v5 with: @@ -127,12 +127,12 @@ jobs: # https://github.com/pypa/setuptools/issues/4300 changed naming. Match both old and new names. run: mv $(ls | grep "apache-beam-\|apache_beam-") apache-beam-source - name: Upload source as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: source path: sdks/python/apache-beam-source - name: Upload compressed sources as artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: source_zip path: sdks/python/dist @@ -170,13 +170,13 @@ jobs: run: mv $(ls | grep "apache-beam-\|apache_beam-") apache-beam-source-rc - name: Upload RC source as artifact if: steps.is_rc.outputs.is_rc == 1 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: source_rc${{ steps.get_rc_version.outputs.RC_NUM }} path: sdks/python/apache-beam-source-rc - name: Upload compressed RC sources as artifacts if: steps.is_rc.outputs.is_rc == 1 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: source_zip_rc${{ steps.get_rc_version.outputs.RC_NUM }} path: sdks/python/dist @@ -202,7 +202,7 @@ jobs: if: needs.check_env_variables.outputs.gcp-variables-set == 'true' steps: - name: Download compressed sources from artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: source_zip path: source/ @@ -231,13 +231,13 @@ jobs: py_version: ["cp310-", "cp311-", "cp312-", "cp313-", "cp314-"] steps: - name: Download python source distribution from artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: source path: apache-beam-source - name: Download Python SDK RC source distribution from artifacts if: ${{ needs.build_source.outputs.is_rc == 1 }} - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: name: source_rc${{ needs.build_source.outputs.rc_num }} path: apache-beam-source-rc @@ -245,7 +245,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.11' - - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 if: ${{matrix.os_python.arch == 'aarch64'}} name: Set up QEMU - name: Install cibuildwheel @@ -275,7 +275,7 @@ jobs: shell: bash - name: Upload wheels as artifacts if: ${{ contains(matrix.os_python.python, matrix.py_version) }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheelhouse-${{ matrix.py_version }}${{ matrix.os_python.os }}${{ (matrix.os_python.arch == 'aarch64' && '-aarch64') || '' }} path: apache-beam-source/wheelhouse/ @@ -300,7 +300,7 @@ jobs: shell: bash - name: Upload RC wheels as artifacts if: ${{ needs.build_source.outputs.is_rc == 1 }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: wheelhouse-rc${{ needs.build_source.outputs.rc_num }}-${{ matrix.py_version }}${{ matrix.os_python.os }}${{ (matrix.os_python.arch == 'aarch64' && '-aarch64') || '' }} path: apache-beam-source-rc/wheelhouse/ @@ -314,7 +314,7 @@ jobs: if: needs.check_env_variables.outputs.gcp-variables-set == 'true' && github.event_name != 'pull_request' steps: - name: Download wheels from artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: pattern: wheelhouse-* merge-multiple: true @@ -365,7 +365,7 @@ jobs: if: github.repository_owner == 'apache' && github.event_name == 'schedule' steps: - name: Checkout code on master branch - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml index f826b22e043b..d8960a5a5815 100644 --- a/.github/workflows/cancel.yml +++ b/.github/workflows/cancel.yml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/code_completion_plugin_tests.yml b/.github/workflows/code_completion_plugin_tests.yml index b183385383be..a9930567d648 100644 --- a/.github/workflows/code_completion_plugin_tests.yml +++ b/.github/workflows/code_completion_plugin_tests.yml @@ -57,13 +57,13 @@ jobs: # Check out beam repository - name: Fetch beam Sources - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: main # Check out intellij community repository for tests - name: Fetch intellij-community Sources - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: JetBrains/intellij-community path: intellij @@ -106,7 +106,7 @@ jobs: # Collect Tests Result of failed tests - name: Collect Tests Result if: ${{ failure() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: tests-result path: ${{ github.workspace }}/build/reports/tests diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000000..0a7aa09530a8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -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. + +# Enables code scanning alerts in repo. + +name: "CodeQL Advanced" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + workflow_dispatch: + schedule: + - cron: '25 10 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + # - language: c-cpp + # build-mode: autobuild + - language: go + build-mode: manual + - language: java-kotlin + build-mode: autobuild + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + - language: rust + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up Go + if: matrix.language == 'go' + uses: actions/setup-go@v6 + with: + go-version-file: 'sdks/go.mod' + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + 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 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/cut_release_branch.yml b/.github/workflows/cut_release_branch.yml index bedbd91c14a9..8b591524ba55 100644 --- a/.github/workflows/cut_release_branch.yml +++ b/.github/workflows/cut_release_branch.yml @@ -61,7 +61,7 @@ jobs: exit 1 fi - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set git config run: | git config user.name $GITHUB_ACTOR @@ -110,13 +110,16 @@ jobs: exit 1 fi - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - 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 bbecfb8e18c2..2b279bd3ac2f 100644 --- a/.github/workflows/dask_runner_tests.yml +++ b/.github/workflows/dask_runner_tests.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install python uses: actions/setup-python@v5 with: @@ -52,7 +52,7 @@ jobs: working-directory: ./sdks/python/dist run: mv $(ls | grep "apache-beam.*tar\.gz\|apache_beam.*tar\.gz") apache-beam-source.tar.gz - name: Upload compressed sources as artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python_sdk_source path: sdks/python/dist/apache-beam-source.tar.gz @@ -69,7 +69,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install python uses: actions/setup-python@v5 with: @@ -88,7 +88,7 @@ jobs: working-directory: ./sdks/python run: tox -c tox.ini -e ${{ matrix.params.tox_env }}-win-dask - name: Upload test logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: pytest-${{matrix.os}}-${{matrix.params.py_ver}} diff --git a/.github/workflows/deploy_release_candidate_pypi.yaml b/.github/workflows/deploy_release_candidate_pypi.yaml index 67438bee43c6..b8eb07c44172 100644 --- a/.github/workflows/deploy_release_candidate_pypi.yaml +++ b/.github/workflows/deploy_release_candidate_pypi.yaml @@ -30,7 +30,7 @@ jobs: PYPI_API_TOKEN=$(jq -r '.inputs.PYPI_API_TOKEN' $GITHUB_EVENT_PATH) echo "::add-mask::$PYPI_API_TOKEN" - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/finalize_release.yml b/.github/workflows/finalize_release.yml index 476314d4f9b6..16c3c2df2c0f 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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee 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,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Mask PyPi password run: | # Workaround for Actions bug - https://github.com/actions/runner/issues/643 @@ -114,6 +121,8 @@ jobs: disable-cache: true - name: Install dependencies run: | + sudo apt-get update --yes + sudo apt-get install -y wget pip install python-dateutil pip install requests pip install twine @@ -132,14 +141,18 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout - uses: actions/checkout@v4 + 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 git config user.email actions@"$RUNNER_NAME".local - name: Import GPG key id: import_gpg - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Push tags @@ -156,14 +169,14 @@ jobs: # Tag for Go SDK git tag "sdks/$VERSION_TAG" "$RC_TAG"^{} -m "Tagging release" --local-user="${{steps.import_gpg.outputs.name}}" - git push https://github.com/apache/beam "sdks/$VERSION_TAG" + git push origin "sdks/$VERSION_TAG" # Tag for repo root. git tag "$VERSION_TAG" "$RC_TAG"^{} -m "Tagging release" --local-user="${{steps.import_gpg.outputs.name}}" - git push https://github.com/apache/beam "$VERSION_TAG" + git push origin "$VERSION_TAG" git checkout -b "$POST_RELEASE_BRANCH" "$VERSION_TAG" - git push https://github.com/apache/beam "$POST_RELEASE_BRANCH" + git push origin "$POST_RELEASE_BRANCH" update_master: needs: push_git_tags @@ -172,7 +185,10 @@ jobs: POST_RELEASE_BRANCH: "release-${{ github.event.inputs.RELEASE }}-postrelease" steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + token: ${{ github.event.inputs.REPO_TOKEN }} + ref: master - name: Set git config run: | git config user.name $GITHUB_ACTOR diff --git a/.github/workflows/flaky_test_detection.yml b/.github/workflows/flaky_test_detection.yml index 32e2bb32c69e..30d51147c666 100644 --- a/.github/workflows/flaky_test_detection.yml +++ b/.github/workflows/flaky_test_detection.yml @@ -38,7 +38,7 @@ jobs: flaky-test-detection: runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-python@v5 with: python-version: 3.11 diff --git a/.github/workflows/git_tag_released_version.yml b/.github/workflows/git_tag_released_version.yml index 0c6782603856..ecb63232be5c 100644 --- a/.github/workflows/git_tag_released_version.yml +++ b/.github/workflows/git_tag_released_version.yml @@ -37,7 +37,7 @@ jobs: VERSION_PATH: ${{ github.event.inputs.VERSION_TAG }} steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set git config run: | git config user.name $GITHUB_ACTOR diff --git a/.github/workflows/go_tests.yml b/.github/workflows/go_tests.yml index e5289e096787..644d9a52cb12 100644 --- a/.github/workflows/go_tests.yml +++ b/.github/workflows/go_tests.yml @@ -40,7 +40,7 @@ jobs: name: Go Build steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 2 - name: Setup environment diff --git a/.github/workflows/issue-tagger.yml b/.github/workflows/issue-tagger.yml index dbfe2e996d5e..a893a168934f 100644 --- a/.github/workflows/issue-tagger.yml +++ b/.github/workflows/issue-tagger.yml @@ -24,7 +24,7 @@ jobs: permissions: issues: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: damccorm/tag-ur-it@6fa72bbf1a2ea157b533d7e7abeafdb5855dbea5 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/java_tests.yml b/.github/workflows/java_tests.yml index 393016c1a883..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@v4 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive @@ -64,7 +64,7 @@ jobs: gradle-command: test arguments: -p sdks/java/core/ - name: Upload test logs for :sdks:java:core:test - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: java_unit_tests-sdks-java-core-test-${{ matrix.os }} @@ -77,7 +77,7 @@ jobs: arguments: -p sdks/java/harness/ if: always() - name: Upload test logs for :sdks:java:harness:test - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: java_unit_tests-sdks-java-harness-test-${{ matrix.os }} @@ -90,7 +90,7 @@ jobs: arguments: -p runners/core-java/ if: always() - name: Upload test logs for :runners:core-java:test - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: java_unit_tests-runners-core-java-test-${{ matrix.os }} @@ -105,7 +105,7 @@ jobs: os: [[self-hosted, ubuntu-24.04, main], macos-latest, windows-latest] steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive @@ -122,7 +122,7 @@ jobs: -DintegrationTestRunner=direct -DintegrationTestPipelineOptions=[\"--runner=DirectRunner\",\"--tempRoot=./tmp\"] - name: Upload test logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: java_wordcount_direct_runner-${{matrix.os}} diff --git a/.github/workflows/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_One_Hot_Encoding_Batch.txt b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt new file mode 100644 index 000000000000..27648d0c0fb0 --- /dev/null +++ b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--region=us-central1 +--machine_type=n1-standard-2 +--num_workers=50 +--disk_size_gb=50 +--autoscaling_algorithm=NONE +--staging_location=gs://temp-storage-for-perf-tests/loadtests +--temp_location=gs://temp-storage-for-perf-tests/loadtests +--sdk_location=container +--requirements_file=apache_beam/ml/transforms/mltransform_tests_requirements.txt +--publish_to_big_query=true +--metrics_dataset=beam_run_inference +--metrics_table=mltransform_one_hot_encoding_batch +--input_options={} +--influx_measurement=mltransform_one_hot_encoding_batch +# Note: output_file and artifact_location are set in the workflow with unique timestamps +--input_file=gs://apache-beam-ml/testing/inputs/sentences_50k.txt +--input_format=text +--categorical_columns=category +--num_records=1000000 +--runner=DataflowRunner diff --git a/.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/local_env_tests.yml b/.github/workflows/local_env_tests.yml index 3983bfe7e7b9..96682a46f4dd 100644 --- a/.github/workflows/local_env_tests.yml +++ b/.github/workflows/local_env_tests.yml @@ -45,7 +45,7 @@ jobs: name: "Ubuntu run local environment shell script" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -62,7 +62,7 @@ jobs: name: "Mac run local environment shell script" runs-on: macos-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/playground_frontend_test.yml b/.github/workflows/playground_frontend_test.yml index 1a0dff44d2e8..b54fdc0ecfd6 100644 --- a/.github/workflows/playground_frontend_test.yml +++ b/.github/workflows/playground_frontend_test.yml @@ -45,10 +45,10 @@ jobs: FLUTTER_VERSION: '3.10.4' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: 'Cache Flutter Dependencies' - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: /opt/hostedtoolcache/flutter key: ${{ runner.OS }}-flutter-install-cache-${{ env.FLUTTER_VERSION }} @@ -89,7 +89,7 @@ jobs: working-directory: playground/frontend run: flutter test - - uses: nanasess/setup-chromedriver@v2 + - uses: nanasess/setup-chromedriver@v3 - name: 'Integration tests' run: | diff --git a/.github/workflows/pr-bot-new-prs.yml b/.github/workflows/pr-bot-new-prs.yml index 590824002012..04219453b1ea 100644 --- a/.github/workflows/pr-bot-new-prs.yml +++ b/.github/workflows/pr-bot-new-prs.yml @@ -33,7 +33,7 @@ jobs: if: github.repository == 'apache/beam' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/pr-bot-pr-updates.yml b/.github/workflows/pr-bot-pr-updates.yml index 86cc291e87bb..8f31a70f29c0 100644 --- a/.github/workflows/pr-bot-pr-updates.yml +++ b/.github/workflows/pr-bot-pr-updates.yml @@ -36,7 +36,7 @@ jobs: steps: # Pin to master so users can't do anything malicious on their own branch and run it here. - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: ref: 'master' - name: Setup Node diff --git a/.github/workflows/pr-bot-prs-needing-attention.yml b/.github/workflows/pr-bot-prs-needing-attention.yml index eb6adfcaa823..977e730a7721 100644 --- a/.github/workflows/pr-bot-prs-needing-attention.yml +++ b/.github/workflows/pr-bot-prs-needing-attention.yml @@ -33,7 +33,7 @@ jobs: if: github.repository == 'apache/beam' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/publish_github_release_notes.yml b/.github/workflows/publish_github_release_notes.yml index 473e0deef83d..27257e095957 100644 --- a/.github/workflows/publish_github_release_notes.yml +++ b/.github/workflows/publish_github_release_notes.yml @@ -36,7 +36,7 @@ jobs: properties: ${{ steps.test-properties.outputs.properties }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - id: test-properties uses: ./.github/actions/setup-default-test-properties @@ -49,7 +49,7 @@ jobs: name: Publish Github Release Notes steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Publish github release notes run: | POST_PATH="website/www/site/content/en/blog/beam-${{env.RELEASE_VERSION}}.md" diff --git a/.github/workflows/python_dependency_tests.yml b/.github/workflows/python_dependency_tests.yml index ad5d5c4629a3..8a9010bf7817 100644 --- a/.github/workflows/python_dependency_tests.yml +++ b/.github/workflows/python_dependency_tests.yml @@ -34,7 +34,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install libsnappy-dev run: sudo apt-get update && sudo apt-get install -y libsnappy-dev - name: Install python diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml index a8a3716ce8af..b650f2ea5cda 100644 --- a/.github/workflows/python_tests.yml +++ b/.github/workflows/python_tests.yml @@ -45,7 +45,7 @@ jobs: outputs: gcp-variables-set: ${{ steps.check_gcp_variables.outputs.gcp-variables-set }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -68,7 +68,7 @@ jobs: runs-on: [self-hosted, ubuntu-24.04, main] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -80,7 +80,7 @@ jobs: working-directory: ./sdks/python/dist run: mv $(ls | grep "apache-beam.*tar\.gz\|apache_beam.*tar\.gz") apache-beam-source.tar.gz - name: Upload compressed sources as artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python_sdk_source path: sdks/python/dist/apache-beam-source.tar.gz @@ -101,7 +101,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: @@ -121,7 +121,7 @@ jobs: working-directory: ./sdks/python run: tox -c tox.ini run -e ${{ matrix.params.tox_env }}-win - name: Upload test logs - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: pytest-${{matrix.os}}-${{matrix.params.py_ver}} @@ -137,7 +137,7 @@ jobs: python: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install python uses: ./.github/actions/setup-environment-action with: diff --git a/.github/workflows/refresh_looker_metrics.yml b/.github/workflows/refresh_looker_metrics.yml index 0d6f76c627a1..187752549741 100644 --- a/.github/workflows/refresh_looker_metrics.yml +++ b/.github/workflows/refresh_looker_metrics.yml @@ -31,7 +31,7 @@ jobs: refresh_looker_metrics: runs-on: [self-hosted, ubuntu-24.04, main] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-python@v5 with: python-version: 3.11 diff --git a/.github/workflows/reportGenerator.yml b/.github/workflows/reportGenerator.yml index 7a4abdb66a08..fbd33d8ee6c2 100644 --- a/.github/workflows/reportGenerator.yml +++ b/.github/workflows/reportGenerator.yml @@ -26,7 +26,7 @@ jobs: name: Generate issue report runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 with: diff --git a/.github/workflows/republish_released_docker_containers.yml b/.github/workflows/republish_released_docker_containers.yml index 7feae365ce51..6c72422d3d5a 100644 --- a/.github/workflows/republish_released_docker_containers.yml +++ b/.github/workflows/republish_released_docker_containers.yml @@ -32,13 +32,13 @@ on: - cron: "0 6 * * 1" env: docker_registry: gcr.io - release: "${{ github.event.inputs.RELEASE || '2.72.0' }}" - rc: "${{ github.event.inputs.RC || '5' }}" + release: "${{ github.event.inputs.RELEASE || '2.74.0' }}" + rc: "${{ github.event.inputs.RC || '3' }}" jobs: build: - runs-on: ubuntu-22.04 + runs-on: [self-hosted, ubuntu-24.04, main] strategy: fail-fast: false matrix: @@ -57,7 +57,7 @@ jobs: ] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: "release-${{ env.release }}-postrelease" repository: apache/beam @@ -80,7 +80,7 @@ jobs: - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Remove default github maven configuration # This step is a workaround to avoid a decryption issue of Beam's # net.linguica.gradle.maven.settings plugin and github's provided maven diff --git a/.github/workflows/run_perf_alert_tool.yml b/.github/workflows/run_perf_alert_tool.yml index 81031e4ea3b6..301fe0d12ce5 100644 --- a/.github/workflows/run_perf_alert_tool.yml +++ b/.github/workflows/run_perf_alert_tool.yml @@ -35,7 +35,7 @@ jobs: issues: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/run_rc_validation_go_wordcount.yml b/.github/workflows/run_rc_validation_go_wordcount.yml index ca3964bf6421..2112bec6b3f8 100644 --- a/.github/workflows/run_rc_validation_go_wordcount.yml +++ b/.github/workflows/run_rc_validation_go_wordcount.yml @@ -48,7 +48,7 @@ jobs: runs-on: self-hosted steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up environment uses: ./.github/actions/setup-environment-action diff --git a/.github/workflows/run_rc_validation_java_mobile_gaming.yml b/.github/workflows/run_rc_validation_java_mobile_gaming.yml index aecfd76db9b0..0a2d08b80517 100644 --- a/.github/workflows/run_rc_validation_java_mobile_gaming.yml +++ b/.github/workflows/run_rc_validation_java_mobile_gaming.yml @@ -78,7 +78,7 @@ jobs: run: echo "GCS_BUCKET_NAME=$(echo ${{ github.event.inputs.GCS_BUCKET }} | sed 's/^gs:\/\///')" >> $GITHUB_ENV - name: Checkout code at RC tag - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: v${{ github.event.inputs.RELEASE_VER }}-RC${{ github.event.inputs.RC_NUM }} @@ -129,7 +129,7 @@ jobs: # Reporting (Optional: Keep if test results are generated) - name: Archive JUnit Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() # Upload only on failure with: name: JUnit Test Results (Java MobileGaming RC) diff --git a/.github/workflows/run_rc_validation_java_quickstart.yml b/.github/workflows/run_rc_validation_java_quickstart.yml index 269ccaab8d57..10e743b9cfee 100644 --- a/.github/workflows/run_rc_validation_java_quickstart.yml +++ b/.github/workflows/run_rc_validation_java_quickstart.yml @@ -68,7 +68,7 @@ jobs: timeout-minutes: 60 # Adjust timeout as needed steps: - name: Checkout code at RC tag - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ env.RC_TAG }} @@ -88,7 +88,7 @@ jobs: - name: Run QuickStart Java Flink Runner uses: ./.github/actions/gradle-command-self-hosted-action with: - gradle-command: :runners:flink:2.0:runQuickstartJavaFlinkLocal + gradle-command: :runners:flink:2.2:runQuickstartJavaFlinkLocal arguments: | -Prepourl=${{ env.APACHE_REPO_URL }} \ -Pver=${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/run_rc_validation_python_mobile_gaming.yml b/.github/workflows/run_rc_validation_python_mobile_gaming.yml index a8609e22b73e..8776fa2b70bf 100644 --- a/.github/workflows/run_rc_validation_python_mobile_gaming.yml +++ b/.github/workflows/run_rc_validation_python_mobile_gaming.yml @@ -89,7 +89,7 @@ jobs: steps: - name: Checkout code at RC tag - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ env.RC_TAG }} diff --git a/.github/workflows/run_rc_validation_python_yaml.yml b/.github/workflows/run_rc_validation_python_yaml.yml index 7395d1601505..64da2d6b0465 100644 --- a/.github/workflows/run_rc_validation_python_yaml.yml +++ b/.github/workflows/run_rc_validation_python_yaml.yml @@ -81,7 +81,7 @@ jobs: steps: - name: Checkout code at RC tag - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ env.RC_TAG }} diff --git a/.github/workflows/self-assign.yml b/.github/workflows/self-assign.yml index 13459bbfa986..17bf72c1722e 100644 --- a/.github/workflows/self-assign.yml +++ b/.github/workflows/self-assign.yml @@ -25,7 +25,7 @@ jobs: if: ${{ !github.event.issue.pull_request }} runs-on: ubuntu-latest steps: - - uses: actions/github-script@v8 + - uses: actions/github-script@v9 with: script: | const body = context.payload.comment.body.replace( /\r\n/g, " " ).replace( /\n/g, " " ).split(' '); diff --git a/.github/workflows/tour_of_beam_backend.yml b/.github/workflows/tour_of_beam_backend.yml index fb7b61f6b05c..c61c0607d115 100644 --- a/.github/workflows/tour_of_beam_backend.yml +++ b/.github/workflows/tour_of_beam_backend.yml @@ -41,7 +41,7 @@ jobs: run: working-directory: ./learning/tour-of-beam/backend steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-go@v6 with: # pin to the biggest Go version supported by Cloud Functions runtime @@ -58,7 +58,7 @@ jobs: run: go test -v ./... - name: golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v3 with: version: v1.49.0 working-directory: learning/tour-of-beam/backend diff --git a/.github/workflows/tour_of_beam_backend_integration.yml b/.github/workflows/tour_of_beam_backend_integration.yml index c18b51eb3176..81dbdfc1b2e9 100644 --- a/.github/workflows/tour_of_beam_backend_integration.yml +++ b/.github/workflows/tour_of_beam_backend_integration.yml @@ -76,7 +76,7 @@ jobs: run: working-directory: ./learning/tour-of-beam/backend steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action diff --git a/.github/workflows/tour_of_beam_frontend_test.yml b/.github/workflows/tour_of_beam_frontend_test.yml index 1dc13c3fc758..c6df5bf6ea17 100644 --- a/.github/workflows/tour_of_beam_frontend_test.yml +++ b/.github/workflows/tour_of_beam_frontend_test.yml @@ -47,10 +47,10 @@ jobs: FLUTTER_VERSION: '3.10.4' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: 'Cache Flutter Dependencies' - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: /opt/hostedtoolcache/flutter key: ${{ runner.OS }}-flutter-install-cache-${{ env.FLUTTER_VERSION }} @@ -67,7 +67,7 @@ jobs: cd playground/frontend/playground_components_dev && flutter pub get && cd - cd learning/tour-of-beam/frontend && flutter pub get && cd - - - uses: nanasess/setup-chromedriver@v2 + - uses: nanasess/setup-chromedriver@v3 - name: 'Integration tests' run: | diff --git a/.github/workflows/typescript_tests.yml b/.github/workflows/typescript_tests.yml index dc28334162c9..973f7a8f91ef 100644 --- a/.github/workflows/typescript_tests.yml +++ b/.github/workflows/typescript_tests.yml @@ -54,7 +54,7 @@ jobs: os: [[self-hosted, ubuntu-24.04, main], macos-latest] steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive @@ -96,7 +96,7 @@ jobs: fail-fast: false steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive @@ -111,6 +111,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.10' + - name: Install Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' - name: Setup Beam Python working-directory: ./sdks/python run: | @@ -137,7 +142,7 @@ jobs: outputs: gcp-variables-set: ${{ steps.check_gcp_variables.outputs.gcp-variables-set }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: "Check are GCP variables set" run: "./scripts/ci/ci_check_are_gcp_variables_set.sh" id: check_gcp_variables @@ -159,7 +164,7 @@ jobs: fail-fast: false steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false submodules: recursive @@ -174,6 +179,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.10' + - name: Install Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' - name: Setup Beam Python working-directory: ./sdks/python run: | diff --git a/.github/workflows/update_python_dependencies.yml b/.github/workflows/update_python_dependencies.yml index 0da9698c3700..36752bc3c65a 100644 --- a/.github/workflows/update_python_dependencies.yml +++ b/.github/workflows/update_python_dependencies.yml @@ -41,7 +41,7 @@ jobs: properties: ${{ steps.test-properties.outputs.properties }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - id: test-properties uses: ./.github/actions/setup-default-test-properties @@ -51,7 +51,7 @@ jobs: name: Update Python Dependencies steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup environment uses: ./.github/actions/setup-environment-action with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d03641ffe669..5a42dfee100e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,4 +38,4 @@ repos: hooks: - id: ruff-check files: ^sdks/python/apache_beam/ - args: ["--config=sdks/python/ruff.toml"] + args: ["--config=sdks/python/pyproject.toml"] diff --git a/.test-infra/BUILD_STATUS.md b/.test-infra/BUILD_STATUS.md deleted file mode 100644 index 1972076df1e2..000000000000 --- a/.test-infra/BUILD_STATUS.md +++ /dev/null @@ -1,390 +0,0 @@ - - - -`ValidatesRunner` compliance status (on master branch) --------------------------------------------------------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LangULRDataflowFlinkSamzaSparkTwister2
Go--- - - Build Status - - - - Build Status - - - - Build Status - - - - Build Status - - ---
Java - - Build Status - - - - Build Status -
- - Build Status -
- - Build Status -
- - Build Status -
- - Build Status -
-
- - Build Status -
- - Build Status -
- - Build Status -
- - Build Status - -
- - Build Status -
- - Build Status - -
- - Build Status -
- - Build Status -
- - Build Status - -
- - Build Status - -
Python--- - - Build Status -
- - Build Status -
- - Build Status - -
- - Build Status -
- - Build Status - -
- - Build Status - - - - Build Status - - ---
XLang - - Build Status - - - - Build Status -
- - Build Status -
- - Build Status -
-
- - Build Status - - - - Build Status - - - - Build Status - - ---
- -Examples testing status on various runners --------------------------------------------------------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
LangULRDataflowFlinkSamzaSparkTwister2
Go---------------------
Java--- - - Build Status -
- - Build Status -
- - Build Status -
-
---------------
Python---------------------
XLang---------------------
- -Post-Commit SDK/Transform Integration Tests Status (on master branch) ------------------------------------------------------------------------------------------------- - - - - - - - - - - - - - - - - -
GoJavaPython
- - Build Status - - - - Build Status - - - - Build Status -
- - Build Status -
- - Build Status - -
- -Pre-Commit Tests Status (on master branch) ------------------------------------------------------------------------------------------------- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
---JavaPythonGoWebsiteWhitespaceTypescript
Non-portable - - Build Status -
-
- - Build Status -
- - Build Status -
- - Build Status -
- - Build Status - -
- - Build Status - - - - Build Status - - - - Build Status - - - - Build Status - -
Portable--- - - Build Status - - - - Build Status - - ---------
- -See [.test-infra/jenkins/README](https://github.com/apache/beam/blob/master/.test-infra/jenkins/README.md) for trigger phrase, status and link of all Jenkins jobs. - - -GitHub Actions Tests Status (on master branch) ------------------------------------------------------------------------------------------------- -[![Build python source distribution and wheels](https://github.com/apache/beam/workflows/Build%20python%20source%20distribution%20and%20wheels/badge.svg?branch=master&event=schedule)](https://github.com/apache/beam/actions?query=workflow%3A%22Build+python+source+distribution+and+wheels%22+branch%3Amaster+event%3Aschedule) -[![Python tests](https://github.com/apache/beam/workflows/Python%20tests/badge.svg?branch=master&event=schedule)](https://github.com/apache/beam/actions?query=workflow%3A%22Python+Tests%22+branch%3Amaster+event%3Aschedule) -[![Java tests](https://github.com/apache/beam/workflows/Java%20Tests/badge.svg?branch=master&event=schedule)](https://github.com/apache/beam/actions?query=workflow%3A%22Java+Tests%22+branch%3Amaster+event%3Aschedule) - -See [CI.md](https://github.com/apache/beam/blob/master/CI.md) for more information about GitHub Actions CI. diff --git a/.test-infra/dataproc/flink_cluster.sh b/.test-infra/dataproc/flink_cluster.sh index 4a97850f5ac1..0cb76c815d55 100755 --- a/.test-infra/dataproc/flink_cluster.sh +++ b/.test-infra/dataproc/flink_cluster.sh @@ -17,7 +17,7 @@ # Provide the following environment to run this script: # # GCLOUD_ZONE: Google cloud zone. Optional. Default: "us-central1-a" -# DATAPROC_VERSION: Dataproc version. Optional. Default: 2.2 +# DATAPROC_VERSION: Dataproc version. Optional. Default: 3.0-debian # CLUSTER_NAME: Cluster name # GCS_BUCKET: GCS bucket url for Dataproc resources (init actions) # HARNESS_IMAGES_TO_PULL: Urls to SDK Harness' images to pull on dataproc workers (optional: 0, 1 or multiple urls for every harness image) @@ -35,7 +35,7 @@ # HARNESS_IMAGES_TO_PULL='gcr.io//python:latest gcr.io//java:latest' \ # JOB_SERVER_IMAGE=gcr.io//job-server-flink:latest \ # ARTIFACTS_DIR=gs:// \ -# FLINK_DOWNLOAD_URL=https://archive.apache.org/dist/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz \ +# FLINK_DOWNLOAD_URL=https://archive.apache.org/dist/flink/flink-2.0.1/flink-2.0.1-bin-scala_2.12.tgz \ # HADOOP_DOWNLOAD_URL=https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-9.0.jar \ # FLINK_NUM_WORKERS=2 \ # FLINK_TASKMANAGER_SLOTS=1 \ @@ -46,7 +46,7 @@ set -Eeuxo pipefail # GCloud properties GCLOUD_ZONE="${GCLOUD_ZONE:=us-central1-a}" -DATAPROC_VERSION="${DATAPROC_VERSION:=2.2-debian}" +DATAPROC_VERSION="${DATAPROC_VERSION:=3.0-debian}" GCLOUD_REGION=`echo $GCLOUD_ZONE | sed -E "s/(-[a-z])?$//"` MASTER_NAME="$CLUSTER_NAME-m" diff --git a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/config.yaml b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/config.yaml index 3b4038bae64c..154bd1fe8884 100644 --- a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/config.yaml +++ b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/config.yaml @@ -76,7 +76,6 @@ categories: - "PostCommit Python ValidatesRunner Dataflow" - "PostCommit Python ValidatesRunner Spark" - "PostCommit Python ValidatesRunner Flink" - - "PostCommit Python ValidatesRunner Samza" - "Build python source distribution and wheels" - "Java Tests" - "PostCommit Java" @@ -113,7 +112,6 @@ categories: - "PreCommit Java Kafka IO Direct" - "PostCommit Java Examples Direct" - "PreCommit Java JDBC IO Direct" - - "PostCommit Java ValidatesRunner Samza" - "PreCommit Java Mqtt IO Direct" - "PreCommit Java Kinesis IO Direct" - "PreCommit Java MongoDb IO Direct" @@ -138,7 +136,6 @@ categories: - "PreCommit Java Thrift IO Direct" - "PreCommit Java Snowflake IO Direct" - "PreCommit Java Solr IO Direct" - - "PostCommit Java PVR Samza" - "PreCommit Java Tika IO Direct" - "PostCommit Java SingleStoreIO IT" - "PostCommit Java ValidatesRunner Direct" @@ -209,7 +206,6 @@ categories: - "PerformanceTests BigQueryIO Batch Java Json" - "PerformanceTests SQLBigQueryIO Batch Java" - "PerformanceTests XmlIOIT" - - "PostCommit XVR Samza" - "PerformanceTests ManyFiles TextIOIT" - "PerformanceTests XmlIOIT HDFS" - "PerformanceTests ParquetIOIT" @@ -291,8 +287,7 @@ categories: tests: - "PerformanceTests MongoDBIO IT" - "PreCommit GoPortable" - - "PreCommit GoPrism" - - "PostCommit Go VR Samza" + - "PreCommit GoPrism" - "PostCommit Go Dataflow ARM" - "LoadTests Go CoGBK Dataflow Batch" - "LoadTests Go Combine Dataflow Batch" diff --git a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py index 044866813fa7..2b91c862a7f6 100644 --- a/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py +++ b/.test-infra/metrics/sync/github/github_runs_prefetcher/code/main.py @@ -182,16 +182,20 @@ def filter_workflow_runs(run, issue): success_rate = 1.0 if len(workflow_runs): - failed_runs = list(filter(lambda r: r.status == "failure" | r.status == "cancelled", workflow_runs)) + failed_runs = list(filter(lambda r: r.status == "failure" or r.status == "cancelled", workflow_runs)) print(f"Number of failed workflow runs: {len(failed_runs)}") success_rate -= len(failed_runs) / len(workflow_runs) print(f"Success rate: {success_rate}") - # Check if last 5 runs are all failures - last_5_failed = len(workflow_runs) >= 5 and all(run.status == "failure" for run in workflow_runs[:5]) + # Check if last 5 runs are all failures or cancelled (e.g. job timeout) + last_5_failed = len(workflow_runs) >= 5 and all( + run.status in ("failure", "cancelled") for run in workflow_runs[:5] + ) if last_5_failed: - print(f"The last 5 workflow runs for {workflow.name} have all failed") + print( + f"The last 5 workflow runs for {workflow.name} have all failed or been cancelled" + ) return success_rate < workflow.threshold or last_5_failed diff --git a/.test-infra/tools/refresh_looker_metrics.py b/.test-infra/tools/refresh_looker_metrics.py index afd8ffa6f861..c8d66f4a4bd3 100644 --- a/.test-infra/tools/refresh_looker_metrics.py +++ b/.test-infra/tools/refresh_looker_metrics.py @@ -44,7 +44,9 @@ ("85", ["268", "269", "270", "271", "272"]), # PyTorch Sentiment Batch DistilBERT base uncased ("86", ["284", "285", "286", "287", "288"]), # VLLM Batch Gemma ("96", ["270", "304", "305", "353", "354"]), # Table Row Inference Sklearn Batch - ("106", ["355", "356", "357", "358", "359"]) # Table Row Inference Sklearn Streaming + ("106", ["355", "356", "357", "358", "359"]), # Table Row Inference Sklearn Streaming + ("107", ["360", "361", "362", "363", "364"]), # MLTransform Generate Vocab Batch + ("108", ["365", "366", "367", "368", "369"]) # MLTransform One-Hot Encoding Batch ] def get_look(id: str) -> models.Look: diff --git a/.test-infra/validate-runner/build.gradle b/.test-infra/validate-runner/build.gradle index 1817d7014a6c..3992abb24dd4 100644 --- a/.test-infra/validate-runner/build.gradle +++ b/.test-infra/validate-runner/build.gradle @@ -31,6 +31,19 @@ repositories { } } +// Flink 2.1+ uses at.yawk.lz4:lz4-java while Spark uses org.lz4:lz4-java +// Resolve capability conflict by preferring Flink's version when both are present +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') { + def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4') } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } +} + dependencies { implementation 'com.offbytwo.jenkins:jenkins-client:0.3.8' implementation library.java.jackson_databind diff --git a/.test-infra/validate-runner/src/main/resources/configuration.yaml b/.test-infra/validate-runner/src/main/resources/configuration.yaml index 31934d2146d7..4eb8cd018f99 100644 --- a/.test-infra/validate-runner/src/main/resources/configuration.yaml +++ b/.test-infra/validate-runner/src/main/resources/configuration.yaml @@ -19,6 +19,6 @@ batch: - dataflow: beam_PostCommit_Java_VR_Dataflow_V2 stream: - flink: beam_PostCommit_Java_PVR_Flink_Streaming - - samza: beam_PostCommit_Java_PVR_Samza + - spark: beam_PostCommit_Java_PVR_Spark3_Streaming server: https://ci-beam.apache.org/ jsonapi: testReport/api/json diff --git a/CHANGES.md b/CHANGES.md index 39e0d5c89716..e20194f9ab09 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -55,31 +55,39 @@ * ([#X](https://github.com/apache/beam/issues/X)). --> -# [2.74.0] - Unreleased +# [2.75.0] - Unreleased ## Highlights -* New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). -* New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). +* 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)). * Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). -* IcebergIO: support declaring a table's sort order on dynamic table creation via the new `sort_fields` config ([#38269](https://github.com/apache/beam/issues/38269)). +* ClickHouseIO: support writing `DateTime64(precision[, 'timezone'])` columns with sub-second precision (Java) ([#38466](https://github.com/apache/beam/issues/38466)). +* Upgraded IO Expansion Service to Java 17 ([#38974](https://github.com/apache/beam/issues/38974)). ## New Features / Improvements -* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). -* TriggerStateMachineRunner changes from BitSetCoder to SentinelBitSetCoder to - encode finished bitset. SentinelBitSetCoder and BitSetCoder are state - compatible. Both coders can decode encoded bytes from the other coder - ([#38139](https://github.com/apache/beam/issues/38139)). -* (Python) Added type alias for with_exception_handling to be used for typehints. ([#38173](https://github.com/apache/beam/issues/38173)). -* Added plugin mechanism to support different Lineage implementations (Java) ([#36790](https://github.com/apache/beam/issues/36790)). +* 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 -* X behavior was changed ([#X](https://github.com/apache/beam/issues/X)). +* (Python) Typehints of dataclass fields are honored during type inferences. To restore the behavior of fallback-to-any, + use pipeline option `--exclude_infer_dataclass_field_type` ([#38797](https://github.com/apache/beam/issues/38797)). + However fixing forward is recommended. ## Deprecations @@ -87,8 +95,9 @@ ## 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)). -* Added `max_batch_duration_secs` passthrough support in Python Enrichment BigQuery and CloudSQL handlers so batching duration can be forwarded to `BatchElements` ([#38243](https://github.com/apache/beam/issues/38243)). +* Fixed IcebergIO writing manifest column bounds padded with trailing `0x00` bytes, which broke equality predicate pushdown in some query engines (Java) ([#38580](https://github.com/apache/beam/issues/38580)). ## Security Fixes @@ -98,11 +107,53 @@ [comment]: # ( When updating known issues after release, make sure also update website blog in website/www/site/content/blog.) * ([#X](https://github.com/apache/beam/issues/X)). +* (Java) Projects using the Flink runner with Flink 2.1 or later alongside libraries requiring `org.lz4:lz4-java` (e.g., Kafka clients) may encounter a Gradle capability conflict, because Flink 2.1+ ships `at.yawk.lz4:lz4-java` which declares the same capability. To resolve, add a `capabilitiesResolution` rule to your `build.gradle` that selects `at.yawk.lz4:lz4-java` ([#38947](https://github.com/apache/beam/issues/38947)). + +# [2.74.0] - 2026-06-02 + +## 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)). -# [2.73.0] - 2026-04-?? +## New Features / Improvements + +* Capability introduces an indicator for aggregations and timers firing during a pipeline drain, allowing users and sinks to recognize and appropriately handle potentially incomplete or partial data ([#36884](https://github.com/apache/beam/issues/36884)). +* Added support for setting disk provisioned IOPS and throughput in Dataflow runner via `--diskProvisionedIops` and `--diskProvisionedThroughputMibps` pipeline options (Java/Go/Python) ([#38349](https://github.com/apache/beam/issues/38349)). +* TriggerStateMachineRunner changes from BitSetCoder to SentinelBitSetCoder to + encode finished bitset. SentinelBitSetCoder and BitSetCoder are state + compatible. Both coders can decode encoded bytes from the other coder + ([#38139](https://github.com/apache/beam/issues/38139)). +* (Python) Added type alias for with_exception_handling to be used for typehints. ([#38173](https://github.com/apache/beam/issues/38173)). +* (Java) BatchElements transform for Java SDK ([#38369](https://github.com/apache/beam/issues/38369)) +* Added plugin mechanism to support different Lineage implementations (Java) ([#36790](https://github.com/apache/beam/issues/36790)). +* (Python) Supported Python user type in Beam SQL. For example, SQL statements like `SELECT some_field from PCOLLECTION` can now operate a PCollection of Beam Row containing pickable Python user type ([#20738](https://github.com/apache/beam/issues/20738)). +* (Python) Introduced `beam.coders.registry.register_row` as preferred API to register a named tuple or dataclass with a Beam Row. At pipelne runtime, the original type associated with the registered row are preserved across the serialization boundary ([#38108](https://github.com/apache/beam/issues/38108)). +* (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)) + +## Deprecations + +* Dropped Java 8 support ([#31678](https://github.com/apache/beam/issues/31678)). +* Removed Samza Runner support ([#35448](https://github.com/apache/beam/issues/35448)). + +## Bugfixes + +* Fixed BigQueryEnrichmentHandler batch mode dropping earlier requests when multiple requests share the same enrichment key (Python) ([#38035](https://github.com/apache/beam/issues/38035)). +* Added `max_batch_duration_secs` passthrough support in Python Enrichment BigQuery and CloudSQL handlers so batching duration can be forwarded to `BatchElements` ([#38243](https://github.com/apache/beam/issues/38243)). + +# [2.73.0] - 2026-04-29 ## Highlights + ## I/Os * DebeziumIO (Java): added `OffsetRetainer` interface and `FileSystemOffsetRetainer` implementation to persist and restore CDC offsets across pipeline restarts, and exposed `withStartOffset` / `withOffsetRetainer` on `DebeziumIO.Read` and the cross-language `ReadBuilder` ([#28248](https://github.com/apache/beam/issues/28248)). diff --git a/README.md b/README.md index db254d407ea9..2cf529c53eb8 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Here are some resources actively maintained by the Beam community to help you ge Apache Beam Website - Offical website with documentation, concepts, and guides for Apache Beam. + Official website with documentation, concepts, and guides for Apache Beam. Java Quickstart diff --git a/build.gradle.kts b/build.gradle.kts index 805851c59696..4af8fa3f1ab4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -281,8 +281,6 @@ tasks.register("javaPreCommit") { dependsOn(":runners:local-java:build") dependsOn(":runners:portability:java:build") dependsOn(":runners:prism:java:build") - dependsOn(":runners:samza:build") - dependsOn(":runners:samza:job-server:build") dependsOn(":runners:spark:3:build") dependsOn(":runners:spark:3:job-server:build") dependsOn(":runners:twister2:build") @@ -410,7 +408,6 @@ tasks.register("javaPostCommit") { } tasks.register("javaPostCommitSickbay") { - dependsOn(":runners:samza:validatesRunnerSickbay") for (version in project.ext.get("allFlinkVersions") as Array<*>) { dependsOn(":runners:flink:${version}:validatesRunnerSickbay") } diff --git a/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/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 005a8b587804..34132bc8149d 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -605,17 +605,18 @@ class BeamModulePlugin implements Plugin { def cdap_version = "6.5.1" def checkerframework_version = "3.42.0" def classgraph_version = "4.8.162" + def delta_lake_version = "4.2.0" def dbcp2_version = "2.9.0" def errorprone_version = "2.31.0" // [bomupgrader] determined by: com.google.api:gax, consistent with: google_cloud_platform_libraries_bom - def gax_version = "2.79.0" + def gax_version = "2.80.0" def google_ads_version = "33.0.0" def google_clients_version = "2.0.0" - def google_cloud_bigdataoss_version = "2.2.26" + def google_cloud_bigdataoss_version = "3.1.16" def google_code_gson_version = "2.10.1" def google_oauth_clients_version = "1.34.1" // [bomupgrader] determined by: io.grpc:grpc-netty, consistent with: google_cloud_platform_libraries_bom - 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,7 +624,7 @@ class BeamModulePlugin implements Plugin { def httpclient_version = "4.5.13" def httpcore_version = "4.4.14" def iceberg_bqms_catalog_version = "1.6.1-1.0.1" - def jackson_version = "2.15.4" + def jackson_version = "2.18.6" def jaxb_api_version = "2.3.3" def jsr305_version = "3.0.2" def everit_json_version = "1.14.2" @@ -631,14 +632,13 @@ class BeamModulePlugin implements Plugin { def log4j2_version = "2.25.4" def nemo_version = "0.1" // [bomupgrader] determined by: io.grpc:grpc-netty, consistent with: google_cloud_platform_libraries_bom - def netty_version = "4.1.130.Final" + def netty_version = "4.1.132.Final" // [bomupgrader] determined by: io.opentelemetry:opentelemetry-sdk, consistent with: google_cloud_platform_libraries_bom - def opentelemetry_version = "1.51.0" + def opentelemetry_version = "1.57.0" + def opentelemetry_contrib_version = "1.52.0" def postgres_version = "42.6.2" // [bomupgrader] determined by: com.google.protobuf:protobuf-java, consistent with: google_cloud_platform_libraries_bom 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" @@ -649,6 +649,7 @@ class BeamModulePlugin implements Plugin { def solace_version = "10.21.0" def spark2_version = "2.4.8" def spark3_version = "3.5.0" + def spark4_version = "4.0.2" def spotbugs_version = "4.8.3" def testcontainers_version = "1.21.4" // [bomupgrader] determined by: org.apache.arrow:arrow-memory-core, consistent with: google_cloud_platform_libraries_bom @@ -658,6 +659,7 @@ class BeamModulePlugin implements Plugin { // Export Spark versions, so they are defined in a single place only project.ext.spark3_version = spark3_version + project.ext.spark4_version = spark4_version // version for BigQueryMetastore catalog (used by sdks:java:io:iceberg:bqms) // TODO: remove this and download the jar normally when the catalog gets // open-sourced (https://github.com/apache/iceberg/pull/11039) @@ -679,8 +681,8 @@ class BeamModulePlugin implements Plugin { aircompressor : "io.airlift:aircompressor:2.0.3", args4j : "args4j:args4j:2.33", auto_value_annotations : "com.google.auto.value:auto-value-annotations:$autovalue_version", - // TODO: https://github.com/apache/beam/issues/34993 after stopping supporting Java 8 - avro : "org.apache.avro:avro:1.11.4", + // TODO: upgrade post 1.12.1 once https://issues.apache.org/jira/browse/AVRO-4209 resolved + avro : "org.apache.avro:avro:1.12.0", aws_java_sdk2_apache_client : "software.amazon.awssdk:apache-client:$aws_java_sdk2_version", aws_java_sdk2_netty_client : "software.amazon.awssdk:netty-nio-client:$aws_java_sdk2_version", aws_java_sdk2_auth : "software.amazon.awssdk:auth:$aws_java_sdk2_version", @@ -699,9 +701,9 @@ class BeamModulePlugin implements Plugin { aws_java_sdk2_profiles : "software.amazon.awssdk:profiles:$aws_java_sdk2_version", azure_sdk_bom : "com.azure:azure-sdk-bom:1.2.14", bigdataoss_gcsio : "com.google.cloud.bigdataoss:gcsio:$google_cloud_bigdataoss_version", - bigdataoss_gcs_connector : "com.google.cloud.bigdataoss:gcs-connector:hadoop2-$google_cloud_bigdataoss_version", + bigdataoss_gcs_connector : "com.google.cloud.bigdataoss:gcs-connector:$google_cloud_bigdataoss_version", bigdataoss_util : "com.google.cloud.bigdataoss:util:$google_cloud_bigdataoss_version", - bigdataoss_util_hadoop : "com.google.cloud.bigdataoss:util-hadoop:hadoop2-$google_cloud_bigdataoss_version", + bigdataoss_util_hadoop : "com.google.cloud.bigdataoss:util-hadoop:$google_cloud_bigdataoss_version", byte_buddy : "net.bytebuddy:byte-buddy:1.17.7", cassandra_driver_core : "com.datastax.cassandra:cassandra-driver-core:$cassandra_driver_version", cassandra_driver_mapping : "com.datastax.cassandra:cassandra-driver-mapping:$cassandra_driver_version", @@ -726,6 +728,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", @@ -740,7 +744,7 @@ class BeamModulePlugin implements Plugin { google_api_common : "com.google.api:api-common", // google_cloud_platform_libraries_bom sets version google_api_services_bigquery : "com.google.apis:google-api-services-bigquery:v2-rev20251012-2.0.0", // [bomupgrader] sets version google_api_services_cloudresourcemanager : "com.google.apis:google-api-services-cloudresourcemanager:v1-rev20250606-2.0.0", // [bomupgrader] sets version - google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-rev20260118-$google_clients_version", + google_api_services_dataflow : "com.google.apis:google-api-services-dataflow:v1b3-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 @@ -748,21 +752,21 @@ class BeamModulePlugin implements Plugin { google_auth_library_oauth2_http : "com.google.auth:google-auth-library-oauth2-http", // google_cloud_platform_libraries_bom sets version google_cloud_bigquery : "com.google.cloud:google-cloud-bigquery", // google_cloud_platform_libraries_bom sets version google_cloud_bigquery_storage : "com.google.cloud:google-cloud-bigquerystorage", // google_cloud_platform_libraries_bom sets version - google_cloud_bigtable : "com.google.cloud:google-cloud-bigtable:$bigtable_version", // google_cloud_platform_libraries_bom sets version + google_cloud_bigtable : "com.google.cloud:google-cloud-bigtable", // google_cloud_platform_libraries_bom sets version google_cloud_bigtable_client_core_config : "com.google.cloud.bigtable:bigtable-client-core-config:1.28.0", google_cloud_bigtable_emulator : "com.google.cloud:google-cloud-bigtable-emulator", // google_cloud_platform_libraries_bom sets version google_cloud_core : "com.google.cloud:google-cloud-core", // google_cloud_platform_libraries_bom sets version google_cloud_core_grpc : "com.google.cloud:google-cloud-core-grpc", // google_cloud_platform_libraries_bom sets version google_cloud_datacatalog_v1beta1 : "com.google.cloud:google-cloud-datacatalog", // google_cloud_platform_libraries_bom sets version google_cloud_dataflow_java_proto_library_all: "com.google.cloud.dataflow:google-cloud-dataflow-java-proto-library-all:0.5.160304", - google_cloud_datastore_v1_proto_client : "com.google.cloud.datastore:datastore-v1-proto-client:2.40.0", // [bomupgrader] sets version + google_cloud_datastore_v1_proto_client : "com.google.cloud.datastore:datastore-v1-proto-client:3.0.0", // [bomupgrader] sets version google_cloud_firestore : "com.google.cloud:google-cloud-firestore", // google_cloud_platform_libraries_bom sets version google_cloud_kms : "com.google.cloud:google-cloud-kms", // google_cloud_platform_libraries_bom sets version google_cloud_logging : "com.google.cloud:google-cloud-logging", // google_cloud_platform_libraries_bom sets version google_cloud_pubsub : "com.google.cloud:google-cloud-pubsub", // google_cloud_platform_libraries_bom sets version // [bomupgrader] the BOM version is set by scripts/tools/bomupgrader.py. If update manually, also update // libraries-bom version on sdks/java/container/license_scripts/dep_urls_java.yaml - google_cloud_platform_libraries_bom : "com.google.cloud:libraries-bom:26.80.0", + google_cloud_platform_libraries_bom : "com.google.cloud:libraries-bom:26.83.0", google_cloud_secret_manager : "com.google.cloud:google-cloud-secretmanager", // google_cloud_platform_libraries_bom sets version google_cloud_spanner : "com.google.cloud:google-cloud-spanner", // google_cloud_platform_libraries_bom sets version google_cloud_storage : "com.google.cloud:google-cloud-storage", // google_cloud_platform_libraries_bom sets version @@ -820,6 +824,7 @@ class BeamModulePlugin implements Plugin { jackson_datatype_jsr310 : "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version", jackson_module_scala_2_11 : "com.fasterxml.jackson.module:jackson-module-scala_2.11:$jackson_version", jackson_module_scala_2_12 : "com.fasterxml.jackson.module:jackson-module-scala_2.12:$jackson_version", + jackson_module_scala_2_13 : "com.fasterxml.jackson.module:jackson-module-scala_2.13:$jackson_version", jamm : 'com.github.jbellis:jamm:0.4.0', jaxb_api : "jakarta.xml.bind:jakarta.xml.bind-api:$jaxb_api_version", jaxb_impl : "com.sun.xml.bind:jaxb-impl:$jaxb_api_version", @@ -846,6 +851,7 @@ class BeamModulePlugin implements Plugin { log4j2_log4j12_api : "org.apache.logging.log4j:log4j-1.2-api:$log4j2_version", mockito_core : "org.mockito:mockito-core:4.11.0", mockito_inline : "org.mockito:mockito-inline:4.11.0", + mockito_junit_jupiter : "org.mockito:mockito-junit-jupiter:4.11.0", mongo_java_driver : "org.mongodb:mongodb-driver-sync:5.5.0", mongo_bson : "org.mongodb:bson:5.5.0", mongodb_driver_core : "org.mongodb:mongodb-driver-core:5.5.0", @@ -855,8 +861,15 @@ class BeamModulePlugin implements Plugin { netty_tcnative_boringssl_static : "io.netty:netty-tcnative-boringssl-static:2.0.52.Final", netty_transport : "io.netty:netty-transport:$netty_version", netty_transport_native_epoll : "io.netty:netty-transport-native-epoll:$netty_version", - opentelemetry_api : "io.opentelemetry:opentelemetry-api", // google_cloud_platform_libraries_bom sets version + opentelemetry_api : "io.opentelemetry:opentelemetry-api", // opentelemetry-bom sets version opentelemetry_bom : "io.opentelemetry:opentelemetry-bom-alpha:$opentelemetry_version-alpha", // alpha required by extensions + opentelemetry_context : "io.opentelemetry:opentelemetry-context:$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_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", @@ -941,6 +954,18 @@ class BeamModulePlugin implements Plugin { /** ***********************************************************************************************/ + // Resolves a capability conflict for a given configuration by selecting the preferred capability provider. + project.ext.resolveCapabilitiesConflict = { Configuration configuration, String capability, String preferred -> + configuration.resolutionStrategy.capabilitiesResolution.withCapability(capability) { + def candidate = candidates.find { it.id.toString().contains(preferred) } + if (candidate != null) { + select(candidate) + } else { + selectHighestVersion() + } + } + } + // Returns a string representing the relocated path to be used with the shadow plugin when // given a suffix such as "com.google.common". project.ext.getJavaRelocatedPath = { String suffix -> @@ -965,6 +990,9 @@ class BeamModulePlugin implements Plugin { // set compiler options for java version overrides to compile with a different java version project.ext.setJavaVerOptions = { CompileOptions options, String ver -> + if (!project.findProperty("java${ver}Home")) { + return + } if (ver == '8') { def java8Home = project.findProperty("java8Home") options.fork = true @@ -1132,6 +1160,8 @@ class BeamModulePlugin implements Plugin { project.javaVersion = '17' } else if (JavaVersion.VERSION_21.equals(configuration.requireJavaVersion)) { project.javaVersion = '21' + } else if (JavaVersion.VERSION_25.equals(configuration.requireJavaVersion)) { + project.javaVersion = '25' } else { throw new GradleException( "requireJavaVersion has to be supported LTS version greater than the default Java version. Actual: " + @@ -1146,14 +1176,17 @@ 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.findProperty('java25Home')) { + forkJavaVersion = '25' } else { logger.config("Module ${project.name} disabled. To enable, either " + "compile on newer Java version or pass java${project.javaVersion}Home project property") @@ -1502,72 +1535,66 @@ class BeamModulePlugin implements Plugin { project.tasks.analyzeDependencies.enabled = false } - // errorprone requires java9+ compiler. It can be used with Java8 but then sets a java9+ errorproneJavac. - // However, the redirect ignores any task that forks and defines either a javaHome or an executable, - // see https://github.com/tbroyer/gradle-errorprone-plugin#jdk-8-support - // which means errorprone cannot run when gradle runs on Java11+ but serve `-testJavaVersion=8 -Pjava8Home` options - if (!(project.findProperty('testJavaVersion') == '8')) { - // Enable errorprone static analysis - project.apply plugin: 'net.ltgt.errorprone' + // Enable errorprone static analysis + project.apply plugin: 'net.ltgt.errorprone' - project.dependencies { - errorprone("com.google.errorprone:error_prone_core:$errorprone_version") - errorprone("jp.skypencil.errorprone.slf4j:errorprone-slf4j:0.1.28") - } - - project.configurations.errorprone { resolutionStrategy.force "com.google.errorprone:error_prone_core:$errorprone_version" } - - project.tasks.withType(JavaCompile) { - options.errorprone.disableWarningsInGeneratedCode = true - options.errorprone.excludedPaths = '(.*/)?(build/generated-src|build/generated.*avro-java|build/generated)/.*' - - // Error Prone requires some packages to be exported/opened on Java versions that support modules, - // i.e. Java 9 and up. The flags became mandatory in Java 17 with JEP-403. - // The -J prefix is not needed if forkOptions.javaHome is unset, - // see http://github.com/gradle/gradle/issues/22747 - if (options.forkOptions.javaHome == null) { - options.fork = true - options.forkOptions.jvmArgs += errorProneAddModuleOpts - } - def disabledChecks = [ - // TODO(https://github.com/apache/beam/issues/20955): Enable errorprone checks - "AutoValueImmutableFields", - "ComparableType", - "DoNotMockAutoValue", - "EmptyBlockTag", - "ExtendsAutoValue", - "InlineMeSuggester", - "InvalidBlockTag", - "JodaConstructors", - "MixedMutabilityReturnType", - "PreferJavaTimeOverload", - "Slf4jSignOnlyFormat", - "UnrecognisedJavadocTag", - // errorprone 3.2.0+ checks - "DirectInvocationOnMock", - "MockNotUsedInProduction", - "NullableWildcard", - "SuperCallToObjectMethod", - // Intended suppressions with justifications - // for encoding efficiency and backward compatibility - "EnumOrdinal", - // widely used in non-public methods - "NotJavadoc", - // return values used for assignments widely, and for backward compatibility. - "NonApiType", - // Used to test self equal - "SelfAssertion", - // Sometimes a static logger is preferred, which is the convention currently used in beam. See docs: - // https://github.com/KengoTODA/findbugs-slf4j#slf4j_logger_should_be_non_static - "Slf4jLoggerShouldBeNonStatic", - // allow implicit Locale.Default - "StringCaseLocaleUsage", - // DoFn methods are executed reflectively at pipeline runtime - "UnusedMethod", - ] - disabledChecks.each { - options.errorprone.errorproneArgs.add("-Xep:${it}:OFF") - } + project.dependencies { + errorprone("com.google.errorprone:error_prone_core:$errorprone_version") + errorprone("jp.skypencil.errorprone.slf4j:errorprone-slf4j:0.1.28") + } + + project.configurations.errorprone { resolutionStrategy.force "com.google.errorprone:error_prone_core:$errorprone_version" } + + project.tasks.withType(JavaCompile) { + options.errorprone.disableWarningsInGeneratedCode = true + options.errorprone.excludedPaths = '(.*/)?(build/generated-src|build/generated.*avro-java|build/generated)/.*' + + // Error Prone requires some packages to be exported/opened on Java versions that support modules, + // i.e. Java 9 and up. The flags became mandatory in Java 17 with JEP-403. + // The -J prefix is not needed if forkOptions.javaHome is unset, + // see http://github.com/gradle/gradle/issues/22747 + if (options.forkOptions.javaHome == null) { + options.fork = true + options.forkOptions.jvmArgs += errorProneAddModuleOpts + } + def disabledChecks = [ + // TODO(https://github.com/apache/beam/issues/20955): Enable errorprone checks + "AutoValueImmutableFields", + "ComparableType", + "DoNotMockAutoValue", + "EmptyBlockTag", + "ExtendsAutoValue", + "InlineMeSuggester", + "InvalidBlockTag", + "JodaConstructors", + "MixedMutabilityReturnType", + "PreferJavaTimeOverload", + "Slf4jSignOnlyFormat", + "UnrecognisedJavadocTag", + // errorprone 3.2.0+ checks + "DirectInvocationOnMock", + "MockNotUsedInProduction", + "NullableWildcard", + "SuperCallToObjectMethod", + // Intended suppressions with justifications + // for encoding efficiency and backward compatibility + "EnumOrdinal", + // widely used in non-public methods + "NotJavadoc", + // return values used for assignments widely, and for backward compatibility. + "NonApiType", + // Used to test self equal + "SelfAssertion", + // Sometimes a static logger is preferred, which is the convention currently used in beam. See docs: + // https://github.com/KengoTODA/findbugs-slf4j#slf4j_logger_should_be_non_static + "Slf4jLoggerShouldBeNonStatic", + // allow implicit Locale.Default + "StringCaseLocaleUsage", + // DoFn methods are executed reflectively at pipeline runtime + "UnusedMethod", + ] + disabledChecks.each { + options.errorprone.errorproneArgs.add("-Xep:${it}:OFF") } } @@ -1577,7 +1604,7 @@ class BeamModulePlugin implements Plugin { options.encoding = "UTF-8" // If compiled on newer JDK, set byte code compatibility if (requireJavaVersion.compareTo(JavaVersion.current()) < 0) { - def compatVersion = project.javaVersion == '1.8' ? '8' : project.javaVersion + def compatVersion = project.javaVersion == '11' ? '11' : project.javaVersion options.compilerArgs += ['--release', compatVersion] // TODO(https://github.com/apache/beam/issues/23901): Fix // optimizerOuterThis breakage @@ -1610,16 +1637,19 @@ class BeamModulePlugin implements Plugin { preserveFileTimestamps(false) } + String testJavaVersion = project.findProperty('testJavaVersion') + if (!testJavaVersion && forkJavaVersion) { + testJavaVersion = forkJavaVersion + } // if specified test java version, modify the compile and runtime versions accordingly - if (['8', '11', '17', '21', '25'].contains(project.findProperty('testJavaVersion'))) { - String ver = project.getProperty('testJavaVersion') - def testJavaHome = project.getProperty("java${ver}Home") + if (['11', '17', '21', '25'].contains(testJavaVersion)) { + def testJavaHome = project.findProperty("java${testJavaVersion}Home") // redirect java compiler to specified version for compileTestJava only project.tasks.compileTestJava { - setCompileAndRuntimeJavaVersion(options.compilerArgs, ver) - project.ext.setJavaVerOptions(options, ver) - if (ver == '25') { + setCompileAndRuntimeJavaVersion(options.compilerArgs, testJavaVersion) + project.ext.setJavaVerOptions(options, testJavaVersion) + if (testJavaVersion == '25') { // TODO: Upgrade errorprone version to support Java25. Currently compile crashes // java.lang.NoSuchFieldError: Class com.sun.tools.javac.code.TypeTag does not have member field // 'com.sun.tools.javac.code.TypeTag UNKNOWN' @@ -1631,6 +1661,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) { @@ -2689,7 +2725,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") { @@ -2769,7 +2806,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' @@ -2958,10 +2996,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) { @@ -3123,15 +3162,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/examples/java/common.gradle b/examples/java/common.gradle index 10ea43628bc8..f32667d733fb 100644 --- a/examples/java/common.gradle +++ b/examples/java/common.gradle @@ -35,6 +35,8 @@ 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') + dependencies { directRunnerPreCommit project(path: ":runners:direct-java", configuration: "shadow") diff --git a/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java b/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java index 84155683e9cc..505252e76a6d 100644 --- a/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java +++ b/examples/java/sql/src/main/java/org/apache/beam/examples/SchemaTransformExample.java @@ -22,7 +22,7 @@ // description: Demonstration of Schema transform usage. // multifile: false // default_example: false -// context_line: 60 +// context_line: 65 // categories: // - Schemas // - Combiners diff --git a/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java b/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java index 9ca2dda313c6..f2a3a21195f8 100644 --- a/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java +++ b/examples/java/sql/src/main/java/org/apache/beam/examples/SqlTransformExample.java @@ -22,7 +22,7 @@ // description: Demonstration of SQL transform usage. // multifile: false // default_example: false -// context_line: 60 +// context_line: 62 // categories: // - Beam SQL // - Combiners diff --git a/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java b/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java index 0f968e88d392..daf943484ff2 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ApproximateQuantilesExample.java @@ -36,7 +36,7 @@ // description: Demonstration of ApproximateQuantiles transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC 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/CoCombineTransformExample.java b/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java index 1aafd50a1621..a46ac539aff6 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CoCombineTransformExample.java @@ -22,7 +22,7 @@ // description: Demonstration of Composed Combine transform usage. // multifile: false // default_example: false -// context_line: 143 +// context_line: 145 // categories: // - Schemas // - Combiners diff --git a/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java index 46905ad71daf..a04c9256a1e0 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CoGroupByKeyExample.java @@ -39,7 +39,7 @@ // description: Demonstration of CoGroupByKey transform usage. // multifile: false // default_example: false -// context_line: 54 +// context_line: 56 // categories: // - Core Transforms // - Joins diff --git a/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java b/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java index 5f6901ea5ec4..c4952bfb8d97 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CombineExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Combine transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // - Combiners diff --git a/examples/java/src/main/java/org/apache/beam/examples/CountExample.java b/examples/java/src/main/java/org/apache/beam/examples/CountExample.java index f95ec3fe4759..80e01b5d6b7f 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CountExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CountExample.java @@ -35,7 +35,7 @@ // description: Demonstration of Count transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java index f02c262ccbab..017a736e56f6 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CountPerKeyExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Count.perKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java b/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java index b5eb5ba3e9de..4052a9031470 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/CreateExample.java @@ -40,7 +40,7 @@ // description: Demonstration of Create transform usage. // multifile: false // default_example: false -// context_line: 51 +// context_line: 53 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java b/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java index 07c5be76d753..693aadc7850e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java +++ b/examples/java/src/main/java/org/apache/beam/examples/DebuggingWordCount.java @@ -23,7 +23,7 @@ // filter("Flourish|stomach"). // multifile: false // pipeline_options: --output output.txt -// context_line: 180 +// context_line: 182 // categories: // - Debugging // - Filtering diff --git a/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java b/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java index 28fd4af87066..3dc5c776f2ba 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/DistinctExample.java @@ -35,7 +35,7 @@ // description: Demonstration of Distinct transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java index 4e9493ac1cf2..d944ab75ebf0 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/FlatMapElementsExample.java @@ -38,7 +38,7 @@ // description: Demonstration of FlatMapElements transform usage. // multifile: false // default_example: false -// context_line: 50 +// context_line: 52 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java b/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java index 212079579ee0..012b951fe8a9 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/GroupIntoBatchesExample.java @@ -36,7 +36,7 @@ // description: Demonstration of GroupIntoBatches transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java index 5b26455e3c5f..144093b704a7 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaPassengerCountJson.java @@ -22,7 +22,7 @@ // description: Read NYC Taxi dataset from Kafka server to count passengers for each vendor // multifile: false // default_example: false -// context_line: 64 +// context_line: 81 // categories: // - Emulated Data Source // - IO diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java index 327d321d8ad3..83c36d18925a 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaStreaming.java @@ -21,7 +21,7 @@ // name: KafkaStreaming // description: Example of streaming data processing using Kafka // multifile: false -// context_line: 186 +// context_line: 113 // never_run: true // always_run: true // categories: diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java index fad5668945e7..50a5e746d0c4 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountAvro.java @@ -21,7 +21,7 @@ // name: KafkaWordCountAvro // description: Read CountWords dataset (CountWords.avro) from Kafka to count words // multifile: false -// context_line: 64 +// context_line: 66 // categories: // - Emulated Data Source // - IO diff --git a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java index 036cc6702123..e0d0976a1053 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KafkaWordCountJson.java @@ -21,7 +21,7 @@ // name: KafkaWordCountJson // description: Read CountWords dataset (CountWords.json) from Kafka to count words // multifile: false -// context_line: 65 +// context_line: 67 // categories: // - Emulated Data Source // - IO diff --git a/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java b/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java index c9dc40170a57..725fb3586fe5 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KeysExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Keys transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java b/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java index 192dc893795b..f91623f24d3c 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/KvSwapExample.java @@ -36,7 +36,7 @@ // description: Demonstration of KvSwap transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java b/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java index ef4c50d24c73..691727382cef 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/LatestExample.java @@ -38,7 +38,7 @@ // description: Demonstration of Latest transform usage. // multifile: false // default_example: false -// context_line: 49 +// context_line: 51 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java b/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java index 60908cf83a48..08a11ccbbb19 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MapElementsExample.java @@ -36,7 +36,7 @@ // description: Demonstration of MapElements transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java b/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java index 845b107a2214..ba164aa6c435 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MaxExample.java @@ -35,7 +35,7 @@ // description: Demonstration of Max transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java index 5ad156a343e1..ae0a60664ed4 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MaxPerKeyExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Max.perKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java b/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java index fbb51636253a..506d4a6196ff 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MeanExample.java @@ -35,7 +35,7 @@ // description: Demonstration of Mean transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java index 97f598d4abb4..a1e3a1f6ff58 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MeanPerKeyExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Mean.perKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/MinExample.java b/examples/java/src/main/java/org/apache/beam/examples/MinExample.java index db46f1323121..17a4e0d6554f 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MinExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MinExample.java @@ -35,7 +35,7 @@ // description: Demonstration of Min transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java index e8c3c51d3c23..6fb8a0468ae0 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/MinPerKeyExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Min.perKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java b/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java index c202aedf76c0..780c795b0874 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/PartitionExample.java @@ -46,7 +46,7 @@ // description: Demonstration of Partition transform usage. // multifile: false // default_example: false -// context_line: 58 +// context_line: 60 // categories: // - Core Transforms // - Coders diff --git a/examples/java/src/main/java/org/apache/beam/examples/RateLimitedPubSubReader.java b/examples/java/src/main/java/org/apache/beam/examples/RateLimitedPubSubReader.java index b1156dabee3e..060a9195e6a3 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/RateLimitedPubSubReader.java +++ b/examples/java/src/main/java/org/apache/beam/examples/RateLimitedPubSubReader.java @@ -104,8 +104,8 @@ public void teardown() { } @ProcessElement - public void processElement(ProcessContext c) throws Exception { - String element = c.element(); + public void processElement(@Element String element, OutputReceiver receiver) + throws Exception { try { Preconditions.checkNotNull(rateLimiter).allow(1); } catch (Exception e) { @@ -115,7 +115,7 @@ public void processElement(ProcessContext c) throws Exception { // Simulate external API call or simply log the read entry Thread.sleep(100); LOG.info("Received and rate-limited record: {}", element); - c.output(element); + receiver.output(element); } } diff --git a/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java b/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java index d80bc9fedbaa..c71ce8bb9b4e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/RegexExample.java @@ -35,7 +35,7 @@ // description: Demonstration of Regex transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java b/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java index 8d9db0cf3a86..d5db80403d9c 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SampleExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Sample transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/SumExample.java b/examples/java/src/main/java/org/apache/beam/examples/SumExample.java index a571d8dc5596..58c7cc9d2d2c 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SumExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SumExample.java @@ -35,7 +35,7 @@ // description: Demonstration of Sum transform usage. // multifile: false // default_example: false -// context_line: 45 +// context_line: 47 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java b/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java index a2c92ef0c80e..8cc4baa2b4a3 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/SumPerKeyExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Sum.integersPerKey transform usage. // multifile: false // default_example: false -// context_line: 47 +// context_line: 49 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java b/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java index 54e783231908..b2020ee65abb 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ToStringExample.java @@ -36,7 +36,7 @@ // description: Demonstration of ToString transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/TopExample.java b/examples/java/src/main/java/org/apache/beam/examples/TopExample.java index f602aa345569..fa76f1308e3c 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/TopExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/TopExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Top transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java b/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java index 1b9839ac03f0..9502bd6824d2 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ValuesExample.java @@ -36,7 +36,7 @@ // description: Demonstration of Values transform usage. // multifile: false // default_example: false -// context_line: 46 +// context_line: 48 // categories: // - Core Transforms // complexity: BASIC diff --git a/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java b/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java index d40d6cc333ef..39e15affcfae 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/ViewExample.java @@ -39,7 +39,7 @@ // description: Demonstration of View transform usage. // multifile: false // default_example: false -// context_line: 49 +// context_line: 52 // categories: // - Core Transforms // complexity: MEDIUM @@ -113,9 +113,7 @@ public LogOutput(String prefix) { } @ProcessElement - public void processElement( - @Element KV element, OutputReceiver> receiver) - throws Exception { + public void processElement(@Element T element, OutputReceiver receiver) throws Exception { LOG.info("{}{}", prefix, element); receiver.output(element); } diff --git a/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java b/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java index d6a3dff2fa04..a669a0eca771 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java +++ b/examples/java/src/main/java/org/apache/beam/examples/WindowExample.java @@ -41,7 +41,7 @@ // description: Demonstration of Window transform usage. // multifile: false // default_example: false -// context_line: 54 +// context_line: 56 // categories: // - Core Transforms // - Windowing diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java index a61bd4c88651..2b5c8b42ee1e 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TfIdf.java @@ -23,7 +23,7 @@ // GCS prefix. // multifile: true // pipeline_options: --output output.txt -// context_line: 447 +// context_line: 462 // categories: // - Combiners // - Options diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java index d156777e0f97..e79f09c9d084 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TopWikipediaSessions.java @@ -24,7 +24,7 @@ // each month. // multifile: true // pipeline_options: --output output.txt -// context_line: 236 +// context_line: 244 // categories: // - Combiners // - Options diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java index b22e3fed6698..ab80a220f571 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficMaxLaneFlow.java @@ -23,7 +23,7 @@ // window, it finds the lane that had the highest flow recorded, for each sensor station. // It writes those max values along with auxiliary info to a BigQuery table. // multifile: true -// context_line: 402 +// context_line: 410 // categories: // - Combiners // - Streaming diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java index 38ba4322f3e6..3918f2d72a64 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/complete/TrafficRoutes.java @@ -24,7 +24,7 @@ // of predefined 'routes', and looks for 'slowdowns' in those routes. It writes its // results to a BigQuery table. // multifile: true -// context_line: 399 +// context_line: 411 // categories: // - Combiners // - Streaming diff --git a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java index 0c39c600b6cc..62f7b492c72b 100644 --- a/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java +++ b/examples/java/src/main/java/org/apache/beam/examples/cookbook/MinimalBigQueryTornadoes.java @@ -43,7 +43,7 @@ // always_run: true // default_example: false // pipeline_options: --project apache-beam-testing -// context_line: 102 +// context_line: 125 // categories: // - Filtering // - IO 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/gradle.properties b/gradle.properties index 2cff0b656fa7..1200e0a9f7f5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,17 +30,20 @@ signing.gnupg.useLegacyGpg=true # buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy. # To build a custom Beam version make sure you change it in both places, see # https://github.com/apache/beam/issues/21302. -version=2.74.0-SNAPSHOT -sdk_version=2.74.0.dev +version=2.76.0-SNAPSHOT +sdk_version=2.76.0.dev -javaVersion=1.8 +javaVersion=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 +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..31c1354319d6 100644 --- a/infra/enforcement/account_keys.py +++ b/infra/enforcement/account_keys.py @@ -106,6 +106,53 @@ def _denormalize_username(self, username: str) -> str: return username.split(":", 1)[1].strip().lower() return username + def _get_user_managed_keys_from_iam(self, account_email: str) -> List[str]: + """" + Retrieves the list of user-managed keys for a given service account from IAM. + + Args: + account_email (str): The email of the service account to retrieve keys for. + + Returns: + List[str]: A list of key IDs for the user-managed keys associated with the service account + """ + request = types.ListServiceAccountKeysRequest() + request.name = f"projects/{self.project_id}/serviceAccounts/{account_email}" + request.key_types = [types.ListServiceAccountKeysRequest.KeyType.USER_MANAGED] + + try: + response = self.service_account_client.list_service_account_keys(request=request) + return [key.name.split("/")[-1] for key in response.keys] + except Exception as e: + self.logger.error(f"Failed to retrieve keys for service account '{account_email}': {e}") + return [] + + def _get_verified_keys_from_secret_manager(self, secret_name: str) -> List[str]: + """ + Retrieves the list of verified keys for a given service account from Secret Manager. + + Args: + secret_name (str): The name of the secret to retrieve keys for. + + Returns: + List[str]: A list of key IDs for the verified keys associated with the service account. + """ + verified_keys = [] + parent = self.secret_client.secret_path(self.project_id, secret_name) + + try: + versions = self.secret_client.list_secret_versions(request={"parent": parent}) + for version in versions: + if version.state.name == secretmanager.SecretVersion.State.ENABLED: + response = self.secret_client.access_secret_version(request={"name": version.name}) + data_str = response.payload.data.decode("UTF-8") + key_id = data_str.split(":",1)[0] + verified_keys.append(key_id) + return verified_keys + except Exception as e: + self.logger.error(f"Failed to retrieve verified keys from Secret Manager for secret '{secret_name}': {e}") + return [] + def _get_all_live_service_accounts(self) -> List[str]: """ Retrieves all service accounts that are currently active (not disabled) in the project. @@ -259,21 +306,35 @@ def check_compliance(self) -> List[str]: self.logger.info(f"No service account keys found in the {self.service_account_keys_file}.") compliance_issues = [] + live_service_accounts = self._get_all_live_service_accounts() + managed_secrets = self._get_all_live_managed_secrets() # Check that all service accounts that exist are declared - for service_account in self._get_all_live_service_accounts(): + for service_account in live_service_accounts: if self._denormalize_account_email(service_account) not in [account["account_id"] for account in file_service_accounts]: msg = f"Service account '{service_account}' is not declared in the service account keys file." compliance_issues.append(msg) self.logger.warning(msg) + else: + iam_keys = self._get_user_managed_keys_from_iam(service_account) + if iam_keys: + secret_name = f"{self._denormalize_account_email(service_account)}-key" + legal_keys = [] + if secret_name in managed_secrets: + legal_keys = self._get_verified_keys_from_secret_manager(secret_name) + unmanaged_keys = set(iam_keys) - set(legal_keys) + for unmanaged_key in unmanaged_keys: + msg = f"SECURITY ALERT: Unmanaged key '{unmanaged_key}' detected on account '{service_account}'. This key was created outside of Beam's service account management system. " + compliance_issues.append(msg) + self.logger.warning(msg) - managed_secrets = self._get_all_live_managed_secrets() extracted_secrets = [f"{self._denormalize_account_email(account['account_id'])}-key" for account in file_service_accounts] # Check for managed secrets that are not declared for secret in managed_secrets: if secret not in extracted_secrets: - msg = f"Managed secret '{secret}' is not declared in the service account keys file." + masked_secret = f"{secret[:4]}***{secret[-4:]}" if len(secret) >= 8 else "***" + msg = f"Managed secret '{masked_secret}' is not declared in the service account keys file." compliance_issues.append(msg) self.logger.warning(msg) @@ -307,23 +368,34 @@ def create_announcement(self, recipient: str) -> None: """ if not self.sending_client: raise ValueError("SendingClient is required for creating announcements") - + diff = self.check_compliance() if not diff: self.logger.info("No compliance issues found, no announcement will be created.") - return + return - title = f"Account Keys Compliance Issue Detected" - body = f"Account keys for project {self.project_id} are not compliant with the defined policies on {self.service_account_keys_file}\n\n" - for issue in diff: - body += f"- {issue}\n" + unmanaged_keys_issues = [issue for issue in diff if "SECURITY ALERT" in issue] + general_issues = [issue for issue in diff if "SECURITY ALERT" not in issue] - announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the Account Keys policy for project {self.project_id}.\n\n" - announcement += f"We found {len(diff)} compliance issue(s) that need your attention.\n" - announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations." + if general_issues: + self.logger.info(f"Found {len(general_issues)} general compliance issues. Triggering announcement...") + title = f"Account Keys Compliance Issue Detected" + body = f"Account keys for project {self.project_id} are not compliant with the defined policies on {self.service_account_keys_file}\n\n" + for issue in general_issues: + body += f"- {issue}\n" - self.sending_client.create_announcement(title, body, recipient, announcement) + announcement = f"Dear team,\n\nThis is an automated notification about compliance issues detected in the Account Keys policy for project {self.project_id}.\n\n" + announcement += f"We found {len(general_issues)} compliance issue(s) that need your attention.\n" + announcement += f"\nPlease check the GitHub issue for detailed information and take appropriate action to resolve these compliance violations." + + self.sending_client.create_announcement(title, body, recipient, announcement) + if unmanaged_keys_issues: + self.logger.info(f"Found {len(unmanaged_keys_issues)} unmanaged key security alerts. Dispatching to GitHub security issue...") + self.sending_client.report_unmanaged_keys(self.project_id, unmanaged_keys_issues) + else: + self.logger.info("No unmanaged key security alerts found, Checking if there are open security issues to auto-close...") + self.sending_client.resolve_unmanaged_keys() def print_announcement(self, recipient: str) -> None: """ @@ -382,7 +454,8 @@ def generate_compliance(self) -> None: # Check for managed secrets that are not declared, if not, add them for secret in managed_secrets: if secret not in extracted_secrets: - self.logger.info(f"Managed secret '{secret}' is not declared in the service account keys file, adding it") + masked_secret = f"{secret[:4]}***{secret[-4:]}" if len(secret) >= 8 else "***" + self.logger.info(f"Managed secret '{masked_secret}' is not declared in the service account keys file, adding it") file_service_accounts.append({ "account_id": secret.strip("-key"), "display_name": self._normalize_account_email(secret.strip("-key")), @@ -514,7 +587,7 @@ def main(): logger.error(f"Unknown action: {action}") return 1 except Exception as e: - logger.error(f"Error executing action '{action}': {e}") + logger.exception(f"Error executing action '{action}': {e}") return 1 return 0 diff --git a/infra/enforcement/sending.py b/infra/enforcement/sending.py index 961674ca2f17..37de025a207f 100644 --- a/infra/enforcement/sending.py +++ b/infra/enforcement/sending.py @@ -102,10 +102,21 @@ def _get_open_issues(self, title: str) -> List[GitHubIssue]: Args: title (str): The title of the GitHub issue. """ - endpoint = f"search/issues/?q=is:issue+repo:{self.github_repo}+in:title+{title}+is:open" + endpoint = f"search/issues?q=is:issue+repo:{self.github_repo}+in:title+{title}+is:open" response = self._make_github_request("GET", endpoint) issues = response.json().get('items', []) - return [GitHubIssue(**issue) for issue in issues] + parsed_issues = [] + for issue in issues: + parsed_issues.append(GitHubIssue( + number=issue.get("number"), + title=issue.get("title"), + body=issue.get("body"), + state=issue.get("state"), + html_url=issue.get("html_url"), + created_at=issue.get("created_at"), + updated_at=issue.get("updated_at") + )) + return parsed_issues def create_issue(self, title: str, body: str) -> GitHubIssue: """ @@ -119,7 +130,16 @@ def create_issue(self, title: str, body: str) -> GitHubIssue: payload = {"title": title, "body": body} response = self._make_github_request("POST", endpoint, json=payload) self.logger.info(f"Successfully created GitHub issue: {title}") - return GitHubIssue(**response.json()) + data = response.json() + return GitHubIssue( + number=data.get("number"), + title=data.get("title"), + body=data.get("body"), + state=data.get("state"), + html_url=data.get("html_url"), + created_at=data.get("created_at"), + updated_at=data.get("updated_at") + ) def update_issue_body(self, issue_number: int, new_body: str) -> None: """ @@ -134,6 +154,85 @@ def update_issue_body(self, issue_number: int, new_body: str) -> None: self._make_github_request("PATCH", endpoint, json=payload) self.logger.info(f"Successfully updated body on GitHub issue: #{issue_number}") + def create_issue_comment(self, issue_number: int, comment_body: str) -> None: + """ + Adds a new comment to an existing GitHub issue in the specified repository. + + Args: + issue_number (int): The number of the GitHub issue to comment on. + comment_body (str): The content of the comment to add to the GitHub issue. + """ + endpoint = f"repos/{self.github_repo}/issues/{issue_number}/comments" + payload = {"body": comment_body} + self._make_github_request("POST", endpoint, json=payload) + self.logger.info(f"Successfully added comment to GitHub issue: #{issue_number}") + + def report_unmanaged_keys(self, project_id: str, compilance_issues: List[str]) -> None: + """ + Report compliance issues regarding unmanaged keys into a single GitHub issue. + Creates a new issue if none exists. If it exists, updates the body with the newest + report and moves the previous content into a collapsed history section. + + Args: + project_id (str): The ID of the project associated with the unmanaged keys. + compilance_issues (List[str]): A list of compliance issues related to the unmanaged keys. + """ + if not compilance_issues: + self.logger.info("No compliance issues to report to Github.") + return + + issue_title = "[SECURITY] Action Required: Unmanaged Service Account Keys Detected" + #markdown body + timestamp = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + new_report = f"### Unmanaged Keys Audit Report ({timestamp})\n" + new_report += f"The following unauthorized or unmanaged keys were detected in `{project_id}`:\n\n" + + for issue_text in compilance_issues: + new_report += f"- {issue_text}\n" + + new_report += "\n*Please investigate and revoke these keys if they are not part of the official rotation system.*" + open_issues = self._get_open_issues(issue_title) + + if open_issues: + target_issue = open_issues[0] + self.logger.info(f"Appending report and archiving history to existing security issue #{target_issue.number}") + + old_body = target_issue.body or "" + history_marker = "### History\n
\nClick to expand\n\n" + + if history_marker in old_body: + # If history already exists, append the new report to it + headed = old_body.split(history_marker) + last_report = headed[0].strip() + old_history = headed[1].replace("
", "").strip() + + combined_history = f"{last_report}\n\n---\n\n{old_history}" + else: + combined_history = old_body.strip() + + final_body = f"{new_report}\n\n{history_marker}{combined_history}\n
" + self.update_issue_body(target_issue.number, final_body) + else: + self.logger.info("Creating new security issue for unmanaged keys report.") + new_issue = self.create_issue(issue_title, new_report) + self.logger.info(f"Created new security issue : {new_issue.html_url}.") + + def resolve_unmanaged_keys(self) -> None: + """ + Finds any open security issues regarding rogue keys and automatically closes them + if the infrastructure is now healthy. + """ + issue_title = "[SECURITY] Action Required: Unmanaged Service Account Keys Detected" + open_issues = self._get_open_issues(issue_title) + if open_issues: + target_issue = open_issues[0] + self.logger.info(f"All rogue keys resolved! Auto-closing issue #{target_issue.number}.") + self.create_issue_comment(target_issue.number, "All previously reported unmanaged keys have been resolved. Closing this issue.") + endpoint = f"repos/{self.github_repo}/issues/{target_issue.number}" + payload = {"state": "closed"} + self._make_github_request("PATCH", endpoint, json=payload) + + def create_announcement(self, title: str, body: str, recipient: str, announcement: str) -> None: """ This method sends an email with an announcement. The email will point to a GitHub issue. diff --git a/infra/iam/users.yml b/infra/iam/users.yml index 42b56a6de53d..618420b39b06 100644 --- a/infra/iam/users.yml +++ b/infra/iam/users.yml @@ -486,6 +486,12 @@ - role: roles/cloudfunctions.invoker - role: roles/iam.serviceAccountTokenCreator - role: roles/storage.objectViewer +- username: hansmarcus + email: hansmarcus14@gmail.com + member_type: user + permissions: + - role: projects/apache-beam-testing/roles/beam_viewer + - role: projects/apache-beam-testing/roles/beam_writer - username: harrisonlim email: harrisonlim@google.com member_type: user @@ -1241,4 +1247,4 @@ member_type: user permissions: - role: projects/apache-beam-testing/roles/beam_viewer - - role: projects/apache-beam-testing/roles/beam_writer + - role: projects/apache-beam-testing/roles/beam_writer \ No newline at end of file diff --git a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java index 45c22b1f0d55..5ca6a8c4655c 100644 --- a/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java +++ b/it/google-cloud-platform/src/main/java/org/apache/beam/it/gcp/bigquery/BigQueryResourceManager.java @@ -156,6 +156,20 @@ private synchronized Table getTableIfExists(String tableId) throws IllegalStateE return table; } + private void validateTableCreationInputs(String tableName, Schema schema) { + BigQueryResourceManagerUtils.checkValidTableId(tableName); + if (schema == null) { + throw new IllegalArgumentException("A valid schema must be provided to create a table."); + } + } + + private void ensureDatasetExists() { + if (dataset == null) { + createDataset(DEFAULT_DATASET_REGION); + } + checkHasDataset(); + } + /** * Helper method for logging individual errors thrown by inserting rows to a table. This method is * used to log errors thrown by inserting certain rows when other rows were successful. @@ -235,18 +249,8 @@ public synchronized TableId createTable(String tableName, Schema schema) public synchronized TableId createTable( String tableName, Schema schema, Long expirationTimeMillis) throws BigQueryResourceManagerException { - // Check table ID - BigQueryResourceManagerUtils.checkValidTableId(tableName); - - // Check schema - if (schema == null) { - throw new IllegalArgumentException("A valid schema must be provided to create a table."); - } - // Create a default dataset if this resource manager has not already created one - if (dataset == null) { - createDataset(DEFAULT_DATASET_REGION); - } - checkHasDataset(); + validateTableCreationInputs(tableName, schema); + ensureDatasetExists(); LOG.info("Creating table using tableName '{}'.", tableName); // Create the table if it does not already exist in the dataset @@ -313,13 +317,7 @@ public synchronized TableId createTimePartitionedTable( public synchronized TableId createTimePartitionedTable( String tableName, Schema schema, TimePartitioning timePartitioning, Long expirationTimeMillis) throws BigQueryResourceManagerException { - // Check table ID - BigQueryResourceManagerUtils.checkValidTableId(tableName); - - // Check schema - if (schema == null) { - throw new IllegalArgumentException("A valid schema must be provided to create a table."); - } + validateTableCreationInputs(tableName, schema); // Check time partition details if (timePartitioning == null) { @@ -327,11 +325,7 @@ public synchronized TableId createTimePartitionedTable( "A valid TimePartition object must be provided to create a time paritioned table. Use createTable instead to create non-partitioned tables."); } - // Create a default dataset if this resource manager has not already created one - if (dataset == null) { - createDataset(DEFAULT_DATASET_REGION); - } - checkHasDataset(); + ensureDatasetExists(); LOG.info( "Creating time partitioned table using tableName '{}' on field '{}'.", diff --git a/learning/tour-of-beam/learning-content/introduction/introduction-concepts/runner-concepts/description.md b/learning/tour-of-beam/learning-content/introduction/introduction-concepts/runner-concepts/description.md index 31743b29b8cd..2ee57e07f990 100644 --- a/learning/tour-of-beam/learning-content/introduction/introduction-concepts/runner-concepts/description.md +++ b/learning/tour-of-beam/learning-content/introduction/introduction-concepts/runner-concepts/description.md @@ -390,103 +390,6 @@ python -m apache_beam.examples.wordcount --input /path/to/inputfile \ ``` {{end}} -{{if (eq .Sdk "java" "go")}} -### Samza runner - -The Apache Samza Runner can be used to execute Beam pipelines using Apache Samza. The Samza Runner executes Beam pipeline in a Samza application and can run locally. The application can further be built into a .tgz file, and deployed to a YARN cluster or Samza standalone cluster with Zookeeper. - -The Samza Runner and Samza are suitable for large scale, stateful streaming jobs, and provide: - -* First class support for local state (with RocksDB store). This allows fast state access for high frequency streaming jobs. -* Fault-tolerance with support for incremental checkpointing of state instead of full snapshots. This enables Samza to scale to applications with very large state. -* A fully asynchronous processing engine that makes remote calls efficient. -* Flexible deployment model for running the applications in any hosting environment with Zookeeper. -* Features like canaries, upgrades and rollbacks that support extremely large deployments with minimal downtime. - -Additionally, you can read more about the Samza Runner [here](https://beam.apache.org/documentation/runners/samza/) - -#### Run example -{{end}} - -{{if (eq .Sdk "go")}} - -Need import: -``` -"github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" -``` - -It is necessary to give an endpoint where the runner is raised with `--endpoint`: -``` -$ go install github.com/apache/beam/sdks/v2/go/examples/wordcount -# As part of the initial setup, for non linux users - install package unix before run -$ go get -u golang.org/x/sys/unix -$ wordcount --input gs://dataflow-samples/shakespeare/kinglear.txt \ ---output gs:///counts \ ---runner samza \ ---project your-gcp-project \ ---region your-gcp-region \ ---temp_location gs:///tmp/ \ ---staging_location gs:///binaries/ \ ---worker_harness_container_image=apache/beam_go_sdk:latest \ ---endpoint=localhost:8081 -``` -{{end}} - -{{if (eq .Sdk "java")}} -You can specify your dependency on the Samza Runner by adding the following to your `pom.xml`: - -``` - - org.apache.beam - beam-runners-samza - 2.42.0 - runtime - - - - - org.apache.samza - samza-api - ${samza.version} - - - - org.apache.samza - samza-core_2.11 - ${samza.version} - - - - org.apache.samza - samza-kafka_2.11 - ${samza.version} - runtime - - - - org.apache.samza - samza-kv_2.11 - ${samza.version} - runtime - - - - org.apache.samza - samza-kv-rocksdb_2.11 - ${samza.version} - runtime - -``` - -Console: -``` -$ mvn exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ - -Psamza-runner \ - -Dexec.args="--runner=SamzaRunner \ - --inputFile=/path/to/input \ - --output=/path/to/counts" -``` - ### Nemo runner The Apache Nemo Runner can be used to execute Beam pipelines using Apache Nemo. The Nemo Runner can optimize Beam pipelines with the Nemo compiler through various optimization passes and execute them in a distributed fashion using the Nemo runtime. You can also deploy a self-contained application for local mode or run using resource managers like YARN or Mesos. diff --git a/learning/tour-of-beam/terraform/build.gradle.kts b/learning/tour-of-beam/terraform/build.gradle.kts index 30d30309273c..3080f74a5149 100644 --- a/learning/tour-of-beam/terraform/build.gradle.kts +++ b/learning/tour-of-beam/terraform/build.gradle.kts @@ -21,7 +21,7 @@ import java.io.ByteArrayOutputStream import java.util.regex.Pattern plugins { - id("com.pswidersk.terraform-plugin") version "1.0.0" + id("com.pswidersk.terraform-plugin") version "1.1.1" } terraformPlugin { diff --git a/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto b/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto index 22b19ef03289..80a0fa6f7f28 100644 --- a/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto +++ b/model/fn-execution/src/main/proto/org/apache/beam/model/fn_execution/v1/beam_fn_api.proto @@ -120,6 +120,9 @@ message RemoteGrpcPort { service BeamFnControl { // Instructions sent by the runner to the SDK requesting different types // of work. + // + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc Control( // A stream of responses to instructions the SDK was asked to be // performed. @@ -130,6 +133,9 @@ service BeamFnControl { // Used to get the full process bundle descriptors for bundles one // is asked to process. + // + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc GetProcessBundleDescriptor(GetProcessBundleDescriptorRequest) returns ( ProcessBundleDescriptor) {} } @@ -416,14 +422,22 @@ message ProcessBundleRequest { // at https://s.apache.org/beam-fn-api-control-data-embedding. Elements elements = 3; - // indicates that the runner has no stare for the keys in this bundle + // Indicates that the runner has no state for the keys in this bundle // so SDk can safely begin stateful processing with a locally-generated - // initial empty state + // initial empty state. bool has_no_state = 4; - // indicates that the runner will never process another bundle for the keys + // Indicates that the runner will never process another bundle for the keys // in this bundle so state need not be included in the bundle commit. bool only_bundle_for_keys = 5; + + // (Optional) If non-empty, the ID of the data stream to use for all data + // requests related to this bundle. See comments at BeamFnData.Data for + // more details. + // + // The runner should only populate this field if the sdk advertises the + // beam:protocol:named_data_streams:v1 capability. + string data_stream_id = 6; } message ProcessBundleResponse { @@ -757,6 +771,29 @@ message Elements { } } + // The type of change operation represented by a Change Data Capture (CDC) record + message ValueKind { + enum Enum { + // Unspecified ValueKind. Will be treated as an INSERT + VALUE_KIND_UNSPECIFIED = 0; + + // Indicates a new record was created in the source system. + INSERT = 1; + + // Indicates the state of a record immediately before an update occurred. + // This is typically used to identify the previous values of modified columns + // or to locate the record via its primary key. + UPDATE_BEFORE = 2; + + // Indicates the state of a record immediately after an update occurred. + // Represents the current, valid state of the record following the change. + UPDATE_AFTER = 3; + + // Indicates that an existing record was removed from the source system. + DELETE = 4; + } + } + // Element metadata passed as part of WindowedValue to make WindowedValue // extensible and backward compatible message ElementMetadata { @@ -770,6 +807,9 @@ message Elements { // across IOs - Kafka, PubSub, http. // Example value: congo=t61rcWkgMzE optional string tracestate = 3; + // (Optional) The kind of value for CDC metadata. + // If missing or unspecified, implies INSERT for backwards compatibility. + optional ValueKind.Enum value_kind = 4; } // Represent the encoded user timer for a given instruction, transform and @@ -808,7 +848,15 @@ message Elements { // Stable service BeamFnData { - // Used to send data between harnesses. + // Used to send data between harnesses. Sdks default to using an unnamed data stream + // (without "data_stream_id" header value) for bundles unless the runner requests another named stream to be + // used for a bundle. SDKs can advertise that they support named data streams with the capability + // `beam:protocol:named_data_streams:v1`. + // + // Header metadata has the specified keys pairs: + // - "worker_id": value is the id of the sdk + // - "data_stream_id": value is the id of the data stream, distinguishing it from other data streams from the same + // sdk. This field should only be populated if requested in a received ProcessBundleRequest from the runner. rpc Data( // A stream of data representing input. stream Elements) @@ -874,6 +922,9 @@ message StateResponse { service BeamFnState { // Used to get/append/clear state stored by the runner on behalf of the SDK. + // + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc State( // A stream of state instructions requested of the runner. stream StateRequest) @@ -1269,6 +1320,9 @@ message LogControl {} service BeamFnLogging { // Allows for the SDK to emit log entries which the runner can // associate with the active job. + // + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc Logging( // A stream of log entries batched into lists emitted by the SDK harness. stream LogEntry.List) @@ -1330,6 +1384,8 @@ message WorkerStatusResponse { // API for SDKs to report debug-related statuses to runner during pipeline execution. service BeamFnWorkerStatus { + // Header metadata has the specified keys pairs: + // - "worker_id": the id of the sdk rpc WorkerStatus (stream WorkerStatusResponse) returns (stream WorkerStatusRequest) {} } diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto index 67df8b9e8003..5824c9bf4b73 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/beam_runner_api.proto @@ -1689,6 +1689,10 @@ message StandardProtocols { // Indicates whether the SDK supports multimap state. MULTIMAP_STATE = 12 [(beam_urn) = "beam:protocol:multimap_state:v1"]; + + // Indicates whether the SDK supports data stream ids being requested by the runner in + // ProcessBundleRequests. + NAMED_DATA_STREAMS = 13 [(beam_urn) = "beam:protocol:named_data_streams:v1"]; } } diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto index 043a72dd34f2..c73986eed48b 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/external_transforms.proto @@ -88,6 +88,8 @@ message ManagedTransforms { "beam:schematransform:org.apache.beam:sql_server_read:v1"]; SQL_SERVER_WRITE = 12 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:schematransform:org.apache.beam:sql_server_write:v1"]; + DELTA_LAKE_READ = 13 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:schematransform:org.apache.beam:delta_lake_read:v1"]; } } diff --git a/playground/backend/containers/go/build.gradle b/playground/backend/containers/go/build.gradle index ad236e10d50f..6e75929b1444 100644 --- a/playground/backend/containers/go/build.gradle +++ b/playground/backend/containers/go/build.gradle @@ -88,7 +88,7 @@ docker { buildArgs( ['BASE_IMAGE' : project.rootProject.hasProperty(["base-image"]) ? project.rootProject["base-image"] : - "golang:1.25", + "golang:1.26", 'SDK_TAG' : project.rootProject.hasProperty(["sdk-tag"]) ? project.rootProject["sdk-tag"] : project.rootProject.sdk_version, 'SDK_TAG_LOCAL': project.rootProject.sdk_version, 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/infrastructure/requirements.txt b/playground/infrastructure/requirements.txt index 93b4c4a44d9f..0e1da1660781 100644 --- a/playground/infrastructure/requirements.txt +++ b/playground/infrastructure/requirements.txt @@ -16,7 +16,7 @@ # under the License. mock==4.0.3 -pytest==6.2.5 +pytest==9.0.3 pytest-asyncio==0.18.2 pytest-mock==3.6.1 PyYAML==6.0.2 diff --git a/playground/terraform/build.gradle.kts b/playground/terraform/build.gradle.kts index fb1520e50df3..ea10fa0e94ac 100644 --- a/playground/terraform/build.gradle.kts +++ b/playground/terraform/build.gradle.kts @@ -39,7 +39,7 @@ val licenseText = "############################################################# "################################################################################" plugins { - id("com.pswidersk.terraform-plugin") version "1.0.0" + id("com.pswidersk.terraform-plugin") version "1.1.1" } terraformPlugin { diff --git a/release/build.gradle.kts b/release/build.gradle.kts index 5be707428605..54165dc49654 100644 --- a/release/build.gradle.kts +++ b/release/build.gradle.kts @@ -39,7 +39,7 @@ task("runJavaExamplesValidationTask") { dependsOn(":runners:direct-java:runQuickstartJavaDirect") dependsOn(":runners:google-cloud-dataflow-java:runQuickstartJavaDataflow") dependsOn(":runners:spark:3:runQuickstartJavaSpark") - dependsOn(":runners:flink:2.0:runQuickstartJavaFlinkLocal") + dependsOn(":runners:flink:2.2:runQuickstartJavaFlinkLocal") dependsOn(":runners:direct-java:runMobileGamingJavaDirect") if (project.hasProperty("ver") || !project.version.toString().endsWith("SNAPSHOT")) { // only run one variant of MobileGaming on Dataflow for nightly diff --git a/release/src/main/scripts/jenkins_jobs.txt b/release/src/main/scripts/jenkins_jobs.txt deleted file mode 100644 index ae4c13b24d4c..000000000000 --- a/release/src/main/scripts/jenkins_jobs.txt +++ /dev/null @@ -1,115 +0,0 @@ -Run Chicago Taxi on Dataflow,beam_PostCommit_Python_Chicago_Taxi_Dataflow_PR -Run Chicago Taxi on Flink,beam_PostCommit_Python_Chicago_Taxi_Flink_PR -Run Dataflow Runner Nexmark Tests,beam_PostCommit_Java_Nexmark_Dataflow_PR -Run Dataflow Runner Tpcds Tests,beam_PostCommit_Java_Tpcds_Dataflow_PR -Run Dataflow Runner V2 Java 11 Nexmark Tests,beam_PostCommit_Java_Nexmark_DataflowV2_Java11_PR -Run Dataflow Runner V2 Java 17 Nexmark Tests,beam_PostCommit_Java_Nexmark_DataflowV2_Java17_PR -Run Dataflow Runner V2 Nexmark Tests,beam_PostCommit_Java_Nexmark_DataflowV2_PR -Run Dataflow Streaming ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Dataflow_Streaming_PR -Run Dataflow ValidatesRunner Java 11,beam_PostCommit_Java_ValidatesRunner_Dataflow_Java11_PR -Run Dataflow ValidatesRunner Java 17,beam_PostCommit_Java_ValidatesRunner_Dataflow_Java17_PR -Run Dataflow ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Dataflow_PR -Run Direct Runner Nexmark Tests,beam_PostCommit_Java_Nexmark_Direct_PR -Run Direct ValidatesRunner Java 11,beam_PostCommit_Java_ValidatesRunner_Direct_Java11_PR -Run Direct ValidatesRunner Java 17,beam_PostCommit_Java_ValidatesRunner_Direct_Java17_PR -Run Direct ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Direct_PR -Run Flink Runner Nexmark Tests,beam_PostCommit_Java_Nexmark_Flink_PR -Run Flink Runner Tpcds Tests,beam_PostCommit_Java_Tpcds_Flink_PR -Run Flink ValidatesRunner Java 11,beam_PostCommit_Java_ValidatesRunner_Flink_Java11_PR -Run Flink ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Flink_PR -Run Go Flink ValidatesRunner,beam_PostCommit_Go_VR_Flink_PR -Run Go PostCommit,beam_PostCommit_Go_PR -Run Go Samza ValidatesRunner,beam_PostCommit_Go_VR_Samza_PR -Run Go Spark ValidatesRunner,beam_PostCommit_Go_VR_Spark_PR -Run Java 11 Examples on Dataflow Runner V2,beam_PostCommit_Java_Examples_Dataflow_V2_java11_PR -Run Java 17 Examples on Dataflow Runner V2,beam_PostCommit_Java_Examples_Dataflow_V2_java17_PR -Run Java Dataflow V2 ValidatesRunner Streaming,beam_PostCommit_Java_VR_Dataflow_V2_Streaming_PR -Run Java Dataflow V2 ValidatesRunner,beam_PostCommit_Java_VR_Dataflow_V2_PR -Run Java Examples on Dataflow Runner V2,beam_PostCommit_Java_Examples_Dataflow_V2_PR -Run Java Examples_Direct,beam_PostCommit_Java_Examples_Direct_PR -Run Java Examples_Flink,beam_PostCommit_Java_Examples_Flink_PR -Run Java Examples_Spark,beam_PostCommit_Java_Examples_Spark_PR -Run Java Flink PortableValidatesRunner Streaming,beam_PostCommit_Java_PVR_Flink_Streaming_PR -Run Java InfluxDbIO_IT,beam_PostCommit_Java_InfluxDbIO_IT_PR -Run Java PostCommit,beam_PostCommit_Java_PR -Run Java PreCommit,beam_PreCommit_Java_Phrase -Run Java Samza PortableValidatesRunner,beam_PostCommit_Java_PVR_Samza_PR -Run Java Sickbay,beam_PostCommit_Java_Sickbay_PR -Run Java SingleStoreIO_IT,beam_PostCommit_Java_SingleStoreIO_IT_PR -Run Java Spark PortableValidatesRunner Batch,beam_PostCommit_Java_PVR_Spark_Batch_PR -Run Java Spark v3 PortableValidatesRunner Streaming,beam_PostCommit_Java_PVR_Spark3_Streaming_PR -Run Java examples on Dataflow Java 11,beam_PostCommit_Java_Examples_Dataflow_Java11_PR -Run Java examples on Dataflow Java 17,beam_PostCommit_Java_Examples_Dataflow_Java17_PR -Run Java_Amazon-Web-Services2_IO_Direct PreCommit,beam_PreCommit_Java_Amazon-Web-Services2_IO_Direct_Phrase -Run Java_Amazon-Web-Services_IO_Direct PreCommit,beam_PreCommit_Java_Amazon-Web-Services_IO_Direct_Phrase -Run Java_Azure_IO_Direct PreCommit,beam_PreCommit_Java_Azure_IO_Direct_Phrase -Run Java_GCP_IO_Direct PreCommit,beam_PreCommit_Java_GCP_IO_Direct_Phrase -Run Java_Google-ads_IO_Direct PreCommit,beam_PreCommit_Java_Google-ads_IO_Direct_Phrase -Run Java_IOs_Direct PreCommit,beam_PreCommit_Java_IOs_Direct_Phrase -Run Java_Kinesis_IO_Direct PreCommit,beam_PreCommit_Java_Kinesis_IO_Direct_Phrase -Run Java_PVR_Flink_Batch PreCommit,beam_PreCommit_Java_PVR_Flink_Batch_Phrase -Run Java_Pulsar_IO_Direct PreCommit,beam_PreCommit_Java_Pulsar_IO_Direct_Phrase -Run Java_hadoop_IO_Direct PreCommit,beam_PreCommit_Java_Hadoop_IO_Direct_Phrase -Run Javadoc PostCommit,beam_PostCommit_Javadoc_PR -Run Jpms Dataflow Java 11 PostCommit,beam_PostCommit_Java_Jpms_Dataflow_Java11_PR -Run Jpms Dataflow Java 17 PostCommit,beam_PostCommit_Java_Jpms_Dataflow_Java17_PR -Run Jpms Direct Java 11 PostCommit,beam_PostCommit_Java_Jpms_Direct_Java11_PR -Run Jpms Direct Java 17 PostCommit,beam_PostCommit_Java_Jpms_Direct_Java17_PR -Run Jpms Flink Java 11 PostCommit,beam_PostCommit_Java_Jpms_Flink_Java11_PR -Run Jpms Spark Java 11 PostCommit,beam_PostCommit_Java_Jpms_Spark_Java11_PR -Run PortableJar_Flink PostCommit,beam_PostCommit_PortableJar_Flink_PR -Run PortableJar_Spark PostCommit,beam_PostCommit_PortableJar_Spark_PR -Run PostCommit_Java_Avro_Versions,beam_PostCommit_Java_Avro_Versions_PR -Run PostCommit_Java_Dataflow,beam_PostCommit_Java_DataflowV1_PR -Run PostCommit_Java_DataflowV2,beam_PostCommit_Java_DataflowV2_PR -Run PostCommit_Java_Hadoop_Versions,beam_PostCommit_Java_Hadoop_Versions_PR -Run Python 3.10 PostCommit Sickbay,beam_PostCommit_Sickbay_Python310_PR -Run Python 3.10 PostCommit,beam_PostCommit_Python310_PR -Run Python 3.11 PostCommit Sickbay,beam_PostCommit_Sickbay_Python311_PR -Run Python 3.11 PostCommit,beam_PostCommit_Python311_PR -Run Python 3.8 PostCommit Sickbay,beam_PostCommit_Sickbay_Python38_PR -Run Python 3.8 PostCommit,beam_PostCommit_Python38_PR -Run Python 3.9 PostCommit Sickbay,beam_PostCommit_Sickbay_Python39_PR -Run Python 3.9 PostCommit,beam_PostCommit_Python39_PR -Run Python Dataflow ValidatesContainer,beam_PostCommit_Py_ValCont_PR -Run Python Dataflow ValidatesRunner,beam_PostCommit_Py_VR_Dataflow_PR -Run Python Direct Runner Nexmark Tests,beam_PostCommit_Python_Nexmark_Direct_PR -Run Python Examples_Dataflow,beam_PostCommit_Python_Examples_Dataflow_PR -Run Python Examples_Direct,beam_PostCommit_Python_Examples_Direct_PR -Run Python Examples_Flink,beam_PostCommit_Python_Examples_Flink_PR -Run Python Examples_Spark,beam_PostCommit_Python_Examples_Spark_PR -Run Python Flink ValidatesRunner,beam_PostCommit_Python_VR_Flink_PR -Run Python MongoDBIO_IT,beam_PostCommit_Python_MongoDBIO_IT_PR -Run Python PreCommit,beam_PreCommit_Python_Phrase -Run Python RC Dataflow ValidatesContainer,beam_PostCommit_Py_ValCont_with_RC_PR -Run Python Samza ValidatesRunner,beam_PostCommit_Python_VR_Samza_PR -Run Python Spark ValidatesRunner,beam_PostCommit_Python_VR_Spark_PR -Run PythonDocker PreCommit,beam_PreCommit_PythonDocker_Phrase -Run Python_Coverage PreCommit,beam_PreCommit_Python_Coverage_Phrase -Run Python_Dataframes PreCommit,beam_PreCommit_Python_Dataframes_Phrase -Run Python_Examples PreCommit,beam_PreCommit_Python_Examples_Phrase -Run Python_Integration PreCommit,beam_PreCommit_Python_Integration_Phrase -Run Python_PVR_Flink PreCommit,beam_PreCommit_Python_PVR_Flink_Phrase -Run Python_Runners PreCommit,beam_PreCommit_Python_Runners_Phrase -Run Python_Transforms PreCommit,beam_PreCommit_Python_Transforms_Phrase -Run Python_Xlang_Gcp_Dataflow PostCommit,beam_PostCommit_Python_Xlang_Gcp_Dataflow_PR -Run Python_Xlang_Gcp_Direct PostCommit,beam_PostCommit_Python_Xlang_Gcp_Direct_PR -Run Python_Xlang_IO_Dataflow PostCommit,beam_PostCommit_Python_Xlang_IO_Dataflow_PR -Run SQL PostCommit,beam_PostCommit_SQL_PR -Run Samza ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Samza_PR -Run Spark Runner Nexmark Tests,beam_PostCommit_Java_Nexmark_Spark_PR -Run Spark Runner Tpcds Tests,beam_PostCommit_Java_Tpcds_Spark_PR -Run Spark StructuredStreaming ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_SparkStructuredStreaming_PR -Run Spark ValidatesRunner Java 11,beam_PostCommit_Java_ValidatesRunner_Spark_Java11_PR -Run Spark ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Spark_PR -Run TransformService_Direct PostCommit,beam_PostCommit_TransformService_Direct_PR -Run Twister2 ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_Twister2_PR -Run ULR Loopback ValidatesRunner,beam_PostCommit_Java_ValidatesRunner_ULR_PR -Run XVR_Direct PostCommit,beam_PostCommit_XVR_Direct_PR -Run XVR_Flink PostCommit,beam_PostCommit_XVR_Flink_PR -Run XVR_GoUsingJava_Dataflow PostCommit,beam_PostCommit_XVR_GoUsingJava_Dataflow_PR -Run XVR_JavaUsingPython_Dataflow PostCommit,beam_PostCommit_XVR_JavaUsingPython_Dataflow_PR -Run XVR_PythonUsingJavaSQL_Dataflow PostCommit,beam_PostCommit_XVR_PythonUsingJavaSQL_Dataflow_PR -Run XVR_PythonUsingJava_Dataflow PostCommit,beam_PostCommit_XVR_PythonUsingJava_Dataflow_PR -Run XVR_Samza PostCommit,beam_PostCommit_XVR_Samza_PR -Run XVR_Spark3 PostCommit,beam_PostCommit_XVR_Spark3_PR diff --git a/release/src/main/scripts/mass_comment.py b/release/src/main/scripts/mass_comment.py deleted file mode 100644 index 7dec20dbbba5..000000000000 --- a/release/src/main/scripts/mass_comment.py +++ /dev/null @@ -1,176 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -"""Script for mass-commenting Jenkins test triggers on a Beam PR.""" - -import os -import requests -import socket -import time - - -def executeGHGraphqlQuery(accessToken, query): - '''Runs graphql query on GitHub.''' - url = 'https://api.github.com/graphql' - headers = {'Authorization': 'Bearer %s' % accessToken} - r = requests.post(url=url, json={'query': query}, headers=headers) - return r.json() - - -def getSubjectId(accessToken, prNumber): - query = ''' -query FindPullRequestID { - repository(owner:"apache", name:"beam") { - pullRequest(number:%s) { - id - } - } -} -''' % prNumber - response = executeGHGraphqlQuery(accessToken, query) - return response['data']['repository']['pullRequest']['id'] - - -def addPrComment(accessToken, subjectId, commentBody): - '''Adds a pr comment to the PR defined by subjectId''' - query = ''' -mutation AddPullRequestComment { - addComment(input:{subjectId:"%s",body: "%s"}) { - commentEdge { - node { - createdAt - body - } - } - subject { - id - } - } -} -''' % (subjectId, commentBody) - return executeGHGraphqlQuery(accessToken, query) - -def getPrStatuses(accessToken, prNumber): - query = ''' -query GetPRChecks { - repository(name: "beam", owner: "apache") { - pullRequest(number: %s) { - commits(last: 1) { - nodes { - commit { - status { - contexts { - targetUrl - context - } - } - } - } - } - } - } -} -''' % (prNumber) - return executeGHGraphqlQuery(accessToken, query) - - -def postComments(accessToken, subjectId, commentsToAdd): - ''' - Main workhorse method. Posts comments to GH. - ''' - - for comment in commentsToAdd: - jsonData = addPrComment(accessToken, subjectId, comment[0]) - print(jsonData) - # Space out comments 30 seconds apart to avoid overwhelming Jenkins - time.sleep(30) - - -def probeGitHubIsUp(): - ''' - Returns True if GitHub responds to simple queries. Else returns False. - ''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = sock.connect_ex(('github.com', 443)) - return True if result == 0 else False - -def getRemainingComments(accessToken, pr, initialComments): - ''' - Filters out the comments that already have statuses associated with them from initial comments - ''' - queryResult = getPrStatuses(accessToken, pr) - pull = queryResult["data"]["repository"]["pullRequest"] - commit = pull["commits"]["nodes"][0]["commit"] - check_urls = str(list(map(lambda c : c["targetUrl"], commit["status"]["contexts"]))) - remainingComments = [] - for comment in initialComments: - if f'/{comment[1]}_Phrase/' not in check_urls and f'/{comment[1]}_PR/' not in check_urls \ - and f'/{comment[1]}_Commit/' not in check_urls and f'/{comment[1]}/' not in check_urls \ - and 'Sickbay' not in comment[1]: - print(comment) - remainingComments.append(comment) - return remainingComments - -################################################################################ -if __name__ == '__main__': - ''' - This script is supposed to be invoked directly. - However for testing purposes and to allow importing, - wrap work code in module check. - ''' - print("Started.") - comments = [] - dirname = os.path.dirname(__file__) - with open(os.path.join(dirname, 'jenkins_jobs.txt')) as file: - comments = [line.strip() for line in file if len(line.strip()) > 0] - - for i in range(len(comments)): - parts = comments[i].split(',') - comments[i] = (parts[0], parts[1]) - - if not probeGitHubIsUp(): - print("GitHub is unavailable, skipping fetching data.") - exit() - - print("GitHub is available start fetching data.") - - accessToken = input("Enter your Github access token: ") - - pr = input("Enter the Beam PR number to test (e.g. 11403): ") - subjectId = getSubjectId(accessToken, pr) - - # TODO(yathu): also auto rerun failed GitHub Action workflow - remainingComments = getRemainingComments(accessToken, pr, comments) - if len(remainingComments) == 0: - print('Jobs have been started for all comments. If you would like to retry all jobs, create a new commit before running this script.') - while len(remainingComments) > 0: - postComments(accessToken, subjectId, remainingComments) - # Sleep 60 seconds to allow checks to start to status - time.sleep(60) - remainingComments = getRemainingComments(accessToken, pr, remainingComments) - if len(remainingComments) > 0: - print(f'{len(remainingComments)} comments must be reposted because no check has been created for them: {str(remainingComments)}') - print('Sleeping for 1 hour to allow Jenkins to recover and to give it time to status.') - for i in range(60): - time.sleep(60) - print(f'{i} minutes elapsed, {60-i} minutes remaining') - remainingComments = getRemainingComments(accessToken, pr, remainingComments) - if len(remainingComments) == 0: - print(f'{len(remainingComments)} comments still must be reposted: {str(remainingComments)}') - print('Trying to repost comments.') - - print('Done.') diff --git a/runners/core-java/build.gradle b/runners/core-java/build.gradle index 9f24ce39b974..403cf4f2bc5a 100644 --- a/runners/core-java/build.gradle +++ b/runners/core-java/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation project(path: ":model:pipeline", configuration: "shadow") implementation project(path: ":sdks:java:core", configuration: "shadow") implementation project(path: ":model:job-management", configuration: "shadow") + implementation project(path: ":model:fn-execution", configuration: "shadow") implementation library.java.vendored_guava_32_1_2_jre implementation library.java.joda_time implementation library.java.vendored_grpc_1_69_0 diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadata.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadata.java new file mode 100644 index 000000000000..bdce67f6bad5 --- /dev/null +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadata.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.core; + +import com.google.auto.value.AutoValue; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.apache.beam.model.fnexecution.v1.BeamFnApi; +import org.apache.beam.sdk.coders.AtomicCoder; +import org.apache.beam.sdk.values.CausedByDrain; + +/** + * Encapsulates metadata that propagates with elements in the pipeline. + * + *

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

The purpose of this class is to group targeted metadata fields together. This makes it easier + * to define combination strategies (e.g., when accumulating state in {@code ReduceFnRunner}) when + * multiple elements are merged or grouped, without having to extend method signatures or state + * handling for every new metadata field. + */ +@AutoValue +public abstract class CombinedMetadata { + public abstract CausedByDrain causedByDrain(); + + public static CombinedMetadata create(CausedByDrain causedByDrain) { + return new AutoValue_CombinedMetadata(causedByDrain); + } + + public static CombinedMetadata createDefault() { + return create(CausedByDrain.NORMAL); + } + + public static class Coder extends AtomicCoder { + private static final Coder INSTANCE = new Coder(); + + public static Coder of() { + return INSTANCE; + } + + @Override + public void encode(CombinedMetadata value, OutputStream outStream) throws IOException { + BeamFnApi.Elements.ElementMetadata proto = + BeamFnApi.Elements.ElementMetadata.newBuilder() + .setDrain( + value.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN + ? BeamFnApi.Elements.DrainMode.Enum.DRAINING + : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) + .build(); + proto.writeDelimitedTo(outStream); + } + + @Override + public CombinedMetadata decode(InputStream inStream) throws IOException { + BeamFnApi.Elements.ElementMetadata proto = + BeamFnApi.Elements.ElementMetadata.parseDelimitedFrom(inStream); + if (proto == null) { + return CombinedMetadata.createDefault(); + } + + CausedByDrain causedByDrain = + proto.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING + ? CausedByDrain.CAUSED_BY_DRAIN + : CausedByDrain.NORMAL; + + return CombinedMetadata.create(causedByDrain); + } + } +} diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadataCombiner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadataCombiner.java new file mode 100644 index 000000000000..a2f3f26520ef --- /dev/null +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/CombinedMetadataCombiner.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.core; + +import org.apache.beam.sdk.transforms.Combine.CombineFn; +import org.apache.beam.sdk.values.CausedByDrain; + +/** Combiner for CombinedMetadata. */ +class CombinedMetadataCombiner + extends CombineFn { + private static final CombinedMetadataCombiner INSTANCE = new CombinedMetadataCombiner(); + + public static CombinedMetadataCombiner of() { + return INSTANCE; + } + + @Override + public CombinedMetadata createAccumulator() { + return CombinedMetadata.create(CausedByDrainCombiner.of().createAccumulator()); + } + + @Override + public CombinedMetadata addInput(CombinedMetadata accumulator, CombinedMetadata input) { + return CombinedMetadata.create( + CausedByDrainCombiner.of().addInput(accumulator.causedByDrain(), input.causedByDrain())); + } + + @Override + public CombinedMetadata mergeAccumulators(Iterable accumulators) { + CombinedMetadata result = createAccumulator(); + for (CombinedMetadata accum : accumulators) { + result = addInput(result, accum); + } + return result; + } + + @Override + public CombinedMetadata extractOutput(CombinedMetadata accumulator) { + return accumulator; + } + + /** Combiner for CausedByDrain metadata. */ + static class CausedByDrainCombiner implements MetadataCombiner { + private static final CausedByDrainCombiner INSTANCE = new CausedByDrainCombiner(); + + public static CausedByDrainCombiner of() { + return INSTANCE; + } + + @Override + public CausedByDrain createAccumulator() { + return CausedByDrain.NORMAL; + } + + @Override + public CausedByDrain addInput(CausedByDrain current, CausedByDrain input) { + if (current == CausedByDrain.CAUSED_BY_DRAIN || input == CausedByDrain.CAUSED_BY_DRAIN) { + return CausedByDrain.CAUSED_BY_DRAIN; + } + return CausedByDrain.NORMAL; + } + } +} diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/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/LateDataDroppingDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/LateDataDroppingDoFnRunner.java index f5587b46598a..dfab198f8932 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; /** @@ -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); @@ -145,7 +151,15 @@ public Iterable> filter( } else { nonLateElements.add( WindowedValues.of( - element.getValue(), element.getTimestamp(), window, element.getPaneInfo())); + element.getValue(), + element.getTimestamp(), + window, + element.getPaneInfo(), + element.getRecordId(), + element.getRecordOffset(), + element.causedByDrain(), + element.getOpenTelemetryContext(), + element.getValueKind())); } } } diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/package-info.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/MetadataCombiner.java similarity index 81% rename from runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/package-info.java rename to runners/core-java/src/main/java/org/apache/beam/runners/core/MetadataCombiner.java index 582194440c9b..55884a8e43a8 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/adapter/package-info.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/MetadataCombiner.java @@ -15,6 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.apache.beam.runners.core; -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.adapter; +/** Interface for combining pipeline metadata. */ +interface MetadataCombiner { + T createAccumulator(); + + T addInput(T accumulator, T input); +} diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/OutputAndTimeBoundedSplittableProcessElementInvoker.java index 07e4756885dd..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; @@ -451,7 +491,9 @@ public void outputWithTimestamp(TupleTag tag, T value, Instant timestamp) element.getPaneInfo(), element.getRecordId(), element.getRecordOffset(), - element.causedByDrain())); + element.causedByDrain(), + element.getOpenTelemetryContext(), + element.getValueKind())); } @Override @@ -474,7 +516,9 @@ public void outputWindowedValue( paneInfo, element.getRecordId(), element.getRecordOffset(), - element.causedByDrain())); + element.causedByDrain(), + element.getOpenTelemetryContext(), + element.getValueKind())); } @Override diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java index 0721ddc4685e..7fe3b711aa0a 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java @@ -37,7 +37,9 @@ 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; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; @@ -93,6 +95,11 @@ }) // TODO(https://github.com/apache/beam/issues/20497) public class ReduceFnRunner { + // Experiments guarding optimizations in development. No backward compatibility guarantees. + public static final String UNSTABLE_NOT_UPDATE_COMPATIBLE_NEW_WINDOW_OPTIMIZATION = + "unstable_not_update_compatible_new_window_optimization"; + public static final String UNSTABLE_DISABLE_WATERMARK_KNOWN_EMPTY_OPTIMIZATION = + "unstable_disable_watermark_known_empty_optimization"; /** * The {@link ReduceFnRunner} depends on most aspects of the {@link WindowingStrategy}. * @@ -107,6 +114,12 @@ public class ReduceFnRunner { *

  • It uses discarding or accumulation mode according to the {@link WindowingStrategy}. * */ + static final StateTag> + METADATA_TAG = + StateTags.makeSystemTagInternal( + StateTags.combiningValue( + "combinedMetadata", CombinedMetadata.Coder.of(), CombinedMetadataCombiner.of())); + private final WindowingStrategy windowingStrategy; private final WindowedValueReceiver> outputter; @@ -211,6 +224,9 @@ public class ReduceFnRunner { */ private final NonEmptyPanes nonEmptyPanes; + private final boolean useNewWindowOptimization; + private final boolean disableWatermarkKnownEmptyOptimization; + public ReduceFnRunner( K key, WindowingStrategy windowingStrategy, @@ -237,6 +253,15 @@ public ReduceFnRunner( this.nonEmptyPanes = NonEmptyPanes.create(this.windowingStrategy, this.reduceFn); + this.useNewWindowOptimization = + windowingStrategy.getWindowFn().isNonMerging() + && ExperimentalOptions.hasExperiment( + options, UNSTABLE_NOT_UPDATE_COMPATIBLE_NEW_WINDOW_OPTIMIZATION); + + this.disableWatermarkKnownEmptyOptimization = + ExperimentalOptions.hasExperiment( + options, UNSTABLE_DISABLE_WATERMARK_KNOWN_EMPTY_OPTIMIZATION); + // Note this may incur I/O to load persisted window set data. this.activeWindows = createActiveWindowSet(); @@ -256,7 +281,8 @@ public ReduceFnRunner( new TriggerStateMachineRunner<>( triggerStateMachine, new TriggerStateMachineContextFactory<>( - windowingStrategy.getWindowFn(), stateInternals, activeWindows)); + windowingStrategy.getWindowFn(), stateInternals, activeWindows), + this.useNewWindowOptimization); } private ActiveWindowSet createActiveWindowSet() { @@ -275,6 +301,16 @@ boolean hasNoActiveWindows() { return activeWindows.getActiveAndNewWindows().isEmpty(); } + @VisibleForTesting + TriggerStateMachineRunner getTriggerRunner() { + return triggerRunner; + } + + @VisibleForTesting + ReduceFnContextFactory getContextFactory() { + return contextFactory; + } + private Set windowsThatAreOpen(Collection windows) { Set result = new HashSet<>(); for (W window : windows) { @@ -376,7 +412,7 @@ public void processElements(Iterable> values) throws Excep emit( contextFactory.base(window, StateStyle.DIRECT), contextFactory.base(window, StateStyle.RENAMED), - CausedByDrain.NORMAL); + CombinedMetadata.createDefault()); } // We're all done with merging and emitting elements so can compress the activeWindow state. @@ -590,6 +626,7 @@ private void processElement(Map windowToMergeResult, WindowedValue value.getTimestamp(), StateStyle.DIRECT, value.causedByDrain()); + if (triggerRunner.isClosed(directContext.state())) { // This window has already been closed. droppedDueToClosedWindow.inc(); @@ -604,6 +641,11 @@ private void processElement(Map windowToMergeResult, WindowedValue continue; } + CombinedMetadata metadata = CombinedMetadata.create(value.causedByDrain()); + if (!metadata.equals(CombinedMetadata.createDefault())) { + directContext.state().access(METADATA_TAG).add(metadata); + } + activeWindows.ensureWindowIsActive(window); ReduceFn.ProcessValueContext renamedContext = contextFactory.forValue( @@ -613,6 +655,20 @@ private void processElement(Map windowToMergeResult, WindowedValue StateStyle.RENAMED, value.causedByDrain()); + // TODO: Make sure the NewWindowOptimization does not create unbounded trigger state + // in GlobalWindow + if (useNewWindowOptimization && triggerRunner.isNew(directContext.state())) { + // Blindly clear state to ensure Windmill doesn't do unnecessary reads. + // TODO: Instead of the clears here, we could mark these states as empty locally + // in the state cache and/or explicitly tell that the entries are non-existent via api + reduceFn.clearState(renamedContext); + paneInfoTracker.clear(directContext.state()); + if (!disableWatermarkKnownEmptyOptimization) { + watermarkHold.setKnownEmpty(renamedContext); + } + nonEmptyPanes.clearPane(renamedContext.state()); + } + nonEmptyPanes.recordContent(renamedContext.state()); scheduleGarbageCollectionTimer(directContext); @@ -649,15 +705,15 @@ private class WindowActivation { // garbage collect the window. We'll consider any timer at or after the // end-of-window time to be a signal to garbage collect. public final boolean isGarbageCollection; - public final CausedByDrain causedByDrain; + public final CombinedMetadata combinedMetadata; WindowActivation( ReduceFn.Context directContext, ReduceFn.Context renamedContext, - CausedByDrain causedByDrain) { + CombinedMetadata combinedMetadata) { this.directContext = directContext; this.renamedContext = renamedContext; - this.causedByDrain = causedByDrain; + this.combinedMetadata = combinedMetadata; W window = directContext.window(); // The output watermark is before the end of the window if it is either unknown @@ -742,12 +798,13 @@ public void onTimers(Iterable timers) throws Exception { ReduceFn.Context renamedContext = contextFactory.base(window, StateStyle.RENAMED); WindowActivation windowActivation = - new WindowActivation(directContext, renamedContext, timer.causedByDrain()); + new WindowActivation( + directContext, renamedContext, CombinedMetadata.create(timer.causedByDrain())); windowActivations.put(window, windowActivation); // Perform prefetching of state to determine if the trigger should fire. if (windowActivation.isGarbageCollection) { - triggerRunner.prefetchIsClosed(directContext.state()); + triggerRunner.prefetchFinishedSet(directContext.state()); } else { triggerRunner.prefetchShouldFire(directContext.window(), directContext.state()); } @@ -778,7 +835,7 @@ public void onTimers(Iterable timers) throws Exception { directContext.window(), timerInternals.currentInputWatermarkTime(), timerInternals.currentOutputWatermarkTime(), - windowActivation.causedByDrain); + windowActivation.combinedMetadata.causedByDrain()); boolean windowIsActiveAndOpen = windowActivation.windowIsActiveAndOpen(); if (windowIsActiveAndOpen) { @@ -792,7 +849,7 @@ public void onTimers(Iterable timers) throws Exception { renamedContext, true /* isFinished */, windowActivation.isEndOfWindow, - windowActivation.causedByDrain); + windowActivation.combinedMetadata); checkState(newHold == null, "Hold placed at %s despite isFinished being true.", newHold); } @@ -810,7 +867,7 @@ public void onTimers(Iterable timers) throws Exception { if (windowActivation.windowIsActiveAndOpen() && triggerRunner.shouldFire( directContext.window(), directContext.timers(), directContext.state())) { - emit(directContext, renamedContext, windowActivation.causedByDrain); + emit(directContext, renamedContext, windowActivation.combinedMetadata); } if (windowActivation.isEndOfWindow) { @@ -874,6 +931,7 @@ private void clearAllState( triggerRunner.clearState( directContext.window(), directContext.timers(), directContext.state()); paneInfoTracker.clear(directContext.state()); + directContext.state().access(METADATA_TAG).clear(); } else { // If !windowIsActiveAndOpen then !activeWindows.isActive (1) or triggerRunner.isClosed (2). // For (1), if !activeWindows.isActive then the window must be merging and has been @@ -926,7 +984,7 @@ private void prefetchEmit( ReduceFn.Context directContext, ReduceFn.Context renamedContext) { triggerRunner.prefetchShouldFire(directContext.window(), directContext.state()); - triggerRunner.prefetchIsClosed(directContext.state()); + triggerRunner.prefetchFinishedSet(directContext.state()); prefetchOnTrigger(directContext, renamedContext); } @@ -934,8 +992,9 @@ private void prefetchEmit( private void emit( ReduceFn.Context directContext, ReduceFn.Context renamedContext, - CausedByDrain causedByDrain) + CombinedMetadata metadata) throws Exception { + checkState( triggerRunner.shouldFire( directContext.window(), directContext.timers(), directContext.state())); @@ -950,13 +1009,14 @@ private void emit( // Run onTrigger to produce the actual pane contents. // As a side effect it will clear all element holds, but not necessarily any // end-of-window or garbage collection holds. - onTrigger(directContext, renamedContext, isFinished, false /*isEndOfWindow*/, causedByDrain); + onTrigger(directContext, renamedContext, isFinished, false /*isEndOfWindow*/, metadata); // Now that we've triggered, the pane is empty. nonEmptyPanes.clearPane(renamedContext.state()); // Cleanup buffered data if appropriate if (shouldDiscard) { + directContext.state().access(METADATA_TAG).clear(); // Cleanup flavor C: The user does not want any buffered data to persist between panes. reduceFn.clearState(renamedContext); } @@ -996,12 +1056,14 @@ 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()); } /** * Run the {@link ReduceFn#onTrigger} method and produce any necessary output. * + * @param metadata from timer or default * @return output watermark hold added, or {@literal null} if none. */ private @Nullable Instant onTrigger( @@ -1009,8 +1071,19 @@ private void prefetchOnTrigger( ReduceFn.Context renamedContext, final boolean isFinished, boolean isEndOfWindow, - CausedByDrain causedByDrain) + CombinedMetadata metadata) throws Exception { + CombiningState metadataState = + directContext.state().access(METADATA_TAG); + CombinedMetadata aggregatedMetadata = metadataState.read(); + CombinedMetadata fullyAggregatedMetadata = + CombinedMetadataCombiner.of().addInput(aggregatedMetadata, metadata); + final CausedByDrain aggregatedCausedByDrain = fullyAggregatedMetadata.causedByDrain(); + if (isFinished) { + metadataState.clear(); + } else if (!metadata.equals(CombinedMetadata.createDefault())) { + metadataState.add(metadata); + } // Extract the window hold, and as a side effect clear it. final WatermarkHold.OldAndNewHolds pair = watermarkHold.extractAndRelease(renamedContext, isFinished).read(); @@ -1081,12 +1154,12 @@ private void prefetchOnTrigger( .setValue(KV.of(key, toOutput)) .setTimestamp(outputTimestamp) .setWindows(windows) - .setCausedByDrain(causedByDrain) + .setCausedByDrain(aggregatedCausedByDrain) .setPaneInfo(paneInfo) .setReceiver(outputter) .output(); }, - causedByDrain); + aggregatedCausedByDrain); reduceFn.onTrigger(renamedTriggerContext); } diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java index d5f09522a3b0..90d974653b6a 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SimpleDoFnRunner.java @@ -61,6 +61,7 @@ import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowingStrategy; @@ -229,6 +230,9 @@ public void finishBundle() { } } + @Override + public void finishKey(KeyT key) {} + @Override public void onWindowExpiration(BoundedWindow window, Instant timestamp, KeyT key) { invoker.invokeOnWindowExpiration( @@ -506,6 +510,11 @@ public Instant timestamp() { return elem.getRecordOffset(); } + @Override + public ValueKind valueKind() { + return elem.getValueKind(); + } + public Collection windows() { return elem.getWindows(); } @@ -576,11 +585,32 @@ public Instant timestamp(DoFn doFn) { return timestamp(); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + return currentRecordId(); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + return currentRecordOffset(); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access fire timestamp outside of @OnTimer method."); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { return elem.causedByDrain(); } + @Override + public ValueKind valueKind(DoFn doFn) { + return elem.getValueKind(); + } + @Override public String timerId(DoFn doFn) { throw new UnsupportedOperationException( @@ -622,6 +652,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."); @@ -843,8 +880,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 @@ -857,11 +903,33 @@ public Instant timestamp(DoFn doFn) { return timestamp(); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access record id outside of @ProcessElement method."); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access record offset outside of @ProcessElement method."); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + return fireTimestamp(); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { return causedByDrain; } + @Override + public ValueKind valueKind(DoFn doFn) { + throw new UnsupportedOperationException("ValueKind parameters are not supported."); + } + @Override public String timerId(DoFn doFn) { return timerId; @@ -900,6 +968,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."); @@ -1147,8 +1222,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 @@ -1166,6 +1250,24 @@ public CausedByDrain causedByDrain(DoFn doFn) { throw new UnsupportedOperationException("CausedByDrain parameters are not supported."); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access record id outside of @ProcessElement method."); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access record offset outside of @ProcessElement method."); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access fire timestamp outside of @OnTimer method."); + } + @Override public String timerId(DoFn doFn) { throw new UnsupportedOperationException("Timer parameters are not supported."); @@ -1177,6 +1279,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; @@ -1209,6 +1316,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 3519a74aada7..a750b01963f6 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableParDoViaKeyedWorkItems.java @@ -22,6 +22,7 @@ import com.google.auto.service.AutoService; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.apache.beam.model.pipeline.v1.RunnerApi; import org.apache.beam.sdk.coders.ByteArrayCoder; import org.apache.beam.sdk.coders.Coder; @@ -281,6 +282,7 @@ public static class ProcessFn invoker; + private transient @Nullable Consumer backlogBytesCallback; public ProcessFn( DoFn fn, @@ -323,6 +325,10 @@ public void setProcessElementInvoker( this.processElementInvoker = invoker; } + public void setBacklogBytesCallback(Consumer backlogBytesCallback) { + this.backlogBytesCallback = backlogBytesCallback; + } + public DoFn getFn() { return fn; } @@ -472,7 +478,9 @@ public String getErrorContext() { read.getPaneInfo(), read.getRecordId(), read.getRecordOffset(), - CausedByDrain.CAUSED_BY_DRAIN); + CausedByDrain.CAUSED_BY_DRAIN, + read.getOpenTelemetryContext(), + read.getValueKind()); } elementAndRestriction = KV.of(read, restrictionState.read()); watermarkEstimatorStateT = watermarkEstimatorState.read(); @@ -622,6 +630,9 @@ public String getErrorContext() { } else { holdState.clear(); } + if (backlogBytesCallback != null && result.getBacklogBytes() >= 0) { + backlogBytesCallback.accept(result.getBacklogBytes()); + } } private DoFnInvoker.ArgumentProvider wrapOptionsAsSetup( diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableProcessElementInvoker.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableProcessElementInvoker.java index 1ff66d6e517c..d311806e0a2a 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableProcessElementInvoker.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/SplittableProcessElementInvoker.java @@ -42,6 +42,10 @@ public class Result { private final DoFn.ProcessContinuation continuation; private final @Nullable Instant futureOutputWatermark; private final @Nullable WatermarkEstimatorStateT futureWatermarkEstimatorState; + private final double backlogBytes; + + /* Constant representing an unknown amount of backlog. */ + public static final double BACKLOG_UNKNOWN = -1.0; @SuppressFBWarnings( value = "NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE", @@ -50,12 +54,27 @@ public Result( @Nullable RestrictionT residualRestriction, DoFn.ProcessContinuation continuation, @Nullable Instant futureOutputWatermark, - @Nullable WatermarkEstimatorStateT futureWatermarkEstimatorState) { + @Nullable WatermarkEstimatorStateT futureWatermarkEstimatorState, + double backlogBytes) { checkArgument(continuation != null, "continuation must not be null"); this.continuation = continuation; this.residualRestriction = residualRestriction; this.futureOutputWatermark = futureOutputWatermark; this.futureWatermarkEstimatorState = futureWatermarkEstimatorState; + this.backlogBytes = backlogBytes; + } + + public Result( + @Nullable RestrictionT residualRestriction, + DoFn.ProcessContinuation continuation, + @Nullable Instant futureOutputWatermark, + @Nullable WatermarkEstimatorStateT futureWatermarkEstimatorState) { + this( + residualRestriction, + continuation, + futureOutputWatermark, + futureWatermarkEstimatorState, + BACKLOG_UNKNOWN); } /** @@ -76,6 +95,10 @@ public DoFn.ProcessContinuation getContinuation() { public @Nullable WatermarkEstimatorStateT getFutureWatermarkEstimatorState() { return futureWatermarkEstimatorState; } + + public double getBacklogBytes() { + return backlogBytes; + } } /** diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/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/triggers/FinishedTriggersBitSet.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/FinishedTriggersBitSet.java index 7eebb4474c6c..967e1ef43f08 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/FinishedTriggersBitSet.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/FinishedTriggersBitSet.java @@ -18,6 +18,7 @@ package org.apache.beam.runners.core.triggers; import java.util.BitSet; +import org.checkerframework.checker.nullness.qual.Nullable; /** A {@link FinishedTriggers} implementation based on an underlying {@link BitSet}. */ public class FinishedTriggersBitSet implements FinishedTriggers { @@ -60,4 +61,17 @@ public void clearRecursively(ExecutableTriggerStateMachine trigger) { public FinishedTriggersBitSet copy() { return new FinishedTriggersBitSet((BitSet) bitSet.clone()); } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof FinishedTriggersBitSet)) { + return false; + } + return bitSet.equals(((FinishedTriggersBitSet) obj).bitSet); + } + + @Override + public int hashCode() { + return bitSet.hashCode(); + } } diff --git a/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java index e3791821b728..90daa5ac75b2 100644 --- a/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java +++ b/runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java @@ -63,13 +63,16 @@ public class TriggerStateMachineRunner { private final ExecutableTriggerStateMachine rootTrigger; private final TriggerStateMachineContextFactory contextFactory; + private final boolean useNewWindowOptimization; public TriggerStateMachineRunner( ExecutableTriggerStateMachine rootTrigger, - TriggerStateMachineContextFactory contextFactory) { + TriggerStateMachineContextFactory contextFactory, + boolean useNewWindowOptimization) { checkState(rootTrigger.getTriggerIndex() == 0); this.rootTrigger = rootTrigger; this.contextFactory = contextFactory; + this.useNewWindowOptimization = useNewWindowOptimization; } private FinishedTriggersBitSet readFinishedBits(ValueState state) { @@ -81,9 +84,11 @@ private FinishedTriggersBitSet readFinishedBits(ValueState state) { } @Nullable BitSet bitSet = state.read(); - return bitSet == null - ? FinishedTriggersBitSet.emptyWithCapacity(rootTrigger.getFirstIndexAfterSubtree()) - : FinishedTriggersBitSet.fromBitSet(bitSet); + if (bitSet == null) { + return FinishedTriggersBitSet.emptyWithCapacity(rootTrigger.getFirstIndexAfterSubtree()); + } + + return FinishedTriggersBitSet.fromBitSet(bitSet); } private void clearFinishedBits(ValueState state) { @@ -99,19 +104,29 @@ public boolean isClosed(StateAccessor state) { return readFinishedBits(state.access(FINISHED_BITS_TAG)).isFinished(rootTrigger); } - public void prefetchIsClosed(StateAccessor state) { + /** Return true if the window is new (no trigger state has ever been persisted). */ + public boolean isNew(StateAccessor state) { + return isFinishedSetNeeded() && state.access(FINISHED_BITS_TAG).read() == null; + } + + @VisibleForTesting + public BitSet getFinishedBits(StateAccessor state) { + return readFinishedBits(state.access(FINISHED_BITS_TAG)).getBitSet(); + } + + public void prefetchFinishedSet(StateAccessor state) { if (isFinishedSetNeeded()) { state.access(FINISHED_BITS_TAG).readLater(); } } public void prefetchForValue(W window, StateAccessor state) { - prefetchIsClosed(state); + prefetchFinishedSet(state); rootTrigger.invokePrefetchOnElement(contextFactory.createPrefetchContext(window, rootTrigger)); } public void prefetchShouldFire(W window, StateAccessor state) { - prefetchIsClosed(state); + prefetchFinishedSet(state); rootTrigger.invokePrefetchShouldFire(contextFactory.createPrefetchContext(window, rootTrigger)); } @@ -180,13 +195,23 @@ public void onFire(W window, Timers timers, StateAccessor state) throws Excep persistFinishedSet(state, finishedSet); } - private void persistFinishedSet( - StateAccessor state, FinishedTriggersBitSet modifiedFinishedSet) { + @VisibleForTesting + void persistFinishedSet(StateAccessor state, FinishedTriggersBitSet modifiedFinishedSet) { if (!isFinishedSetNeeded()) { return; } ValueState finishedSetState = state.access(FINISHED_BITS_TAG); + + if (useNewWindowOptimization) { + if (finishedSetState.read() == null + || !readFinishedBits(finishedSetState).equals(modifiedFinishedSet)) { + // Write a value even if the bitset was empty + finishedSetState.write(modifiedFinishedSet.getBitSet()); + } + return; + } + if (!readFinishedBits(finishedSetState).equals(modifiedFinishedSet)) { if (modifiedFinishedSet.getBitSet().isEmpty()) { finishedSetState.clear(); diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/CombinedMetadataTest.java b/runners/core-java/src/test/java/org/apache/beam/runners/core/CombinedMetadataTest.java new file mode 100644 index 000000000000..29d3efe53b03 --- /dev/null +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/CombinedMetadataTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.core; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import org.apache.beam.sdk.values.CausedByDrain; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CombinedMetadata}. */ +@RunWith(JUnit4.class) +public class CombinedMetadataTest { + + @Test + public void testCoderEncodeDecode() throws Exception { + CombinedMetadata metadata = CombinedMetadata.create(CausedByDrain.CAUSED_BY_DRAIN); + CombinedMetadata.Coder coder = CombinedMetadata.Coder.of(); + + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + coder.encode(metadata, outStream); + + ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray()); + CombinedMetadata decoded = coder.decode(inStream); + + assertEquals(metadata, decoded); + } + + @Test + public void testCoderDecodeEOF() throws Exception { + CombinedMetadata.Coder coder = CombinedMetadata.Coder.of(); + + // Stream with no data (EOF immediately) + ByteArrayInputStream inStream = new ByteArrayInputStream(new byte[0]); + CombinedMetadata decoded = coder.decode(inStream); + + assertEquals(CombinedMetadata.createDefault(), decoded); + } + + @Test + public void testCoderDecodeEmptyMessage() throws Exception { + CombinedMetadata.Coder coder = CombinedMetadata.Coder.of(); + + // Stream with a 0-length delimited message + ByteArrayInputStream inStream = new ByteArrayInputStream(new byte[] {0}); + CombinedMetadata decoded = coder.decode(inStream); + + // ElementMetadata.parseDelimitedFrom(0-length) should yield default proto with NOT_DRAINING + // which translates to CausedByDrain.NORMAL, which is the default! + assertEquals(CombinedMetadata.createDefault(), decoded); + } +} diff --git a/runners/core-java/src/test/java/org/apache/beam/runners/core/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 43b6a3cb0cb0..fe2c0a253467 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java @@ -129,6 +129,19 @@ ReduceFnTester, W> nonCombining( NullSideInputReader.empty()); } + public static + ReduceFnTester, W> nonCombining( + WindowingStrategy windowingStrategy, PipelineOptions options) throws Exception { + return new ReduceFnTester<>( + windowingStrategy, + TriggerStateMachines.stateMachineForTrigger( + TriggerTranslation.toProto(windowingStrategy.getTrigger())), + SystemReduceFn.buffering(VarIntCoder.of()), + IterableCoder.of(VarIntCoder.of()), + options, + NullSideInputReader.empty()); + } + /** * Creates a {@link ReduceFnTester} for the given {@link WindowingStrategy} and {@link * TriggerStateMachine}, for mocking the interactions between {@link ReduceFnRunner} and the @@ -318,7 +331,7 @@ public boolean hasNoActiveWindows() { public final void assertHasOnlyGlobalAndFinishedSetsFor(W... expectedWindows) { assertHasOnlyGlobalAndAllowedTags( ImmutableSet.copyOf(expectedWindows), - ImmutableSet.of(TriggerStateMachineRunner.FINISHED_BITS_TAG)); + ImmutableSet.of(TriggerStateMachineRunner.FINISHED_BITS_TAG, ReduceFnRunner.METADATA_TAG)); } @SafeVarargs @@ -331,7 +344,8 @@ public final void assertHasOnlyGlobalAndStateFor(W... expectedWindows) { PaneInfoTracker.PANE_INFO_TAG, WatermarkHold.watermarkHoldTagForTimestampCombiner( objectStrategy.getTimestampCombiner()), - WatermarkHold.EXTRA_HOLD_TAG)); + WatermarkHold.EXTRA_HOLD_TAG, + ReduceFnRunner.METADATA_TAG)); } @SafeVarargs diff --git a/runners/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..381e41c98705 100644 --- a/runners/core-java/src/test/java/org/apache/beam/runners/core/SplittableParDoProcessFnTest.java +++ b/runners/core-java/src/test/java/org/apache/beam/runners/core/SplittableParDoProcessFnTest.java @@ -140,6 +140,8 @@ private static class ProcessFnTester< private InMemoryTimerInternals timerInternals; private TestInMemoryStateInternals stateInternals; private InMemoryBundleFinalizer bundleFinalizer; + private final ProcessFn + processFn; ProcessFnTester( Instant currentProcessingTime, @@ -154,15 +156,14 @@ private static class ProcessFnTester< // encode IntervalWindow's because that's what all tests here use. WindowingStrategy windowingStrategy = (WindowingStrategy) WindowingStrategy.of(FixedWindows.of(Duration.standardSeconds(1))); - final ProcessFn - processFn = - new ProcessFn<>( - fn, - inputCoder, - restrictionCoder, - watermarkEstimatorStateCoder, - windowingStrategy, - Collections.emptyMap()); + this.processFn = + new ProcessFn<>( + fn, + inputCoder, + restrictionCoder, + watermarkEstimatorStateCoder, + windowingStrategy, + Collections.emptyMap()); this.tester = DoFnTester.of(processFn); this.timerInternals = new InMemoryTimerInternals(); this.stateInternals = new TestInMemoryStateInternals<>("dummy"); @@ -386,6 +387,61 @@ public WatermarkEstimators.Manual newWatermarkEstimator( } } + private static class GetSizeFn extends DoFn { + @ProcessElement + public ProcessContinuation process( + ProcessContext c, RestrictionTracker tracker) { + for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) { + c.output(String.valueOf(i)); + if (i == 2) { + return resume(); + } + } + return stop(); + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction() { + return new OffsetRange(0, 10); + } + + @NewTracker + public OffsetRangeTracker newTracker(@Restriction OffsetRange range) { + return new OffsetRangeTracker(range); + } + + @GetSize + public double getSize(@Restriction OffsetRange range) { + return range.getTo() - range.getFrom(); + } + } + + // Used to check that backlog can be computed from the restriction tracker if GetSize is not + // defined. + private static class SdfWithoutGetSize extends DoFn { + @ProcessElement + public ProcessContinuation process( + ProcessContext c, RestrictionTracker tracker) { + for (long i = tracker.currentRestriction().getFrom(); tracker.tryClaim(i); ++i) { + c.output(String.valueOf(i)); + if (i == 2) { + return resume(); + } + } + return stop(); + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction() { + return new OffsetRange(0, 10); + } + + @NewTracker + public OffsetRangeTracker newTracker(@Restriction OffsetRange range) { + return new OffsetRangeTracker(range); + } + } + @Test public void testDrains() throws Exception { DoFn fn = new WatermarkUpdateFn(); @@ -684,4 +740,54 @@ public void testInvokesLifecycleMethods() throws Exception { tester.startElement(42, new SomeRestriction()); } } + + @Test + public void testReportsBacklog() throws Exception { + DoFn fn = new GetSizeFn(); + Instant base = Instant.now(); + final List backlogs = new ArrayList<>(); + + try (ProcessFnTester tester = + new ProcessFnTester<>( + base, + fn, + BigEndianIntegerCoder.of(), + SerializableCoder.of(OffsetRange.class), + VoidCoder.of(), + MAX_OUTPUTS_PER_BUNDLE, + MAX_BUNDLE_DURATION)) { + tester.processFn.setBacklogBytesCallback(backlogs::add); + + tester.startElement(42, new OffsetRange(0, 10)); + // First call outputs 0, 1, and 2, and then resumes. + // The residual range should be [3, 10), so size is 7. + assertEquals(1, backlogs.size()); + assertEquals(7.0, backlogs.get(0), 0.001); + } + } + + @Test + public void testReportsBacklogWithoutGetSize() throws Exception { + DoFn fn = new SdfWithoutGetSize(); + Instant base = Instant.now(); + final List backlogs = new ArrayList<>(); + + try (ProcessFnTester tester = + new ProcessFnTester<>( + base, + fn, + BigEndianIntegerCoder.of(), + SerializableCoder.of(OffsetRange.class), + VoidCoder.of(), + MAX_OUTPUTS_PER_BUNDLE, + MAX_BUNDLE_DURATION)) { + tester.processFn.setBacklogBytesCallback(backlogs::add); + + tester.startElement(42, new OffsetRange(0, 10)); + // First call outputs 0, 1, and 2, and then resumes. + // The residual range should be [3, 10), so size is 7. + assertEquals(1, backlogs.size()); + assertEquals(7.0, backlogs.get(0), 0.001); + } + } } diff --git a/runners/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/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/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/2.0/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java index 0d48526e1d0a..4a1c1cb0c5e1 100644 --- a/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java +++ b/runners/flink/2.0/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java @@ -36,6 +36,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.net.HostAndPort; import org.apache.flink.api.common.ExecutionConfig; import org.apache.flink.api.common.RuntimeExecutionMode; +import org.apache.flink.api.common.serialization.SerializerConfigImpl; import org.apache.flink.configuration.CheckpointingOptions; import org.apache.flink.configuration.Configuration; import org.apache.flink.configuration.CoreOptions; @@ -270,6 +271,7 @@ public static StreamExecutionEnvironment createStreamExecutionEnvironment( flinkStreamEnv.getConfig().setAutoWatermarkInterval(options.getAutoWatermarkInterval()); } configureWebUIOptions(flinkStreamEnv.getConfig(), options.as(PipelineOptions.class)); + configureCustomKryoSerializers(flinkStreamEnv.getConfig()); return flinkStreamEnv; } @@ -294,6 +296,14 @@ private static void configureWebUIOptions( } } + private static void configureCustomKryoSerializers(ExecutionConfig config) { + SerializerConfigImpl serializerConfig = (SerializerConfigImpl) config.getSerializerConfig(); + // Force Beam schema to use JavaSerializer to fix serialization involving ImmutableMap + serializerConfig.registerTypeWithKryoSerializer( + org.apache.beam.sdk.schemas.Schema.class, + com.esotericsoftware.kryo.serializers.JavaSerializer.class); + } + private static class GlobalJobParametersImpl extends ExecutionConfig.GlobalJobParameters { private final Map jobOptions; diff --git a/runners/flink/2.1/build.gradle b/runners/flink/2.1/build.gradle new file mode 100644 index 000000000000..1e0d565b50d9 --- /dev/null +++ b/runners/flink/2.1/build.gradle @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +project.ext { + flink_major = '2.1' + flink_version = '2.1.3' + excluded_files = [ + 'main': [ + // Used by DataSet API only + "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapter.java", + "org/apache/beam/runners/flink/FlinkBatchPipelineTranslator.java", + "org/apache/beam/runners/flink/FlinkBatchPortablePipelineTranslator.java", + "org/apache/beam/runners/flink/FlinkBatchTransformTranslators.java", + "org/apache/beam/runners/flink/translation/functions/FlinkNonMergingReduceFunction.java", + // Moved to org.apache.flink.runtime.state.StateBackendFactory + "org/apache/beam/runners/flink/FlinkStateBackendFactory.java", + ], + 'test': [ + // Used by DataSet API only + "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapterTest.java", + "org/apache/beam/runners/flink/batch/NonMergingGroupByKeyTest.java", + "org/apache/beam/runners/flink/batch/ReshuffleTest.java", + ] + ] +} + +// Load the main build script which contains all build logic. +apply from: "../flink_runner.gradle" + +// Flink 2.1 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java +// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} + 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.18/job-server/build.gradle b/runners/flink/2.1/job-server/build.gradle similarity index 80% rename from runners/flink/1.18/job-server/build.gradle rename to runners/flink/2.1/job-server/build.gradle index e70fdcc0c581..0910fef11208 100644 --- a/runners/flink/1.18/job-server/build.gradle +++ b/runners/flink/2.1/job-server/build.gradle @@ -24,8 +24,15 @@ 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.1-job-server' } // Load the main build script which contains all build logic. apply from: "$basePath/flink_job_server.gradle" + +// Flink 2.1 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java +// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} + diff --git a/runners/flink/2.2/build.gradle b/runners/flink/2.2/build.gradle new file mode 100644 index 000000000000..0321dcf42d10 --- /dev/null +++ b/runners/flink/2.2/build.gradle @@ -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. + */ + +// Flink 2.2 API changes addressed in this module's source overrides: +// - CheckpointingMode moved (Flink 2.2): org.apache.flink.streaming.api.CheckpointingMode +// is now @Deprecated; canonical path is org.apache.flink.core.execution.CheckpointingMode. +// FlinkExecutionEnvironments.java, DoFnOperator.java, and FlinkPipelineOptionsTest.java +// are overridden here to use the non-deprecated import. +// +// Flink 2.2 breaking changes that are SQL/cluster-layer only (no runner code change needed): +// - TLS cipher suite default changed (FLINK-39022): JDK 11.0.30+/17.0.18+/21.0.10+/24+ +// disabled TLS_RSA_* ciphers; new default uses ECDHE suites. Verify cipher support +// on TLS-enabled clusters before upgrading. +// - StreamingMultiJoinOperator state format change (FLINK-38209): savepoints from Flink 2.1 +// with table.optimizer.multi-join.enabled=true are NOT compatible with 2.2. +// - SQL row NOT NULL now enforced (FLINK-38181): use table.legacy-nested-row-nullability +// to restore prior (silent-ignore) behavior if needed. + +project.ext { + flink_major = '2.2' + flink_version = '2.2.1' + excluded_files = [ + 'main': [ + // Used by DataSet API only + "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapter.java", + "org/apache/beam/runners/flink/FlinkBatchPipelineTranslator.java", + "org/apache/beam/runners/flink/FlinkBatchPortablePipelineTranslator.java", + "org/apache/beam/runners/flink/FlinkBatchTransformTranslators.java", + "org/apache/beam/runners/flink/translation/functions/FlinkNonMergingReduceFunction.java", + // Moved to org.apache.flink.runtime.state.StateBackendFactory + "org/apache/beam/runners/flink/FlinkStateBackendFactory.java", + ], + 'test': [ + // Used by DataSet API only + "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapterTest.java", + "org/apache/beam/runners/flink/batch/NonMergingGroupByKeyTest.java", + "org/apache/beam/runners/flink/batch/ReshuffleTest.java", + ] + ] +} + +// Load the main build script which contains all build logic. +apply from: "../flink_runner.gradle" + +// Flink 2.2 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java +// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} + 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.17/build.gradle b/runners/flink/2.2/job-server/build.gradle similarity index 57% rename from runners/flink/1.17/build.gradle rename to runners/flink/2.2/job-server/build.gradle index ae69b879eba9..f116f5a1dcbf 100644 --- a/runners/flink/1.17/build.gradle +++ b/runners/flink/2.2/job-server/build.gradle @@ -16,10 +16,23 @@ * limitations under the License. */ +def basePath = '../../job-server' + project.ext { - flink_major = '1.17' - flink_version = '1.17.0' + // Look for the source code in the parent module + main_source_dirs = ["$basePath/src/main/java"] + test_source_dirs = ["$basePath/src/test/java"] + main_resources_dirs = ["$basePath/src/main/resources"] + test_resources_dirs = ["$basePath/src/test/resources"] + archives_base_name = 'beam-runners-flink-2.2-job-server' } // Load the main build script which contains all build logic. -apply from: "../flink_runner.gradle" +apply from: "$basePath/flink_job_server.gradle" + +// Flink 2.2 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java +// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict +configurations.all { + resolveCapabilitiesConflict(it, 'org.lz4:lz4-java', 'at.yawk.lz4') +} + 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 98e7d547b4d3..837561ec71b7 100644 --- a/runners/flink/flink_runner.gradle +++ b/runners/flink/flink_runner.gradle @@ -230,6 +230,13 @@ dependencies { // configuration (https://issues.apache.org/jira/browse/BEAM-11732). permitUnusedDeclared "org.apache.flink:flink-clients:$flink_version" + // align with Flink's kryo version (groupId changed) + if (flink_major.startsWith('2')) { + implementation "com.esotericsoftware:kryo:5.6.2" + } else { + implementation "com.esotericsoftware.kryo:kryo:2.24.0" + } + implementation "org.apache.flink:flink-streaming-java:$flink_version" testImplementation "org.apache.flink:flink-statebackend-rocksdb:$flink_version" testImplementation "org.apache.flink:flink-streaming-java:$flink_version:tests" diff --git a/runners/flink/job-server/flink_job_server.gradle b/runners/flink/job-server/flink_job_server.gradle index b85f8fc98aaa..a6abca5e8586 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' @@ -248,10 +248,26 @@ def setupTask = project.tasks.register("flinkJobServerSetup", Exec) { def flinkJobServerJar = shadowJar.archivePath def flinkDir = project.project(":runners:flink").projectDir def additionalArgs = "" - if (project.hasProperty('flinkConfDir')) + + if (project.hasProperty('flinkConfDir')) { additionalArgs += " --flink-conf-dir=${project.property('flinkConfDir')}" - else + } + else if (isFlink2) { + def flinkConfDir = "$flinkDir/2.0/src/test/resources" + additionalArgs += "--flink-conf-dir=${project.buildDir}/flink-conf" + + doFirst { + copy { + from "$flinkDir/2.0/src/test/resources/flink-test-config.yaml" + into "${project.buildDir}/flink-conf" + + // Rename the file during the copy process + rename 'flink-test-config.yaml', 'config.yaml' + } + } + } else { additionalArgs += "--flink-conf-dir=$flinkDir/src/test/resources" + } executable 'sh' args '-c', "$pythonDir/scripts/run_job_server.sh stop --group_id ${project.name} && $pythonDir/scripts/run_job_server.sh start --group_id ${project.name} --job_port ${jobPort} --artifact_port ${artifactPort} --job_server_jar ${flinkJobServerJar} --additional_args \"${additionalArgs}\"" diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkDetachedRunnerResult.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkDetachedRunnerResult.java index f7d82065b658..b26e865526dd 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkDetachedRunnerResult.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkDetachedRunnerResult.java @@ -18,6 +18,7 @@ package org.apache.beam.runners.flink; import java.io.IOException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import org.apache.beam.runners.core.metrics.MetricsContainerStepMap; import org.apache.beam.runners.flink.metrics.FlinkMetricContainer; @@ -25,6 +26,8 @@ import org.apache.beam.sdk.metrics.MetricResults; import org.apache.flink.api.common.JobStatus; import org.apache.flink.core.execution.JobClient; +import org.apache.flink.core.execution.SavepointFormatType; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,6 +42,7 @@ public class FlinkDetachedRunnerResult implements PipelineResult { private JobClient jobClient; private int jobCheckIntervalInSecs; + private volatile @Nullable CompletableFuture drainSavepointFuture; FlinkDetachedRunnerResult(JobClient jobClient, int jobCheckIntervalInSecs) { this.jobClient = jobClient; @@ -47,10 +51,25 @@ public class FlinkDetachedRunnerResult implements PipelineResult { @Override public State getState() { + CompletableFuture drainFuture = drainSavepointFuture; + if (drainFuture != null) { + try { + return getDrainState(drainFuture); + } catch (IOException e) { + LOG.warn("Failed to drain Flink job. Querying Flink job state instead.", e); + } + } + return getFlinkJobState(); + } + + private State getFlinkJobState() { try { return toBeamJobState(jobClient.getJobStatus().get()); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("Fail to get flink job state", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Failed to get Flink job state", e); + } catch (ExecutionException e) { + throw new RuntimeException("Failed to get Flink job state", e); } } @@ -66,7 +85,11 @@ private MetricsContainerStepMap getMetricsContainerStepMap() { .getAccumulators() .get() .getOrDefault(FlinkMetricContainer.ACCUMULATOR_NAME, new MetricsContainerStepMap()); - } catch (InterruptedException | ExecutionException e) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warn("Fail to get flink job accumulators", e); + return new MetricsContainerStepMap(); + } catch (ExecutionException e) { LOG.warn("Fail to get flink job accumulators", e); return new MetricsContainerStepMap(); } @@ -76,12 +99,40 @@ private MetricsContainerStepMap getMetricsContainerStepMap() { public State cancel() throws IOException { try { this.jobClient.cancel().get(); - } catch (InterruptedException | ExecutionException e) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Fail to cancel flink job", e); + } catch (ExecutionException e) { throw new RuntimeException("Fail to cancel flink job", e); } return getState(); } + @Override + public synchronized State drain() throws IOException { + CompletableFuture drainFuture = drainSavepointFuture; + if (drainFuture == null || drainFuture.isCompletedExceptionally()) { + drainFuture = this.jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT); + drainSavepointFuture = drainFuture; + } + return getDrainState(drainFuture); + } + + private State getDrainState(CompletableFuture drainFuture) throws IOException { + if (!drainFuture.isDone()) { + return State.RUNNING; + } + try { + drainFuture.get(); + return State.DONE; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Failed to drain Flink job", e); + } catch (ExecutionException e) { + throw new IOException("Failed to drain Flink job", e.getCause()); + } + } + @Override public State waitUntilFinish() { return waitUntilFinish(Duration.millis(Long.MAX_VALUE)); @@ -100,6 +151,7 @@ public State waitUntilFinish(Duration duration) { try { Thread.sleep(jobCheckIntervalInSecs * 1000L); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new RuntimeException(e); } } diff --git a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java index a0e5908cc99d..de92dd94605a 100644 --- a/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java +++ b/runners/flink/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java @@ -298,6 +298,7 @@ public static StreamExecutionEnvironment createStreamExecutionEnvironment( configureStateBackend(options, flinkStreamEnv); configureWebUIOptions(flinkStreamEnv.getConfig(), options.as(PipelineOptions.class)); + configureCustomKryoSerializers(flinkStreamEnv.getConfig()); return flinkStreamEnv; } @@ -322,6 +323,13 @@ private static void configureWebUIOptions( } } + private static void configureCustomKryoSerializers(ExecutionConfig config) { + // Force Beam schema to use JavaSerializer to fix serialization involving ImmutableMap + config.registerTypeWithKryoSerializer( + org.apache.beam.sdk.schemas.Schema.class, + com.esotericsoftware.kryo.serializers.JavaSerializer.class); + } + private static class GlobalJobParametersImpl extends ExecutionConfig.GlobalJobParameters { private final Map jobOptions; diff --git a/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/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/wrappers/streaming/ExecutableStageDoFnOperator.java b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/ExecutableStageDoFnOperator.java index 4ebb359fceae..f7bc62a8ec5a 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 @@ -1064,6 +1064,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/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..908d940f5efb 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkRunnerResultTest.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/FlinkRunnerResultTest.java @@ -19,9 +19,20 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.io.IOException; import java.util.Collections; +import java.util.concurrent.CompletableFuture; import org.apache.beam.sdk.PipelineResult; +import org.apache.flink.api.common.JobStatus; +import org.apache.flink.core.execution.JobClient; +import org.apache.flink.core.execution.SavepointFormatType; import org.joda.time.Duration; import org.junit.Test; @@ -47,4 +58,85 @@ public void testCancelDoesNotThrowAnException() { result.cancel(); assertThat(result.getState(), is(PipelineResult.State.DONE)); } + + @Test + public void testDrainDoneResultDoesNotThrowAnException() throws Exception { + FlinkRunnerResult result = new FlinkRunnerResult(Collections.emptyMap(), 100); + assertThat(result.drain(), is(PipelineResult.State.DONE)); + } + + @Test + public void testDetachedDrainReturnsRunningThenDone() throws Exception { + JobClient jobClient = mock(JobClient.class); + CompletableFuture drainFuture = new CompletableFuture<>(); + when(jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT)) + .thenReturn(drainFuture); + FlinkDetachedRunnerResult result = new FlinkDetachedRunnerResult(jobClient, 1); + + assertThat(result.drain(), is(PipelineResult.State.RUNNING)); + assertThat(result.getState(), is(PipelineResult.State.RUNNING)); + + drainFuture.complete("savepoint"); + assertThat(result.getState(), is(PipelineResult.State.DONE)); + verify(jobClient).stopWithSavepoint(true, null, SavepointFormatType.DEFAULT); + } + + @Test + public void testDetachedDrainFailureThrowsIOException() throws Exception { + JobClient jobClient = mock(JobClient.class); + CompletableFuture drainFuture = new CompletableFuture<>(); + RuntimeException failure = new RuntimeException("savepoint failed"); + drainFuture.completeExceptionally(failure); + when(jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT)) + .thenReturn(drainFuture); + FlinkDetachedRunnerResult result = new FlinkDetachedRunnerResult(jobClient, 1); + + try { + result.drain(); + fail("Expected IOException"); + } catch (IOException e) { + assertThat(e.getMessage(), is("Failed to drain Flink job")); + assertSame(failure, e.getCause()); + } + } + + @Test + public void testDetachedGetStateFallsBackAfterDrainFailure() throws Exception { + JobClient jobClient = mock(JobClient.class); + CompletableFuture drainFuture = new CompletableFuture<>(); + drainFuture.completeExceptionally(new RuntimeException("savepoint failed")); + when(jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT)) + .thenReturn(drainFuture); + when(jobClient.getJobStatus()).thenReturn(CompletableFuture.completedFuture(JobStatus.RUNNING)); + FlinkDetachedRunnerResult result = new FlinkDetachedRunnerResult(jobClient, 1); + + try { + result.drain(); + fail("Expected IOException"); + } catch (IOException expected) { + assertThat(result.getState(), is(PipelineResult.State.RUNNING)); + } + } + + @Test + public void testDetachedDrainRetriesAfterFailure() throws Exception { + JobClient jobClient = mock(JobClient.class); + CompletableFuture failedDrainFuture = new CompletableFuture<>(); + failedDrainFuture.completeExceptionally(new RuntimeException("savepoint failed")); + CompletableFuture retryDrainFuture = new CompletableFuture<>(); + when(jobClient.stopWithSavepoint(true, null, SavepointFormatType.DEFAULT)) + .thenReturn(failedDrainFuture, retryDrainFuture); + FlinkDetachedRunnerResult result = new FlinkDetachedRunnerResult(jobClient, 1); + + try { + result.drain(); + fail("Expected IOException"); + } catch (IOException expected) { + assertThat(result.drain(), is(PipelineResult.State.RUNNING)); + } + + retryDrainFuture.complete("savepoint"); + assertThat(result.getState(), is(PipelineResult.State.DONE)); + verify(jobClient, times(2)).stopWithSavepoint(true, null, SavepointFormatType.DEFAULT); + } } diff --git a/runners/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/WindowDoFnOperatorTest.java b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperatorTest.java index 3bee828f23dd..6287ad119ac2 100644 --- a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperatorTest.java +++ b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/WindowDoFnOperatorTest.java @@ -156,6 +156,11 @@ public void testTimerCleanupOfPendingTimerList() throws Exception { // Note that the following is 1 because the state is key-partitioned assertThat(Iterables.size(timerInternals.pendingTimersById.keys()), is(1)); + // Expected 6 state entries: + // - 2 entries for user buffer state ("buf") - one per key + // - 2 entries for watermark hold state ("hold") - one per key + // - 2 entries for non-empty panes count state ("count") - one per key + // - additional 2 entries for "combinedMetadata" state if non default metadata will be added assertThat(testHarness.numKeyedStateEntries(), is(6)); // close bundle testHarness.setProcessingTime( @@ -169,6 +174,11 @@ public void testTimerCleanupOfPendingTimerList() throws Exception { // Note that the following is zero because we only the first key is active assertThat(Iterables.size(timerInternals.pendingTimersById.keys()), is(0)); + // Expected 4 state entries remaining for the second key (which is still active): + // - 1 entry for user buffer state ("buf") + // - 1 entry for watermark hold state ("hold") + // - 1 entry for non-empty panes count state ("count") + // - 1 entry for "combinedMetadata" state if non default metadata will be added assertThat(testHarness.numKeyedStateEntries(), is(3)); // close bundle 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 ef2ce36dc5ae..3d1f8d78777c 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -52,8 +52,8 @@ evaluationDependsOn(":sdks:java:container:java11") ext.dataflowLegacyEnvironmentMajorVersion = '8' ext.dataflowFnapiEnvironmentMajorVersion = '8' -ext.dataflowLegacyContainerVersion = 'beam-master-20260424' -ext.dataflowFnapiContainerVersion = 'beam-master-20260424' +ext.dataflowLegacyContainerVersion = 'beam-master-20260624' +ext.dataflowFnapiContainerVersion = 'beam-master-20260624' ext.dataflowContainerBaseRepository = 'gcr.io/cloud-dataflow/v1beta3' processResources { @@ -112,6 +112,7 @@ dependencies { permitUnusedDeclared library.java.google_http_client_gson // BEAM-11761 implementation library.java.google_cloud_logging permitUnusedDeclared library.java.google_cloud_logging // BEAM-11761 + implementation library.java.opentelemetry_context implementation library.java.hamcrest implementation library.java.jackson_annotations implementation library.java.jackson_core @@ -166,6 +167,7 @@ def legacyPipelineOptions = [ "--region=${gcpRegion}", "--tempRoot=${dataflowValidatesTempRoot}", "--dataflowWorkerJar=${dataflowLegacyWorkerJar}", + "--usePublicIps=false", "--experiments=enable_lineage" ] @@ -183,6 +185,7 @@ def runnerV2CommonPipelineOptions = [ "--tempRoot=${dataflowValidatesTempRoot}", "--experiments=use_unified_worker,use_runner_v2", "--firestoreDb=${firestoreDb}", + "--usePublicIps=false", "--experiments=enable_lineage" ] @@ -430,7 +433,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}" @@ -470,7 +473,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', @@ -483,6 +486,9 @@ def validatesRunnerStreamingConfig = [ 'org.apache.beam.sdk.transforms.ParDoLifecycleTest.testTeardownCalledAfterExceptionInSetupStateful', 'org.apache.beam.sdk.transforms.ParDoLifecycleTest.testTeardownCalledAfterExceptionInStartBundle', 'org.apache.beam.sdk.transforms.ParDoLifecycleTest.testTeardownCalledAfterExceptionInStartBundleStateful', + // Bundle finalizer tests are flaky https://github.com/apache/beam/issues/38710 + 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testBundleFinalizationOccursOnBoundedSplittableDoFn', + 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testBundleFinalizationOccursOnUnboundedSplittableDoFn', ] ] @@ -569,7 +575,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, @@ -609,7 +615,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'], @@ -879,7 +885,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 d4cd1af386e3..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 @@ -20,6 +20,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opentelemetry.context.Context; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -73,6 +74,7 @@ import org.apache.beam.sdk.values.PCollectionView; import org.apache.beam.sdk.values.TupleTag; import org.apache.beam.sdk.values.TupleTagList; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; @@ -1379,6 +1381,11 @@ public T getValue() { return value; } + @Override + public ValueKind getValueKind() { + return ValueKind.INSERT; + } + @Override public CausedByDrain causedByDrain() { return CausedByDrain.NORMAL; @@ -1404,6 +1411,11 @@ public PaneInfo getPaneInfo() { return null; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return null; + } + @Override public @Nullable Long getRecordOffset() { return null; diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java index 400f161dee2f..0d7e5eaf68d9 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineJob.java @@ -432,55 +432,69 @@ private Exception processJobMessages( } private AtomicReference> cancelState = new AtomicReference<>(); + private AtomicReference> drainState = new AtomicReference<>(); @SuppressWarnings("Slf4jFormatShouldBeConst") @Override public State cancel() throws IOException { - // Enforce that a cancel() call on the job is done at most once - as - // a workaround for Dataflow service's current bugs with multiple - // cancellation, where it may sometimes return an error when cancelling - // a job that was already cancelled, but still report the job state as - // RUNNING. - // To partially work around these issues, we absorb duplicate cancel() - // calls. This, of course, doesn't address the case when the job terminates - // externally almost concurrently to calling cancel(), but at least it - // makes it possible to safely call cancel() multiple times and from - // multiple threads in one program. - FutureTask tentativeCancelTask = + return requestJobState(cancelState, "JOB_STATE_CANCELLED", "cancel", "Cancel"); + } + + @Override + public State drain() throws IOException { + return requestJobState(drainState, "JOB_STATE_DRAINED", "drain", "Drain"); + } + + @SuppressWarnings("Slf4jFormatShouldBeConst") + private State requestJobState( + AtomicReference> requestedState, + String dataflowRequestedState, + String action, + String capitalizedAction) + throws IOException { + // Enforce that a lifecycle request on the job is done at most once. This preserves the + // existing cancel() behavior and keeps duplicate drain() calls idempotent from one client. + FutureTask tentativeTask = new FutureTask<>( () -> { Job content = new Job(); content.setProjectId(getProjectId()); String currentJobId = getJobId(); content.setId(currentJobId); - content.setRequestedState("JOB_STATE_CANCELLED"); + content.setRequestedState(dataflowRequestedState); try { Job job = dataflowClient.updateJob(currentJobId, content); return MonitoringUtil.toState(job.getCurrentState()); } catch (IOException e) { State state = getState(); + String message = e.getMessage(); if (state.isTerminal()) { - LOG.warn("Cancel failed because job is already terminated. State is {}", state); + LOG.warn( + "{} failed because job is already terminated. State is {}", + capitalizedAction, + state); return state; - } else if (e.getMessage().contains("has terminated")) { + } else if (message != null && message.contains("has terminated")) { // This handles the case where the getState() call above returns RUNNING but the - // cancel was rejected because the job is in fact done. Hopefully, someday we can + // request was rejected because the job is in fact done. Hopefully, someday we can // delete this code if there is better consistency between the State and whether - // Cancel succeeds. + // lifecycle requests succeed. // // Example message: // Workflow modification failed. Causes: (7603adc9e9bff51e): Cannot perform // operation 'cancel' on Job: 2017-04-01_22_50_59-9269855660514862348. Job has // terminated in state SUCCESS: Workflow job: // 2017-04-01_22_50_59-9269855660514862348 succeeded. - LOG.warn("Cancel failed because job is already terminated.", e); + LOG.warn("{} failed because job is already terminated.", capitalizedAction, e); return state; } else { String errorMsg = String.format( - "Failed to cancel job in state %s, " - + "please go to the Developers Console to cancel it manually: %s", + "Failed to %s job in state %s, " + + "please go to the Developers Console to %s it manually: %s", + action, state, + action, MonitoringUtil.getJobMonitoringPageURL( getProjectId(), getRegion(), getJobId())); LOG.warn(errorMsg); @@ -488,14 +502,17 @@ public State cancel() throws IOException { } } }); - if (cancelState.compareAndSet(null, tentativeCancelTask)) { - // This thread should perform cancellation, while others will - // only wait for the result. - cancelState.get().run(); + if (requestedState.compareAndSet(null, tentativeTask)) { + // This thread should perform the lifecycle request, while others will only wait for the + // result. + requestedState.get().run(); } try { - return cancelState.get().get(); - } catch (InterruptedException | ExecutionException e) { + return requestedState.get().get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } catch (ExecutionException e) { throw new IOException(e); } } diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java index c57b5e3b1a0e..f66185c47941 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslator.java @@ -69,6 +69,7 @@ import org.apache.beam.sdk.coders.IterableCoder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; +import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.runners.AppliedPTransform; @@ -414,19 +415,8 @@ public Job translate(List packages) { // back end as well. If streaming engine is not enabled make sure the experiments are also // not enabled. if (options.isEnableStreamingEngine()) { - List experiments = options.getExperiments(); - if (experiments == null) { - experiments = new ArrayList(); - } else { - experiments = new ArrayList(experiments); - } - if (!experiments.contains(GcpOptions.STREAMING_ENGINE_EXPERIMENT)) { - experiments.add(GcpOptions.STREAMING_ENGINE_EXPERIMENT); - } - if (!experiments.contains(GcpOptions.WINDMILL_SERVICE_EXPERIMENT)) { - experiments.add(GcpOptions.WINDMILL_SERVICE_EXPERIMENT); - } - options.setExperiments(experiments); + ExperimentalOptions.addExperiment(options, GcpOptions.STREAMING_ENGINE_EXPERIMENT); + ExperimentalOptions.addExperiment(options, GcpOptions.WINDMILL_SERVICE_EXPERIMENT); } else { List experiments = options.getExperiments(); if (experiments != null) { @@ -489,6 +479,13 @@ public Job translate(List packages) { if (options.getDiskSizeGb() > 0) { workerPool.setDiskSizeGb(options.getDiskSizeGb()); } + if (options.getDiskProvisionedIops() != null && options.getDiskProvisionedIops() > 0) { + workerPool.setDiskProvisionedIops(options.getDiskProvisionedIops()); + } + if (options.getDiskProvisionedThroughputMibps() != null + && options.getDiskProvisionedThroughputMibps() > 0) { + workerPool.setDiskProvisionedThroughputMibps(options.getDiskProvisionedThroughputMibps()); + } AutoscalingSettings settings = new AutoscalingSettings(); if (options.getAutoscalingAlgorithm() != null) { settings.setAlgorithm(options.getAutoscalingAlgorithm().getAlgorithm()); diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java index b375de661885..e979cc44d827 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/DataflowRunner.java @@ -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,39 +1244,31 @@ 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)) { - List experiments = firstNonNull(options.getExperiments(), Collections.emptyList()); - if (!experiments.contains("use_runner_v2")) { - LOG.info( - "Automatically enabling Dataflow Runner v2 since the pipeline used cross-language" - + " transforms or pipeline needed a transform upgrade."); - options.setExperiments( - ImmutableList.builder().addAll(experiments).add("use_runner_v2").build()); + if (!useUnifiedWorker(options)) { + if (!firstNonNull(options.getExperiments(), Collections.emptyList()) + .contains("use_runner_v2")) { + LOG.info( + "Automatically enabling Dataflow Portable Runner since the pipeline used cross-language" + + " transforms or pipeline needed a transform upgrade."); + ExperimentalOptions.addExperiment(options, "use_runner_v2"); + } } } if (useUnifiedWorker(options)) { if (hasExperiment(options, "disable_runner_v2") || hasExperiment(options, "disable_runner_v2_until_2023") - || hasExperiment(options, "disable_prime_runner_v2")) { + || hasExperiment(options, "disable_prime_runner_v2") + || hasExperiment(options, "disable_portable_runner") + || hasExperiment(options, "enable_streaming_java_runner")) { throw new IllegalArgumentException( - "Runner V2 both disabled and enabled: at least one of ['beam_fn_api', 'use_unified_worker', 'use_runner_v2', 'use_portable_job_submission'] is set and also one of ['disable_runner_v2', 'disable_runner_v2_until_2023', 'disable_prime_runner_v2'] is set."); - } - List experiments = - new ArrayList<>(options.getExperiments()); // non-null if useUnifiedWorker is true - if (!experiments.contains("use_runner_v2")) { - experiments.add("use_runner_v2"); + "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."); } - if (!experiments.contains("use_unified_worker")) { - experiments.add("use_unified_worker"); - } - if (!experiments.contains("beam_fn_api")) { - experiments.add("beam_fn_api"); - } - if (!experiments.contains("use_portable_job_submission")) { - experiments.add("use_portable_job_submission"); - } - options.setExperiments(ImmutableList.copyOf(experiments)); + ExperimentalOptions.addExperiment(options, "use_runner_v2"); + ExperimentalOptions.addExperiment(options, "use_unified_worker"); + ExperimentalOptions.addExperiment(options, "beam_fn_api"); + ExperimentalOptions.addExperiment(options, "use_portable_job_submission"); // Ensure that logging via the FnApi is enabled options.as(SdkHarnessOptions.class).setEnableLogViaFnApi(true); } @@ -1301,14 +1295,16 @@ public DataflowPipelineJob run(Pipeline pipeline) { options.setStreaming(true); { - List experiments = - options.getExperiments() == null - ? new ArrayList<>() - : new ArrayList<>(options.getExperiments()); // Experiment marking that the harness supports tag encoding v2 // Backend will enable tag encoding v2 only if the harness supports it. - experiments.add("streaming_engine_state_tag_encoding_v2_supported"); - options.setExperiments(ImmutableList.copyOf(experiments)); + ExperimentalOptions.addExperiment( + options, "streaming_engine_state_tag_encoding_v2_supported"); + // Experiment requesting tag encoding v2 on new jobs starting with 2.75.0. During job + // updates old job's tag encoding version is carried over by the backend. + if (!StreamingOptions.updateCompatibilityVersionLessThan(options, "2.75.0")) { + ExperimentalOptions.addExperiment( + options, "enable_streaming_engine_state_tag_encoding_v2"); + } } if (useUnifiedWorker(options)) { @@ -1319,6 +1315,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 " @@ -1368,11 +1379,17 @@ public DataflowPipelineJob run(Pipeline pipeline) { options.getStager().stageToFile(serializedProtoPipeline, PIPELINE_FILE_NAME); dataflowOptions.setPipelineUrl(stagedPipeline.getLocation()); + String pipelineProtoHash = Hashing.sha256().hashBytes(serializedProtoPipeline).toString(); + options.as(SdkHarnessOptions.class).setPipelineProtoHash(pipelineProtoHash); + if (useUnifiedWorker(options)) { - LOG.info("Skipping v1 transform replacements since job will run on v2."); + 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 @@ -1383,7 +1400,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); @@ -1413,15 +1430,7 @@ public DataflowPipelineJob run(Pipeline pipeline) { pipeline, dataflowV1PipelineProto, dataflowV1Components, this, packages); if (!isNullOrEmpty(dataflowOptions.getDataflowWorkerJar()) && !useUnifiedWorker(options)) { - List experiments = - firstNonNull(dataflowOptions.getExperiments(), Collections.emptyList()); - if (!experiments.contains("use_staged_dataflow_worker_jar")) { - dataflowOptions.setExperiments( - ImmutableList.builder() - .addAll(experiments) - .add("use_staged_dataflow_worker_jar") - .build()); - } + ExperimentalOptions.addExperiment(options, "use_staged_dataflow_worker_jar"); } Job newJob = jobSpecification.getJob(); @@ -1480,11 +1489,8 @@ public DataflowPipelineJob run(Pipeline pipeline) { .collect(Collectors.toList()); if (minCpuFlags.isEmpty()) { - dataflowOptions.setExperiments( - ImmutableList.builder() - .addAll(experiments) - .add("min_cpu_platform=" + dataflowOptions.getMinCpuPlatform()) - .build()); + ExperimentalOptions.addExperiment( + dataflowOptions, "min_cpu_platform=" + dataflowOptions.getMinCpuPlatform()); } else { LOG.warn( "Flag min_cpu_platform is defined in both top level PipelineOption, " @@ -1520,12 +1526,9 @@ public DataflowPipelineJob run(Pipeline pipeline) { // enable upload_graph when the graph is too large byte[] jobGraphBytes = DataflowPipelineTranslator.jobToString(newJob).getBytes(UTF_8); int jobGraphByteSize = jobGraphBytes.length; - if (jobGraphByteSize >= CREATE_JOB_REQUEST_LIMIT_BYTES - && !hasExperiment(options, "upload_graph") - && !useUnifiedWorker(options)) { - List experiments = firstNonNull(options.getExperiments(), Collections.emptyList()); - options.setExperiments( - ImmutableList.builder().addAll(experiments).add("upload_graph").build()); + if (jobGraphByteSize >= CREATE_JOB_REQUEST_LIMIT_BYTES && !useUnifiedWorker(options) + && !hasExperiment(options, "upload_graph")) { + ExperimentalOptions.addExperiment(options, "upload_graph"); LOG.info( "The job graph size ({} in bytes) is larger than {}. Automatically add " + "the upload_graph option to experiments.", @@ -1539,7 +1542,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 @@ -2726,7 +2729,8 @@ static boolean useUnifiedWorker(DataflowPipelineOptions options) { return hasExperiment(options, "beam_fn_api") || hasExperiment(options, "use_runner_v2") || hasExperiment(options, "use_unified_worker") - || hasExperiment(options, "use_portable_job_submission"); + || hasExperiment(options, "use_portable_job_submission") + || hasExperiment(options, "enable_portable_runner"); } static void verifyDoFnSupported( diff --git a/runners/google-cloud-dataflow-java/src/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/DataflowPipelineWorkerPoolOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java index fd4af6d5e043..3fcf69f4cacf 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowPipelineWorkerPoolOptions.java @@ -193,6 +193,20 @@ public String getAlgorithm() { void setWorkerDiskType(String value); + /** Provisioned IOPS for the worker disk. */ + @Description("Provisioned IOPS for the worker disk.") + @Nullable + Long getDiskProvisionedIops(); + + void setDiskProvisionedIops(Long value); + + /** Provisioned throughput in MiB/s for the worker disk. */ + @Description("Provisioned throughput in MiB/s for the worker disk.") + @Nullable + Long getDiskProvisionedThroughputMibps(); + + void setDiskProvisionedThroughputMibps(Long value); + /** * Specifies whether worker pools should be started with public IP addresses. * diff --git a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java index 9727048e47aa..cca261fff627 100644 --- a/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java +++ b/runners/google-cloud-dataflow-java/src/main/java/org/apache/beam/runners/dataflow/options/DataflowStreamingPipelineOptions.java @@ -191,11 +191,18 @@ public interface DataflowStreamingPipelineOptions extends PipelineOptions { void setMaxStackTraceDepthToReport(int value); @Description("Necessary duration for a commit to be considered stuck and invalidated.") - @Default.Integer(10 * 60 * 1000) + @Default.Integer(60 * 60 * 1000) int getStuckCommitDurationMillis(); void setStuckCommitDurationMillis(int value); + @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'." @@ -249,6 +256,16 @@ public interface DataflowStreamingPipelineOptions extends PipelineOptions { void setIsWindmillServiceDirectPathEnabled(boolean isWindmillServiceDirectPathEnabled); + /** + * The maximum size of cached entries in bytes. Entries (eg: values, bags) larger than this limit + * will not be cached by the windmill state cache + */ + @Description("The maximum size of cached entries in bytes.") + @Default.Long(Long.MAX_VALUE) + Long getMaxWindmillStateCacheEntryBytes(); + + void setMaxWindmillStateCacheEntryBytes(Long value); + /** * Factory for creating local Windmill address. Reads from system propery 'windmill.hostport' for * backwards compatibility. @@ -333,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/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..4b088eb41a7d 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineJobTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineJobTest.java @@ -406,6 +406,26 @@ public void testCancelUnterminatedJobThatSucceeds() throws IOException { verifyNoMoreInteractions(mockJobs); } + @Test + public void testDrainUnterminatedJobThatSucceeds() throws IOException { + Dataflow.Projects.Locations.Jobs.Update update = + mock(Dataflow.Projects.Locations.Jobs.Update.class); + when(mockJobs.update(eq(PROJECT_ID), eq(REGION_ID), eq(JOB_ID), any(Job.class))) + .thenReturn(update); + when(update.execute()).thenReturn(new Job().setCurrentState("JOB_STATE_DRAINING")); + + DataflowPipelineJob job = + new DataflowPipelineJob(DataflowClient.create(options), JOB_ID, options, null); + + assertEquals(State.RUNNING, job.drain()); + Job content = new Job(); + content.setProjectId(PROJECT_ID); + content.setId(JOB_ID); + content.setRequestedState("JOB_STATE_DRAINED"); + verify(mockJobs).update(eq(PROJECT_ID), eq(REGION_ID), eq(JOB_ID), eq(content)); + verifyNoMoreInteractions(mockJobs); + } + @Test public void testCancelUnterminatedJobThatFails() throws IOException { Dataflow.Projects.Locations.Jobs.Get statusRequest = @@ -432,6 +452,32 @@ public void testCancelUnterminatedJobThatFails() throws IOException { job.cancel(); } + @Test + public void testCancelUnterminatedJobWithNullFailureMessage() throws IOException { + Dataflow.Projects.Locations.Jobs.Get statusRequest = + mock(Dataflow.Projects.Locations.Jobs.Get.class); + + Job statusResponse = new Job(); + statusResponse.setCurrentState("JOB_STATE_RUNNING"); + when(mockJobs.get(PROJECT_ID, REGION_ID, JOB_ID)).thenReturn(statusRequest); + when(statusRequest.execute()).thenReturn(statusResponse); + + Dataflow.Projects.Locations.Jobs.Update update = + mock(Dataflow.Projects.Locations.Jobs.Update.class); + when(mockJobs.update(eq(PROJECT_ID), eq(REGION_ID), eq(JOB_ID), any(Job.class))) + .thenReturn(update); + when(update.execute()).thenThrow(new IOException()); + + DataflowPipelineJob job = + new DataflowPipelineJob(DataflowClient.create(options), JOB_ID, options, null); + + thrown.expect(IOException.class); + thrown.expectMessage( + "Failed to cancel job in state RUNNING, " + + "please go to the Developers Console to cancel it manually:"); + job.cancel(); + } + /** * Test that {@link DataflowPipelineJob#cancel} doesn't throw if the Dataflow service returns * non-terminal state even though the cancel API call failed, which can happen in practice. diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java index b0ab553d237c..f8818931a68f 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowPipelineTranslatorTest.java @@ -757,6 +757,37 @@ public void testDiskSizeGbConfig() throws IOException { assertEquals(diskSizeGb, job.getEnvironment().getWorkerPools().get(0).getDiskSizeGb()); } + @Test + public void testDiskProvisionedOptionsConfig() throws IOException { + final Long diskProvisionedIops = 1000L; + final Long diskProvisionedThroughputMibps = 100L; + + DataflowPipelineOptions options = buildPipelineOptions(); + options.setDiskProvisionedIops(diskProvisionedIops); + options.setDiskProvisionedThroughputMibps(diskProvisionedThroughputMibps); + + Pipeline p = buildPipeline(options); + p.traverseTopologically(new RecordingPipelineVisitor()); + SdkComponents sdkComponents = createSdkComponents(options); + RunnerApi.Pipeline pipelineProto = PipelineTranslation.toProto(p, sdkComponents, true); + Job job = + DataflowPipelineTranslator.fromOptions(options) + .translate( + p, + pipelineProto, + sdkComponents, + DataflowRunner.fromOptions(options), + Collections.emptyList()) + .getJob(); + + assertEquals(1, job.getEnvironment().getWorkerPools().size()); + assertEquals( + diskProvisionedIops, job.getEnvironment().getWorkerPools().get(0).getDiskProvisionedIops()); + assertEquals( + diskProvisionedThroughputMibps, + job.getEnvironment().getWorkerPools().get(0).getDiskProvisionedThroughputMibps()); + } + /** A composite transform that returns an output that is unrelated to the input. */ private static class UnrelatedOutputCreator extends PTransform, PCollection> { diff --git a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java index 8c33123be6d5..073b30f928dc 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/DataflowRunnerTest.java @@ -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; @@ -1783,7 +1791,11 @@ public void testSdkHarnessConfigurationPrime() throws IOException { public void testSettingAnyFnApiExperimentEnablesUnifiedWorker() throws Exception { for (String experiment : ImmutableList.of( - "beam_fn_api", "use_runner_v2", "use_unified_worker", "use_portable_job_submission")) { + "beam_fn_api", + "use_runner_v2", + "use_unified_worker", + "use_portable_job_submission", + "enable_portable_runner")) { DataflowPipelineOptions options = buildPipelineOptions(); ExperimentalOptions.addExperiment(options, experiment); Pipeline p = Pipeline.create(options); @@ -1798,7 +1810,11 @@ public void testSettingAnyFnApiExperimentEnablesUnifiedWorker() throws Exception for (String experiment : ImmutableList.of( - "beam_fn_api", "use_runner_v2", "use_unified_worker", "use_portable_job_submission")) { + "beam_fn_api", + "use_runner_v2", + "use_unified_worker", + "use_portable_job_submission", + "enable_portable_runner")) { DataflowPipelineOptions options = buildPipelineOptions(); options.setStreaming(true); ExperimentalOptions.addExperiment(options, experiment); @@ -1822,16 +1838,27 @@ public void testSettingAnyFnApiExperimentEnablesUnifiedWorker() throws Exception public void testSettingConflictingEnableAndDisableExperimentsThrowsException() throws Exception { for (String experiment : ImmutableList.of( - "beam_fn_api", "use_runner_v2", "use_unified_worker", "use_portable_job_submission")) { + "beam_fn_api", + "use_runner_v2", + "use_unified_worker", + "use_portable_job_submission", + "enable_portable_runner")) { for (String disabledExperiment : ImmutableList.of( - "disable_runner_v2", "disable_runner_v2_until_2023", "disable_prime_runner_v2")) { + "disable_runner_v2", + "disable_runner_v2_until_2023", + "disable_prime_runner_v2", + "enable_streaming_java_runner", + "disable_portable_runner")) { DataflowPipelineOptions options = buildPipelineOptions(); ExperimentalOptions.addExperiment(options, experiment); ExperimentalOptions.addExperiment(options, disabledExperiment); 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); } } } @@ -2761,4 +2788,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/options/DataflowPipelineOptionsTest.java b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java index b381396f2bcc..faa19eefddc5 100644 --- a/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java +++ b/runners/google-cloud-dataflow-java/src/test/java/org/apache/beam/runners/dataflow/options/DataflowPipelineOptionsTest.java @@ -322,4 +322,13 @@ public void destroy() { TimeoutException.class, () -> DefaultGcpRegionFactory.getRegionFromGcloudCli(1L)); } } + + @Test + public void testDiskProvisionedIopsAndThroughput() { + DataflowPipelineOptions options = PipelineOptionsFactory.as(DataflowPipelineOptions.class); + options.setDiskProvisionedIops(1000L); + options.setDiskProvisionedThroughputMibps(100L); + assertEquals(Long.valueOf(1000), options.getDiskProvisionedIops()); + assertEquals(Long.valueOf(100), options.getDiskProvisionedThroughputMibps()); + } } diff --git a/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 610fa411459f..21879861e9d6 100644 --- a/runners/google-cloud-dataflow-java/worker/build.gradle +++ b/runners/google-cloud-dataflow-java/worker/build.gradle @@ -212,6 +212,7 @@ dependencies { implementation library.java.jackson_core implementation library.java.jackson_databind implementation library.java.joda_time + implementation library.java.opentelemetry_context implementation library.java.slf4j_api implementation library.java.vendored_grpc_1_69_0 implementation library.java.error_prone_annotations diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/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/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/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..e204a78a7d2e 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); 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/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..964cf2323d51 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java @@ -0,0 +1,518 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; + +import java.io.Closeable; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.beam.runners.core.DoFnRunner; +import org.apache.beam.runners.core.SideInputReader; +import org.apache.beam.runners.core.StateInternals; +import org.apache.beam.runners.core.StateNamespaces; +import org.apache.beam.runners.core.StateTag; +import org.apache.beam.runners.core.StateTags; +import org.apache.beam.runners.core.TimerInternals; +import org.apache.beam.runners.dataflow.options.DataflowPipelineDebugOptions; +import org.apache.beam.runners.dataflow.worker.counters.Counter; +import org.apache.beam.runners.dataflow.worker.counters.CounterFactory; +import org.apache.beam.runners.dataflow.worker.counters.CounterName; +import org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter; +import org.apache.beam.runners.dataflow.worker.util.common.worker.OutputReceiver; +import org.apache.beam.runners.dataflow.worker.util.common.worker.Receiver; +import org.apache.beam.sdk.coders.Coder; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.StreamingOptions; +import org.apache.beam.sdk.state.State; +import org.apache.beam.sdk.state.StateSpec; +import org.apache.beam.sdk.state.TimeDomain; +import org.apache.beam.sdk.transforms.DoFnSchemaInformation; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature; +import org.apache.beam.sdk.transforms.reflect.DoFnSignatures; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.util.DoFnInfo; +import org.apache.beam.sdk.util.WindowedValueMultiReceiver; +import org.apache.beam.sdk.values.PCollectionView; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.sdk.values.WindowingStrategy; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings({ + "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) + "nullness" // TODO(https://github.com/apache/beam/issues/20497) +}) +class SimpleParDoFnHelpers { + private static final Logger LOG = LoggerFactory.getLogger(SimpleParDoFnHelpers.class); + + // TODO: Remove once Distributions has shipped. + @VisibleForTesting + static final String OUTPUTS_PER_ELEMENT_EXPERIMENT = "outputs_per_element_counter"; + + private static final String COUNTER_NAME = "per-element-output-count"; + + final PipelineOptions options; + final DoFnInstanceManager doFnInstanceManager; + + private final SideInputReader sideInputReader; + final DataflowOperationContext operationContext; + private final TupleTag mainOutputTag; + private final Map, Integer> outputTupleTagsToReceiverIndices; + private final List> sideOutputTags; + final DataflowExecutionContext.DataflowStepContext stepContext; + final DataflowExecutionContext.DataflowStepContext userStepContext; + private final CounterFactory counterFactory; + private final DoFnRunnerFactory runnerFactory; + final boolean hasStreamingSideInput; + final OutputsPerElementTracker outputsPerElementTracker; + private final DoFnSchemaInformation doFnSchemaInformation; + private final Map> sideInputMapping; + + // Various DoFn helpers, null between bundles + @Nullable DoFnRunner fnRunner; + @Nullable DoFnInfo fnInfo; + private Receiver @Nullable [] receivers; + + // This may additionally be null if it is not a real DoFn but an OldDoFn or + // GroupAlsoByWindowViaWindowSetDoFn + protected @Nullable DoFnSignature fnSignature; + + SimpleParDoFnHelpers( + PipelineOptions options, + DoFnInstanceManager doFnInstanceManager, + SideInputReader sideInputReader, + TupleTag mainOutputTag, + Map, Integer> outputTupleTagsToReceiverIndices, + DataflowExecutionContext.DataflowStepContext stepContext, + DataflowOperationContext operationContext, + DoFnSchemaInformation doFnSchemaInformation, + Map> sideInputMapping, + DoFnRunnerFactory runnerFactory) { + this.options = options; + this.doFnInstanceManager = doFnInstanceManager; + + // We vend a freshly deserialized version for each run + this.sideInputReader = sideInputReader; + this.operationContext = operationContext; + checkArgument(!outputTupleTagsToReceiverIndices.isEmpty(), "expected at least one output"); + this.mainOutputTag = mainOutputTag; + this.outputTupleTagsToReceiverIndices = outputTupleTagsToReceiverIndices; + ImmutableList.Builder> sideOutputTagsBuilder = ImmutableList.builder(); + for (TupleTag tag : outputTupleTagsToReceiverIndices.keySet()) { + if (!mainOutputTag.equals(tag)) { + sideOutputTagsBuilder.add(tag); + } + } + this.sideOutputTags = sideOutputTagsBuilder.build(); + this.stepContext = stepContext; + + // StepContext provides a TimerInternals and StateInternals for use by the system - this class. + // For the user, we request a user-scoped StepContext to provide a user-scoped + // StateInternals and TimerInternals. + this.userStepContext = stepContext.namespacedToUser(); + + this.counterFactory = operationContext.counterFactory(); + this.runnerFactory = runnerFactory; + this.hasStreamingSideInput = + options.as(StreamingOptions.class).isStreaming() && !sideInputReader.isEmpty(); + this.outputsPerElementTracker = createOutputsPerElementTracker(); + this.doFnSchemaInformation = doFnSchemaInformation; + this.sideInputMapping = sideInputMapping; + } + + boolean hasState() { + return fnSignature != null && !fnSignature.stateDeclarations().isEmpty(); + } + + void startBundle(Receiver... receivers) throws Exception { + checkArgument( + receivers.length == outputTupleTagsToReceiverIndices.size(), + "unexpected number of receivers for DoFn"); + + this.receivers = receivers; + } + + void reallyStartBundle() throws Exception { + checkState(fnRunner == null, "bundle already started (or not properly finished)"); + + WindowedValueMultiReceiver outputManager = + new WindowedValueMultiReceiver() { + final Map, OutputReceiver> undeclaredOutputs = new HashMap<>(); + + private @Nullable Receiver getReceiverOrNull(TupleTag tag) { + Integer receiverIndex = outputTupleTagsToReceiverIndices.get(tag); + if (receiverIndex != null) { + return receivers[receiverIndex]; + } else { + return undeclaredOutputs.get(tag); + } + } + + @Override + public void output(TupleTag tag, WindowedValue output) { + outputsPerElementTracker.onOutput(); + Receiver receiver = getReceiverOrNull(tag); + if (receiver == null) { + // A new undeclared output. + // TODO: plumb through the operationName, so that we can + // name implicit outputs after it. + String outputName = "implicit-" + tag.getId(); + // TODO: plumb through the counter prefix, so we can + // make it available to the OutputReceiver class in case + // it wants to use it in naming output counterFactory. (It + // doesn't today.) + OutputReceiver undeclaredReceiver = new OutputReceiver(); + + ElementCounter outputCounter = + new DataflowOutputCounter( + outputName, counterFactory, stepContext.getNameContext()); + undeclaredReceiver.addOutputCounter(outputCounter); + undeclaredOutputs.put(tag, undeclaredReceiver); + receiver = undeclaredReceiver; + } + + try { + receiver.process(output); + } catch (RuntimeException | Error e) { + // Rethrow unchecked exceptions as-is to avoid excessive nesting + // via a chain of DoFn's. + throw e; + } catch (Exception e) { + // This should never happen in practice with DoFn's, but can happen + // with other Receivers. + throw new RuntimeException(e); + } + } + }; + fnInfo = (DoFnInfo) doFnInstanceManager.get(); + fnSignature = DoFnSignatures.getSignature(fnInfo.getDoFn().getClass()); + + fnRunner = + runnerFactory.createRunner( + fnInfo.getDoFn(), + options, + mainOutputTag, + sideOutputTags, + fnInfo.getSideInputViews(), + sideInputReader, + fnInfo.getInputCoder(), + fnInfo.getOutputCoders(), + fnInfo.getWindowingStrategy(), + stepContext, + userStepContext, + outputManager, + doFnSchemaInformation, + sideInputMapping); + fnRunner.startBundle(); + } + + void finishBundle(StreamingSideInputProcessor sideInputProcessor) throws Exception { + if (fnRunner != null) { + fnRunner.finishBundle(); + if (sideInputProcessor != null) { + sideInputProcessor.handleFinishBundle(); + } + doFnInstanceManager.complete(fnInfo); + fnRunner = null; + fnInfo = null; + fnSignature = null; + sideInputProcessor = null; + } + } + + void abort() throws Exception { + doFnInstanceManager.abort(fnInfo); + fnRunner = null; + fnInfo = null; + } + + @VisibleForTesting static final String CLEANUP_TIMER_ID = "cleanup-timer"; + + enum TimerType { + USER { + @Override + public void processTimer( + SimpleParDoFnHelpers doFn, + TimerInternals.TimerData timer, + Supplier> sideInputProcessor) + throws Exception { + doFn.processUserTimer(timer, sideInputProcessor.get()); + } + }, + FAIL_USER { + @Override + public void processTimer( + SimpleParDoFnHelpers doFn, + TimerInternals.TimerData timer, + Supplier> sideInputProcessor) + throws Exception { + throw new UnsupportedOperationException( + "Attempt to deliver a timer to a DoFn, but timers are not supported here."); + } + }, + SYSTEM { + @Override + public void processTimer( + SimpleParDoFnHelpers doFn, + TimerInternals.TimerData timer, + Supplier> sideInputProcessor) + throws Exception { + doFn.processSystemTimer(timer, sideInputProcessor.get()); + } + }; + + public abstract void processTimer( + SimpleParDoFnHelpers doFn, + TimerInternals.TimerData timer, + Supplier> sideInputProcessor) + throws Exception; + }; + + void processTimers( + TimerType mode, + DataflowExecutionContext.DataflowStepContext context, + Coder windowCoder, + Runnable startKey, + Supplier> sideInputProcessor) + throws Exception { + TimerInternals.TimerData timer = context.getNextFiredTimer(windowCoder); + + if (timer != null && fnRunner == null) { + // If we need to run reallyStartBundle in here, we need to make sure to switch the state + // sampler into the start state. + try (Closeable start = operationContext.enterStart()) { + reallyStartBundle(); + startKey.run(); + } + } + + while (timer != null) { + mode.processTimer(this, timer, sideInputProcessor); + timer = context.getNextFiredTimer(windowCoder); + } + } + + protected void processUserTimer( + TimerInternals.TimerData timer, StreamingSideInputProcessor sideInputProcessor) { + if (fnSignature.timerDeclarations().containsKey(timer.getTimerId()) + || fnSignature.timerFamilyDeclarations().containsKey(timer.getTimerFamilyId())) { + BoundedWindow window = ((StateNamespaces.WindowNamespace) timer.getNamespace()).getWindow(); + if (sideInputProcessor != null) { + sideInputProcessor.handleProcessTimer(timer); + } + fnRunner.onTimer( + timer.getTimerId(), + timer.getTimerFamilyId(), + this.stepContext.stateInternals().getKey(), + window, + timer.getTimestamp(), + timer.getOutputTimestamp(), + timer.getDomain(), + timer.causedByDrain()); + } + } + + private void processSystemTimer( + TimerInternals.TimerData timer, StreamingSideInputProcessor sideInputProcessor) + throws Exception { + // Timer owned by this class, for cleaning up state in expired windows + if (timer.getTimerId().equals(CLEANUP_TIMER_ID)) { + checkState( + timer.getDomain().equals(TimeDomain.EVENT_TIME), + "%s received cleanup timer with domain not EVENT_TIME: %s", + this, + timer); + + checkState( + timer.getNamespace() instanceof StateNamespaces.WindowNamespace, + "%s received cleanup timer not for a %s: %s", + this, + StateNamespaces.WindowNamespace.class.getSimpleName(), + timer); + + if (sideInputProcessor != null) { + // We must call this to ensure the side-input is cached for onWindowExpiration. Since we + // don't set cleanup + // timers until we actually call processElement, the window must be unblocked here. + sideInputProcessor.handleProcessTimer(timer); + } + + BoundedWindow window = ((StateNamespaces.WindowNamespace) timer.getNamespace()).getWindow(); + Instant targetTime = earliestAllowableCleanupTime(window, fnInfo.getWindowingStrategy()); + + checkState( + !targetTime.isAfter(timer.getTimestamp()), + "%s received state cleanup timer for window %s " + + " that is before the appropriate cleanup time %s", + this, + window, + targetTime); + + fnRunner.onWindowExpiration( + window, timer.getOutputTimestamp(), this.stepContext.stateInternals().getKey()); + + // This is for a timer for a window that is expired, so clean it up. + for (DoFnSignature.StateDeclaration stateDecl : fnSignature.stateDeclarations().values()) { + StateTag tag; + try { + tag = + StateTags.tagForSpec( + stateDecl.id(), (StateSpec) stateDecl.field().get(fnInfo.getDoFn())); + } catch (IllegalAccessException e) { + throw new RuntimeException( + String.format( + "Error accessing %s for %s", + StateSpec.class.getName(), fnInfo.getDoFn().getClass().getName()), + e); + } + + StateInternals stateInternals = userStepContext.stateInternals(); + State state = stateInternals.state(timer.getNamespace(), tag); + state.clear(); + } + } + } + + private OutputsPerElementTracker createOutputsPerElementTracker() { + // TODO: Remove once Distributions has shipped. + if (!hasExperiment(OUTPUTS_PER_ELEMENT_EXPERIMENT)) { + return NoopOutputsPerElementTracker.INSTANCE; + } + + // TODO: Remove log statement when functionality is enabled by default. + LOG.info("{} counter enabled.", COUNTER_NAME); + + return new OutputsPerElementTrackerImpl(); + } + + private boolean hasExperiment(String experiment) { + List experiments = options.as(DataflowPipelineDebugOptions.class).getExperiments(); + return experiments != null && experiments.contains(experiment); + } + + /** Simple state tracker to calculate PerElementOutputCount counter. */ + interface OutputsPerElementTracker { + + void onOutput(); + + void onProcessElement(); + + void onProcessElementSuccess(); + } + + private class OutputsPerElementTrackerImpl implements OutputsPerElementTracker { + + private long outputsPerElement; + private final Counter counter; + + public OutputsPerElementTrackerImpl() { + this.counter = + counterFactory.distribution( + CounterName.named(COUNTER_NAME).withOriginalName(stepContext.getNameContext())); + } + + @Override + public void onProcessElement() { + reset(); + } + + @Override + public void onOutput() { + outputsPerElement++; + } + + @Override + public void onProcessElementSuccess() { + counter.addValue(outputsPerElement); + reset(); + } + + private void reset() { + outputsPerElement = 0L; + } + } + + /** No-op {@link OutputsPerElementTracker} implementation used when the counter is disabled. */ + private static class NoopOutputsPerElementTracker implements OutputsPerElementTracker { + + private NoopOutputsPerElementTracker() {} + + public static final OutputsPerElementTracker INSTANCE = new NoopOutputsPerElementTracker(); + + @Override + public void onOutput() {} + + @Override + public void onProcessElement() {} + + @Override + public void onProcessElementSuccess() {} + } + + Instant earliestAllowableCleanupTime(BoundedWindow window, WindowingStrategy windowingStrategy) { + Instant cleanupTime = + window + .maxTimestamp() + .plus(windowingStrategy.getAllowedLateness()) + .plus(Duration.millis(1L)); + return cleanupTime.isAfter(BoundedWindow.TIMESTAMP_MAX_VALUE) + ? BoundedWindow.TIMESTAMP_MAX_VALUE + : cleanupTime; + } + + protected void registerStateCleanup( + WindowingStrategy windowingStrategy, Collection windowsToCleanup) { + Coder windowCoder = windowingStrategy.getWindowFn().windowCoder(); + + for (W window : windowsToCleanup) { + // The stepContext is the thing that know if it is batch or streaming, hence + // whether state needs to be cleaned up or will simply be discarded so the + // timer can be ignored. + Instant cleanupTime = earliestAllowableCleanupTime(window, windowingStrategy); + // Set a cleanup timer for state at the end of the window to trigger onWindowExpiration and + // garbage collect state. We avoid doing this for the global window if there is no window + // expiration set as the state will be up when the pipeline terminates. Setting the timer + // leads to a unbounded growth of timers for pipelines with many unique keys in the global + // window. + if (cleanupTime.isBefore(GlobalWindow.INSTANCE.maxTimestamp()) + || fnSignature.onWindowExpiration() != null) { + // If the DoFn has OnWindowExpiration, then set the watermark hold so that the watermark + // does + // not advance until OnWindowExpiration completes. + Instant cleanupOutputTimestamp = + fnSignature.onWindowExpiration() == null + ? cleanupTime + : cleanupTime.minus(Duration.millis(1L)); + stepContext.setStateCleanupTimer( + CLEANUP_TIMER_ID, window, windowCoder, cleanupTime, cleanupOutputTimestamp); + } + } + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SplittableProcessFnFactory.java index 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 f5e5adab1556..180dda153bb6 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorker.java @@ -50,6 +50,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.WeightedSemaphore; import org.apache.beam.runners.dataflow.worker.streaming.WorkHeartbeatResponseProcessor; import org.apache.beam.runners.dataflow.worker.streaming.config.ComputationConfig; +import org.apache.beam.runners.dataflow.worker.streaming.config.ComputationConfig.Fetcher; import org.apache.beam.runners.dataflow.worker.streaming.config.FixedGlobalConfigHandle; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingApplianceComputationConfigFetcher; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingEngineComputationConfigFetcher; @@ -113,6 +114,8 @@ import org.apache.beam.sdk.io.FileSystems; import org.apache.beam.sdk.io.gcp.bigquery.BigQuerySinkMetrics; import org.apache.beam.sdk.metrics.MetricsEnvironment; +import org.apache.beam.sdk.options.ExperimentalOptions; +import org.apache.beam.sdk.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; @@ -176,6 +179,9 @@ public final class StreamingDataflowWorker { // Experiment make the monitor within BoundedQueueExecutor fair public static final String BOUNDED_QUEUE_EXECUTOR_USE_FAIR_MONITOR_EXPERIMENT = "windmill_bounded_queue_executor_use_fair_monitor"; + // Don't use. Experiment guarding multi key bundles. The feature is work in progress and + // incomplete. + private static final String UNSTABLE_ENABLE_MULTI_KEY_BUNDLE = "unstable_enable_multi_key_bundle"; private final WindmillStateCache stateCache; private AtomicReference statusPages = new AtomicReference<>(); @@ -633,6 +639,10 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o WindmillStateCache.builder() .setSizeMb(options.getWorkerCacheMb()) .setSupportMapViaMultimap(options.isEnableStreamingEngine()) + .setMaxCachedEntryBytes(options.getMaxWindmillStateCacheEntryBytes()) + .setEnableHistogram( + !ExperimentalOptions.hasExperiment( + options, "disable_windmill_user_state_cache_histogram")) .build(); GrpcWindmillStreamFactory.Builder windmillStreamFactoryBuilder = @@ -651,6 +661,15 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o windmillStateCache::forComputation, ID_GENERATOR)); + Fetcher configFetcher = configFetcherComputationStateCacheAndWindmillClient.configFetcher(); + configFetcher + .getGlobalConfigHandle() + .registerConfigObserver( + config -> { + windmillStateCache.setMaxCachedEntryBytesOverride( + config.userWorkerJobSettings().getMaxCachedEntryBytes()); + }); + ComputationStateCache computationStateCache = configFetcherComputationStateCacheAndWindmillClient.computationStateCache(); WindmillServerStub windmillServer = @@ -689,7 +708,7 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o return new StreamingDataflowWorker( windmillServer, clientId, - configFetcherComputationStateCacheAndWindmillClient.configFetcher(), + configFetcher, computationStateCache, windmillStateCache, workExecutor, @@ -737,6 +756,8 @@ public static StreamingDataflowWorker fromOptions(DataflowWorkerHarnessOptions o new WorkHeartbeatResponseProcessor(computationStateCache::get)) .setHealthCheckIntervalMillis( options.getWindmillServiceStreamingRpcHealthCheckPeriodMs()) + .setCommitWorkStreamRetryTimeout( + java.time.Duration.ofMillis(options.getCommitWorkStreamRetryTimeoutMillis())) .build(); return ConfigFetcherComputationStateCacheAndWindmillClient.builder() .setWindmillDispatcherClient(dispatcherClient) @@ -1002,6 +1023,8 @@ private static JobHeader createJobHeader(DataflowWorkerHarnessOptions options, l private static BoundedQueueExecutor createWorkUnitExecutor(DataflowWorkerHarnessOptions options) { boolean useFairMonitor = DataflowRunner.hasExperiment(options, BOUNDED_QUEUE_EXECUTOR_USE_FAIR_MONITOR_EXPERIMENT); + boolean useKeyGroupWorkQueue = + DataflowRunner.hasExperiment(options, UNSTABLE_ENABLE_MULTI_KEY_BUNDLE); return new BoundedQueueExecutor( chooseMaxThreads(options), THREAD_EXPIRATION_TIME_SEC, @@ -1009,7 +1032,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 { @@ -1027,6 +1051,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); 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..d88864745648 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,7 +17,7 @@ */ package org.apache.beam.runners.dataflow.worker; -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.checkStateNotNull; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import com.google.api.services.dataflow.model.CounterUpdate; @@ -56,10 +56,12 @@ import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInput; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputState; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher; +import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataId; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.GlobalDataRequest; 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; @@ -92,6 +94,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.PeekingIterator; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Table; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; @@ -103,11 +106,7 @@ * state pertaining to a processing its owning computation. Can be reused across processing * different WorkItems for the same computation. */ -@SuppressWarnings({ - "deprecation", - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -// TODO(m-trieu) fix nullability issues in StreamingModeExecutionContext.java +@SuppressWarnings({"deprecation"}) @NotThreadSafe @Internal public class StreamingModeExecutionContext extends DataflowExecutionContext { @@ -128,7 +127,7 @@ public class StreamingModeExecutionContext extends DataflowExecutionContext, Map>> sideInputCache; - private WindmillTagEncoding windmillTagEncoding; + private final WindmillTagEncoding windmillTagEncoding; /** * The current user-facing key for this execution context. * @@ -141,13 +140,13 @@ public class StreamingModeExecutionContext extends DataflowExecutionContext activeReader; + private @Nullable WorkExecutor workExecutor; + private boolean finishKeyCalled = false; + public StreamingModeExecutionContext( CounterFactory counterFactory, String computationId, @@ -183,6 +185,12 @@ public StreamingModeExecutionContext( this.stateCache = stateCache; this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN; this.throwExceptionOnLargeOutput = throwExceptionOnLargeOutput; + StreamingGlobalConfig config = globalConfigHandle.getConfig(); + this.operationalLimits = config.operationalLimits(); + this.windmillTagEncoding = + config.enableStateTagEncodingV2() + ? WindmillTagEncodingV2.instance() + : WindmillTagEncodingV1.instance(); } @VisibleForTesting @@ -224,7 +232,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,7 +240,7 @@ public byte[] getCurrentRecordOffset() { throw new RuntimeException( "Unexpected getCurrentRecordOffset() while offset-based deduplication is not enabled."); } - return activeReader.getCurrentRecordOffset(); + return checkStateNotNull(activeReader).getCurrentRecordOffset(); } public void start( @@ -240,20 +248,20 @@ public void start( Work work, WindmillStateReader stateReader, SideInputStateFetcher sideInputStateFetcher, - Windmill.WorkItemCommitRequest.Builder outputBuilder) { + Windmill.WorkItemCommitRequest.Builder outputBuilder, + WorkExecutor workExecutor) { this.key = key; this.work = work; + this.workExecutor = workExecutor; + this.finishKeyCalled = false; this.computationKey = WindmillComputationKey.create(computationId, work.getShardedKey()); this.sideInputStateFetcher = sideInputStateFetcher; StreamingGlobalConfig config = globalConfigHandle.getConfig(); // 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(); + this.backlogBytes = UnboundedReader.BACKLOG_UNKNOWN; clearSinkFullHint(); Instant processingTime = computeProcessingTime(work.getWorkItem().getTimers().getTimersList()); @@ -270,6 +278,17 @@ public void start( } } + public void finishKey() { + checkState(!finishKeyCalled, "finishKey was already called"); + checkStateNotNull(workExecutor, "workExecutor must be set before calling finishKey()"); + try { + workExecutor.finishKey(key); + } catch (Exception e) { + throw new RuntimeException(e); + } + this.finishKeyCalled = true; + } + /** * Ensure that the processing time is greater than any fired processing time timers. Otherwise, a * trigger could ignore the timer and orphan the window. @@ -349,9 +368,9 @@ private SideInput fetchSideInput( return fetchSideInputFromWindmill( view, sideInputWindow, - checkNotNull(stateFamily), + checkStateNotNull(stateFamily), state, - checkNotNull(scopedReadStateSupplier), + checkStateNotNull(scopedReadStateSupplier), tagCache); } @@ -363,8 +382,8 @@ private SideInput fetchSideInputFromWindmill( Supplier scopedReadStateSupplier, Map> tagCache) { SideInput fetched = - sideInputStateFetcher.fetchSideInput( - view, sideInputWindow, stateFamily, state, scopedReadStateSupplier); + checkStateNotNull(sideInputStateFetcher) + .fetchSideInput(view, sideInputWindow, stateFamily, state, scopedReadStateSupplier); if (fetched.isReady()) { tagCache.put(sideInputWindow, fetched); @@ -386,7 +405,7 @@ private List getFiredTimers() { } public WindmillComputationKey getComputationKey() { - return computationKey; + return checkStateNotNull(computationKey); } public long getWorkToken() { @@ -394,7 +413,7 @@ public long getWorkToken() { } public Windmill.WorkItem getWorkItem() { - return checkNotNull( + return checkStateNotNull( work, "work is null. A call to StreamingModeExecutionContext.start(...) is required to set" + " work for execution.") @@ -402,7 +421,7 @@ public Windmill.WorkItem getWorkItem() { } public Windmill.WorkItemCommitRequest.Builder getOutputBuilder() { - return outputBuilder; + return checkStateNotNull(outputBuilder); } /** @@ -451,6 +470,7 @@ public void invalidateCache() { } public Map> flushState() { + checkState(finishKeyCalled, "finishKey must be called before flushState"); Map> callbacks = new HashMap<>(); for (StepContext stepContext : getAllStepContexts()) { @@ -469,15 +489,16 @@ public Map> flushState() { throw new RuntimeException("Exception while running bundle finalizer", e); } })); - outputBuilder.addFinalizeIds(id); + getOutputBuilder().addFinalizeIds(id); } } - if (activeReader != null) { - Windmill.SourceState.Builder sourceStateBuilder = - outputBuilder.getSourceStateUpdatesBuilder(); - final UnboundedSource.CheckpointMark checkpointMark = activeReader.getCheckpointMark(); - final Instant watermark = activeReader.getWatermark(); + UnboundedReader reader = activeReader; + if (reader != null) { + Windmill.WorkItemCommitRequest.Builder builder = getOutputBuilder(); + Windmill.SourceState.Builder sourceStateBuilder = builder.getSourceStateUpdatesBuilder(); + final UnboundedSource.CheckpointMark checkpointMark = reader.getCheckpointMark(); + final Instant watermark = reader.getWatermark(); long id = ThreadLocalRandom.current().nextLong(); sourceStateBuilder.addFinalizeIds(id); callbacks.put( @@ -494,7 +515,7 @@ public Map> flushState() { @SuppressWarnings("unchecked") Coder checkpointCoder = - ((UnboundedSource) activeReader.getCurrentSource()) + ((UnboundedSource) reader.getCurrentSource()) .getCheckpointMarkCoder(); if (checkpointCoder != null) { ByteStringOutputStream stream = new ByteStringOutputStream(); @@ -504,7 +525,7 @@ public Map> flushState() { throw new RuntimeException("Exception while encoding checkpoint", e); } sourceStateBuilder.setState(stream.toByteString()); - if (activeReader.getCurrentSource().offsetBasedDeduplicationSupported()) { + if (reader.getCurrentSource().offsetBasedDeduplicationSupported()) { byte[] offsetLimit = checkpointMark.getOffsetLimit(); if (offsetLimit.length == 0) { throw new RuntimeException("Checkpoint offset limit must be non-empty."); @@ -512,26 +533,30 @@ public Map> flushState() { sourceStateBuilder.setOffsetLimit(ByteString.copyFrom(offsetLimit)); } } - outputBuilder.setSourceWatermark(WindmillTimeUtils.harnessToWindmillTimestamp(watermark)); + builder.setSourceWatermark(WindmillTimeUtils.harnessToWindmillTimestamp(watermark)); - backlogBytes = activeReader.getSplitBacklogBytes(); + backlogBytes = reader.getSplitBacklogBytes(); + ByteString serializedKey = checkStateNotNull(getSerializedKey()); if (backlogBytes == UnboundedReader.BACKLOG_UNKNOWN - && WorkerCustomSources.isFirstUnboundedSourceSplit(getSerializedKey())) { + && WorkerCustomSources.isFirstUnboundedSourceSplit(serializedKey)) { // Only call getTotalBacklogBytes() on the first split. - backlogBytes = activeReader.getTotalBacklogBytes(); + backlogBytes = reader.getTotalBacklogBytes(); } - outputBuilder.setSourceBacklogBytes(backlogBytes); + builder.setSourceBacklogBytes(backlogBytes); readerCache.cacheReader( - getComputationKey(), - getWorkItem().getCacheToken(), - getWorkItem().getWorkToken(), - activeReader); + getComputationKey(), getWorkItem().getCacheToken(), getWorkItem().getWorkToken(), reader); activeReader = null; + } else if (backlogBytes != UnboundedReader.BACKLOG_UNKNOWN && backlogBytes != 1L) { + // If activeReader is null, we might still have backlogBytes from an SDF. We ignore a reported + // backlogBytes of 1 since older versions of the Java SDK use this value as a default when + // RestrictionTracker.getProgress() or GetSize() are not defined. + getOutputBuilder().setSourceBacklogBytes(backlogBytes); } return callbacks; } + @Nullable String getStateFamily(NameContext nameContext) { return nameContext.userName() == null ? null : stateNameMap.get(nameContext.userName()); } @@ -573,7 +598,7 @@ public static class StreamingModeExecutionState extends DataflowExecutionState { public StreamingModeExecutionState( NameContext nameContext, String stateName, - MetricsContainer metricsContainer, + @Nullable MetricsContainer metricsContainer, ProfileScope profileScope) { // TODO: Take in the requesting step name and side input index for streaming. super(nameContext, stateName, null, null, metricsContainer, profileScope); @@ -616,14 +641,16 @@ public static class StreamingModeExecutionStateRegistry extends DataflowExecutio protected DataflowExecutionState createState( NameContext nameContext, String stateName, - String requestingStepName, - Integer inputIndex, - MetricsContainer container, + @Nullable String requestingStepName, + @Nullable Integer inputIndex, + @Nullable MetricsContainer container, ProfileScope profileScope) { return new StreamingModeExecutionState(nameContext, stateName, container, profileScope); } } + private static final Closeable NO_OP_CLOSEABLE = () -> {}; + private static class ScopedReadStateSupplier implements Supplier { private final ExecutionState readState; @@ -636,9 +663,9 @@ private ScopedReadStateSupplier( } @Override - public @Nullable Closeable get() { + public Closeable get() { if (stateTracker == null) { - return null; + return NO_OP_CLOSEABLE; } return stateTracker.enterState(readState); } @@ -699,7 +726,7 @@ public TimerInternals timerInternals() { } @Override - public TimerData getNextFiredTimer(Coder windowCoder) { + public @Nullable TimerData getNextFiredTimer(Coder windowCoder) { return wrapped.getNextFiredUserTimer(windowCoder); } @@ -726,6 +753,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 +778,7 @@ public static StreamingModeSideInputReader of( } @Override - public T get(PCollectionView view, BoundedWindow window) { + public @Nullable T get(PCollectionView view, BoundedWindow window) { if (!contains(view)) { throw new RuntimeException("get() called with unknown view"); } @@ -779,31 +811,32 @@ public boolean isEmpty() { class StepContext extends DataflowExecutionContext.DataflowStepContext implements StreamingModeStepContext { - private final String stateFamily; + private final @Nullable String stateFamily; private final Supplier scopedReadStateSupplier; - private WindmillStateInternals stateInternals; - private WindmillTimerInternals systemTimerInternals; - private WindmillTimerInternals userTimerInternals; + private @MonotonicNonNull WindmillStateInternals stateInternals; + private @MonotonicNonNull WindmillTimerInternals systemTimerInternals; + private @MonotonicNonNull WindmillTimerInternals userTimerInternals; // Lazily initialized - private Iterator cachedFiredSystemTimers = null; + private @Nullable Iterator cachedFiredSystemTimers = null; // Lazily initialized - private PeekingIterator cachedFiredUserTimers = null; + private @Nullable PeekingIterator cachedFiredUserTimers = null; // An ordered list of any timers that were set or modified by user processing earlier in this // bundle. // We use a NavigableSet instead of a priority queue to prevent duplicate elements from ending // up in the queue. - private NavigableSet modifiedUserEventTimersOrdered = null; - private NavigableSet modifiedUserProcessingTimersOrdered = null; - private NavigableSet modifiedUserSynchronizedProcessingTimersOrdered = null; + private final NavigableSet modifiedUserEventTimersOrdered = Sets.newTreeSet(); + private final NavigableSet modifiedUserProcessingTimersOrdered = Sets.newTreeSet(); + private final NavigableSet modifiedUserSynchronizedProcessingTimersOrdered = + Sets.newTreeSet(); // A list of timer keys that were modified by user processing earlier in this bundle. This // serves a tombstone, so that we know not to fire any bundle timers that were modified. - private Table modifiedUserTimerKeys = null; + private final Table modifiedUserTimerKeys = + HashBasedTable.create(); private final WindmillBundleFinalizer bundleFinalizer = new WindmillBundleFinalizer(); public StepContext(DataflowOperationContext operationContext) { super(operationContext.nameContext()); this.stateFamily = getStateFamily(operationContext.nameContext()); - this.scopedReadStateSupplier = new ScopedReadStateSupplier(operationContext, getExecutionStateTracker()); } @@ -814,52 +847,66 @@ public void start( Instant processingTime, WindmillStateCache.ForKey cacheForKey, Watermarks watermarks) { - this.stateInternals = - new WindmillStateInternals<>( - key, - stateFamily, - stateReader, - getWorkItem().getIsNewKey(), - cacheForKey.forFamily(stateFamily), - windmillTagEncoding, - scopedReadStateSupplier); - - this.systemTimerInternals = - new WindmillTimerInternals( - stateFamily, - WindmillTimerType.SYSTEM_TIMER, - processingTime, - watermarks, - windmillTagEncoding, - td -> {}); - - this.userTimerInternals = - new WindmillTimerInternals( - stateFamily, - WindmillTimerType.USER_TIMER, - processingTime, - watermarks, - windmillTagEncoding, - this::onUserTimerModified); - + if (stateFamily != null) { + this.stateInternals = + new WindmillStateInternals<>( + key, + stateFamily, + stateReader, + getWorkItem().getIsNewKey(), + cacheForKey.forFamily(stateFamily), + windmillTagEncoding, + scopedReadStateSupplier); + + this.systemTimerInternals = + new WindmillTimerInternals( + stateFamily, + WindmillTimerType.SYSTEM_TIMER, + processingTime, + watermarks, + windmillTagEncoding, + td -> {}); + + this.userTimerInternals = + new WindmillTimerInternals( + stateFamily, + WindmillTimerType.USER_TIMER, + processingTime, + watermarks, + windmillTagEncoding, + this::onUserTimerModified); + } this.cachedFiredSystemTimers = null; this.cachedFiredUserTimers = null; - modifiedUserEventTimersOrdered = Sets.newTreeSet(); - modifiedUserProcessingTimersOrdered = Sets.newTreeSet(); - modifiedUserSynchronizedProcessingTimersOrdered = Sets.newTreeSet(); - modifiedUserTimerKeys = HashBasedTable.create(); + this.modifiedUserEventTimersOrdered.clear(); + this.modifiedUserProcessingTimersOrdered.clear(); + this.modifiedUserSynchronizedProcessingTimersOrdered.clear(); + this.modifiedUserTimerKeys.clear(); } public void flushState() { - stateInternals.persist(outputBuilder); - systemTimerInternals.persistTo(outputBuilder); - userTimerInternals.persistTo(outputBuilder); + if (stateFamily != null) { + WorkItemCommitRequest.Builder builder = getOutputBuilder(); + checkStateNotNull(stateInternals).persist(builder); + checkStateNotNull(systemTimerInternals).persistTo(builder); + checkStateNotNull(userTimerInternals).persistTo(builder); + } + } + + @Override + public void setBacklogBytes(double backlogBytes) { + StreamingModeExecutionContext.this.backlogBytes = (long) backlogBytes; } @Override - public TimerData getNextFiredTimer(Coder windowCoder) { - if (cachedFiredSystemTimers == null) { - cachedFiredSystemTimers = + public @Nullable TimerData getNextFiredTimer(Coder windowCoder) { + if (stateFamily == null) { + // no timers on stateless stages + return null; + } + Iterator firedSystemTimers = cachedFiredSystemTimers; + if (firedSystemTimers == null) { + firedSystemTimers = FluentIterable.from(StreamingModeExecutionContext.this.getFiredTimers()) .filter(timer -> timer.getStateFamily().equals(stateFamily)) .transform( @@ -871,16 +918,17 @@ timer, windowCoder, getDrainMode())) windmillTimerData.getWindmillTimerType() == WindmillTimerType.SYSTEM_TIMER) .transform(WindmillTimerData::getTimerData) .iterator(); + cachedFiredSystemTimers = firedSystemTimers; } - if (!cachedFiredSystemTimers.hasNext()) { + if (!firedSystemTimers.hasNext()) { return null; } - TimerData nextTimer = cachedFiredSystemTimers.next(); + TimerData nextTimer = firedSystemTimers.next(); // system timers ( GC timer) must be explicitly deleted if only there is a hold. // if timestamp is not equals to outputTimestamp then there should be a hold if (!nextTimer.getTimestamp().equals(nextTimer.getOutputTimestamp())) { - systemTimerInternals.deleteTimer(nextTimer); + checkStateNotNull(systemTimerInternals).deleteTimer(nextTimer); } return nextTimer; } @@ -914,12 +962,19 @@ private boolean isTimerUnmodified(TimerData timerData) { return updatedTimer == null || updatedTimer.equals(timerData); } - public TimerData getNextFiredUserTimer(Coder windowCoder) { - if (cachedFiredUserTimers == null) { + public @Nullable TimerData getNextFiredUserTimer( + Coder windowCoder) { + if (stateFamily == null) { + // no timers on stateless stages + return null; + } + + PeekingIterator firedUserTimers = cachedFiredUserTimers; + if (firedUserTimers == null) { // This is the first call to getNextFiredUserTimer in this bundle. Extract any user timers // from the bundle // and cache the list for the rest of this bundle processing. - cachedFiredUserTimers = + firedUserTimers = Iterators.peekingIterator( FluentIterable.from(StreamingModeExecutionContext.this.getFiredTimers()) .filter(timer -> timer.getStateFamily().equals(stateFamily)) @@ -933,17 +988,20 @@ timer, windowCoder, getDrainMode())) == WindmillTimerType.USER_TIMER) .transform(WindmillTimerData::getTimerData) .iterator()); + cachedFiredUserTimers = firedUserTimers; } - while (cachedFiredUserTimers.hasNext()) { - TimerData nextInBundle = cachedFiredUserTimers.peek(); + WindmillTimerInternals nonNullUserTimerInternals = checkStateNotNull(this.userTimerInternals); + + while (firedUserTimers.hasNext()) { + TimerData nextInBundle = firedUserTimers.peek(); NavigableSet modifiedUserTimersOrdered = getModifiedUserTimersOrdered(nextInBundle.getDomain()); // If there is a modified timer that is earlier than the next timer in the bundle, try and // fire that first. while (!modifiedUserTimersOrdered.isEmpty() && modifiedUserTimersOrdered.first().compareTo(nextInBundle) <= 0) { - TimerData earlierTimer = modifiedUserTimersOrdered.pollFirst(); + TimerData earlierTimer = checkStateNotNull(modifiedUserTimersOrdered.pollFirst()); if (isTimerUnmodified(earlierTimer)) { // We must delete the timer. This prevents it from being committed to the backing store. // It also handles the @@ -951,15 +1009,15 @@ timer, windowCoder, getDrainMode())) // without deleting the // timer, the runner will still have that future timer stored, and would fire it // spuriously. - userTimerInternals.deleteTimer(earlierTimer); + nonNullUserTimerInternals.deleteTimer(earlierTimer); return earlierTimer; } } // There is no earlier timer to fire, so return the next timer in the bundle. - nextInBundle = cachedFiredUserTimers.next(); + nextInBundle = firedUserTimers.next(); if (isTimerUnmodified(nextInBundle)) { // User timers must be explicitly deleted when delivered, to release the implied hold. - userTimerInternals.deleteTimer(nextInBundle); + nonNullUserTimerInternals.deleteTimer(nextInBundle); return nextInBundle; } } @@ -993,12 +1051,6 @@ public Iterable getSideInputNotifications() { return StreamingModeExecutionContext.this.getSideInputNotifications(); } - private void ensureStateful(String errorPrefix) { - if (stateFamily == null) { - throw new IllegalStateException(errorPrefix + " for stateless step: " + getNameContext()); - } - } - @Override public void writePCollectionViewData( TupleTag tag, @@ -1007,7 +1059,8 @@ public void writePCollectionViewData( W window, Coder windowCoder) throws IOException { - if (getSerializedKey().size() != 0) { + ByteString serializedKey = checkStateNotNull(getSerializedKey()); + if (serializedKey.size() != 0) { throw new IllegalStateException("writePCollectionViewData must follow a Combine.globally"); } @@ -1017,7 +1070,7 @@ public void writePCollectionViewData( ByteStringOutputStream windowStream = new ByteStringOutputStream(); windowCoder.encode(window, windowStream, Coder.Context.OUTER); - ensureStateful("Tried to write view data"); + String stateFamily = checkStateNotNull(this.stateFamily, "Tried to write view data"); Windmill.GlobalData.Builder builder = Windmill.GlobalData.newBuilder() @@ -1029,7 +1082,7 @@ public void writePCollectionViewData( .setData(dataStream.toByteString()) .setStateFamily(stateFamily); - outputBuilder.addGlobalDataUpdates(builder.build()); + getOutputBuilder().addGlobalDataUpdates(builder.build()); } /** Fetch the given side input asynchronously and return true if it is present. */ @@ -1044,11 +1097,12 @@ public boolean issueSideInputFetch( /** Note that there is data on the current key that is blocked on the given side input. */ @Override public void addBlockingSideInput(Windmill.GlobalDataRequest sideInput) { - ensureStateful("Tried to set global data request"); + String stateFamily = checkStateNotNull(this.stateFamily, "Tried to set global data request"); sideInput = Windmill.GlobalDataRequest.newBuilder(sideInput).setStateFamily(stateFamily).build(); - outputBuilder.addGlobalDataRequests(sideInput); - outputBuilder.addGlobalDataIdRequests(sideInput.getDataId()); + WorkItemCommitRequest.Builder builder = getOutputBuilder(); + builder.addGlobalDataRequests(sideInput); + builder.addGlobalDataIdRequests(sideInput.getDataId()); } /** Note that there is data on the current key that is blocked on the given side inputs. */ @@ -1061,14 +1115,12 @@ public void addBlockingSideInputs(Iterable sideInput @Override public StateInternals stateInternals() { - ensureStateful("Tried to access state"); - return checkNotNull(stateInternals); + return checkStateNotNull(stateInternals, "Tried to access state"); } @Override public TimerInternals timerInternals() { - ensureStateful("Tried to access timers"); - return checkNotNull(systemTimerInternals); + return checkStateNotNull(systemTimerInternals, "Tried to access timers"); } @Override @@ -1077,8 +1129,7 @@ public BundleFinalizer bundleFinalizer() { } public TimerInternals userTimerInternals() { - ensureStateful("Tried to access user timers"); - return checkNotNull(userTimerInternals); + return checkStateNotNull(userTimerInternals, "Tried to access user timers"); } public ImmutableList> flushBundleFinalizerCallbacks() { diff --git a/runners/google-cloud-dataflow-java/worker/src/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 2347529cf4a5..bd68adeddfb9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/UngroupedWindmillReader.java @@ -38,6 +38,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.sdk.values.WindowedValues.FullWindowedValueCoder; @@ -109,20 +110,12 @@ public NativeReader create( @Override public NativeReaderIterator> iterator() throws IOException { - return new UngroupedWindmillReaderIterator(context.getWorkItem()); + return new UngroupedWindmillReaderIterator(); } class UngroupedWindmillReaderIterator extends WindmillReaderIteratorBase { - UngroupedWindmillReaderIterator(Windmill.WorkItem work) { - super(work, skipUndecodableElements); - } - - @Override - public boolean advance() throws IOException { - if (context.workIsFailed()) { - return false; - } - return super.advance(); + UngroupedWindmillReaderIterator() { + super(context, skipUndecodableElements); } @Override @@ -139,6 +132,7 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce * drain happened upstream */ CausedByDrain drainingValueFromUpstream = CausedByDrain.NORMAL; + ValueKind valueKind = ValueKind.INSERT; if (WindowedValues.WindowedValueCoder.isMetadataSupported()) { BeamFnApi.Elements.ElementMetadata elementMetadata = WindmillSink.decodeAdditionalMetadata(windowsCoder, message.getMetadata()); @@ -146,6 +140,7 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce elementMetadata.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; + valueKind = WindmillValueKindHelper.fromProto(elementMetadata.getValueKind()); } if (valueCoder instanceof KvCoder) { KvCoder kvCoder = (KvCoder) valueCoder; @@ -155,10 +150,20 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce @SuppressWarnings("unchecked") T result = (T) KV.of(decode(kvCoder.getKeyCoder(), key), decode(kvCoder.getValueCoder(), data)); + // todo #37030 parse context from previous stage return WindowedValues.of( - result, timestampMillis, windows, paneInfo, null, null, drainingValueFromUpstream); + result, + timestampMillis, + windows, + paneInfo, + null, + null, + drainingValueFromUpstream, + null, + valueKind); } else { notifyElementRead(data.available() + metadata.available()); + // todo #37030 parse context from previous stage return WindowedValues.of( decode(valueCoder, data), timestampMillis, @@ -166,7 +171,9 @@ protected WindowedValue decodeMessage(Windmill.Message message) throws IOExce paneInfo, null, null, - drainingValueFromUpstream); + drainingValueFromUpstream, + null, + 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 c328719bfb50..c4c0b6ed92d3 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java @@ -42,6 +42,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.util.common.ElementByteSizeObserver; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicate; @@ -148,6 +149,7 @@ public Iterable timersIterable() { * drain happened upstream */ CausedByDrain drainingValueFromUpstream = CausedByDrain.NORMAL; + ValueKind valueKind = ValueKind.INSERT; if (WindowedValues.WindowedValueCoder.isMetadataSupported()) { BeamFnApi.Elements.ElementMetadata elementMetadata = WindmillSink.decodeAdditionalMetadata(windowsCoder, message.getMetadata()); @@ -155,11 +157,20 @@ public Iterable timersIterable() { elementMetadata.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; + valueKind = WindmillValueKindHelper.fromProto(elementMetadata.getValueKind()); } InputStream inputStream = message.getData().newInput(); ElemT value = valueCoder.decode(inputStream, Coder.Context.OUTER); return WindowedValues.of( - value, timestamp, windows, paneInfo, null, null, drainingValueFromUpstream); + value, + timestamp, + windows, + paneInfo, + null, + null, + drainingValueFromUpstream, + null, + valueKind); } catch (RuntimeException | IOException e) { if (!skipUndecodableElements) { throw new RuntimeException(e); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java index 7e6508a4788c..075a1a8a4250 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBase.java @@ -34,6 +34,7 @@ */ public abstract class WindmillReaderIteratorBase extends NativeReader.NativeReaderIterator> { + private final StreamingModeExecutionContext context; private final Windmill.WorkItem work; private int bundleIndex = 0; private int messageIndex = -1; @@ -42,9 +43,10 @@ public abstract class WindmillReaderIteratorBase private static final Logger LOG = LoggerFactory.getLogger(WindmillReaderIteratorBase.class); protected WindmillReaderIteratorBase( - Windmill.WorkItem work, ValueProvider skipUndecodableElements) { + StreamingModeExecutionContext context, ValueProvider skipUndecodableElements) { + this.context = context; this.skipUndecodableElements = skipUndecodableElements; - this.work = work; + this.work = context.getWorkItem(); } @Override @@ -54,9 +56,14 @@ public boolean start() throws IOException { @Override public boolean advance() throws IOException { + if (context.workIsFailed()) { + throw new WorkItemCancelledException(context.getWorkItem().getShardingKey()); + } + while (true) { if (bundleIndex >= work.getMessageBundlesCount()) { current = null; + context.finishKey(); return false; } Windmill.InputMessageBundle bundle = work.getMessageBundles(bundleIndex); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java index b07dc670e326..ae5941f2e82d 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillSink.java @@ -39,6 +39,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.transforms.windowing.PaneInfo.PaneInfoCoder; import org.apache.beam.sdk.util.ByteStringOutputStream; +import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.ValueWithRecordId; import org.apache.beam.sdk.values.ValueWithRecordId.ValueWithRecordIdCoder; @@ -220,7 +221,13 @@ public long add(WindowedValue data) throws IOException { ByteString id = ByteString.EMPTY; // todo #33176 specify additional metadata in the future BeamFnApi.Elements.ElementMetadata additionalMetadata = - BeamFnApi.Elements.ElementMetadata.newBuilder().build(); + BeamFnApi.Elements.ElementMetadata.newBuilder() + .setDrain( + data.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN + ? BeamFnApi.Elements.DrainMode.Enum.DRAINING + : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) + .setValueKind(WindmillValueKindHelper.toProto(data.getValueKind())) + .build(); ByteString metadata = encodeMetadata( stream, windowsCoder, data.getWindows(), data.getPaneInfo(), additionalMetadata); 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..488684769bd9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindowingWindmillReader.java @@ -156,6 +156,7 @@ public NativeReaderIterator>> iterator() throw return new NativeReaderIterator>>() { @Override public boolean start() throws IOException { + context.finishKey(); return false; } @@ -182,6 +183,7 @@ public boolean start() throws IOException { @Override public boolean advance() throws IOException { current = null; + context.finishKey(); return false; } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourceOperationExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourceOperationExecutor.java index 31528e96e07f..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/DataflowWorkerLoggingInitializer.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializer.java index 0f1b5c2750bc..d854ae74ebaf 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializer.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/logging/DataflowWorkerLoggingInitializer.java @@ -217,17 +217,16 @@ private static void applyOverridesToLogger( boolean directLoggingEnabled) { if (!directLoggingEnabled) { logger.setLevel(overrides.disk); - if (overrides.disk.intValue() != Level.OFF.intValue()) { + if (overrides.direct.intValue() != Level.OFF.intValue()) { LOG.warn( - "Ignoring the disk logging level override for {} because --defaultWorkerDirectLoggerLevel was OFF.", + "Ignoring the direct logging level override for {} because --defaultWorkerDirectLoggerLevel was OFF.", logger.getName()); } return; } // Configure the logger to accept logs of the lower value. The log will be sent directly based - // upon the default - // nonDirectLogLevel or the provided hint. + // upon the default nonDirectLogLevel or the provided hint. if (overrides.direct.intValue() < overrides.disk.intValue()) { logger.setLevel(overrides.direct); if (overrides.disk.intValue() != defaultNonDirectLogLevel.intValue()) { @@ -245,9 +244,8 @@ private static void applyOverridesToLogger( logger.setLevel(overrides.disk); if (overrides.disk.intValue() < defaultNonDirectLogLevel.intValue()) { // We need to provide the hint as otherwise the default would be used and send the log - // directly which is not - // desired. We don't bother if the threshold was increased as the right decision would still - // be made. + // directly which is not desired. We don't bother if the threshold was increased as the + // right decision would still be made. logger.setResourceBundle( DataflowWorkerLoggingHandler.resourceBundleForNonDirectLogLevelHint(overrides.disk)); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java new file mode 100644 index 000000000000..1ca534966947 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/BoundedQueueExecutorWorkHandle.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.streaming; + +/** + * A handle to use when requesting pulling more work from @BoundedQueueExecutor + * via @BoundedQueueExecutor.pollWork + */ +public interface BoundedQueueExecutorWorkHandle {} diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java index 8dc681fc640c..b4f3a22a7f52 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationWorkExecutor.java @@ -74,7 +74,7 @@ public final void executeWork( SideInputStateFetcher sideInputStateFetcher, Windmill.WorkItemCommitRequest.Builder outputBuilder) throws Exception { - context().start(key, work, stateReader, sideInputStateFetcher, outputBuilder); + context().start(key, work, stateReader, sideInputStateFetcher, outputBuilder, workExecutor()); workExecutor().execute(); } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/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/Work.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java index cb01e1e508ce..53ed30fdedbb 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/Work.java @@ -25,6 +25,7 @@ import java.util.IntSummaryStatistics; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -52,6 +53,7 @@ import org.apache.beam.sdk.annotations.Internal; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; import org.joda.time.Instant; @@ -74,6 +76,7 @@ public final class Work implements RefreshableWork { private final Instant startTime; private final Map totalDurationPerState; private final WorkId id; + private final KeyGroup keyGroup; private final String latencyTrackingId; private final long serializedWorkItemSize; private volatile TimedState currentState; @@ -101,6 +104,10 @@ private Work( // keyUniverse inside EnumMap every time. this.totalDurationPerState = new EnumMap<>(EMPTY_ENUM_MAP); this.id = WorkId.of(workItem); + this.keyGroup = + workItem.hasKeyGroup() + ? KeyGroup.create(workItem.getKeyGroup().getHigh(), workItem.getKeyGroup().getLow()) + : KeyGroup.DEFAULT; this.latencyTrackingId = Long.toHexString(workItem.getShardingKey()) + '-' @@ -383,6 +390,14 @@ private boolean isCommitPending() { abstract Instant startTime(); } + public String getComputationId() { + return processingContext.computationId(); + } + + public KeyGroup getKeyGroup() { + return keyGroup; + } + @AutoValue public abstract static class ProcessingContext { @@ -416,4 +431,60 @@ private Optional fetchKeyedState(KeyedGetDataRequest reque return Optional.ofNullable(getDataClient().getStateData(computationId(), request)); } } + + /** + * WorkItems with same key group and computation are eligible to be executed together in a + * multi-key bundle. + */ + public static final class KeyGroup { + + // Work items equaling to the default keyGroup will always be executed + // separately and not in a multi-key bundle + public static final KeyGroup DEFAULT = new KeyGroup(0, 0); + + private final long high; + private final long low; + + private KeyGroup(long high, long low) { + this.high = high; + this.low = low; + } + + public static KeyGroup create(long high, long low) { + if (high == 0 && low == 0) { + return DEFAULT; + } + return new KeyGroup(high, low); + } + + public long high() { + return high; + } + + public long low() { + return low; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof KeyGroup)) { + return false; + } + KeyGroup other = (KeyGroup) o; + return high == other.high && low == other.low; + } + + @Override + public int hashCode() { + return Objects.hash(high, low); + } + + @Override + public String toString() { + return String.format("%016x%016x", high, low); + } + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/config/StreamingEngineComputationConfigFetcher.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/config/StreamingEngineComputationConfigFetcher.java index 6ade84403713..08263bd2243a 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/config/StreamingEngineComputationConfigFetcher.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/streaming/config/StreamingEngineComputationConfigFetcher.java @@ -171,7 +171,7 @@ private StreamingGlobalConfig createPipelineConfig(StreamingConfigTask config) { ImmutableSet endpoints = StreamSupport.stream( Splitter.on(',').split(config.getWindmillServiceEndpoint()).spliterator(), - /* isParallel= */ false) + /* parallel= */ false) .map(endpoint -> HostAndPort.fromString(endpoint).withDefaultPort(windmillPort)) .collect(toImmutableSet()); diff --git a/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/util/BoundedQueueExecutor.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java index 9079c3cc69b8..8964246c1160 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutor.java @@ -17,6 +17,9 @@ */ package org.apache.beam.runners.dataflow.worker.util; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; + import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; @@ -24,13 +27,17 @@ 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.Work; +import org.apache.beam.runners.dataflow.worker.streaming.Work.KeyGroup; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Monitor.Guard; +import org.checkerframework.checker.nullness.qual.Nullable; /** An executor for executing work on windmill items. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public class BoundedQueueExecutor { private final ThreadPoolExecutor executor; @@ -38,7 +45,19 @@ public class BoundedQueueExecutor { // Used to guard elementsOutstanding and bytesOutstanding. private final Monitor monitor; - private final ConcurrentLinkedQueue decrementQueue = new ConcurrentLinkedQueue<>(); + + private static class Budget { + + final int elements; + final long bytes; + + Budget(int elements, long bytes) { + this.elements = elements; + this.bytes = bytes; + } + } + + private final ConcurrentLinkedQueue decrementQueue = new ConcurrentLinkedQueue<>(); private final Object decrementQueueDrainLock = new Object(); private final AtomicBoolean isDecrementBatchPending = new AtomicBoolean(false); private int elementsOutstanding = 0; @@ -59,6 +78,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 +88,9 @@ public BoundedQueueExecutor( int maximumElementsOutstanding, long maximumBytesOutstanding, ThreadFactory threadFactory, - boolean useFairMonitor) { + boolean useFairMonitor, + boolean useKeyGroupWorkQueue) { + this.keyGroupWorkQueue = useKeyGroupWorkQueue ? new KeyGroupWorkQueue(useFairMonitor) : null; this.maximumPoolSize = initialMaximumPoolSize; monitor = new Monitor(useFairMonitor); executor = @@ -75,7 +99,7 @@ public BoundedQueueExecutor( initialMaximumPoolSize, keepAliveTime, unit, - new LinkedBlockingQueue<>(), + keyGroupWorkQueue != null ? keyGroupWorkQueue : new LinkedBlockingQueue<>(), threadFactory) { @Override protected void beforeExecute(Thread t, Runnable r) { @@ -106,7 +130,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 +143,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 +251,115 @@ public String summaryHtml() { } } - private void executeMonitorHeld(Runnable work, long workBytes) { + /** + * A handle to use when requesting pulling more work from @BoundedQueueExecutor + * via @BoundedQueueExecutor.pollWork. A single handle aggregates all budgets from work pulled for + * inline execution and releases the budget after the multi work bundle is complete. + */ + final class BoundedQueueExecutorWorkHandleImpl + implements BoundedQueueExecutorWorkHandle, AutoCloseable { + + @GuardedBy("this") + private int elements; + + @GuardedBy("this") + private long bytes; + + @GuardedBy("this") + private boolean closed = false; + + private BoundedQueueExecutorWorkHandleImpl(int elements, long bytes) { + checkArgument(elements >= 0 && bytes >= 0); + this.elements = elements; + this.bytes = bytes; + } + + /** + * Merges the budget from another handle into this handle. + * + *

    This transfers the budget (elements and bytes) from the {@code other} handle to this + * handle, and marks the {@code other} handle as closed to prevent it from releasing the budget + * again if it is closed. + */ + public void merge(BoundedQueueExecutorWorkHandleImpl other) { + checkArgumentNotNull(other); + synchronized (this) { + Preconditions.checkState(!closed, "Cannot merge into a closed handle"); + synchronized (other) { + Preconditions.checkState(!other.closed, "Cannot merge a closed handle"); + this.elements += other.elements; + this.bytes += other.bytes; + other.closed = true; + other.elements = 0; + other.bytes = 0; + } + } + } + + public synchronized boolean isClosed() { + return closed; + } + + @VisibleForTesting + synchronized int elements() { + return elements; + } + + @VisibleForTesting + synchronized long bytes() { + return bytes; + } + + @Override + public synchronized void close() { + if (closed) return; + closed = true; + decrementCounters(this.elements, this.bytes); + } + } + + static final class QueuedWork implements Runnable { + + private final ExecutableWork work; + private final BoundedQueueExecutorWorkHandleImpl handle; + + public QueuedWork(ExecutableWork work, BoundedQueueExecutorWorkHandleImpl handle) { + this.work = work; + this.handle = handle; + } + + public ExecutableWork getWork() { + return work; + } + + public BoundedQueueExecutorWorkHandleImpl getHandle() { + return handle; + } + + @Override + public void run() { + checkArgument(!handle.isClosed()); + try (handle) { + work.run(handle); + } + } + } + + private void executeMonitorHeld(ExecutableWork work, long workBytes) { + ++elementsOutstanding; bytesOutstanding += workBytes; + monitor.leave(); + BoundedQueueExecutorWorkHandleImpl handle = + new BoundedQueueExecutorWorkHandleImpl(1, workBytes); + try { + executor.execute(new QueuedWork(work, handle)); + } catch (Throwable t) { + handle.close(); + throw ExceptionUtils.safeWrapThrowableAsException(t); + } + } + + private void executeMonitorHeld(Runnable work) { ++elementsOutstanding; monitor.leave(); @@ -232,21 +369,41 @@ 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(int elements, long bytes) { + return new BoundedQueueExecutorWorkHandleImpl(elements, bytes); + } + + public @Nullable ExecutableWork pollWork( + String computationId, Work.KeyGroup keyGroup, BoundedQueueExecutorWorkHandle handle) { + checkArgument(handle instanceof BoundedQueueExecutorWorkHandleImpl); + checkArgument(computationId != null && keyGroup != null && !keyGroup.equals(KeyGroup.DEFAULT)); + BoundedQueueExecutorWorkHandleImpl internalHandle = (BoundedQueueExecutorWorkHandleImpl) handle; + if (keyGroupWorkQueue == null) { + return null; + } + @Nullable QueuedWork queuedWork = keyGroupWorkQueue.pollWork(computationId, keyGroup); + if (queuedWork == null) { + return null; } + internalHandle.merge(queuedWork.getHandle()); + return queuedWork.getWork(); } - private void decrementCounters(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 +422,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/samza/src/main/java/org/apache/beam/runners/samza/util/PortableConfigUtils.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java similarity index 50% rename from runners/samza/src/main/java/org/apache/beam/runners/samza/util/PortableConfigUtils.java rename to runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java index ee0bd7b72bb0..d04a385d6e6a 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/PortableConfigUtils.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ExceptionUtils.java @@ -15,29 +15,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.runners.samza.util; +package org.apache.beam.runners.dataflow.worker.util; -import java.util.Map; -import org.apache.beam.runners.samza.SamzaPipelineOptions; +import javax.annotation.CheckReturnValue; +import org.apache.beam.sdk.annotations.Internal; -/** A utility class to encapsulate. */ -public class PortableConfigUtils { - public static final String BEAM_PORTABLE_MODE = "beam.portable.mode"; +/** Utility methods for simplifying work with exceptions and throwables. */ +@Internal +public final class ExceptionUtils { - private PortableConfigUtils() {} + private ExceptionUtils() {} /** - * A helper method to distinguish if a pipeline is run using portable mode or classic mode. - * - * @param options pipeline options - * @return true if the pipeline is run in portable mode + * 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}. */ - public static boolean isPortable(SamzaPipelineOptions options) { - Map override = options.getConfigOverride(); - if (override == null) { - return false; + @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); } - - return Boolean.parseBoolean(override.getOrDefault(BEAM_PORTABLE_MODE, "false")); } } 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/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineExceptionContext.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogram.java similarity index 52% rename from runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineExceptionContext.java rename to runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogram.java index 5bd02b3fc737..6b90ca8df9ef 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineExceptionContext.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogram.java @@ -15,23 +15,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.runners.samza; +package org.apache.beam.runners.dataflow.worker.util; -/** Helper that is used to metadata associated with an exception thrown by Samza Runner. */ -public class SamzaPipelineExceptionContext { - private final String transformFullName; - private final Exception exception; +/** A simple histogram to track byte sizes. */ +public class SimpleByteHistogram { + private final long[] buckets = new long[7]; - public SamzaPipelineExceptionContext(String transformFullName, Exception exception) { - this.transformFullName = transformFullName; - this.exception = exception; + public void add(long weight) { + buckets[getBucket(weight)]++; } - public String getTransformFullName() { - return transformFullName; + private int getBucket(long weight) { + if (weight < 128) return 0; + if (weight < 256) return 1; + if (weight < 512) return 2; + if (weight < 1024) return 3; + if (weight < 10 * 1024) return 4; + if (weight < 1024 * 1024) return 5; + return 6; } - public Exception getException() { - return exception; + public String format() { + return String.format( + "[<128B:%d, <256B:%d, <512B:%d, <1KB:%d, <10KB:%d, <1MB:%d, >=1MB:%d]", + buckets[0], buckets[1], buckets[2], buckets[3], buckets[4], buckets[5], buckets[6]); } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java index 2f7a5ce54fbf..d5a493577f46 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/util/ValueInEmptyWindows.java @@ -17,12 +17,14 @@ */ package org.apache.beam.runners.dataflow.worker.util; +import io.opentelemetry.context.Context; import java.util.Collection; import java.util.Collections; import java.util.Objects; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.checkerframework.checker.nullness.qual.Nullable; @@ -65,6 +67,16 @@ public CausedByDrain causedByDrain() { return CausedByDrain.NORMAL; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return null; + } + + @Override + public ValueKind getValueKind() { + return ValueKind.INSERT; + } + @Override public Iterable> explodeWindows() { return Collections.emptyList(); diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/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/grpc/GrpcCommitWorkStream.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java index d24676652fd8..160b0cce0133 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStream.java @@ -20,7 +20,6 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; -import com.google.auto.value.AutoValue; import java.io.PrintWriter; import java.time.Duration; import java.util.HashMap; @@ -77,6 +76,7 @@ private static class StreamAndRequest { private final JobHeader jobHeader; private final int streamingRpcBatchLimit; private volatile boolean logMissingResponse = true; + private final Duration maxRetryDuration; private GrpcCommitWorkStream( String backendWorkerToken, @@ -90,6 +90,7 @@ private GrpcCommitWorkStream( AtomicLong idGenerator, int streamingRpcBatchLimit, Duration halfClosePhysicalStreamAfter, + Duration maxRetryDuration, ScheduledExecutorService executor) { super( LOG, @@ -104,6 +105,7 @@ private GrpcCommitWorkStream( this.idGenerator = idGenerator; this.jobHeader = jobHeader; this.streamingRpcBatchLimit = streamingRpcBatchLimit; + this.maxRetryDuration = maxRetryDuration; } static GrpcCommitWorkStream create( @@ -118,6 +120,7 @@ static GrpcCommitWorkStream create( AtomicLong idGenerator, int streamingRpcBatchLimit, Duration halfClosePhysicalStreamAfter, + Duration maxRetryDuration, ScheduledExecutorService executor) { return new GrpcCommitWorkStream( backendWorkerToken, @@ -130,6 +133,7 @@ static GrpcCommitWorkStream create( idGenerator, streamingRpcBatchLimit, halfClosePhysicalStreamAfter, + maxRetryDuration, executor); } @@ -224,14 +228,48 @@ public void onResponse(StreamingCommitResponse response) { failureHandler.throwIfNonEmpty(); } - @Override @SuppressWarnings("ReferenceEquality") + private boolean belongsToThisHandler(StreamAndRequest streamAndRequest) { + return streamAndRequest.handler == this; + } + + @Override public boolean hasPendingRequests() { - return pending.entrySet().stream().anyMatch(e -> e.getValue().handler == this); + return pending.entrySet().stream().anyMatch(e -> belongsToThisHandler(e.getValue())); } @Override + @SuppressWarnings("ReferenceEquality") public void onDone(Status status) { + if (maxRetryDuration.compareTo(Duration.ZERO) > 0) { + // Remove the requests that have exceeded the retry time so they are not retried. + long nowNanos = System.nanoTime(); + long maxRetryDurationNanos = maxRetryDuration.toNanos(); + Iterator> iterator = pending.entrySet().iterator(); + int keptRequests = 0, removedRequests = 0; + while (iterator.hasNext()) { + StreamAndRequest streamAndRequest = checkNotNull(iterator.next().getValue()); + PendingRequest pendingRequest = streamAndRequest.request; + if (!belongsToThisHandler(streamAndRequest) + || nowNanos - pendingRequest.getStartTimeNanos() < maxRetryDurationNanos) { + ++keptRequests; + continue; + } + ++removedRequests; + iterator.remove(); + try { + pendingRequest.completeWithStatus(CommitStatus.ABORTED); + } catch (RuntimeException e) { + LOG.warn("Exception while aborting commit due to retry timeout.", e); + } + } + if (removedRequests > 0) { + LOG.info( + "Aborting {} commits which have exceeded retry deadline, kept {}. Work will be retried as needed by service.", + removedRequests, + keptRequests); + } + } if (status.isOk() && hasPendingRequests()) { LOG.warn("Unexpected requests without responses on drained physical stream, retrying."); } @@ -270,7 +308,7 @@ private void flushInternal(Map requests) if (requests.size() == 1) { Map.Entry elem = requests.entrySet().iterator().next(); - if (elem.getValue().request().getSerializedSize() + if (elem.getValue().getRequest().getSerializedSize() > AbstractWindmillStream.RPC_STREAM_CHUNK_SIZE) { issueMultiChunkRequest(elem.getKey(), elem.getValue()); } else { @@ -286,7 +324,7 @@ private void issueSingleRequest(long id, PendingRequest pendingRequest) StreamingCommitWorkRequest.Builder requestBuilder = StreamingCommitWorkRequest.newBuilder(); requestBuilder .addCommitChunkBuilder() - .setComputationId(pendingRequest.computationId()) + .setComputationId(pendingRequest.getComputationId()) .setRequestId(id) .setShardingKey(pendingRequest.shardingKey()) .setSerializedWorkItemCommit(pendingRequest.serializedCommit()); @@ -311,9 +349,9 @@ private void issueBatchedRequest(Map requests) for (Map.Entry entry : requests.entrySet()) { PendingRequest request = entry.getValue(); StreamingCommitRequestChunk.Builder chunkBuilder = requestBuilder.addCommitChunkBuilder(); - if (lastComputation == null || !lastComputation.equals(request.computationId())) { - chunkBuilder.setComputationId(request.computationId()); - lastComputation = request.computationId(); + if (lastComputation == null || !lastComputation.equals(request.getComputationId())) { + chunkBuilder.setComputationId(request.getComputationId()); + lastComputation = request.getComputationId(); } chunkBuilder .setRequestId(entry.getKey()) @@ -338,7 +376,7 @@ private void issueBatchedRequest(Map requests) private void issueMultiChunkRequest(long id, PendingRequest pendingRequest) throws WindmillStreamShutdownException { - checkNotNull(pendingRequest.computationId(), "Cannot commit WorkItem w/o a computationId."); + checkNotNull(pendingRequest.getComputationId(), "Cannot commit WorkItem w/o a computationId."); ByteString serializedCommit = pendingRequest.serializedCommit(); synchronized (this) { if (isShutdown) { @@ -359,7 +397,7 @@ private void issueMultiChunkRequest(long id, PendingRequest pendingRequest) StreamingCommitRequestChunk.newBuilder() .setRequestId(id) .setSerializedWorkItemCommit(chunk) - .setComputationId(pendingRequest.computationId()) + .setComputationId(pendingRequest.getComputationId()) .setShardingKey(pendingRequest.shardingKey()); int remaining = serializedCommit.size() - end; if (remaining > 0) { @@ -376,34 +414,46 @@ private void issueMultiChunkRequest(long id, PendingRequest pendingRequest) } } - @AutoValue - abstract static class PendingRequest { + private static class PendingRequest { + private final String computationId; + private final WorkItemCommitRequest request; + private final Consumer onDone; + private final long startTimeNanos; // System.nanoTime() of when request began. - private static PendingRequest create( + private PendingRequest( String computationId, WorkItemCommitRequest request, Consumer onDone) { - return new AutoValue_GrpcCommitWorkStream_PendingRequest(computationId, request, onDone); + this.computationId = computationId; + this.request = request; + this.onDone = onDone; + this.startTimeNanos = System.nanoTime(); } - abstract String computationId(); + String getComputationId() { + return computationId; + } - abstract WorkItemCommitRequest request(); + WorkItemCommitRequest getRequest() { + return request; + } - abstract Consumer onDone(); + long getStartTimeNanos() { + return startTimeNanos; + } private long getBytes() { - return (long) request().getSerializedSize() + computationId().length(); + return (long) request.getSerializedSize() + computationId.length(); } private ByteString serializedCommit() { - return request().toByteString(); + return request.toByteString(); } private void completeWithStatus(CommitStatus commitStatus) { - onDone().accept(commitStatus); + onDone.accept(commitStatus); } private long shardingKey() { - return request().getShardingKey(); + return request.getShardingKey(); } private void abort() { @@ -462,7 +512,7 @@ public boolean commitWorkItem( return false; } - PendingRequest request = PendingRequest.create(computation, commitRequest, onDone); + PendingRequest request = new PendingRequest(computation, commitRequest, onDone); add(idGenerator.incrementAndGet(), request); return true; } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java index 4d5acdc8071e..2a7823e69091 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcWindmillServer.java @@ -55,6 +55,7 @@ import org.apache.beam.runners.dataflow.worker.windmill.client.grpc.stubs.WindmillStubFactoryFactory; import org.apache.beam.runners.dataflow.worker.windmill.work.WorkItemReceiver; import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; +import org.apache.beam.sdk.options.ExperimentalOptions; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.util.BackOff; import org.apache.beam.sdk.util.BackOffUtils; @@ -113,13 +114,13 @@ private static DataflowWorkerHarnessOptions testOptions( options.setProject("project"); options.setJobId("job"); options.setWorkerId("worker"); - List experiments = - options.getExperiments() == null ? new ArrayList<>() : options.getExperiments(); + if (enableStreamingEngine) { - experiments.add(GcpOptions.STREAMING_ENGINE_EXPERIMENT); + ExperimentalOptions.addExperiment(options, GcpOptions.STREAMING_ENGINE_EXPERIMENT); + } + for (String experiment : additionalExperiments) { + ExperimentalOptions.addExperiment(options, experiment); } - experiments.addAll(additionalExperiments); - options.setExperiments(experiments); options.setWindmillServiceStreamingRpcBatchLimit(Integer.MAX_VALUE); options.setWindmillServiceStreamingRpcHealthCheckPeriodMs(NO_HEALTH_CHECK); 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/WindmillStateCache.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCache.java index 07c9599c866a..7515db000852 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCache.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCache.java @@ -32,6 +32,7 @@ import org.apache.beam.runners.dataflow.worker.status.BaseStatusServlet; import org.apache.beam.runners.dataflow.worker.status.StatusDataProvider; import org.apache.beam.runners.dataflow.worker.streaming.ShardedKey; +import org.apache.beam.runners.dataflow.worker.util.SimpleByteHistogram; import org.apache.beam.runners.dataflow.worker.util.common.worker.InternedByteString; import org.apache.beam.sdk.state.State; import org.apache.beam.sdk.util.Weighted; @@ -75,9 +76,18 @@ public class WindmillStateCache implements StatusDataProvider { private final ConcurrentMap keyIndex; private final long workerCacheBytes; // Copy workerCacheMb and convert to bytes. private final boolean supportMapViaMultimap; - - WindmillStateCache(long sizeMb, boolean supportMapViaMultimap) { + private final long defaultMaxCachedEntryBytes; + private final boolean enableHistogram; + private volatile long maxCachedEntryBytesOverride = -1L; + + WindmillStateCache( + long sizeMb, + boolean supportMapViaMultimap, + long maxCachedEntryBytes, + boolean enableHistogram) { this.workerCacheBytes = sizeMb * MEGABYTES; + this.defaultMaxCachedEntryBytes = maxCachedEntryBytes; + this.enableHistogram = enableHistogram; int stateCacheConcurrencyLevel = Math.max(STATE_CACHE_CONCURRENCY_LEVEL, Runtime.getRuntime().availableProcessors()); this.stateCache = @@ -99,11 +109,27 @@ public interface Builder { Builder setSupportMapViaMultimap(boolean supportMapViaMultimap); + Builder setMaxCachedEntryBytes(long maxCachedEntryBytes); + + Builder setEnableHistogram(boolean enableHistogram); + WindmillStateCache build(); } public static Builder builder() { - return new AutoBuilder_WindmillStateCache_Builder().setSupportMapViaMultimap(false); + return new AutoBuilder_WindmillStateCache_Builder() + .setSupportMapViaMultimap(false) + .setMaxCachedEntryBytes(Long.MAX_VALUE) + .setEnableHistogram(true); + } + + public void setMaxCachedEntryBytesOverride(long limit) { + this.maxCachedEntryBytesOverride = limit; + } + + private long getMaxCachedEntryBytesLimit() { + long override = maxCachedEntryBytesOverride; + return override >= 0 ? override : defaultMaxCachedEntryBytes; } private EntryStats calculateEntryStats() { @@ -111,10 +137,20 @@ private EntryStats calculateEntryStats() { BiConsumer consumer = (stateId, stateCacheEntry) -> { stats.entries++; - stats.idWeight += stateId.getWeight(); - stats.entryWeight += stateCacheEntry.getWeight(); + long idWeight = stateId.getWeight(); + stats.idWeight += idWeight; + long entryWeight = stateCacheEntry.getWeight(); + stats.entryWeight += entryWeight; stats.entryValues += stateCacheEntry.values.size(); stats.maxEntryValues = Math.max(stats.maxEntryValues, stateCacheEntry.values.size()); + if (enableHistogram) { + stats.addKeyWeight(idWeight); + stats.addEntryWeight(entryWeight); + stateCacheEntry.values.forEach( + (encodedAddress, weightedValue) -> { + stats.addValueWeight(weightedValue.weight); + }); + } }; stateCache.asMap().forEach(consumer); return stats; @@ -142,23 +178,44 @@ public ForComputation forComputation(String computation) { @Override public void appendSummaryHtml(PrintWriter response) { response.println("Cache Stats:
    "); - response.println( - "" - + "" - + "" - + ""); CacheStats cacheStats = stateCache.stats(); EntryStats entryStats = calculateEntryStats(); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println(""); - response.println("
    Hit RatioEvictionsEntriesEntry ValuesMax Entry ValuesId WeightEntry WeightMax WeightKeys
    " + cacheStats.hitRate() + "" + cacheStats.evictionCount() + "" + entryStats.entries + "(" + stateCache.size() + " inc. weak) " + entryStats.entryValues + "" + entryStats.maxEntryValues + "" + entryStats.idWeight / MEGABYTES + "MB" + entryStats.entryWeight / MEGABYTES + "MB" + getMaxWeight() / MEGABYTES + "MB" + keyIndex.size() + "

    "); + + response.println("Hit Ratio" + cacheStats.hitRate() + ""); + response.println("Evictions" + cacheStats.evictionCount() + ""); + response.println( + "Entries" + + entryStats.entries + + " (" + + stateCache.size() + + " inc. weak)"); + response.println("Entry Values" + entryStats.entryValues + ""); + response.println( + "Max Entry Values" + entryStats.maxEntryValues + ""); + response.println( + "Id Weight" + entryStats.idWeight / MEGABYTES + "MB"); + response.println( + "Entry Weight" + entryStats.entryWeight / MEGABYTES + "MB"); + response.println("Max Weight" + getMaxWeight() / MEGABYTES + "MB"); + response.println("Keys" + keyIndex.size() + ""); + response.println( + "Entry Size Limit" + getMaxCachedEntryBytesLimit() + " bytes"); + if (enableHistogram) { + response.println( + "Entry Weight Dist" + + entryStats.entryWeightHistogram.format() + + ""); + response.println( + "Value Weight Dist" + + entryStats.valueWeightHistogram.format() + + ""); + response.println( + "Key Weight Dist" + + entryStats.keyWeightHistogram.format() + + ""); + } + + response.println("
    "); } public BaseStatusServlet statusServlet() { @@ -180,6 +237,21 @@ private static class EntryStats { long entryWeight; long entryValues; long maxEntryValues; + SimpleByteHistogram entryWeightHistogram = new SimpleByteHistogram(); + SimpleByteHistogram valueWeightHistogram = new SimpleByteHistogram(); + SimpleByteHistogram keyWeightHistogram = new SimpleByteHistogram(); + + void addEntryWeight(long weight) { + entryWeightHistogram.add(weight); + } + + void addValueWeight(long weight) { + valueWeightHistogram.add(weight); + } + + void addKeyWeight(long weight) { + keyWeightHistogram.add(weight); + } } /** @@ -413,7 +485,15 @@ public void put( } public void persist() { - localCache.forEach(stateCache::put); + long limit = WindmillStateCache.this.getMaxCachedEntryBytesLimit(); + localCache.forEach( + (id, entry) -> { + if (entry.getWeight() <= limit) { + stateCache.put(id, entry); + } else { + stateCache.invalidate(id); + } + }); } } } diff --git a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillWatermarkHold.java index 613d87c127b7..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 @@ -72,9 +76,24 @@ public class WindmillWatermarkHold extends WindmillState implements WatermarkHol @Override public void clear() { + localAdditions = null; + if (cachedValue != null && !cachedValue.isPresent()) { + // No need to clear the backend as it is known empty. + return; + } cleared = true; cachedValue = Optional.absent(); - localAdditions = null; + } + + @Override + 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 @@ -133,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 (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)); - 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); - - 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/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..a3f23aebdf8f 100644 --- a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java +++ b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/StreamingWorkScheduler.java @@ -35,6 +35,7 @@ import org.apache.beam.runners.dataflow.worker.ReaderCache; import org.apache.beam.runners.dataflow.worker.WorkItemCancelledException; import org.apache.beam.runners.dataflow.worker.logging.DataflowWorkerLoggingMDC; +import org.apache.beam.runners.dataflow.worker.streaming.BoundedQueueExecutorWorkHandle; import org.apache.beam.runners.dataflow.worker.streaming.ComputationState; import org.apache.beam.runners.dataflow.worker.streaming.ComputationWorkExecutor; import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; @@ -47,6 +48,7 @@ import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcherFactory; import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor; +import org.apache.beam.runners.dataflow.worker.util.ExceptionUtils; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.Windmill.LatencyAttribution; import org.apache.beam.runners.dataflow.worker.windmill.client.commits.Commit; @@ -214,11 +216,13 @@ 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))); + (work, handle) -> processWork(computationState, work, getWorkStreamLatencies, handle))); } /** Adds any applied finalize ids to the commit finalizer to have their callbacks executed. */ @@ -232,16 +236,19 @@ public void queueAppliedFinalizeIds(ImmutableList appliedFinalizeIds) { * internally if processing fails due to uncaught {@link Exception}(s). * * @implNote This will block the calling thread during execution of user DoFns. + * @param handle handled to pass to BoundedQueueExecutor.pollWork, currently unused */ private void processWork( ComputationState computationState, Work work, - ImmutableList getWorkStreamLatencies) { + ImmutableList getWorkStreamLatencies, + BoundedQueueExecutorWorkHandle handle) { work.recordGetWorkStreamLatencies(getWorkStreamLatencies); - processWork(computationState, work); + processWork(computationState, work, handle); } - private void processWork(ComputationState computationState, Work work) { + private void processWork( + ComputationState computationState, Work work, BoundedQueueExecutorWorkHandle unusedHandle) { Windmill.WorkItem workItem = work.getWorkItem(); String computationId = computationState.getComputationId(); ByteString key = workItem.getKey(); @@ -250,9 +257,6 @@ private void processWork(ComputationState computationState, Work work) { 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()); if (workItem.getSourceState().getOnlyFinalize()) { Windmill.WorkItemCommitRequest.Builder outputBuilder = initializeOutputBuilder(key, workItem); outputBuilder.setSourceStateUpdates(Windmill.SourceState.newBuilder().setOnlyFinalize(true)); @@ -289,7 +293,7 @@ private void processWork(ComputationState computationState, Work work) { try { workFailureProcessor.logAndProcessFailure( computationId, - ExecutableWork.create(work, retry -> processWork(computationState, retry)), + ExecutableWork.create(work, (retry, h) -> processWork(computationState, retry, h)), t, invalidWork -> computationState.completeWorkAndScheduleNextWorkForKey( @@ -297,7 +301,8 @@ private void processWork(ComputationState computationState, Work work) { } catch (OutOfMemoryError oom) { throw oom; } catch (Throwable t2) { - throw new RuntimeException(t2); + LOG.warn("Failed to process work failure safely for work {}", work.id(), t2); + throw ExceptionUtils.safeWrapThrowableAsException(t2); } } finally { // Update total processing time counters. Updating in finally clause ensures that diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/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/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 3a25a671ca92..5bcdffcc2564 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingDataflowWorkerTest.java @@ -373,7 +373,9 @@ private static ExecutableWork createMockWork( computationId, new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.class)), false, Instant::now), - processWorkFn); + (work, handle) -> { + processWorkFn.accept(work); + }); } private byte[] intervalWindowBytes(IntervalWindow window) throws Exception { @@ -1807,6 +1809,7 @@ public void testMergeWindows() throws Exception { String timerTagPrefix = "/s" + window + "+0"; ByteString bufferTag = ByteString.copyFromUtf8(window + "+ubuf"); ByteString paneInfoTag = ByteString.copyFromUtf8(window + "+upaneInfo"); + ByteString combinedMetadataTag = ByteString.copyFromUtf8(window + "+ucombinedMetadata"); String watermarkDataHoldTag = window + "+uhold"; String watermarkExtraHoldTag = window + "+uextra"; String stateFamily = "MergeWindows"; @@ -1836,10 +1839,10 @@ public void testMergeWindows() throws Exception { // Set timer verifyTimers(actualOutput, buildWatermarkTimer(timerTagPrefix, 999)); - + // no combined metadata as it's default assertThat( actualOutput.getBagUpdatesList(), - Matchers.contains( + Matchers.containsInAnyOrder( Matchers.equalTo( Windmill.TagBag.newBuilder() .setTag(bufferTag) @@ -1915,6 +1918,13 @@ public void testMergeWindows() throws Exception { .getValueBuilder() .setTimestamp(0) .setData(ByteString.EMPTY); + dataBuilder + .addBagsBuilder() + .setTag(combinedMetadataTag) + .setStateFamily(stateFamily) + // 0x02: Protobuf Delimited Length (Payload is 2 bytes), 0x08: Protobuf Tag + // (Field #1), 0x01: Protobuf Value (Enum 1 = NOT_DRAINING). + .addValues(ByteString.copyFrom(new byte[] {0x02, 0x08, 0x01})); server.whenGetDataCalled().thenReturn(dataResponse.build()); expectedBytesRead += dataBuilder.build().getSerializedSize(); @@ -1960,12 +1970,18 @@ public void testMergeWindows() throws Exception { assertThat( "" + actualOutput.getBagUpdatesList(), actualOutput.getBagUpdatesList(), - Matchers.contains( + Matchers.containsInAnyOrder( Matchers.equalTo( Windmill.TagBag.newBuilder() .setTag(bufferTag) .setStateFamily(stateFamily) .setDeleteAll(true) + .build()), + Matchers.equalTo( + Windmill.TagBag.newBuilder() + .setTag(combinedMetadataTag) + .setStateFamily(stateFamily) + .setDeleteAll(true) .build()))); verifyHolds( @@ -2097,6 +2113,7 @@ public void testMergeWindowsCaching() throws Exception { String timerTagPrefix = "/s" + window + "+0"; ByteString bufferTag = ByteString.copyFromUtf8(window + "+ubuf"); ByteString paneInfoTag = ByteString.copyFromUtf8(window + "+upaneInfo"); + ByteString combinedMetadataTag = ByteString.copyFromUtf8(window + "+ucombinedMetadata"); String watermarkDataHoldTag = window + "+uhold"; String watermarkExtraHoldTag = window + "+uextra"; String stateFamily = "MergeWindows"; @@ -2127,9 +2144,10 @@ public void testMergeWindowsCaching() throws Exception { // Set timer verifyTimers(actualOutput, buildWatermarkTimer(timerTagPrefix, 999)); + // no combinedMetadataTag as it is default assertThat( actualOutput.getBagUpdatesList(), - Matchers.contains( + Matchers.containsInAnyOrder( Matchers.equalTo( Windmill.TagBag.newBuilder() .setTag(bufferTag) @@ -2205,6 +2223,11 @@ public void testMergeWindowsCaching() throws Exception { .getValueBuilder() .setTimestamp(0) .setData(ByteString.EMPTY); + dataBuilder + .addBagsBuilder() + .setTag(combinedMetadataTag) + .setStateFamily(stateFamily) + .addValues(ByteString.copyFrom(new byte[] {0x02, 0x08, 0x01})); server.whenGetDataCalled().thenReturn(dataResponse.build()); expectedBytesRead += dataBuilder.build().getSerializedSize(); @@ -2250,12 +2273,18 @@ public void testMergeWindowsCaching() throws Exception { assertThat( "" + actualOutput.getBagUpdatesList(), actualOutput.getBagUpdatesList(), - Matchers.contains( + Matchers.containsInAnyOrder( Matchers.equalTo( Windmill.TagBag.newBuilder() .setTag(bufferTag) .setStateFamily(stateFamily) .setDeleteAll(true) + .build()), + Matchers.equalTo( + Windmill.TagBag.newBuilder() + .setTag(combinedMetadataTag) + .setStateFamily(stateFamily) + .setDeleteAll(true) .build()))); verifyHolds( @@ -2374,9 +2403,6 @@ public void testMergeSessionWindows_singleLateWindow() throws Exception { new Action( buildSessionInput( 1, 40, 0, Collections.singletonList(1L), Collections.EMPTY_LIST)) - .withHolds( - buildHold("/gAAAAAAAAAsK/+uhold", -1, true), - buildHold("/gAAAAAAAAAsK/+uextra", -1, true)) .withTimers(buildWatermarkTimer("/s/gAAAAAAAAAsK/+0", 3600010)))); } @@ -2404,10 +2430,7 @@ public void testMergeSessionWindows() throws Exception { 0, Collections.EMPTY_LIST, Collections.singletonList(buildWatermarkTimer("/s/gAAAAAAAAAsK/+0", 10)))) - .withTimers(buildWatermarkTimer("/s/gAAAAAAAAAsK/+0", 3600010)) - .withHolds( - buildHold("/gAAAAAAAAAsK/+uhold", -1, true), - buildHold("/gAAAAAAAAAsK/+uextra", -1, true)), + .withTimers(buildWatermarkTimer("/s/gAAAAAAAAAsK/+0", 3600010)), new Action( buildSessionInput( 3, 30, 0, Collections.singletonList(8L), Collections.EMPTY_LIST)) @@ -2436,8 +2459,8 @@ public void testMergeSessionWindows() throws Exception { .withHolds( buildHold("/gAAAAAAAACkK/+uhold", -1, true), buildHold("/gAAAAAAAACkK/+uextra", -1, true), - buildHold("/gAAAAAAAAAsK/+uhold", 40, true), - buildHold("/gAAAAAAAAAsK/+uextra", 3600040, true)), + buildHold("/gAAAAAAAAAsK/+uhold", 40, false), + buildHold("/gAAAAAAAAAsK/+uextra", 3600040, false)), new Action( buildSessionInput( 6, @@ -3013,7 +3036,8 @@ public void testMaxThreadMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3074,7 +3098,8 @@ public void testActiveThreadMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3144,7 +3169,8 @@ public void testOutstandingBytesMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3218,7 +3244,8 @@ public void testOutstandingBundlesMetric() throws Exception { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); ComputationState computationState = new ComputationState( @@ -3636,8 +3663,8 @@ public void testActiveWorkFailure() throws Exception { server.waitForAndGetCommitsWithTimeout(1, Duration.standardSeconds(5)); assertEquals(1, commits.size()); - assertEquals(0, BlockingFn.teardownCounter.get()); - assertEquals(1, BlockingFn.setupCounter.get()); + assertEquals(1, BlockingFn.teardownCounter.get()); + assertEquals(2, BlockingFn.setupCounter.get()); worker.stop(); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowFnsTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowFnsTest.java index 7136538753db..c31c39de9ccb 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowFnsTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowFnsTest.java @@ -81,6 +81,7 @@ import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; import org.joda.time.Duration; import org.joda.time.Instant; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -109,6 +110,11 @@ public void setUp() { stepContext = new TestStepContext(STEP_NAME); } + @After + public void tearDown() { + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } + @Test public void testReshufle() throws Exception { GroupAlsoByWindowFn fn = diff --git a/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..13601410bfd9 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/StreamingModeExecutionContextTest.java @@ -61,7 +61,9 @@ import org.apache.beam.runners.dataflow.worker.streaming.Work; import org.apache.beam.runners.dataflow.worker.streaming.config.FakeGlobalConfigHandle; import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfig; +import org.apache.beam.runners.dataflow.worker.streaming.config.StreamingGlobalConfigHandle; import org.apache.beam.runners.dataflow.worker.streaming.sideinput.SideInputStateFetcher; +import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache; @@ -99,6 +101,7 @@ public class StreamingModeExecutionContextTest { @Rule public transient Timeout globalTimeout = Timeout.seconds(600); @Mock private SideInputStateFetcher sideInputStateFetcher; @Mock private WindmillStateReader stateReader; + @Mock private WorkExecutor workExecutor; private static final String COMPUTATION_ID = "computationId"; @@ -108,36 +111,40 @@ public class StreamingModeExecutionContextTest { DataflowWorkerHarnessOptions options; private FakeGlobalConfigHandle globalConfigHandle; + private StreamingModeExecutionContext createExecutionContext( + StreamingGlobalConfigHandle configHandle) { + CounterSet counterSet = new CounterSet(); + ConcurrentHashMap stateNameMap = new ConcurrentHashMap<>(); + stateNameMap.put(NameContextsForTests.nameContextForTest().userName(), "testStateFamily"); + return new StreamingModeExecutionContext( + counterSet, + COMPUTATION_ID, + new ReaderCache(Duration.standardMinutes(1), Executors.newCachedThreadPool()), + stateNameMap, + WindmillStateCache.builder() + .setSizeMb(options.getWorkerCacheMb()) + .build() + .forComputation("comp"), + StreamingStepMetricsContainer.createRegistry(), + new DataflowExecutionStateTracker( + ExecutionStateSampler.newForTest(), + executionStateRegistry.getState( + NameContext.forStage("stage"), "other", null, NoopProfileScope.NOOP), + counterSet, + PipelineOptionsFactory.create(), + "test-work-item-id"), + executionStateRegistry, + configHandle, + Long.MAX_VALUE, + /*throwExceptionOnLargeOutput=*/ false); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); options = PipelineOptionsFactory.as(DataflowWorkerHarnessOptions.class); - CounterSet counterSet = new CounterSet(); - ConcurrentHashMap stateNameMap = new ConcurrentHashMap<>(); globalConfigHandle = new FakeGlobalConfigHandle(StreamingGlobalConfig.builder().build()); - stateNameMap.put(NameContextsForTests.nameContextForTest().userName(), "testStateFamily"); - executionContext = - new StreamingModeExecutionContext( - counterSet, - COMPUTATION_ID, - new ReaderCache(Duration.standardMinutes(1), Executors.newCachedThreadPool()), - stateNameMap, - WindmillStateCache.builder() - .setSizeMb(options.getWorkerCacheMb()) - .build() - .forComputation("comp"), - StreamingStepMetricsContainer.createRegistry(), - new DataflowExecutionStateTracker( - ExecutionStateSampler.newForTest(), - executionStateRegistry.getState( - NameContext.forStage("stage"), "other", null, NoopProfileScope.NOOP), - counterSet, - PipelineOptionsFactory.create(), - "test-work-item-id"), - executionStateRegistry, - globalConfigHandle, - Long.MAX_VALUE, - /*throwExceptionOnLargeOutput=*/ false); + executionContext = createExecutionContext(globalConfigHandle); } private static Work createMockWork(Windmill.WorkItem workItem, Watermarks watermarks) { @@ -152,7 +159,7 @@ COMPUTATION_ID, new FakeGetDataClient(), ignored -> {}, mock(HeartbeatSender.cla } @Test - public void testTimerInternalsSetTimer() { + public void testTimerInternalsSetTimer() throws Exception { Windmill.WorkItemCommitRequest.Builder outputBuilder = Windmill.WorkItemCommitRequest.newBuilder(); NameContext nameContext = NameContextsForTests.nameContextForTest(); @@ -168,7 +175,8 @@ public void testTimerInternalsSetTimer() { Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), stateReader, sideInputStateFetcher, - outputBuilder); + outputBuilder, + workExecutor); TimerInternals timerInternals = stepContext.timerInternals(); @@ -179,6 +187,7 @@ public void testTimerInternalsSetTimer() { new Instant(5000), TimeDomain.EVENT_TIME, CausedByDrain.NORMAL)); + executionContext.finishKey(); executionContext.flushState(); Windmill.Timer timer = outputBuilder.buildPartial().getOutputTimers(0); @@ -218,7 +227,8 @@ public void testTimerInternalsProcessingTimeSkew() { Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), stateReader, sideInputStateFetcher, - outputBuilder); + outputBuilder, + workExecutor); TimerInternals timerInternals = stepContext.timerInternals(); assertTrue(timerTimestamp.isBefore(timerInternals.currentProcessingTime())); } @@ -416,19 +426,38 @@ 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(configHandle); + assertEquals(expectedEncoding, context.getWindmillTagEncoding().getClass()); } } + + @Test + public void testSetBacklogBytes() { + Windmill.WorkItemCommitRequest.Builder outputBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + NameContext nameContext = NameContextsForTests.nameContextForTest(); + DataflowOperationContext operationContext = + executionContext.createOperationContext(nameContext); + StreamingModeExecutionContext.StepContext stepContext = + executionContext.getStepContext(operationContext); + + executionContext.start( + "key", + createMockWork( + Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(17L).build(), + Watermarks.builder().setInputDataWatermark(new Instant(1000)).build()), + stateReader, + sideInputStateFetcher, + outputBuilder, + workExecutor); + + stepContext.setBacklogBytes(1234.0); + executionContext.finishKey(); + executionContext.flushState(); + + assertEquals(1234, outputBuilder.getSourceBacklogBytes()); + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/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/WindmillKeyedWorkItemTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItemTest.java index c1568058435b..e6738246b536 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItemTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItemTest.java @@ -46,6 +46,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.transforms.windowing.PaneInfo.Timing; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; @@ -374,6 +375,48 @@ public void testDrainPropagated() throws Exception { assertThat( keyedWorkItem.timersIterable(), Matchers.contains(makeDrainingTimer(STATE_NAMESPACE_2, 3, TimeDomain.EVENT_TIME))); + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } + + @Test + public void testValueKindPropagated() throws Exception { + WindowedValues.WindowedValueCoder.setMetadataSupported(); + Windmill.WorkItem.Builder workItem = + Windmill.WorkItem.newBuilder().setKey(SERIALIZED_KEY).setWorkToken(17); + Windmill.InputMessageBundle.Builder chunk1 = workItem.addMessageBundlesBuilder(); + chunk1.setSourceComputationId("computation"); + addElementWithMetadata( + chunk1, + 5, + "hello", + WINDOW_1, + paneInfo(0), + BeamFnApi.Elements.ElementMetadata.newBuilder() + .setValueKind(BeamFnApi.Elements.ValueKind.Enum.UPDATE_AFTER) + .build()); + addElementWithMetadata( + chunk1, + 7, + "world", + WINDOW_2, + paneInfo(2), + BeamFnApi.Elements.ElementMetadata.newBuilder() + .setValueKind(BeamFnApi.Elements.ValueKind.Enum.DELETE) + .build()); + KeyedWorkItem keyedWorkItem = + new WindmillKeyedWorkItem<>( + KEY, + workItem.build(), + WINDOW_CODER, + WINDOWS_CODER, + VALUE_CODER, + windmillTagEncoding, + true); + + Iterator> iterator = keyedWorkItem.elementsIterable().iterator(); + Assert.assertEquals(ValueKind.UPDATE_AFTER, iterator.next().getValueKind()); + Assert.assertEquals(ValueKind.DELETE, iterator.next().getValueKind()); + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); } private static TimeDomain timerTypeToTimeDomain(Windmill.Timer.Type type) { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java index 61e2f4250d06..539c38eeb1da 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WindmillReaderIteratorBaseTest.java @@ -19,6 +19,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.IOException; import java.util.ArrayList; @@ -40,8 +45,8 @@ public class WindmillReaderIteratorBaseTest { private static class TestWindmillReaderIterator extends WindmillReaderIteratorBase { protected TestWindmillReaderIterator( - Windmill.WorkItem work, ValueProvider skipUndecodableElements) { - super(work, skipUndecodableElements); + StreamingModeExecutionContext context, ValueProvider skipUndecodableElements) { + super(context, skipUndecodableElements); } @Override @@ -81,6 +86,51 @@ public void testSkipErrors() throws IOException { testForMessageBundleCounts(true, 0, 0, 1, 3, 0, 1, 0, 0, 0, 0); } + @Test + public void testWorkItemCancelledException() throws IOException { + StreamingModeExecutionContext mockContext = mock(StreamingModeExecutionContext.class); + when(mockContext.workIsFailed()).thenReturn(true); + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder().setKey(ByteString.EMPTY).setWorkToken(0L).build(); + when(mockContext.getWorkItem()).thenReturn(workItem); + + try (TestWindmillReaderIterator iter = + new TestWindmillReaderIterator(mockContext, ValueProvider.StaticValueProvider.of(false))) { + iter.start(); + fail("Expected WorkItemCancelledException"); + } catch (WorkItemCancelledException e) { + // Expected + } + } + + @Test + public void testFinishKeyCalled() throws Exception { + StreamingModeExecutionContext mockContext = mock(StreamingModeExecutionContext.class); + when(mockContext.workIsFailed()).thenReturn(false); + Windmill.WorkItem workItem = + Windmill.WorkItem.newBuilder() + .setKey(ByteString.EMPTY) + .setWorkToken(0L) + .addMessageBundles( + Windmill.InputMessageBundle.newBuilder() + .setSourceComputationId("foo") + .addMessages( + Windmill.Message.newBuilder() + .setTimestamp(0) + .setData(ByteString.EMPTY) + .build()) + .build()) + .build(); + when(mockContext.getWorkItem()).thenReturn(workItem); + + try (TestWindmillReaderIterator iter = + new TestWindmillReaderIterator(mockContext, ValueProvider.StaticValueProvider.of(false))) { + assertTrue(iter.start()); + assertFalse(iter.advance()); // This should trigger finishKey + verify(mockContext).finishKey(); + } + } + private void testForMessageBundleCounts(int... messageBundleCounts) throws IOException { testForMessageBundleCounts(false, messageBundleCounts); } @@ -111,9 +161,13 @@ private void testForMessageBundleCounts(boolean skipErrors, int... messageBundle .setWorkToken(0L) .addAllMessageBundles(bundles) .build(); + + StreamingModeExecutionContext mockContext = mock(StreamingModeExecutionContext.class); + when(mockContext.getWorkItem()).thenReturn(workItem); + try (TestWindmillReaderIterator iter = new TestWindmillReaderIterator( - workItem, ValueProvider.StaticValueProvider.of(skipErrors))) { + mockContext, ValueProvider.StaticValueProvider.of(skipErrors))) { List actual = ReaderTestUtils.windowedValuesToValues( ReaderUtils.readRemainingFromIterator(iter, false)); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java index ce8ad32f71aa..d5cf2948d928 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/WorkerCustomSourcesTest.java @@ -97,6 +97,7 @@ import org.apache.beam.runners.dataflow.worker.testing.TestCountingSource; import org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader; import org.apache.beam.runners.dataflow.worker.util.common.worker.NativeReader.NativeReaderIterator; +import org.apache.beam.runners.dataflow.worker.util.common.worker.WorkExecutor; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; import org.apache.beam.runners.dataflow.worker.windmill.state.WindmillStateCache; @@ -643,7 +644,8 @@ public void testReadUnboundedReader() throws Exception { Watermarks.builder().setInputDataWatermark(new Instant(0)).build()), mock(WindmillStateReader.class), mock(SideInputStateFetcher.class), - Windmill.WorkItemCommitRequest.newBuilder()); + Windmill.WorkItemCommitRequest.newBuilder(), + mock(WorkExecutor.class)); @SuppressWarnings({"unchecked", "rawtypes"}) NativeReader>>> reader = @@ -1023,7 +1025,8 @@ public void testFailedWorkItemsAbort() throws Exception { dummyWork, mock(WindmillStateReader.class), mock(SideInputStateFetcher.class), - Windmill.WorkItemCommitRequest.newBuilder()); + Windmill.WorkItemCommitRequest.newBuilder(), + mock(WorkExecutor.class)); @SuppressWarnings({"unchecked", "rawtypes"}) NativeReader>>> reader = @@ -1038,14 +1041,19 @@ public void testFailedWorkItemsAbort() throws Exception { NativeReaderIterator>>> readerIterator = reader.iterator(); int numReads = 0; - while ((numReads == 0) ? readerIterator.start() : readerIterator.advance()) { - WindowedValue>> value = readerIterator.getCurrent(); - assertEquals(KV.of(0, numReads), value.getValue().getValue()); - numReads++; - // Fail the work item after reading two elements. - if (numReads == 2) { - dummyWork.setFailed(); + try { + while ((numReads == 0) ? readerIterator.start() : readerIterator.advance()) { + WindowedValue>> value = readerIterator.getCurrent(); + assertEquals(KV.of(0, numReads), value.getValue().getValue()); + numReads++; + // Fail the work item after reading two elements. + if (numReads == 2) { + dummyWork.setFailed(); + } } + fail("Expected WorkItemCancelledException"); + } catch (WorkItemCancelledException e) { + // Expected } assertThat(numReads, equalTo(2)); } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/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/streaming/ActiveWorkStateTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java index 865ae2612803..0f14efdd0c0b 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ActiveWorkStateTest.java @@ -73,7 +73,7 @@ private static ExecutableWork createWork(Windmill.WorkItem workItem) { createWorkProcessingContext(), false, Instant::now), - ignored -> {}); + (work, handle) -> {}); } private static ExecutableWork expiredWork(Windmill.WorkItem workItem) { @@ -85,7 +85,7 @@ private static ExecutableWork expiredWork(Windmill.WorkItem workItem) { createWorkProcessingContext(), false, () -> Instant.EPOCH), - ignored -> {}); + (work, handle) -> {}); } private static Work.ProcessingContext createWorkProcessingContext() { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java index 1c8b8fca131d..30ad97140e1e 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/streaming/ComputationStateCacheTest.java @@ -77,7 +77,7 @@ private static ExecutableWork createWork(ShardedKey shardedKey, long workToken, mock(HeartbeatSender.class)), false, Instant::now), - ignored -> {}); + (work, handle) -> {}); } @Before diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java index d7ea039bb809..a98102751fb2 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/BoundedQueueExecutorTest.java @@ -20,6 +20,8 @@ import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -32,6 +34,8 @@ import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; import org.apache.beam.runners.dataflow.worker.streaming.Watermarks; import org.apache.beam.runners.dataflow.worker.streaming.Work; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.BoundedQueueExecutorWorkHandleImpl; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill; 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; @@ -65,13 +69,30 @@ public static Collection useFairMonitor() { @Rule public transient Timeout globalTimeout = Timeout.seconds(300); private BoundedQueueExecutor executor; + private static final Work.KeyGroup DEFAULT_KEY_GROUP = Work.KeyGroup.create(1, 2); + private static ExecutableWork createWork(Consumer executeWorkFn) { + return createWorkWithCompId("computationId", executeWorkFn); + } + + private static ExecutableWork createWorkWithCompId( + String computationId, Consumer executeWorkFn) { + return createWorkWithCompIdAndKeyGroup(computationId, DEFAULT_KEY_GROUP, executeWorkFn); + } + + private static ExecutableWork createWorkWithCompIdAndKeyGroup( + String computationId, Work.KeyGroup keyGroup, Consumer executeWorkFn) { WorkItem workItem = WorkItem.newBuilder() .setKey(ByteString.EMPTY) .setShardingKey(1) .setWorkToken(33) .setCacheToken(1) + .setKeyGroup( + Windmill.Uint128Proto.newBuilder() + .setHigh(keyGroup.high()) + .setLow(keyGroup.low()) + .build()) .build(); return ExecutableWork.create( Work.create( @@ -79,24 +100,24 @@ 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), - executeWorkFn); + (work, handle) -> { + executeWorkFn.accept(work); + }); } - private Runnable createSleepProcessWorkFn(CountDownLatch start, CountDownLatch stop) { - return () -> { - start.countDown(); - try { - stop.await(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; + private ExecutableWork createSleepProcessWork(CountDownLatch start, CountDownLatch stop) { + return createWork( + ignored -> { + start.countDown(); + try { + stop.await(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } @Before @@ -112,7 +133,8 @@ public void setUp() { .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - useFairMonitor); + useFairMonitor, + /*useKeyGroupWorkQueue=*/ false); } @Test @@ -123,9 +145,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 +174,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 +209,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 +247,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 +286,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 +330,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 +373,55 @@ public void testRecordTotalTimeMaxActiveThreadsUsedWhenMaximumPoolSizeIsReduced( executor.shutdown(); } + @Test + public void testRunnableExceptionPropagationDecrementsCounters() throws Exception { + CountDownLatch processStart = new CountDownLatch(1); + CountDownLatch processStop = new CountDownLatch(1); + + Runnable work = + () -> { + processStart.countDown(); + try { + processStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + throw new RuntimeException("Simulated finalizer processing exception"); + }; + + executor.forceExecute(work); + processStart.await(); + + assertEquals(1, executor.elementsOutstanding()); + + processStop.countDown(); + + // Wait until outstanding elements are released + while (executor.elementsOutstanding() != 0) { + Thread.sleep(10); + } + + assertEquals(0, executor.elementsOutstanding()); + } + + @Test + public void testHandleMerge() throws Exception { + BoundedQueueExecutorWorkHandleImpl handle1 = executor.createBudgetHandle(1, 100L); + BoundedQueueExecutorWorkHandleImpl handle2 = executor.createBudgetHandle(2, 200L); + + handle1.merge(handle2); + + // Verify that handle2 has 0 budget and is closed. + assertEquals(0, handle2.elements()); + assertEquals(0, handle2.bytes()); + assertTrue(handle2.isClosed()); + + // Verify that handle1 has the combined budget and is not closed. + assertEquals(3, handle1.elements()); + assertEquals(300L, handle1.bytes()); + assertFalse(handle1.isClosed()); + } + @Test public void testRenderSummaryHtml() { String expectedSummaryHtml = @@ -360,4 +431,126 @@ public void testRenderSummaryHtml() { + "Work Queue Bytes: 0/10000000
    /n"; assertEquals(expectedSummaryHtml, executor.summaryHtml()); } + + @Test + public void testPollWork() throws Exception { + // Create separate BoundedQueueExecutor with 1 thread so we can block it easily + BoundedQueueExecutor testExecutor = + new BoundedQueueExecutor( + 1, + 60, + TimeUnit.SECONDS, + 100, + 10000000, + new ThreadFactoryBuilder().setNameFormat("testStealing-%d").setDaemon(true).build(), + useFairMonitor, + /*useKeyGroupWorkQueue=*/ true); + + // 1. Create blocker task to occupy the worker thread + CountDownLatch blockerStart = new CountDownLatch(1); + CountDownLatch blockerStop = new CountDownLatch(1); + ExecutableWork blockerWork = + createWorkWithCompIdAndKeyGroup( + "blockerComp", + DEFAULT_KEY_GROUP, + ignored -> { + blockerStart.countDown(); + try { + blockerStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + testExecutor.execute(blockerWork, 0); + blockerStart.await(); + + // 2. Create two distinct key groups + Work.KeyGroup keyGroup1 = Work.KeyGroup.create(1, 1); + Work.KeyGroup keyGroup2 = Work.KeyGroup.create(1, 2); + + // Create executable tasks + CountDownLatch targetStart = new CountDownLatch(1); + ExecutableWork work1 = createWorkWithCompIdAndKeyGroup("compA", keyGroup1, ignored -> {}); + ExecutableWork work2 = + createWorkWithCompIdAndKeyGroup( + "compA", + keyGroup2, + ignored -> { + targetStart.countDown(); + }); + + // Enqueue tasks (they will wait in the queue because the thread is blocked) + testExecutor.execute(work1, 100); + testExecutor.execute(work2, 150); + + // Total outstanding elements must be 3 (blocker + work1 + work2) + assertEquals(3, testExecutor.elementsOutstanding()); + + // Steal work2 using pollWork with compA and keyGroup2 + try (BoundedQueueExecutorWorkHandleImpl stealHandle = testExecutor.createBudgetHandle(0, 0L)) { + ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup2, stealHandle); + assertNotNull(stolen); + assertEquals(work2, stolen); + + // Run the stolen task + stolen.run(stealHandle); + targetStart.await(); + } + + // Steal work1 using pollWork with compA and keyGroup1 + try (BoundedQueueExecutorWorkHandleImpl stealHandle = testExecutor.createBudgetHandle(0, 0L)) { + ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup1, stealHandle); + assertNotNull(stolen); + assertEquals(work1, stolen); + } + + // Unblock the blocker and shut down + blockerStop.countDown(); + testExecutor.shutdown(); + } + + @Test + public void testPollWorkWithLinkedBlockingQueue() throws Exception { + BoundedQueueExecutor testExecutor = + new BoundedQueueExecutor( + 1, + 60, + TimeUnit.SECONDS, + 100, + 10000000, + new ThreadFactoryBuilder().setNameFormat("testLinkedQueue-%d").setDaemon(true).build(), + useFairMonitor, + /* useKeyGroupWorkQueue= */ false); + + CountDownLatch blockerStart = new CountDownLatch(1); + CountDownLatch blockerStop = new CountDownLatch(1); + ExecutableWork blockerWork = + createWorkWithCompIdAndKeyGroup( + "blockerComp", + DEFAULT_KEY_GROUP, + ignored -> { + blockerStart.countDown(); + try { + blockerStop.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + testExecutor.execute(blockerWork, 0); + blockerStart.await(); + + Work.KeyGroup keyGroup = Work.KeyGroup.create(1, 1); + ExecutableWork work = createWorkWithCompIdAndKeyGroup("compA", keyGroup, ignored -> {}); + testExecutor.execute(work, 100); + + try (BoundedQueueExecutorWorkHandleImpl stealHandle = testExecutor.createBudgetHandle(0, 0L)) { + ExecutableWork stolen = testExecutor.pollWork("compA", keyGroup, stealHandle); + assertNull(stolen); + } + + blockerStop.countDown(); + testExecutor.shutdown(); + } } diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java new file mode 100644 index 000000000000..994aa2030f3f --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/KeyGroupWorkQueueTest.java @@ -0,0 +1,504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import java.lang.Thread.State; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.beam.runners.dataflow.worker.streaming.ExecutableWork; +import org.apache.beam.runners.dataflow.worker.streaming.Watermarks; +import org.apache.beam.runners.dataflow.worker.streaming.Work; +import org.apache.beam.runners.dataflow.worker.util.BoundedQueueExecutor.QueuedWork; +import org.apache.beam.runners.dataflow.worker.windmill.Windmill.WorkItem; +import org.apache.beam.runners.dataflow.worker.windmill.client.getdata.FakeGetDataClient; +import org.apache.beam.runners.dataflow.worker.windmill.work.refresh.HeartbeatSender; +import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Instant; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class KeyGroupWorkQueueTest { + + @Parameters(name = "fairQueue={0}") + public static Iterable data() { + return Arrays.asList(new Object[][] {{true}, {false}}); + } + + @Parameterized.Parameter public boolean fairQueue; + + private BoundedQueueExecutor executor; + + @Before + public void setUp() { + executor = + new BoundedQueueExecutor( + 2, + 60, + TimeUnit.SECONDS, + 100, + 10000000, + new ThreadFactoryBuilder().setNameFormat("Test-%d").setDaemon(true).build(), + fairQueue, + /*useKeyGroupWorkQueue=*/ true); + } + + private static final Work.KeyGroup TEST_KEY_GROUP = Work.KeyGroup.create(1, 2); + + private QueuedWork createQueuedWork(String computationId, long workBytes) { + return createQueuedWork(computationId, TEST_KEY_GROUP, workBytes); + } + + private QueuedWork createQueuedWork( + String computationId, Work.@Nullable KeyGroup keyGroup, long workBytes) { + WorkItem.Builder workItemBuilder = + WorkItem.newBuilder() + .setKey(ByteString.EMPTY) + .setShardingKey(1) + .setWorkToken(33) + .setCacheToken(1); + if (keyGroup != null) { + workItemBuilder.setKeyGroup( + org.apache.beam.runners.dataflow.worker.windmill.Windmill.Uint128Proto.newBuilder() + .setHigh(keyGroup.high()) + .setLow(keyGroup.low()) + .build()); + } + WorkItem workItem = workItemBuilder.build(); + ExecutableWork work = + ExecutableWork.create( + Work.create( + workItem, + workItem.getSerializedSize(), + Watermarks.builder().setInputDataWatermark(Instant.now()).build(), + Work.createProcessingContext( + computationId, + new FakeGetDataClient(), + ignored -> {}, + mock(HeartbeatSender.class)), + false, + Instant::now), + (w, h) -> {}); + return new QueuedWork(work, executor.createBudgetHandle(1, workBytes)); + } + + private static class NoOpRunnable implements Runnable { + final String id; + + NoOpRunnable(String id) { + this.id = id; + } + + @Override + public void run() {} + + @Override + public String toString() { + return "NoOpRunnable(" + id + ")"; + } + } + + @Test + public void testBasicOfferAndPoll() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + assertTrue(queue.isEmpty()); + assertEquals(0, queue.size()); + + NoOpRunnable task1 = new NoOpRunnable("1"); + NoOpRunnable task2 = new NoOpRunnable("2"); + + assertTrue(queue.offer(task1)); + assertTrue(queue.offer(task2)); + assertEquals(2, queue.size()); + + assertEquals(task1, queue.poll()); + assertEquals(task2, queue.poll()); + assertNull(queue.poll()); + assertTrue(queue.isEmpty()); + } + + @Test + public void testRemove() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + NoOpRunnable task1 = new NoOpRunnable("1"); + NoOpRunnable task2 = new NoOpRunnable("2"); + + queue.offer(task1); + queue.offer(task2); + + assertTrue(queue.remove(task1)); + assertEquals(1, queue.size()); + assertEquals(task2, queue.poll()); + assertFalse(queue.remove(task1)); // Already gone + } + + @Test + public void testDrainTo() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + NoOpRunnable task1 = new NoOpRunnable("1"); + NoOpRunnable task2 = new NoOpRunnable("2"); + queue.offer(task1); + queue.offer(task2); + + List drained = new ArrayList<>(); + assertEquals(2, queue.drainTo(drained)); + assertEquals(2, drained.size()); + assertEquals(task1, drained.get(0)); + assertEquals(task2, drained.get(1)); + assertTrue(queue.isEmpty()); + } + + @Test + public void testIteratorSafeTraversalAndImmutable() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + NoOpRunnable task1 = new NoOpRunnable("1"); + NoOpRunnable task2 = new NoOpRunnable("2"); + queue.offer(task1); + queue.offer(task2); + + Iterator it = queue.iterator(); + assertTrue(it.hasNext()); + assertEquals(task1, it.next()); + assertTrue(it.hasNext()); + assertEquals(task2, it.next()); + assertFalse(it.hasNext()); + + // Assert that mutating the iterator throws UnsupportedOperationException + it = queue.iterator(); + assertTrue(it.hasNext()); + it.next(); + try { + it.remove(); + fail("Iterator must be immutable"); + } catch (UnsupportedOperationException e) { + // Expected + } + } + + @Test + public void testPollWorkTargeted() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + + QueuedWork workA1 = createQueuedWork("compA", 100); + QueuedWork workB1 = createQueuedWork("compB", 200); + QueuedWork workA2 = createQueuedWork("compA", 150); + + queue.offer(workA1); + queue.offer(workB1); + queue.offer(workA2); + + assertEquals(3, queue.size()); + assertFalse(queue.isEmpty()); + + // Targeted poll A + QueuedWork polledA1 = queue.pollWork("compA", TEST_KEY_GROUP); + assertNotNull(polledA1); + assertEquals("compA", polledA1.getWork().getComputationId()); + assertEquals(100, polledA1.getHandle().bytes()); + assertEquals(2, queue.size()); + assertFalse(queue.isEmpty()); + + // Poll next should be B1 (since A1 was stolen, B1 is now first global) + assertEquals(workB1, queue.poll()); + assertEquals(1, queue.size()); + assertFalse(queue.isEmpty()); + + // Last should be A2 + assertEquals(workA2, queue.poll()); + assertEquals(0, queue.size()); + assertTrue(queue.isEmpty()); + + assertEquals(null, queue.poll()); + } + + @Test + public void testConcurrentStress() throws InterruptedException, ExecutionException { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + int producerThreads = 4; + int consumerThreads = 4; + int tasksPerProducer = 1000; + int totalTasks = producerThreads * tasksPerProducer; + Runnable poisonPill = + new Runnable() { + @Override + public void run() {} + + @Override + public String toString() { + return "POISON_PILL"; + } + }; + + ExecutorService executorService = + Executors.newFixedThreadPool(producerThreads + consumerThreads); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch producersDoneLatch = new CountDownLatch(producerThreads); + CountDownLatch consumersDoneLatch = new CountDownLatch(consumerThreads); + CountDownLatch consumedLatch = new CountDownLatch(totalTasks); + List> futures = new ArrayList<>(); + + // Start consumers + for (int i = 0; i < consumerThreads; i++) { + int consumerId = i; + futures.add( + executorService.submit( + () -> { + try { + startLatch.await(); + int iteration = consumerId % 4; + while (true) { + int strategy = iteration; + iteration = (iteration + 1) % 4; + Runnable task = null; + if (strategy == 0) { + String compId = "comp-" + (consumedLatch.getCount() % 5); + task = queue.pollWork(compId, TEST_KEY_GROUP); + } else if (strategy == 1) { + task = queue.poll(); + } else if (strategy == 2) { + task = queue.poll(10, TimeUnit.MICROSECONDS); + } else if (strategy == 3) { + task = queue.take(); + } + + if (task == poisonPill) { + break; + } + if (task != null) { + consumedLatch.countDown(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + consumersDoneLatch.countDown(); + } + })); + } + + // Start producers + for (int i = 0; i < producerThreads; i++) { + futures.add( + executorService.submit( + () -> { + try { + startLatch.await(); + for (int j = 0; j < tasksPerProducer; j++) { + String compId = "comp-" + (j % 5); + queue.offer(createQueuedWork(compId, 10)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + producersDoneLatch.countDown(); + } + })); + } + + // Release the start latch to start the test + startLatch.countDown(); + + // Wait for all tasks to be consumed + assertTrue(consumedLatch.await(30, TimeUnit.SECONDS)); + + // Send poison pills to stop all consumers + for (int i = 0; i < consumerThreads; i++) { + queue.offer(poisonPill); + } + + // Wait for consumers to finish + assertTrue(consumersDoneLatch.await(30, TimeUnit.SECONDS)); + // Wait for producers to finish + assertTrue(producersDoneLatch.await(30, TimeUnit.SECONDS)); + + // Check for exceptions in threads + for (Future future : futures) { + future.get(); + } + + executorService.shutdown(); + assertTrue(executorService.awaitTermination(30, TimeUnit.SECONDS)); + + assertEquals(0, queue.size()); + assertTrue(queue.isEmpty()); + } + + @Test + public void testTakeBlocksAndWakesUp() throws InterruptedException { + final KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + final NoOpRunnable task = new NoOpRunnable("take-task"); + final AtomicReference result = new AtomicReference<>(); + final CountDownLatch started = new CountDownLatch(1); + final CountDownLatch finished = new CountDownLatch(1); + + Thread t = + new Thread( + () -> { + started.countDown(); + try { + result.set(queue.take()); + } catch (InterruptedException e) { + // Ignore + } finally { + finished.countDown(); + } + }); + t.setDaemon(true); + t.start(); + + assertTrue(started.await(30, TimeUnit.SECONDS)); + waitForThreadState(t, State.WAITING); + + queue.offer(task); + + assertTrue(finished.await(30, TimeUnit.SECONDS)); + assertEquals(task, result.get()); + } + + @Test + public void testPollWithTimeout() throws InterruptedException { + final KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + final NoOpRunnable task = new NoOpRunnable("poll-task"); + final AtomicReference result = new AtomicReference<>(); + final CountDownLatch started = new CountDownLatch(1); + final CountDownLatch finished = new CountDownLatch(1); + + // 1. Verify timeout returns null + Thread t1 = + new Thread( + () -> { + started.countDown(); + try { + result.set(queue.poll(500, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + // Ignore + } finally { + finished.countDown(); + } + }); + t1.setDaemon(true); + t1.start(); + + assertTrue(started.await(30, TimeUnit.SECONDS)); + waitForThreadState(t1, State.TIMED_WAITING); + + assertTrue(finished.await(30, TimeUnit.SECONDS)); + assertNull(result.get()); + + // 2. Verify timed poll receives task offered concurrently + final CountDownLatch started2 = new CountDownLatch(1); + final CountDownLatch finished2 = new CountDownLatch(1); + final AtomicReference result2 = new AtomicReference<>(); + + Thread t2 = + new Thread( + () -> { + started2.countDown(); + try { + result2.set(queue.poll(2, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + // Ignore + } finally { + finished2.countDown(); + } + }); + t2.setDaemon(true); + t2.start(); + + assertTrue(started2.await(30, TimeUnit.SECONDS)); + waitForThreadState(t2, State.TIMED_WAITING); + + queue.offer(task); + + assertTrue(finished2.await(30, TimeUnit.SECONDS)); + assertEquals(task, result2.get()); + } + + @Test + public void testPollWorkWithKeyGroup() { + KeyGroupWorkQueue queue = new KeyGroupWorkQueue(fairQueue); + + Work.KeyGroup keyGroup1 = Work.KeyGroup.create(1, 1); + Work.KeyGroup keyGroup2 = Work.KeyGroup.create(1, 2); + Work.KeyGroup keyGroupNotExist = Work.KeyGroup.create(3, 4); + + QueuedWork workA1 = createQueuedWork("compA", keyGroup1, 100); + QueuedWork workA2 = createQueuedWork("compA", keyGroup2, 150); + + queue.offer(workA1); + queue.offer(workA2); + + assertEquals(2, queue.size()); + + QueuedWork polledNotExist = queue.pollWork("compA", keyGroupNotExist); + assertNull(polledNotExist); + assertEquals(2, queue.size()); + + // Poll with keyGroup2 first - should return workA2 + QueuedWork polledA2 = queue.pollWork("compA", keyGroup2); + assertNotNull(polledA2); + assertEquals(workA2, polledA2); + assertEquals(1, queue.size()); + + // Poll with keyGroup2 again - should return null + assertNull(queue.pollWork("compA", keyGroup2)); + + // Poll with keyGroup1 - should return workA1 + QueuedWork polledA1 = queue.pollWork("compA", keyGroup1); + assertNotNull(polledA1); + assertEquals(workA1, polledA1); + assertTrue(queue.isEmpty()); + + polledNotExist = queue.pollWork("compA", keyGroupNotExist); + assertNull(polledNotExist); + assertTrue(queue.isEmpty()); + } + + private void waitForThreadState(Thread t, State state) throws InterruptedException { + long timeoutMs = 30000; + long start = System.currentTimeMillis(); + while (t.getState() != state) { + if (System.currentTimeMillis() - start > timeoutMs) { + fail("Thread did not reach " + state + " state within " + timeoutMs + "ms"); + } + Thread.sleep(1); + } + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogramTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogramTest.java new file mode 100644 index 000000000000..252300a19550 --- /dev/null +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/util/SimpleByteHistogramTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.dataflow.worker.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link SimpleByteHistogram}. */ +@RunWith(JUnit4.class) +public class SimpleByteHistogramTest { + + @Test + public void testHistogram() { + SimpleByteHistogram histogram = new SimpleByteHistogram(); + histogram.add(10); // <128B + histogram.add(127); // <128B + histogram.add(128); // <256B + histogram.add(255); // <256B + histogram.add(256); // <512B + histogram.add(511); // <512B + histogram.add(512); // <1KB + histogram.add(1023); // <1KB + histogram.add(1024); // <10KB + histogram.add(10240 - 1); // <10KB + histogram.add(10240); // <1MB + histogram.add(1048576 - 1); // <1MB + histogram.add(1048576); // >=1MB + histogram.add(2000000); // >=1MB + + String expected = "[<128B:2, <256B:2, <512B:2, <1KB:2, <10KB:2, <1MB:2, >=1MB:2]"; + assertEquals(expected, histogram.format()); + } +} diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/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/grpc/GrpcCommitWorkStreamTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java index e9fd55fa5668..9c3d5c9c3ef3 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/client/grpc/GrpcCommitWorkStreamTest.java @@ -38,6 +38,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import org.apache.beam.runners.dataflow.worker.windmill.CloudWindmillServiceV1Alpha1Grpc; import org.apache.beam.runners.dataflow.worker.windmill.Windmill; @@ -1133,6 +1134,87 @@ public void testCommitWorkItem_multiplePhysicalStreams_multipleHandovers_halfClo assertTrue(commitWorkStream.awaitTermination(10, TimeUnit.SECONDS)); } + @Test + public void testCommitWorkItem_stopsRetriesAfterDuration() throws Exception { + int numCommits = 1; + CountDownLatch commitProcessed = new CountDownLatch(numCommits); + AtomicReference commitStatus = new AtomicReference<>(); + GrpcCommitWorkStream commitWorkStream = + (GrpcCommitWorkStream) + GrpcWindmillStreamFactory.of(TEST_JOB_HEADER) + .setDirectStreamingRpcPhysicalStreamHalfCloseAfter(Duration.ofMinutes(1)) + .setCommitWorkStreamRetryTimeout(Duration.ofNanos(1)) + .build() + .createCommitWorkStream(CloudWindmillServiceV1Alpha1Grpc.newStub(inProcessChannel)); + commitWorkStream.start(); + + FakeWindmillGrpcService.CommitStreamInfo streamInfo = waitForConnectionAndConsumeHeader(); + try (WindmillStream.CommitWorkStream.RequestBatcher batcher = commitWorkStream.batcher()) { + assertTrue( + batcher.commitWorkItem( + COMPUTATION_ID, + workItemCommitRequest(0), + status -> { + commitStatus.set(status); + commitProcessed.countDown(); + })); + } + + // The next request should have some chunks. + assertThat(streamInfo.requests.take().getCommitChunkList()).isNotEmpty(); + + // We won't get responses so we will have some pending requests. + assertThat(commitProcessed.getCount()).isGreaterThan(0); + streamInfo.responseObserver.onError(new IOException("test error")); + commitProcessed.await(); + assertThat(commitStatus.get()).isEqualTo(Windmill.CommitStatus.ABORTED); + } + + @Test + public void testCommitWorkItem_retriesWithLongerCommitRetryTimeout() throws Exception { + int numCommits = 1; + CountDownLatch commitProcessed = new CountDownLatch(numCommits); + AtomicReference commitStatus = new AtomicReference<>(); + GrpcCommitWorkStream commitWorkStream = + (GrpcCommitWorkStream) + GrpcWindmillStreamFactory.of(TEST_JOB_HEADER) + .setDirectStreamingRpcPhysicalStreamHalfCloseAfter(Duration.ofMinutes(1)) + // Verifies that if this is set but is not exceeded that a retry occurs. + .setCommitWorkStreamRetryTimeout(Duration.ofMinutes(10)) + .build() + .createCommitWorkStream(CloudWindmillServiceV1Alpha1Grpc.newStub(inProcessChannel)); + commitWorkStream.start(); + + FakeWindmillGrpcService.CommitStreamInfo streamInfo = waitForConnectionAndConsumeHeader(); + try (WindmillStream.CommitWorkStream.RequestBatcher batcher = commitWorkStream.batcher()) { + assertTrue( + batcher.commitWorkItem( + COMPUTATION_ID, + workItemCommitRequest(0), + status -> { + commitStatus.set(status); + commitProcessed.countDown(); + })); + } + + // The next request should have some chunks. + assertThat(streamInfo.requests.take().getCommitChunkList()).isNotEmpty(); + + // We won't get responses so we will have some pending requests. + assertThat(commitProcessed.getCount()).isGreaterThan(0); + streamInfo.responseObserver.onError(new IOException("test error")); + + // The stream should reconnect and retry the requests. + FakeWindmillGrpcService.CommitStreamInfo reconnectStreamInfo = + waitForConnectionAndConsumeHeader(); + Windmill.StreamingCommitWorkRequest reconnectRequest = reconnectStreamInfo.requests.take(); + assertEquals(1, reconnectRequest.getCommitChunkCount()); + reconnectStreamInfo.responseObserver.onNext( + Windmill.StreamingCommitResponse.newBuilder().addRequestId(1).build()); + commitProcessed.await(); + assertThat(commitStatus.get()).isEqualTo(Windmill.CommitStatus.OK); + } + private FakeWindmillGrpcService.CommitStreamInfo waitForConnectionAndConsumeHeader() { try { FakeWindmillGrpcService.CommitStreamInfo info = fakeService.waitForConnectedCommitStream(); diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCacheTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCacheTest.java index bbb8e4c93c07..2d3d9b5ccff2 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCacheTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateCacheTest.java @@ -270,6 +270,68 @@ public void testMaxWeight() throws Exception { assertEquals(400 * MEGABYTES, cache.getMaxWeight()); } + @Test + public void testMaxCachedEntryBytes() throws Exception { + cache.setMaxCachedEntryBytesOverride( + 100); // Set limit to 100 bytes, per cache entry overhead is 136. + + WindmillStateCache.ForKeyAndFamily keyCache = + cache.forComputation(COMPUTATION).forKey(COMPUTATION_KEY, 0L, 1L).forFamily(STATE_FAMILY); + + TestStateTag tag1 = new TestStateTag("tag1"); + TestStateTag tag2 = new TestStateTag("tag2"); + + putInCache(keyCache, StateNamespaces.global(), tag1, new TestState("g1"), 10); + keyCache.persist(); + + // It should not be in global cache because it's too large. + keyCache = + cache.forComputation(COMPUTATION).forKey(COMPUTATION_KEY, 0L, 2L).forFamily(STATE_FAMILY); + assertEquals(Optional.empty(), getFromCache(keyCache, StateNamespaces.global(), tag1)); + + // Now set limit larger. + cache.setMaxCachedEntryBytesOverride(1000); + + putInCache(keyCache, StateNamespaces.global(), tag2, new TestState("g2"), 10); + keyCache.persist(); + + // It should be in global cache. + keyCache = + cache.forComputation(COMPUTATION).forKey(COMPUTATION_KEY, 0L, 3L).forFamily(STATE_FAMILY); + assertEquals( + Optional.of(new TestState("g2")), getFromCache(keyCache, StateNamespaces.global(), tag2)); + + // Now update it to be larger than limit. + putInCache(keyCache, StateNamespaces.global(), tag2, new TestState("g2_large"), 2000); + keyCache.persist(); + + // It should be removed from global cache. + keyCache = + cache.forComputation(COMPUTATION).forKey(COMPUTATION_KEY, 0L, 4L).forFamily(STATE_FAMILY); + assertEquals(Optional.empty(), getFromCache(keyCache, StateNamespaces.global(), tag2)); + } + + @Test + public void testDisableHistogram() throws Exception { + WindmillStateCache noHistogramCache = + WindmillStateCache.builder().setSizeMb(400).setEnableHistogram(false).build(); + WindmillStateCache.ForKeyAndFamily keyCache = + noHistogramCache + .forComputation(COMPUTATION) + .forKey(COMPUTATION_KEY, 0L, 1L) + .forFamily(STATE_FAMILY); + + putInCache( + keyCache, StateNamespaces.global(), new TestStateTag("tag1"), new TestState("g1"), 2); + keyCache.persist(); + + java.io.StringWriter writer = new java.io.StringWriter(); + noHistogramCache.appendSummaryHtml(new java.io.PrintWriter(writer)); + String summary = writer.toString(); + + org.junit.Assert.assertFalse(summary.contains("Entry Weight Dist")); + } + /** Verifies that values are cached in the appropriate namespaces. */ @Test public void testInvalidation() throws Exception { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/state/WindmillStateInternalsTest.java index de036214b958..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 = @@ -3228,6 +3385,40 @@ public void testNewWatermarkNoFetch() throws Exception { Mockito.verifyNoInteractions(mockReader); } + @Test + public void testWatermarkClearNoOp() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + WatermarkHoldState hold = underTestNewKey.state(NAMESPACE, addr); + + hold.clear(); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTestNewKey.persist(commitBuilder); + + assertEquals(0, commitBuilder.getWatermarkHoldsCount()); + assertBuildable(commitBuilder); + } + + @Test + public void testWatermarkClearWithLocalAdditionsNoop() throws Exception { + StateTag addr = + StateTags.watermarkStateInternal("watermark", TimestampCombiner.EARLIEST); + WatermarkHoldState hold = underTestNewKey.state(NAMESPACE, addr); + + hold.add(new Instant(500)); + + hold.clear(); + + Windmill.WorkItemCommitRequest.Builder commitBuilder = + Windmill.WorkItemCommitRequest.newBuilder(); + underTestNewKey.persist(commitBuilder); + + assertEquals(0, commitBuilder.getWatermarkHoldsCount()); + assertBuildable(commitBuilder); + } + @Test public void testValueSetBeforeRead() throws Exception { StateTag> addr = StateTags.value("value", StringUtf8Coder.of()); 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..0610ed44c27f 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/processing/failures/WorkFailureProcessorTest.java @@ -64,7 +64,8 @@ private static WorkFailureProcessor createWorkFailureProcessor( .setNameFormat("DataflowWorkUnits-%d") .setDaemon(true) .build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); return WorkFailureProcessor.forTesting(workExecutor, failureTracker, Optional::empty, clock, 0); } @@ -98,7 +99,9 @@ private static ExecutableWork createWork(Supplier clock, Consumer mock(HeartbeatSender.class)), false, clock), - processWorkFn); + (work, handle) -> { + processWorkFn.accept(work); + }); } private static ExecutableWork createWork(Consumer processWorkFn) { diff --git a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java index 054db878c869..f32282056e4f 100644 --- a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java +++ b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/windmill/work/refresh/ActiveWorkRefresherTest.java @@ -80,7 +80,8 @@ private static BoundedQueueExecutor workExecutor() { 1, 10000000, new ThreadFactoryBuilder().setNameFormat("DataflowWorkUnits-%d").setDaemon(true).build(), - /*useFairMonitor=*/ false); + /*useFairMonitor=*/ false, + /*useKeyGroupWorkQueue=*/ false); } private static ComputationState createComputationState(int computationIdSuffix) { @@ -137,7 +138,9 @@ private ExecutableWork createOldWork( "computationId", new FakeGetDataClient(), ignored -> {}, heartbeatSender), false, ActiveWorkRefresherTest::aLongTimeAgo), - processWork); + (work, handle) -> { + processWork.accept(work); + }); } @Test diff --git a/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto b/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto index b7579cbacb8e..aaa09c105fc3 100644 --- a/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto +++ b/runners/google-cloud-dataflow-java/worker/windmill/src/main/proto/windmill.proto @@ -421,6 +421,11 @@ message WatermarkHold { optional string state_family = 4; } +message Uint128Proto { + required fixed64 high = 1; + required fixed64 low = 2; +} + // Proto describing a hot key detected on a given WorkItem. message HotKeyInfo { // The age of the hot key measured from when it was first detected. @@ -448,6 +453,8 @@ message WorkItem { // present, this field includes metadata associated with any hot key. optional HotKeyInfo hot_key_info = 11; + optional Uint128Proto key_group = 18; + reserved 12, 13, 14, 15, 16; } @@ -979,13 +986,15 @@ 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; optional ConnectivityType connectivity_type = 4 [default = CONNECTIVITY_TYPE_DEFAULT]; + optional int64 max_cached_entry_bytes = 5 [default = -1]; + reserved 1, 2; } diff --git a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java index 682c45e30795..704d298a195d 100644 --- a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java +++ b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/control/SdkHarnessClient.java @@ -298,7 +298,7 @@ public ActiveBundle newBundle( ImmutableMap.Builder> receiverBuilder = ImmutableMap.builder(); BeamFnDataOutboundAggregator beamFnDataOutboundAggregator = - fnApiDataService.createOutboundAggregator(() -> bundleId, false); + fnApiDataService.createOutboundAggregator(bundleId, false); for (RemoteInputDestination remoteInput : remoteInputs) { LogicalEndpoint endpoint = LogicalEndpoint.data(bundleId, remoteInput.getPTransformId()); receiverBuilder.put( diff --git a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java index 7c5f110eab28..657ec74553bc 100644 --- a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java +++ b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/FnDataService.java @@ -17,7 +17,6 @@ */ package org.apache.beam.runners.fnexecution.data; -import java.util.function.Supplier; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; import org.apache.beam.sdk.fn.data.BeamFnDataOutboundAggregator; import org.apache.beam.sdk.fn.data.CloseableFnDataReceiver; @@ -69,5 +68,5 @@ public interface FnDataService { *

    The returned aggregator is not thread safe. */ BeamFnDataOutboundAggregator createOutboundAggregator( - Supplier processBundleRequestIdSupplier, boolean collectElementsIfNoFlushes); + String processBundleId, boolean collectElementsIfNoFlushes); } diff --git a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java index d4e45c8ccf82..a3a8c3244044 100644 --- a/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java +++ b/runners/java-fn-execution/src/main/java/org/apache/beam/runners/fnexecution/data/GrpcDataService.java @@ -23,7 +23,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; import org.apache.beam.model.fnexecution.v1.BeamFnApi; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; import org.apache.beam.model.fnexecution.v1.BeamFnDataGrpc; @@ -175,13 +174,13 @@ public void unregisterReceiver(String instructionId) { @Override public BeamFnDataOutboundAggregator createOutboundAggregator( - Supplier processBundleRequestIdSupplier, boolean collectElementsIfNoFlushes) { + String instructionId, boolean collectElementsIfNoFlushes) { try { - return new BeamFnDataOutboundAggregator( - options, - processBundleRequestIdSupplier, - connectedClient.get(3, TimeUnit.MINUTES).getOutboundObserver(), - collectElementsIfNoFlushes); + BeamFnDataOutboundAggregator aggregator = + new BeamFnDataOutboundAggregator(options, collectElementsIfNoFlushes); + aggregator.prepareForInstruction( + instructionId, connectedClient.get(3, TimeUnit.MINUTES).getOutboundObserver()); + return aggregator; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); diff --git a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java index f84467077501..363367f1087f 100644 --- a/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java +++ b/runners/java-fn-execution/src/test/java/org/apache/beam/runners/fnexecution/data/GrpcDataServiceTest.java @@ -102,7 +102,7 @@ public void testMessageReceivedBySingleClientWhenThereAreMultipleClients() throw for (int i = 0; i < 3; ++i) { final String instructionId = Integer.toString(i); BeamFnDataOutboundAggregator aggregator = - service.createOutboundAggregator(() -> instructionId, false); + service.createOutboundAggregator(instructionId, false); aggregator.start(); FnDataReceiver> consumer = aggregator.registerOutputDataLocation(TRANSFORM_ID, CODER); diff --git a/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/samza/build.gradle b/runners/samza/build.gradle deleted file mode 100644 index 626588b79a5d..000000000000 --- a/runners/samza/build.gradle +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import groovy.json.JsonOutput - -plugins { id 'org.apache.beam.module' } -applyJavaNature( - exportJavadoc: false, - classesTriggerCheckerBugs: [ - 'GroupWithoutRepartition': 'https://github.com/typetools/checker-framework/issues/3791', - ], - automaticModuleName: 'org.apache.beam.runners.samza', -) - -description = "Apache Beam :: Runners :: Samza" - -/* - * We need to rely on manually specifying these evaluationDependsOn to ensure that - * the following projects are evaluated before we evaluate this project. This is because - * we are attempting to reference the "sourceSets.test.output" directly. - */ -evaluationDependsOn(":sdks:java:core") - -configurations { - validatesRunner -} - -def samza_version = "1.6.0" - -dependencies { - implementation library.java.vendored_guava_32_1_2_jre - implementation project(path: ":sdks:java:core", configuration: "shadow") - implementation project(":runners:core-java") - implementation project(":runners:java-fn-execution") - implementation project(":runners:java-job-service") - implementation library.java.jackson_annotations - implementation library.java.slf4j_api - implementation library.java.joda_time - implementation library.java.args4j - implementation library.java.commons_io - implementation library.java.commons_collections - runtimeOnly "org.rocksdb:rocksdbjni:6.15.2" - runtimeOnly "org.scala-lang:scala-library:2.11.8" - implementation "org.apache.samza:samza-api:$samza_version" - implementation "org.apache.samza:samza-core_2.11:$samza_version" - runtimeOnly "org.apache.samza:samza-kafka_2.11:$samza_version" - runtimeOnly "org.apache.samza:samza-kv_2.11:$samza_version" - implementation "org.apache.samza:samza-kv-rocksdb_2.11:$samza_version" - implementation "org.apache.samza:samza-kv-inmemory_2.11:$samza_version" - implementation "org.apache.samza:samza-yarn_2.11:$samza_version" - compileOnly library.java.error_prone_annotations - runtimeOnly "org.apache.kafka:kafka-clients:2.0.1" - implementation library.java.vendored_grpc_1_69_0 - implementation project(path: ":model:fn-execution", configuration: "shadow") - implementation project(path: ":model:job-management", configuration: "shadow") - implementation project(path: ":model:pipeline", configuration: "shadow") - testImplementation project(path: ":sdks:java:core", configuration: "shadowTest") - testImplementation project(path: ":runners:core-java", configuration: "testRuntimeMigration") - testImplementation library.java.hamcrest - testImplementation library.java.junit - testImplementation library.java.mockito_core - testImplementation library.java.jackson_dataformat_yaml - testImplementation library.java.google_code_gson - validatesRunner project(path: ":sdks:java:core", configuration: "shadowTest") - validatesRunner project(path: ":runners:core-java", configuration: "testRuntimeMigration") - validatesRunner project(project.path) -} - -configurations.all { - exclude group: "org.slf4j", module: "slf4j-jdk14" -} - -def sickbayTests = [ - // TODO(https://github.com/apache/beam/issues/21033) - 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInGlobalWindowBatchSizeByteSizeFn', - 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInStreamingMode', - 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testWithShardedKeyInGlobalWindow', - // TODO(https://github.com/apache/beam/issues/21036) - 'org.apache.beam.sdk.transforms.MapElementsTest.testMapSimpleFunction', - // TODO(https://github.com/apache/beam/issues/21035) - 'org.apache.beam.sdk.transforms.ViewTest.testEmptySingletonSideInput', - 'org.apache.beam.sdk.transforms.ViewTest.testNonSingletonSideInput', - // TODO(https://github.com/apache/beam/issues/21037) - 'org.apache.beam.sdk.transforms.WithTimestampsTest.withTimestampsBackwardsInTimeShouldThrow', - 'org.apache.beam.sdk.transforms.WithTimestampsTest.withTimestampsWithNullTimestampShouldThrow', - // TODO(https://github.com/apache/beam/issues/21039) - 'org.apache.beam.sdk.io.FileIOTest*', - // TODO(https://github.com/apache/beam/issues/21038) - 'org.apache.beam.sdk.io.AvroIOTest*', - // TODO(https://github.com/apache/beam/issues/21040) - 'org.apache.beam.sdk.PipelineTest.testEmptyPipeline', - // TODO(https://github.com/apache/beam/issues/21041) - 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testEncodingNPException', - 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testEncodingIOException', - 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testDecodingNPException', - 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testDecodingIOException', - // https://github.com/apache/beam/issues/19344 - 'org.apache.beam.sdk.io.BoundedReadFromUnboundedSourceTest.testTimeBound', - // https://github.com/apache/beam/issues/31725 - 'org.apache.beam.sdk.io.TextIOWriteTest.testWriteUnboundedWithCustomBatchParameters', -] -tasks.register("validatesRunner", Test) { - group = "Verification" - description "Validates Samza runner" - systemProperty "beamTestPipelineOptions", JsonOutput.toJson([ - "--runner=TestSamzaRunner", - ]) - - classpath = configurations.validatesRunner - testClassesDirs = files(project(":sdks:java:core").sourceSets.test.output.classesDirs) - useJUnit { - includeCategories 'org.apache.beam.sdk.testing.NeedsRunner' - includeCategories 'org.apache.beam.sdk.testing.ValidatesRunner' - excludeCategories 'org.apache.beam.sdk.testing.UsesExternalService' - // Should be run only in a properly configured SDK harness environment - excludeCategories 'org.apache.beam.sdk.testing.UsesSdkHarnessEnvironment' - excludeCategories 'org.apache.beam.sdk.testing.UsesUnboundedSplittableParDo' - excludeCategories 'org.apache.beam.sdk.testing.UsesSchema' - excludeCategories 'org.apache.beam.sdk.testing.LargeKeys$Above100MB' - excludeCategories 'org.apache.beam.sdk.testing.UsesAttemptedMetrics' - excludeCategories 'org.apache.beam.sdk.testing.UsesCommittedMetrics' - excludeCategories 'org.apache.beam.sdk.testing.UsesTestStreamWithProcessingTime' - excludeCategories 'org.apache.beam.sdk.testing.UsesMetricsPusher' - excludeCategories 'org.apache.beam.sdk.testing.UsesParDoLifecycle' - excludeCategories 'org.apache.beam.sdk.testing.UsesProcessingTimeTimers' - excludeCategories 'org.apache.beam.sdk.testing.UsesStrictTimerOrdering' - excludeCategories 'org.apache.beam.sdk.testing.UsesOnWindowExpiration' - excludeCategories 'org.apache.beam.sdk.testing.UsesOrderedListState' - excludeCategories 'org.apache.beam.sdk.testing.UsesMultimapState' - excludeCategories 'org.apache.beam.sdk.testing.UsesBundleFinalizer' - excludeCategories 'org.apache.beam.sdk.testing.UsesLoopingTimer' - excludeCategories 'org.apache.beam.sdk.testing.UsesTriggeredSideInputs' - } - filter { - for (String test : sickbayTests) { - excludeTestsMatching test - } - // TODO(BEAM-10025) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestampDefaultUnbounded' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestamp' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testRelativeTimerWithOutputTimestamp' - // TODO(https://github.com/apache/beam/issues/20847) - excludeTestsMatching 'org.apache.beam.sdk.testing.TestStreamTest.testFirstElementLate' - // TODO(https://github.com/apache/beam/issues/20846) - excludeTestsMatching 'org.apache.beam.sdk.testing.TestStreamTest.testLateDataAccumulating' - - // These tests fail since there is no support for side inputs in Samza's unbounded splittable DoFn integration - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testWindowedSideInputWithCheckpointsUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testSideInputUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testWindowedSideInputUnbounded' - // These tests produce the output but either the pipeline doesn't shutdown or PAssert fails - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testAdditionalOutputUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testPairWithIndexBasicUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testPairWithIndexWindowedTimestampedUnbounded' - excludeTestsMatching 'org.apache.beam.sdk.transforms.SplittableDoFnTest.testOutputAfterCheckpointUnbounded' - } -} - -tasks.register("validatesRunnerSickbay", Test) { - group = "Verification" - description "Validates Samza runner (Sickbay Tests)" - systemProperty "beamTestPipelineOptions", JsonOutput.toJson([ - "--runner=TestSamzaRunner", - ]) - - classpath = configurations.validatesRunner - testClassesDirs = files(project(":sdks:java:core").sourceSets.test.output.classesDirs) - - filter { - for (String test : sickbayTests) { - includeTestsMatching test - } - } -} - -// Generates :runners:samza:runQuickstartJavaSamza -createJavaExamplesArchetypeValidationTask(type: 'Quickstart', runner:'Samza') diff --git a/runners/samza/job-server/build.gradle b/runners/samza/job-server/build.gradle deleted file mode 100644 index 7ffb2becd6d0..000000000000 --- a/runners/samza/job-server/build.gradle +++ /dev/null @@ -1,253 +0,0 @@ -import org.apache.beam.gradle.BeamModulePlugin - -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -apply plugin: 'org.apache.beam.module' -apply plugin: 'application' -// we need to set mainClassName before applying shadow plugin -mainClassName = "org.apache.beam.runners.samza.SamzaJobServerDriver" - -applyJavaNature( - automaticModuleName: 'org.apache.beam.runners.samza.jobserver', - archivesBaseName: project.hasProperty('archives_base_name') ? archives_base_name : archivesBaseName, - validateShadowJar: false, - exportJavadoc: false, - shadowClosure: { - append "reference.conf" - }, -) - -def samzaRunnerProject = project.parent.path - -description = "Apache Beam :: Runners :: Samza :: Job Server" - -configurations { - validatesPortableRunner -} - -dependencies { - implementation project(samzaRunnerProject) - permitUnusedDeclared project(samzaRunnerProject) - runtimeOnly group: "org.slf4j", name: "jcl-over-slf4j", version: dependencies.create(project.library.java.slf4j_api).getVersion() - validatesPortableRunner project(path: samzaRunnerProject, configuration: "testRuntimeMigration") - validatesPortableRunner project(path: ":sdks:java:core", configuration: "shadowTest") - validatesPortableRunner project(path: ":runners:core-java", configuration: "testRuntimeMigration") - validatesPortableRunner project(path: ":runners:portability:java", configuration: "testRuntimeMigration") - runtimeOnly library.java.slf4j_simple -} - -runShadow { - args = [] -} - -def portableValidatesRunnerTask(String name, boolean docker) { - def tempDir = File.createTempDir() - def pipelineOptions = [ - "--configOverride={\"job.non-logged.store.base.dir\":\"" + tempDir + "\"}" - ] - createPortableValidatesRunnerTask( - name: "validatesPortableRunner${name}", - jobServerDriver: "org.apache.beam.runners.samza.SamzaJobServerDriver", - jobServerConfig: "--job-host=localhost,--job-port=0,--artifact-port=0,--expansion-port=0", - testClasspathConfiguration: configurations.validatesPortableRunner, - numParallelTests: 1, - pipelineOpts: pipelineOptions, - environment: docker ? BeamModulePlugin.PortableValidatesRunnerConfiguration.Environment.DOCKER : BeamModulePlugin.PortableValidatesRunnerConfiguration.Environment.EMBEDDED, - testCategories: { - if (docker) { - includeCategories 'org.apache.beam.sdk.testing.UsesSdkHarnessEnvironment' - return - } - // TODO(https://github.com/apache/beam/issues/22657) - // includeCategories 'org.apache.beam.sdk.testing.NeedsRunner' - includeCategories 'org.apache.beam.sdk.testing.ValidatesRunner' - // Should be run only in a properly configured SDK harness environment - excludeCategories 'org.apache.beam.sdk.testing.UsesSdkHarnessEnvironment' - // TODO: BEAM-12350 - excludeCategories 'org.apache.beam.sdk.testing.UsesAttemptedMetrics' - // TODO: BEAM-12681 - excludeCategories 'org.apache.beam.sdk.testing.FlattenWithHeterogeneousCoders' - // Larger keys are possible, but they require more memory. - excludeCategories 'org.apache.beam.sdk.testing.LargeKeys$Above10MB' - excludeCategories 'org.apache.beam.sdk.testing.UsesCommittedMetrics' - excludeCategories 'org.apache.beam.sdk.testing.UsesExternalService' - excludeCategories 'org.apache.beam.sdk.testing.UsesCustomWindowMerging' - excludeCategories 'org.apache.beam.sdk.testing.UsesFailureMessage' - excludeCategories 'org.apache.beam.sdk.testing.UsesGaugeMetrics' - excludeCategories 'org.apache.beam.sdk.testing.UsesMapState' - excludeCategories 'org.apache.beam.sdk.testing.UsesMultimapState' - excludeCategories 'org.apache.beam.sdk.testing.UsesSetState' - excludeCategories 'org.apache.beam.sdk.testing.UsesOrderedListState' - excludeCategories 'org.apache.beam.sdk.testing.UsesStrictTimerOrdering' - excludeCategories 'org.apache.beam.sdk.testing.UsesOnWindowExpiration' - excludeCategories 'org.apache.beam.sdk.testing.UsesBundleFinalizer' - excludeCategories 'org.apache.beam.sdk.testing.UsesOrderedListState' - excludeCategories 'org.apache.beam.sdk.testing.UsesBoundedSplittableParDo' - excludeCategories 'org.apache.beam.sdk.testing.UsesTestStreamWithProcessingTime' - // TODO(https://github.com/apache/beam/issues/21023) - excludeCategories 'org.apache.beam.sdk.testing.UsesTestStreamWithMultipleStages' - excludeCategories 'org.apache.beam.sdk.testing.UsesUnboundedSplittableParDo' - excludeCategories 'org.apache.beam.sdk.testing.UsesLoopingTimer' - excludeCategories 'org.apache.beam.sdk.testing.UsesTriggeredSideInputs' - }, - testFilter: { - // TODO(https://github.com/apache/beam/issues/21042) - excludeTestsMatching "org.apache.beam.sdk.transforms.FlattenTest.testFlattenWithDifferentInputAndOutputCoders2" - excludeTestsMatching "org.apache.beam.sdk.transforms.FlattenTest.testEmptyFlattenAsSideInput" - excludeTestsMatching "org.apache.beam.sdk.transforms.FlattenTest.testFlattenPCollectionsEmptyThenParDo" - excludeTestsMatching "org.apache.beam.sdk.transforms.FlattenTest.testFlattenPCollectionsEmpty" - // TODO(BEAM-10025) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestampDefaultUnbounded' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestamp' - // TODO(https://github.com/apache/beam/issues/20847) - excludeTestsMatching 'org.apache.beam.sdk.testing.TestStreamTest.testFirstElementLate' - // TODO(https://github.com/apache/beam/issues/20846) - excludeTestsMatching 'org.apache.beam.sdk.testing.TestStreamTest.testLateDataAccumulating' - // TODO(https://github.com/apache/beam/issues/21142) - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupByKeyTest$WindowTests.testWindowFnPostMerging' - // TODO(https://github.com/apache/beam/issues/21143) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimestampTests.testParDoShiftTimestampInvalid' - // TODO(https://github.com/apache/beam/issues/21144) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimestampTests.testParDoShiftTimestampInvalidZeroAllowed' - // TODO(https://github.com/apache/beam/issues/32520) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoLifecycleTest.testTeardownCalledAfterExceptionIn*Stateful' - // TODO(https://github.com/apache/beam/issues/21145) - excludeTestsMatching 'org.apache.beam.sdk.transforms.DeduplicateTest.testEventTime' - // TODO(https://github.com/apache/beam/issues/21146) - excludeTestsMatching 'org.apache.beam.sdk.io.TFRecordIOTest.testReadInvalidRecord' - // TODO(https://github.com/apache/beam/issues/21147) - excludeTestsMatching 'org.apache.beam.sdk.io.TFRecordIOTest.testReadInvalidDataMask' - // TODO(https://github.com/apache/beam/issues/21148) - excludeTestsMatching 'org.apache.beam.sdk.io.TFRecordIOTest.testReadInvalidLengthMask' - // TODO(https://github.com/apache/beam/issues/21149) - excludeTestsMatching 'org.apache.beam.sdk.io.TextIOReadTest$CompressedReadTest.testCompressedReadWithoutExtension' - // TODO(https://github.com/apache/beam/issues/21150) - excludeTestsMatching 'org.apache.beam.sdk.io.WriteFilesTest.testWithRunnerDeterminedShardingUnbounded' - // TODO(https://github.com/apache/beam/issues/211505) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$MultipleInputsAndOutputTests.testParDoWritingToUndeclaredTag' - // TODO(https://github.com/apache/beam/issues/21152) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$MultipleInputsAndOutputTests.testParDoReadingFromUnknownSideInput' - // TODO(https://github.com/apache/beam/issues/21153) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ViewTest.testMapSideInputWithNullValuesCatchesDuplicates' - - // TODO(https://github.com/apache/beam/issues/21041) - excludeTestsMatching 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testEncodingNPException' - excludeTestsMatching 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testEncodingIOException' - excludeTestsMatching 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testDecodingNPException' - excludeTestsMatching 'org.apache.beam.sdk.coders.PCollectionCustomCoderTest.testDecodingIOException' - // TODO(https://github.com/apache/beam/issues/21040) - excludeTestsMatching 'org.apache.beam.sdk.PipelineTest.testEmptyPipeline' - // TODO(https://github.com/apache/beam/issues/21038) - excludeTestsMatching 'org.apache.beam.sdk.io.AvroIOTest*' - // TODO(https://github.com/apache/beam/issues/21039) - excludeTestsMatching 'org.apache.beam.sdk.io.FileIOTest*' - // TODO(https://github.com/apache/beam/issues/21037) - excludeTestsMatching 'org.apache.beam.sdk.transforms.WithTimestampsTest.withTimestampsBackwardsInTimeShouldThrow' - excludeTestsMatching 'org.apache.beam.sdk.transforms.WithTimestampsTest.withTimestampsWithNullTimestampShouldThrow' - // TODO(https://github.com/apache/beam/issues/21035) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ViewTest.testEmptySingletonSideInput' - excludeTestsMatching 'org.apache.beam.sdk.transforms.ViewTest.testNonSingletonSideInput' - // TODO(https://github.com/apache/beam/issues/21036) - excludeTestsMatching 'org.apache.beam.sdk.transforms.MapElementsTest.testMapSimpleFunction' - // TODO(https://github.com/apache/beam/issues/21033) - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInGlobalWindowBatchSizeByteSizeFn' - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInStreamingMode' - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testWithShardedKeyInGlobalWindow' - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testWithUnevenBatches' - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupIntoBatchesTest.testInGlobalWindowBatchSizeByteSize' - // TODO(BEAM-10025) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestampDefaultUnbounded' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testOutputTimestamp' - // TODO(https://github.com/apache/beam/issues/20703) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimerTests.testRelativeTimerWithOutputTimestamp' - // TODO(BEAM-13498) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ParDoTest$TimestampTests.testProcessElementSkew' - // TODO(https://github.com/apache/beam/issues/22650) - excludeTestsMatching 'org.apache.beam.sdk.transforms.GroupByKeyTest$BasicTests.testAfterProcessingTimeContinuationTriggerUsingState' - // TODO(https://github.com/apache/beam/issues/29973) - excludeTestsMatching 'org.apache.beam.sdk.transforms.ReshuffleTest.testReshufflePreservesMetadata' - // TODO(https://github.com/apache/beam/issues/31231) - excludeTestsMatching 'org.apache.beam.sdk.transforms.RedistributeTest.testRedistributePreservesMetadata' - } - ) -} - -project.ext.validatesPortableRunnerDocker = portableValidatesRunnerTask("Docker", true) -project.ext.validatesPortableRunnerEmbedded = portableValidatesRunnerTask("Embedded", false) - -tasks.register("validatesPortableRunner") { - dependsOn validatesPortableRunnerDocker - dependsOn validatesPortableRunnerEmbedded -} - -def testJavaVersion = project.findProperty('testJavaVersion') -String testJavaHome = null -if (testJavaVersion) { - testJavaHome = project.findProperty("java${testJavaVersion}Home") -} - -def jobPort = BeamModulePlugin.getRandomPort() -def artifactPort = BeamModulePlugin.getRandomPort() - -def setupTask = project.tasks.register("samzaJobServerSetup", Exec) { - dependsOn shadowJar - def pythonDir = project.project(":sdks:python").projectDir - def samzaJobServerJar = shadowJar.archivePath - if (testJavaHome) { - environment "JAVA_HOME", testJavaHome - } - executable 'sh' - args '-c', "$pythonDir/scripts/run_job_server.sh stop --group_id ${project.name} && $pythonDir/scripts/run_job_server.sh start --group_id ${project.name} --job_port ${jobPort} --artifact_port ${artifactPort} --job_server_jar ${samzaJobServerJar}" -} - -def cleanupTask = project.tasks.register("samzaJobServerCleanup", Exec) { - def pythonDir = project.project(":sdks:python").projectDir - if (testJavaHome) { - environment "JAVA_HOME", testJavaHome - } - executable 'sh' - args '-c', "$pythonDir/scripts/run_job_server.sh stop --group_id ${project.name}" -} - -createCrossLanguageValidatesRunnerTask( - startJobServer: setupTask, - cleanupJobServer: cleanupTask, - classpath: configurations.validatesPortableRunner, - numParallelTests: 1, - pythonPipelineOptions: [ - "--runner=PortableRunner", - "--job_endpoint=localhost:${jobPort}", - "--environment_cache_millis=10000", - "--experiments=beam_fn_api", - ], - javaPipelineOptions: [ - "--runner=PortableRunner", - "--jobEndpoint=localhost:${jobPort}", - "--environmentCacheMillis=10000", - "--experiments=beam_fn_api", - "--customBeamRequirement=${project.project(":sdks:python").projectDir}/build/apache-beam.tar.gz", - ], - goScriptOptions: [ - "--runner samza", - "--tests \"./test/integration/xlang ./test/integration/io/xlang/...\"", - "--endpoint localhost:${jobPort}", - ], -) diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionContext.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionContext.java deleted file mode 100644 index 18bb098bf2cc..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionContext.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.samza.context.ApplicationContainerContext; -import org.apache.samza.context.ApplicationContainerContextFactory; -import org.apache.samza.context.ContainerContext; -import org.apache.samza.context.ExternalContext; -import org.apache.samza.context.JobContext; -import org.apache.samza.metrics.MetricsRegistryMap; - -/** Runtime context for the Samza runner. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaExecutionContext implements ApplicationContainerContext { - - private final SamzaPipelineOptions options; - private SamzaMetricsContainer metricsContainer; - - public SamzaExecutionContext(SamzaPipelineOptions options) { - this.options = options; - } - - public SamzaPipelineOptions getPipelineOptions() { - return options; - } - - public SamzaMetricsContainer getMetricsContainer() { - return this.metricsContainer; - } - - void setMetricsContainer(SamzaMetricsContainer metricsContainer) { - this.metricsContainer = metricsContainer; - } - - @Override - public void start() {} - - @Override - public void stop() {} - - /** The factory to return this {@link SamzaExecutionContext}. */ - public class Factory implements ApplicationContainerContextFactory { - - @Override - public SamzaExecutionContext create( - ExternalContext externalContext, JobContext jobContext, ContainerContext containerContext) { - - final MetricsRegistryMap metricsRegistry = - (MetricsRegistryMap) containerContext.getContainerMetricsRegistry(); - SamzaExecutionContext.this.setMetricsContainer(new SamzaMetricsContainer(metricsRegistry)); - return SamzaExecutionContext.this; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionEnvironment.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionEnvironment.java deleted file mode 100644 index 02c31edfd1dd..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaExecutionEnvironment.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -/** Different Samza execution environments that defines how the Samza job will be deployed. */ -public enum SamzaExecutionEnvironment { - /** - * Runs the Samza job on the local machine with only one container. There is no coordination - * required since there is only one container deployed in a single JVM. This setting is generally - * used for development and testing. - */ - LOCAL, - - /** - * Submits and runs the Samza job on YARN, a remote clustered resource manager. Samza works with - * the YARN to provision and coordinate resources for your application and run it across a cluster - * of machines. It also handles failures of individual instances and automatically restarts them. - */ - YARN, - - /** - * Runs Samza job as a stand alone embedded library mode which can be imported into your Java - * application. You can increase your application's capacity by spinning up multiple instances. - * These instances will then dynamically coordinate with each other and distribute work among - * themselves. If an instance fails, the tasks running on it will be re-assigned to the remaining - * ones. By default, Samza uses Zookeeper for coordination across individual instances. - */ - STANDALONE -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobInvoker.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobInvoker.java deleted file mode 100644 index 6638b35f377d..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobInvoker.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import java.util.UUID; -import javax.annotation.Nullable; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.jobsubmission.JobInvocation; -import org.apache.beam.runners.jobsubmission.JobInvoker; -import org.apache.beam.runners.jobsubmission.PortablePipelineJarCreator; -import org.apache.beam.runners.jobsubmission.PortablePipelineRunner; -import org.apache.beam.sdk.util.construction.PipelineOptionsTranslation; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.Struct; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ListeningExecutorService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaJobInvoker extends JobInvoker { - - private static final Logger LOG = LoggerFactory.getLogger(SamzaJobInvoker.class); - - public static SamzaJobInvoker create( - SamzaJobServerDriver.SamzaServerConfiguration configuration) { - return new SamzaJobInvoker(); - } - - private SamzaJobInvoker() { - this("samza-runner-job-invoker-%d"); - } - - protected SamzaJobInvoker(String name) { - super(name); - } - - @Override - protected JobInvocation invokeWithExecutor( - RunnerApi.Pipeline pipeline, - Struct options, - @Nullable String retrievalToken, - ListeningExecutorService executorService) { - LOG.trace("Parsing pipeline options"); - final SamzaPortablePipelineOptions samzaOptions = - PipelineOptionsTranslation.fromProto(options).as(SamzaPortablePipelineOptions.class); - - final PortablePipelineRunner pipelineRunner; - if (Strings.isNullOrEmpty(samzaOptions.getOutputExecutablePath())) { - pipelineRunner = new SamzaPipelineRunner(samzaOptions); - } else { - /* - * To support --output_executable_path where bundles the input pipeline along with all - * artifacts, etc. required to run the pipeline into a jar that can be executed later. - */ - pipelineRunner = new PortablePipelineJarCreator(SamzaPipelineRunner.class); - } - - final String invocationId = - String.format("%s_%s", samzaOptions.getJobName(), UUID.randomUUID().toString()); - final JobInfo jobInfo = - JobInfo.create(invocationId, samzaOptions.getJobName(), retrievalToken, options); - return new JobInvocation(jobInfo, executorService, pipeline, pipelineRunner); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobServerDriver.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobServerDriver.java deleted file mode 100644 index f8139c0d26f2..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaJobServerDriver.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.runners.jobsubmission.JobServerDriver; -import org.apache.beam.sdk.fn.server.ServerFactory; -import org.apache.beam.sdk.io.FileSystems; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Driver program that starts a job server for the Samza runner. */ -public class SamzaJobServerDriver extends JobServerDriver { - - private static final Logger LOG = LoggerFactory.getLogger(SamzaJobServerDriver.class); - - /** Samza runner-specific Configuration for the jobServer. */ - public static class SamzaServerConfiguration extends ServerConfiguration {} - - public static void main(String[] args) { - // TODO: Expose the fileSystem related options. - PipelineOptions options = PipelineOptionsFactory.create(); - // Register standard file systems. - FileSystems.setDefaultPipelineOptions(options); - fromParams(args).run(); - } - - private static SamzaJobServerDriver fromParams(String[] args) { - return fromConfig(parseArgs(args)); - } - - private static void printUsage(CmdLineParser parser) { - System.err.printf("Usage: java %s arguments...%n", SamzaJobServerDriver.class.getSimpleName()); - parser.printUsage(System.err); - System.err.println(); - } - - private static SamzaJobServerDriver fromConfig(SamzaServerConfiguration configuration) { - return create( - configuration, - createJobServerFactory(configuration), - createArtifactServerFactory(configuration)); - } - - public static SamzaServerConfiguration parseArgs(String[] args) { - SamzaServerConfiguration configuration = new SamzaServerConfiguration(); - CmdLineParser parser = new CmdLineParser(configuration); - try { - parser.parseArgument(args); - } catch (CmdLineException e) { - LOG.error("Unable to parse command line arguments.", e); - printUsage(parser); - throw new IllegalArgumentException("Unable to parse command line arguments.", e); - } - return configuration; - } - - private static SamzaJobServerDriver create( - SamzaServerConfiguration configuration, - ServerFactory jobServerFactory, - ServerFactory artifactServerFactory) { - return new SamzaJobServerDriver(configuration, jobServerFactory, artifactServerFactory); - } - - private SamzaJobServerDriver( - SamzaServerConfiguration configuration, - ServerFactory jobServerFactory, - ServerFactory artifactServerFactory) { - this( - configuration, - jobServerFactory, - artifactServerFactory, - () -> SamzaJobInvoker.create(configuration)); - } - - protected SamzaJobServerDriver( - ServerConfiguration configuration, - ServerFactory jobServerFactory, - ServerFactory artifactServerFactory, - JobInvokerFactory jobInvokerFactory) { - super(configuration, jobServerFactory, artifactServerFactory, jobInvokerFactory); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineLifeCycleListener.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineLifeCycleListener.java deleted file mode 100644 index 47f36a229ac1..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineLifeCycleListener.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.samza.config.Config; -import org.apache.samza.context.ExternalContext; - -/** Life cycle listener for a Samza pipeline during runtime. */ -public interface SamzaPipelineLifeCycleListener { - /** Callback when the pipeline options is created. */ - void onInit(Config config, SamzaPipelineOptions options); - - /** Callback when the pipeline is started. */ - ExternalContext onStart(); - - /** - * Callback after the pipeline is submmitted. This will be invoked only for Samza jobs submitted - * to a cluster. - */ - void onSubmit(); - - /** Callback after the pipeline is finished. */ - void onFinish(); - - /** A registrar for {@link SamzaPipelineLifeCycleListener}. */ - interface Registrar { - SamzaPipelineLifeCycleListener getLifeCycleListener(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptions.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptions.java deleted file mode 100644 index a34303d92552..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptions.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.apache.beam.sdk.options.Default; -import org.apache.beam.sdk.options.DefaultValueFactory; -import org.apache.beam.sdk.options.Description; -import org.apache.beam.sdk.options.Hidden; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; -import org.apache.samza.config.ConfigLoaderFactory; -import org.apache.samza.config.loaders.PropertiesConfigLoaderFactory; -import org.apache.samza.metrics.MetricsReporter; - -/** Options which can be used to configure a Samza PortablePipelineRunner. */ -public interface SamzaPipelineOptions extends PipelineOptions { - - @Description( - "The config file for Samza. It is *optional*. By default Samza supports properties config." - + "Without a config file, Samza uses a default config for local execution.") - String getConfigFilePath(); - - void setConfigFilePath(String filePath); - - @Description("The factory to read config file from config file path.") - @Default.Class(PropertiesConfigLoaderFactory.class) - Class getConfigLoaderFactory(); - - void setConfigLoaderFactory(Class configLoaderFactory); - - @Description( - "The config override to set programmatically. It will be applied on " - + "top of config file if it exits, otherwise used directly as the config.") - Map getConfigOverride(); - - void setConfigOverride(Map configs); - - @Description("The instance name of the job") - @Default.String("1") - String getJobInstance(); - - void setJobInstance(String instance); - - @Description( - "Samza application execution environment." - + "See {@link org.apache.beam.runners.samza.SamzaExecutionEnvironment} for detailed environment descriptions.") - @Default.Enum("LOCAL") - SamzaExecutionEnvironment getSamzaExecutionEnvironment(); - - void setSamzaExecutionEnvironment(SamzaExecutionEnvironment environment); - - @Description("The interval to check for watermarks in milliseconds.") - @Default.Long(1000) - long getWatermarkInterval(); - - void setWatermarkInterval(long interval); - - @Description("The maximum number of messages to buffer for a given system.") - @Default.Integer(5000) - int getSystemBufferSize(); - - void setSystemBufferSize(int consumerBufferSize); - - @Description("The maximum number of event-time timers to buffer in memory for a PTransform") - @Default.Integer(50000) - int getEventTimerBufferSize(); - - void setEventTimerBufferSize(int eventTimerBufferSize); - - @Description("The maximum number of ready timers to process at once per watermark.") - @Default.Integer(Integer.MAX_VALUE) - int getMaxReadyTimersToProcessOnce(); - - void setMaxReadyTimersToProcessOnce(int maxReadyTimersToProcessOnce); - - @Description("The maximum parallelism allowed for any data source.") - @Default.Integer(1) - int getMaxSourceParallelism(); - - void setMaxSourceParallelism(int maxSourceParallelism); - - @Description("The batch get size limit for the state store.") - @Default.Integer(10000) - int getStoreBatchGetSize(); - - void setStoreBatchGetSize(int storeBatchGetSize); - - @Description("Enable/disable Beam metrics in Samza Runner") - @Default.Boolean(true) - Boolean getEnableMetrics(); - - void setEnableMetrics(Boolean enableMetrics); - - @Description("Enable/disable Beam Transform throughput, latency metrics in Samza Runner") - @Default.Boolean(false) - Boolean getEnableTransformMetrics(); - - void setEnableTransformMetrics(Boolean enableMetrics); - - @Description("The config for state to be durable") - @Default.Boolean(false) - Boolean getStateDurable(); - - void setStateDurable(Boolean stateDurable); - - @JsonIgnore - @Description("The metrics reporters that will be used to emit metrics.") - List getMetricsReporters(); - - void setMetricsReporters(List reporters); - - @Description("The maximum number of elements in a bundle.") - @Default.Long(1) - long getMaxBundleSize(); - - void setMaxBundleSize(long maxBundleSize); - - @Description("The maximum time to wait before finalising a bundle (in milliseconds).") - @Default.Long(1000) - long getMaxBundleTimeMs(); - - void setMaxBundleTimeMs(long maxBundleTimeMs); - - @Description( - "Wait if necessary for completing a remote bundle processing for at most the given time (in milliseconds). if the value of timeout is negative, wait forever until the bundle processing is completed. Used only in portable mode for now.") - @Default.Long(-1) - long getBundleProcessingTimeout(); - - void setBundleProcessingTimeout(long timeoutMs); - - @Description( - "The number of threads to run DoFn.processElements in parallel within a bundle. Used only in non-portable mode.") - @Default.Integer(1) - int getNumThreadsForProcessElement(); - - void setNumThreadsForProcessElement(int numThreads); - - @JsonIgnore - @Description( - "The ExecutorService instance to run DoFN.processElements in parallel within a bundle. Used only in non-portable mode.") - @Default.InstanceFactory(ProcessElementExecutorServiceFactory.class) - @Hidden - ExecutorService getExecutorServiceForProcessElement(); - - void setExecutorServiceForProcessElement(ExecutorService executorService); - - class ProcessElementExecutorServiceFactory implements DefaultValueFactory { - - @Override - public ExecutorService create(PipelineOptions options) { - return Executors.newFixedThreadPool( - options.as(SamzaPipelineOptions.class).getNumThreadsForProcessElement(), - new ThreadFactoryBuilder().setNameFormat("Process Element Thread-%d").build()); - } - } - - @Description("Enable/disable late data dropping in GroupByKey/Combine transforms") - @Default.Boolean(false) - boolean getDropLateData(); - - void setDropLateData(boolean dropLateData); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidator.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidator.java deleted file mode 100644 index 1db0974b5d30..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineOptionsValidator.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; -import static org.apache.samza.config.JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE; - -import java.util.HashMap; -import java.util.Map; -import org.apache.samza.config.JobConfig; -import org.apache.samza.config.MapConfig; - -/** Validates that the {@link SamzaPipelineOptions} conforms to all the criteria. */ -public class SamzaPipelineOptionsValidator { - public static void validate(SamzaPipelineOptions opts) { - checkArgument(opts.getMaxSourceParallelism() >= 1); - validateBundlingRelatedOptions(opts); - } - - /* - * Perform some bundling related validation for pipeline option. - * Visible for testing. - */ - static void validateBundlingRelatedOptions(SamzaPipelineOptions pipelineOptions) { - if (pipelineOptions.getMaxBundleSize() > 1) { - final Map configs = - pipelineOptions.getConfigOverride() == null - ? new HashMap<>() - : pipelineOptions.getConfigOverride(); - final JobConfig jobConfig = new JobConfig(new MapConfig(configs)); - - // Validate that the threadPoolSize is not override in the code - checkArgument( - jobConfig.getThreadPoolSize() <= 1, - JOB_CONTAINER_THREAD_POOL_SIZE - + " config should be replaced with SamzaPipelineOptions.numThreadsForProcessElement"); - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineResult.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineResult.java deleted file mode 100644 index e84cf086edc9..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineResult.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import static org.apache.beam.runners.core.metrics.MetricsContainerStepMap.asAttemptedOnlyMetricResults; -import static org.apache.samza.config.TaskConfig.TASK_SHUTDOWN_MS; - -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.PipelineResult; -import org.apache.beam.sdk.metrics.MetricResults; -import org.apache.beam.sdk.util.UserCodeException; -import org.apache.samza.config.Config; -import org.apache.samza.job.ApplicationStatus; -import org.apache.samza.runtime.ApplicationRunner; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.joda.time.Duration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** The result from executing a Samza Pipeline. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaPipelineResult implements PipelineResult { - private static final Logger LOG = LoggerFactory.getLogger(SamzaPipelineResult.class); - // allow some buffer on top of samza's own shutdown timeout - private static final long SHUTDOWN_TIMEOUT_BUFFER = 5000L; - private static final long DEFAULT_TASK_SHUTDOWN_MS = 30000L; - - private final SamzaExecutionContext executionContext; - private final ApplicationRunner runner; - private final SamzaPipelineLifeCycleListener listener; - private final long shutdownTiemoutMs; - - public SamzaPipelineResult( - ApplicationRunner runner, - SamzaExecutionContext executionContext, - SamzaPipelineLifeCycleListener listener, - Config config) { - this.executionContext = executionContext; - this.runner = runner; - this.listener = listener; - this.shutdownTiemoutMs = - config.getLong(TASK_SHUTDOWN_MS, DEFAULT_TASK_SHUTDOWN_MS) + SHUTDOWN_TIMEOUT_BUFFER; - } - - @Override - public State getState() { - return getStateInfo().state; - } - - @Override - public State cancel() { - LOG.info("Start to cancel samza pipeline..."); - runner.kill(); - LOG.info("Start awaiting finish for {} ms.", shutdownTiemoutMs); - return waitUntilFinish(Duration.millis(shutdownTiemoutMs)); - } - - @Override - public State waitUntilFinish(@Nullable Duration duration) { - try { - if (duration == null) { - runner.waitForFinish(); - } else { - runner.waitForFinish(java.time.Duration.ofMillis(duration.getMillis())); - } - } catch (Exception e) { - throw new Pipeline.PipelineExecutionException(e); - } - - final StateInfo stateInfo = getStateInfo(); - - if (listener != null && (stateInfo.state == State.DONE || stateInfo.state == State.FAILED)) { - listener.onFinish(); - } - - if (stateInfo.state == State.FAILED) { - throw stateInfo.error; - } - - LOG.info("Pipeline finished. Final state: {}", stateInfo.state); - return stateInfo.state; - } - - @Override - public State waitUntilFinish() { - return waitUntilFinish(null); - } - - @Override - public MetricResults metrics() { - return asAttemptedOnlyMetricResults(executionContext.getMetricsContainer().getContainers()); - } - - @SuppressWarnings("Slf4jDoNotLogMessageOfExceptionExplicitly") - private StateInfo getStateInfo() { - final ApplicationStatus status = runner.status(); - switch (status.getStatusCode()) { - case New: - return new StateInfo(State.STOPPED); - case Running: - return new StateInfo(State.RUNNING); - case SuccessfulFinish: - return new StateInfo(State.DONE); - case UnsuccessfulFinish: - LOG.error("Pipeline execution failed", status.getThrowable()); - return new StateInfo( - State.FAILED, - new Pipeline.PipelineExecutionException(getUserCodeException(status.getThrowable()))); - default: - return new StateInfo(State.UNKNOWN); - } - } - - private static class StateInfo { - private final State state; - private final Pipeline.PipelineExecutionException error; - - private StateInfo(State state) { - this(state, null); - } - - private StateInfo(State state, Pipeline.PipelineExecutionException error) { - this.state = state; - this.error = error; - } - } - - /** - * Some of the Beam unit tests relying on the exception message to do assertion. This function - * will find the original UserCodeException so the message will be exposed directly. - */ - private static Throwable getUserCodeException(Throwable throwable) { - Throwable t = throwable; - while (t != null) { - if (t instanceof UserCodeException) { - return t; - } - - t = t.getCause(); - } - - return throwable; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineRunner.java deleted file mode 100644 index 897b78cf9e47..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPipelineRunner.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.jobsubmission.PortablePipelineResult; -import org.apache.beam.runners.jobsubmission.PortablePipelineRunner; -import org.apache.beam.runners.samza.translation.SamzaPortablePipelineTranslator; -import org.apache.beam.sdk.util.construction.PTransformTranslation; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.util.construction.graph.GreedyPipelineFuser; -import org.apache.beam.sdk.util.construction.graph.ProtoOverrides; -import org.apache.beam.sdk.util.construction.graph.SplittableParDoExpander; -import org.apache.beam.sdk.util.construction.graph.TrivialNativeTransformExpander; -import org.apache.beam.sdk.util.construction.renderer.PipelineDotRenderer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Runs a Samza job via {@link SamzaRunner}. */ -public class SamzaPipelineRunner implements PortablePipelineRunner { - - private static final Logger LOG = LoggerFactory.getLogger(SamzaPipelineRunner.class); - - private final SamzaPipelineOptions options; - - @Override - public PortablePipelineResult run(final RunnerApi.Pipeline pipeline, JobInfo jobInfo) { - // Expand any splittable DoFns within the graph to enable sizing and splitting of bundles. - RunnerApi.Pipeline pipelineWithSdfExpanded = - ProtoOverrides.updateTransform( - PTransformTranslation.PAR_DO_TRANSFORM_URN, - pipeline, - SplittableParDoExpander.createSizedReplacement()); - - // Don't let the fuser fuse any subcomponents of native transforms. - RunnerApi.Pipeline trimmedPipeline = - TrivialNativeTransformExpander.forKnownUrns( - pipelineWithSdfExpanded, SamzaPortablePipelineTranslator.knownUrns()); - - // Fused pipeline proto. - // TODO: Consider supporting partially-fused graphs. - RunnerApi.Pipeline fusedPipeline = - trimmedPipeline.getComponents().getTransformsMap().values().stream() - .anyMatch(proto -> ExecutableStage.URN.equals(proto.getSpec().getUrn())) - ? trimmedPipeline - : GreedyPipelineFuser.fuse(trimmedPipeline).toPipeline(); - - LOG.info("Portable pipeline to run:"); - LOG.info("{}", PipelineDotRenderer.toDotString(fusedPipeline)); - // the pipeline option coming from sdk will set the sdk specific runner which will break - // serialization - // so we need to reset the runner here to a valid Java runner - options.setRunner(SamzaRunner.class); - try { - final SamzaRunner runner = SamzaRunner.fromOptions(options); - final PortablePipelineResult result = runner.runPortablePipeline(fusedPipeline, jobInfo); - - final SamzaExecutionEnvironment exeEnv = options.getSamzaExecutionEnvironment(); - if (exeEnv == SamzaExecutionEnvironment.LOCAL - || exeEnv == SamzaExecutionEnvironment.STANDALONE) { - // Make run() sync for local mode - result.waitUntilFinish(); - } - return result; - } catch (Exception e) { - throw new RuntimeException("Failed to invoke samza job", e); - } - } - - public SamzaPipelineRunner(SamzaPipelineOptions options) { - this.options = options; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineOptions.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineOptions.java deleted file mode 100644 index aa8e7ceb71d7..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineOptions.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.sdk.options.Description; -import org.apache.beam.sdk.options.PortablePipelineOptions; - -/** Samza pipeline option that contains portability specific logic. For internal usage only. */ -public interface SamzaPortablePipelineOptions - extends SamzaPipelineOptions, PortablePipelineOptions { - @Description( - "The file path for the local file system token. If not set (by default), then the runner would" - + " not use secure server factory.") - String getFsTokenPath(); - - void setFsTokenPath(String path); -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineResult.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineResult.java deleted file mode 100644 index a3452097f511..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaPortablePipelineResult.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import org.apache.beam.model.jobmanagement.v1.JobApi; -import org.apache.beam.runners.jobsubmission.PortablePipelineResult; -import org.apache.samza.application.StreamApplication; -import org.apache.samza.config.Config; -import org.apache.samza.runtime.ApplicationRunner; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** The result from executing a Samza Portable Pipeline. */ -public class SamzaPortablePipelineResult extends SamzaPipelineResult - implements PortablePipelineResult { - - private static final Logger LOG = LoggerFactory.getLogger(SamzaPortablePipelineResult.class); - - SamzaPortablePipelineResult( - StreamApplication app, - ApplicationRunner runner, - SamzaExecutionContext executionContext, - SamzaPipelineLifeCycleListener listener, - Config config) { - super(runner, executionContext, listener, config); - } - - @Override - public JobApi.MetricResults portableMetrics() throws UnsupportedOperationException { - LOG.warn("Collecting monitoring infos is not implemented yet in Samza portable runner."); - return JobApi.MetricResults.newBuilder().build(); - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunner.java deleted file mode 100644 index eb16faa41ac0..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunner.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; -import org.apache.beam.model.pipeline.v1.RunnerApi; -import org.apache.beam.runners.fnexecution.provisioning.JobInfo; -import org.apache.beam.runners.jobsubmission.PortablePipelineResult; -import org.apache.beam.runners.samza.translation.ConfigBuilder; -import org.apache.beam.runners.samza.translation.ConfigContext; -import org.apache.beam.runners.samza.translation.PViewToIdMapper; -import org.apache.beam.runners.samza.translation.PortableTranslationContext; -import org.apache.beam.runners.samza.translation.SamzaPipelineTranslator; -import org.apache.beam.runners.samza.translation.SamzaPortablePipelineTranslator; -import org.apache.beam.runners.samza.translation.SamzaTransformOverrides; -import org.apache.beam.runners.samza.translation.StateIdParser; -import org.apache.beam.runners.samza.translation.TranslationContext; -import org.apache.beam.runners.samza.util.PipelineJsonRenderer; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.PipelineRunner; -import org.apache.beam.sdk.metrics.MetricsEnvironment; -import org.apache.beam.sdk.options.ExperimentalOptions; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.PipelineOptionsValidator; -import org.apache.beam.sdk.util.construction.SplittableParDo; -import org.apache.beam.sdk.util.construction.renderer.PipelineDotRenderer; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; -import org.apache.samza.application.StreamApplication; -import org.apache.samza.config.Config; -import org.apache.samza.context.ExternalContext; -import org.apache.samza.metrics.MetricsReporter; -import org.apache.samza.metrics.MetricsReporterFactory; -import org.apache.samza.runtime.ApplicationRunner; -import org.apache.samza.runtime.ApplicationRunners; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A {@link PipelineRunner} that executes the operations in the {@link Pipeline} into an equivalent - * Samza plan. - * - * @deprecated The support for Samza is scheduled for removal in Beam 3.0. - */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -@Deprecated -public class SamzaRunner extends PipelineRunner { - private static final Logger LOG = LoggerFactory.getLogger(SamzaRunner.class); - private static final String BEAM_DOT_GRAPH = "beamDotGraph"; - public static final String BEAM_JSON_GRAPH = "beamJsonGraph"; - - public static SamzaRunner fromOptions(PipelineOptions opts) { - final SamzaPipelineOptions samzaOptions = - PipelineOptionsValidator.validate(SamzaPipelineOptions.class, opts); - return new SamzaRunner(samzaOptions); - } - - private final SamzaPipelineOptions options; - private final SamzaPipelineLifeCycleListener listener; - - private SamzaRunner(SamzaPipelineOptions options) { - this.options = options; - final Iterator listenerReg = - ServiceLoader.load(SamzaPipelineLifeCycleListener.Registrar.class).iterator(); - this.listener = - listenerReg.hasNext() ? Iterators.getOnlyElement(listenerReg).getLifeCycleListener() : null; - } - - public PortablePipelineResult runPortablePipeline(RunnerApi.Pipeline pipeline, JobInfo jobInfo) { - final String dotGraph = PipelineDotRenderer.toDotString(pipeline); - LOG.info("Portable pipeline to run DOT graph:\n{}", dotGraph); - - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPortablePipelineTranslator.createConfig(pipeline, configBuilder, options); - configBuilder.put(BEAM_DOT_GRAPH, dotGraph); - - final Config config = configBuilder.build(); - options.setConfigOverride(config); - - if (listener != null) { - listener.onInit(config, options); - } - - final SamzaExecutionContext executionContext = new SamzaExecutionContext(options); - final Map reporterFactories = getMetricsReporters(); - final StreamApplication app = - appDescriptor -> { - appDescriptor - .withApplicationContainerContextFactory(executionContext.new Factory()) - .withMetricsReporterFactories(reporterFactories); - SamzaPortablePipelineTranslator.translate( - pipeline, new PortableTranslationContext(appDescriptor, options, jobInfo)); - }; - - ApplicationRunner runner = runSamzaApp(app, config); - return new SamzaPortablePipelineResult(app, runner, executionContext, listener, config); - } - - @Override - public SamzaPipelineResult run(Pipeline pipeline) { - // TODO(https://github.com/apache/beam/issues/20530): Use SDF read as default for non-portable - // execution when we address performance issue. - if (!ExperimentalOptions.hasExperiment(pipeline.getOptions(), "beam_fn_api")) { - SplittableParDo.convertReadBasedSplittableDoFnsToPrimitiveReadsIfNecessary(pipeline); - } - - MetricsEnvironment.setMetricsSupported(true); - - if (LOG.isDebugEnabled()) { - LOG.debug( - "Pre-processed Beam pipeline in dot format:\n{}", - PipelineDotRenderer.toDotString(pipeline)); - } - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - - final String dotGraph = PipelineDotRenderer.toDotString(pipeline); - LOG.info("Beam pipeline DOT graph:\n{}", dotGraph); - - final String jsonGraph = PipelineJsonRenderer.toJsonString(pipeline, configCtx); - LOG.info("Beam pipeline JSON graph:\n{}", jsonGraph); - - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - configBuilder.put(BEAM_DOT_GRAPH, dotGraph); - configBuilder.put(BEAM_JSON_GRAPH, jsonGraph); - - final Config config = configBuilder.build(); - options.setConfigOverride(config); - - if (listener != null) { - listener.onInit(config, options); - } - - final SamzaExecutionContext executionContext = new SamzaExecutionContext(options); - final Map reporterFactories = getMetricsReporters(); - - final StreamApplication app = - appDescriptor -> { - appDescriptor.withApplicationContainerContextFactory(executionContext.new Factory()); - appDescriptor.withMetricsReporterFactories(reporterFactories); - - SamzaPipelineTranslator.translate( - pipeline, new TranslationContext(appDescriptor, idMap, nonUniqueStateIds, options)); - }; - - // perform a final round of validation for the pipeline options now that all configs are - // generated - SamzaPipelineOptionsValidator.validate(options); - ApplicationRunner runner = runSamzaApp(app, config); - return new SamzaPipelineResult(runner, executionContext, listener, config); - } - - private Map getMetricsReporters() { - if (options.getMetricsReporters() != null) { - final Map reporters = new HashMap<>(); - for (int i = 0; i < options.getMetricsReporters().size(); i++) { - final String name = "beam-metrics-reporter-" + i; - final MetricsReporter reporter = options.getMetricsReporters().get(i); - - reporters.put(name, (MetricsReporterFactory) (nm, processorId, config) -> reporter); - LOG.info("{}: {}", name, reporter.getClass().getName()); - } - return reporters; - } else { - return Collections.emptyMap(); - } - } - - private ApplicationRunner runSamzaApp(StreamApplication app, Config config) { - - final ApplicationRunner runner = ApplicationRunners.getApplicationRunner(app, config); - - ExternalContext externalContext = null; - if (listener != null) { - externalContext = listener.onStart(); - } - - runner.run(externalContext); - - if (listener != null - && options.getSamzaExecutionEnvironment() == SamzaExecutionEnvironment.YARN) { - listener.onSubmit(); - } - - return runner; - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerOverrideConfigs.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerOverrideConfigs.java deleted file mode 100644 index 4c5fa432ca82..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerOverrideConfigs.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import java.time.Duration; - -// TODO: can we get rid of this class? Right now the SamzaPipelineOptionsValidator would force -// the pipeline option to be the type SamzaPipelineOption. Ideally, we should be able to keep -// passing SamzaPortablePipelineOption. Alternative, we could merge portable and non-portable -// pipeline option. -/** A helper class for holding all the beam runner specific samza configs. */ -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaRunnerOverrideConfigs { - public static final String BEAM_RUNNER_CONFIG_PREFIX = "beam.override."; - // whether the job is in portable mode - public static final String IS_PORTABLE_MODE = BEAM_RUNNER_CONFIG_PREFIX + "portable"; - // for portable mode only: port number for fn control api - public static final String FN_CONTROL_PORT = BEAM_RUNNER_CONFIG_PREFIX + "control.port"; - // timeout for waiting for control client to connect - public static final String CONTROL_CLIENT_MAX_WAIT_TIME_MS = "controL.wait.time.ms"; - public static final long DEFAULT_CONTROL_CLIENT_MAX_WAIT_TIME_MS = - Duration.ofMinutes(2).toMillis(); - public static final String FS_TOKEN_PATH = BEAM_RUNNER_CONFIG_PREFIX + "fs.token.path"; - public static final String DEFAULT_FS_TOKEN_PATH = null; - - private static boolean containsKey(SamzaPipelineOptions options, String configKey) { - if (options == null || options.getConfigOverride() == null) { - return false; - } - return options.getConfigOverride().containsKey(configKey); - } - - /** Whether the job is in portable mode based on the config override in the pipeline options. */ - public static boolean isPortableMode(SamzaPipelineOptions options) { - if (containsKey(options, IS_PORTABLE_MODE)) { - return options.getConfigOverride().get(IS_PORTABLE_MODE).equals(String.valueOf(true)); - } else { - return false; - } - } - - /** Get fn control port number based on the config override in the pipeline options. */ - public static int getFnControlPort(SamzaPipelineOptions options) { - if (containsKey(options, FN_CONTROL_PORT)) { - return Integer.parseInt(options.getConfigOverride().get(FN_CONTROL_PORT)); - } else { - return -1; - } - } - - /** Get max wait time for control client connection. */ - public static long getControlClientWaitTimeoutMs(SamzaPipelineOptions options) { - if (containsKey(options, CONTROL_CLIENT_MAX_WAIT_TIME_MS)) { - return Long.parseLong(options.getConfigOverride().get(CONTROL_CLIENT_MAX_WAIT_TIME_MS)); - } else { - return DEFAULT_CONTROL_CLIENT_MAX_WAIT_TIME_MS; - } - } - - /** Get fs token path for portable mode. */ - public static String getFsTokenPath(SamzaPipelineOptions options) { - if (containsKey(options, FS_TOKEN_PATH)) { - return options.getConfigOverride().get(FS_TOKEN_PATH); - } else { - return DEFAULT_FS_TOKEN_PATH; - } - } -} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerRegistrar.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerRegistrar.java deleted file mode 100644 index 102838975d13..000000000000 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/SamzaRunnerRegistrar.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza; - -import com.google.auto.service.AutoService; -import org.apache.beam.sdk.PipelineRunner; -import org.apache.beam.sdk.options.PipelineOptions; -import org.apache.beam.sdk.options.PipelineOptionsRegistrar; -import org.apache.beam.sdk.runners.PipelineRunnerRegistrar; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; - -/** - * AutoService registrar - will register SamzaRunner and SamzaOptions as possible pipeline runner - * services. - * - *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Note due to the bug in SAMZA-2761, end-of-stream can cause shutdown while there are still - * messages in process in asynchronous mode. As a temporary solution, we add more bundles to process - * in the test inputs. - */ -public class AsyncDoFnRunnerTest implements Serializable { - - @Rule - public final transient TestPipeline pipeline = - TestPipeline.fromOptions( - PipelineOptionsFactory.fromArgs( - "--runner=TestSamzaRunner", - "--maxBundleSize=5", - "--numThreadsForProcessElement=5") - .create()); - - @Test - @Ignore("https://github.com/apache/beam/issues/23745") - public void testSimplePipeline() { - List input = new ArrayList<>(); - for (int i = 1; i < 20; i++) { - input.add(i); - } - PCollection square = - pipeline - .apply(Create.of(input)) - .apply(Filter.by(x -> x <= 5)) - .apply(MapElements.into(TypeDescriptors.integers()).via(x -> x * x)); - - PAssert.that(square).containsInAnyOrder(Arrays.asList(1, 4, 9, 16, 25)); - - pipeline.run(); - } - - @Test - @Ignore("https://github.com/apache/beam/issues/23745") - public void testPipelineWithState() { - final List> input = - new ArrayList<>( - Arrays.asList( - KV.of("apple", "red"), - KV.of("banana", "yellow"), - KV.of("apple", "yellow"), - KV.of("grape", "purple"), - KV.of("banana", "yellow"))); - final Map expectedCount = ImmutableMap.of("apple", 2, "banana", 2, "grape", 1); - - // TODO: remove after SAMZA-2761 fix - for (int i = 0; i < 20; i++) { - input.add(KV.of("*", "*")); - } - - final DoFn, KV> fn = - new DoFn, KV>() { - - @StateId("cc") - private final StateSpec> countState = - StateSpecs.combiningFromInputInternal(VarIntCoder.of(), Sum.ofIntegers()); - - @ProcessElement - public void processElement( - ProcessContext c, @StateId("cc") CombiningState countState) { - - if (c.element().getKey().equals("*")) { - return; - } - - countState.add(1); - String key = c.element().getKey(); - int n = countState.read(); - if (n >= expectedCount.get(key)) { - c.output(KV.of(key, n)); - } - } - }; - - PCollection> counts = pipeline.apply(Create.of(input)).apply(ParDo.of(fn)); - - PAssert.that(counts) - .containsInAnyOrder( - expectedCount.entrySet().stream() - .map(entry -> KV.of(entry.getKey(), entry.getValue())) - .collect(Collectors.toList())); - - pipeline.run(); - } - - @Test - @Ignore("https://github.com/apache/beam/issues/23745") - public void testPipelineWithAggregation() { - final List> input = - new ArrayList<>( - Arrays.asList( - KV.of("apple", 2L), - KV.of("banana", 5L), - KV.of("apple", 8L), - KV.of("grape", 10L), - KV.of("banana", 5L))); - - // TODO: remove after SAMZA-2761 fix - for (int i = 0; i < 50; i++) { - input.add(KV.of("*", 0L)); - } - - PCollection> sums = - pipeline - .apply(Create.of(input)) - .apply(Filter.by(x -> !x.getKey().equals("*"))) - .apply(Sum.longsPerKey()); - - PAssert.that(sums) - .containsInAnyOrder( - Arrays.asList(KV.of("apple", 10L), KV.of("banana", 10L), KV.of("grape", 10L))); - - pipeline.run(); - } - - @Test - public void testKeyedOutputFutures() { - // We test the scenario that two elements of the same key needs to be processed in order. - final DoFnRunner, Void> doFnRunner = mock(DoFnRunner.class); - final AtomicInteger prev = new AtomicInteger(0); - final CountDownLatch latch = new CountDownLatch(1); - doAnswer( - invocation -> { - latch.await(); - WindowedValue> wv = invocation.getArgument(0); - Integer val = wv.getValue().getValue(); - - // Verify the previous element has been fully processed by checking the prev value - assertEquals(val - 1, prev.get()); - - prev.set(val); - return null; - }) - .when(doFnRunner) - .processElement(any()); - - SamzaPipelineOptions options = PipelineOptionsFactory.as(SamzaPipelineOptions.class); - options.setNumThreadsForProcessElement(4); - - final OpEmitter opEmitter = new OpAdapter.OpEmitterImpl<>(); - final FutureCollector futureCollector = new FutureCollectorImpl<>(); - futureCollector.prepare(); - - final AsyncDoFnRunner, Void> asyncDoFnRunner = - AsyncDoFnRunner.create(doFnRunner, opEmitter, futureCollector, true, options); - - final String appleKey = "apple"; - - final WindowedValue> input1 = - WindowedValues.valueInGlobalWindow(KV.of(appleKey, 1)); - - final WindowedValue> input2 = - WindowedValues.valueInGlobalWindow(KV.of(appleKey, 2)); - - asyncDoFnRunner.processElement(input1); - asyncDoFnRunner.processElement(input2); - // Resume input1 process afterwards - latch.countDown(); - - // Waiting for the futures to be resolved - try { - futureCollector.finish().toCompletableFuture().get(); - } catch (Exception e) { - // ignore interruption here. - } - - // The final val should be the last element value - assertEquals(2, prev.get()); - // The appleKey in keyedOutputFutures map should be removed - assertFalse(asyncDoFnRunner.hasOutputFuturesForKey(appleKey)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/ClassicBundleManagerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/ClassicBundleManagerTest.java deleted file mode 100644 index 09b51349aa49..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/ClassicBundleManagerTest.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collection; -import java.util.Collections; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -/** Unit tests for {@linkplain ClassicBundleManager}. */ -public final class ClassicBundleManagerTest { - private static final long MAX_BUNDLE_SIZE = 3; - private static final long MAX_BUNDLE_TIME_MS = 2000; - private static final String BUNDLE_CHECK_TIMER_ID = "bundle-check-test-timer"; - - private FutureCollector mockFutureCollector; - private ClassicBundleManager bundleManager; - private BundleManager.BundleProgressListener bundleProgressListener; - private Scheduler> mockScheduler; - - @Before - public void setUp() { - mockFutureCollector = mock(FutureCollector.class); - bundleProgressListener = mock(ClassicBundleManager.BundleProgressListener.class); - mockScheduler = mock(Scheduler.class); - bundleManager = - new ClassicBundleManager<>( - bundleProgressListener, - mockFutureCollector, - MAX_BUNDLE_SIZE, - MAX_BUNDLE_TIME_MS, - mockScheduler, - BUNDLE_CHECK_TIMER_ID); - } - - @Test - public void testWhenFirstTryStartBundleThenStartsBundle() { - bundleManager.tryStartBundle(); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - assertEquals( - "Expected the number of element in the current bundle to be 1", - 1L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryStartBundle() did not start the bundle", bundleManager.isBundleStarted()); - } - - @Test(expected = IllegalArgumentException.class) - public void testWhenCurrentBundleDoneFutureIsNotNullThenStartBundleFails() { - bundleManager.setCurrentBundleDoneFuture(CompletableFuture.completedFuture(null)); - bundleManager.tryStartBundle(); - } - - @Test - public void testWhenSignalFailureThenResetCurrentBundle() { - doThrow(new RuntimeException("User start bundle threw an exception")) - .when(bundleProgressListener) - .onBundleStarted(); - - try { - bundleManager.tryStartBundle(); - } catch (RuntimeException e) { - bundleManager.signalFailure(e); - } - - // verify if the signal failure only resets appropriate attributes of bundle - verify(mockFutureCollector, times(1)).prepare(); - verify(mockFutureCollector, times(1)).discard(); - assertEquals( - "Expected the number of element in the current bundle to 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("Error didn't reset the bundle as expected.", bundleManager.isBundleStarted()); - } - - @Test - public void testWhenMultipleTryStartThenOnlyStartBundleOnce() { - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - - // second invocation should not start the bundle - verify(bundleProgressListener, times(1)).onBundleStarted(); - assertEquals( - "Expected the number of element in the current bundle to be 2", - 2L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryStartBundle() did not start the bundle", bundleManager.isBundleStarted()); - } - - /* - * Setup the bundle manager with default max bundle size as 3 and max bundle close timeout to 2 seconds. - * The test verifies the following - * 1. Bundle gets closed on tryFinishBundle() - * a. pending bundle count == 0 - * b. element in current bundle == 0 - * c. isBundleStarted == false - * 2. onBundleFinished callback is invoked on the progress listener - */ - @Test - public void testWhenTryFinishBundleThenBundleIsReset() { - OpEmitter mockEmitter = mock(OpEmitter.class); - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - bundleManager.tryFinishBundle(mockEmitter); - - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(1)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("tryFinishBundle() did not close the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testTryFinishBundleClosesBundleOnMaxWatermark() { - OpEmitter mockEmitter = mock(OpEmitter.class); - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - bundleManager.setBundleWatermarkHold(BoundedWindow.TIMESTAMP_MAX_VALUE); - - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - bundleManager.tryFinishBundle(mockEmitter); - - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(1)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("tryFinishBundle() did not close the bundle", bundleManager.isBundleStarted()); - } - - /* - * Set up the bundle manager with defaults and ensure the bundle manager doesn't close the current active bundle. - */ - @Test - public void testTryFinishBundleShouldNotCloseBundle() { - OpEmitter mockEmitter = mock(OpEmitter.class); - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - - bundleManager.tryStartBundle(); - bundleManager.tryFinishBundle(mockEmitter); - - verify(mockFutureCollector, times(1)).finish(); - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(0)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 1", - 1L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryFinishBundle() did not close the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testTryFinishBundleWhenNoBundleInProgress() { - OpEmitter mockEmitter = mock(OpEmitter.class); - when(mockFutureCollector.finish()) - .thenReturn(CompletableFuture.completedFuture(Collections.emptyList())); - - bundleManager.tryFinishBundle(mockEmitter); - - verify(mockEmitter, times(1)).emitFuture(any()); - assertNull( - "tryFinishBundle() should not set the future when no bundle in progress", - bundleManager.getCurrentBundleDoneFuture()); - } - - @Test - public void testProcessWatermarkWhenNoBundleInProgress() { - Instant now = Instant.now(); - OpEmitter mockEmitter = mock(OpEmitter.class); - bundleManager.processWatermark(now, mockEmitter); - verify(bundleProgressListener, times(1)).onWatermark(now, mockEmitter); - } - - /* - * The test validates processing watermark during an active bundle in progress and also validates - * if the watermark hold is propagated down stream after the output futures are resolved. - */ - @Test - public void testProcessWatermarkWithPendingBundles() { - CountDownLatch latch = new CountDownLatch(1); - Instant watermark = Instant.now(); - OpEmitter mockEmitter = mock(OpEmitter.class); - - // We need to capture the finish bundle future to know if we can check for output watermark - // and verify other callbacks get invoked. - Class>>> outputFutureClass = - (Class>>>) (Class) CompletionStage.class; - ArgumentCaptor>>> captor = - ArgumentCaptor.forClass(outputFutureClass); - - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.supplyAsync( - () -> { - try { - latch.await(); - } catch (InterruptedException e) { - throw new AssertionError("Test interrupted when waiting for latch"); - } - - return Collections.singleton(mock(WindowedValue.class)); - })); - - testWatermarkHoldWhenPendingBundleInProgress(mockEmitter, captor, watermark); - testWatermarkHoldPropagatesAfterFutureResolution(mockEmitter, captor, latch, watermark); - } - - @Test - public void testMaxWatermarkPropagationForPendingBundle() { - Instant watermark = BoundedWindow.TIMESTAMP_MAX_VALUE; - OpEmitter mockEmitter = mock(OpEmitter.class); - bundleManager.setPendingBundleCount(1); - bundleManager.processWatermark(watermark, mockEmitter); - verify(bundleProgressListener, times(1)).onWatermark(watermark, mockEmitter); - } - - @Test - public void testMaxWatermarkWithBundleInProgress() { - Instant watermark = BoundedWindow.TIMESTAMP_MAX_VALUE; - OpEmitter mockEmitter = mock(OpEmitter.class); - - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - - // should force close bundle - bundleManager.processWatermark(watermark, mockEmitter); - verify(bundleProgressListener, times(1)).onWatermark(watermark, mockEmitter); - } - - @Test - public void testProcessTimerWithBundleTimeElapsed() { - ClassicBundleManager bundleManager = - new ClassicBundleManager<>( - bundleProgressListener, - mockFutureCollector, - MAX_BUNDLE_SIZE, - 0, - mockScheduler, - BUNDLE_CHECK_TIMER_ID); - OpEmitter mockEmitter = mock(OpEmitter.class); - KeyedTimerData mockTimer = mock(KeyedTimerData.class); - TimerInternals.TimerData mockTimerData = mock(TimerInternals.TimerData.class); - - when(mockFutureCollector.finish()) - .thenReturn( - CompletableFuture.completedFuture(Collections.singleton(mock(WindowedValue.class)))); - when(mockTimerData.getTimerId()).thenReturn(BUNDLE_CHECK_TIMER_ID); - when(mockTimer.getTimerData()).thenReturn(mockTimerData); - - bundleManager.tryStartBundle(); - bundleManager.processTimer(mockTimer, mockEmitter); - - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(1)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("tryFinishBundle() did not close the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testProcessTimerWithTimeLessThanMaxBundleTime() { - OpEmitter mockEmitter = mock(OpEmitter.class); - KeyedTimerData mockTimer = mock(KeyedTimerData.class); - TimerInternals.TimerData mockTimerData = mock(TimerInternals.TimerData.class); - - when(mockTimerData.getTimerId()).thenReturn(BUNDLE_CHECK_TIMER_ID); - when(mockTimer.getTimerData()).thenReturn(mockTimerData); - - when(mockFutureCollector.finish()) - .thenReturn(CompletableFuture.completedFuture(Collections.emptyList())); - - bundleManager.tryStartBundle(); - bundleManager.processTimer(mockTimer, mockEmitter); - - verify(mockFutureCollector, times(1)).finish(); - verify(mockEmitter, times(1)).emitFuture(any()); - verify(bundleProgressListener, times(0)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 1", - 1L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryFinishBundle() closed the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testProcessTimerIgnoresNonBundleTimers() { - OpEmitter mockEmitter = mock(OpEmitter.class); - KeyedTimerData mockTimer = mock(KeyedTimerData.class); - TimerInternals.TimerData mockTimerData = mock(TimerInternals.TimerData.class); - - when(mockTimerData.getTimerId()).thenReturn("NotBundleTimer"); - when(mockTimer.getTimerData()).thenReturn(mockTimerData); - - bundleManager.tryStartBundle(); - bundleManager.processTimer(mockTimer, mockEmitter); - - verify(mockFutureCollector, times(0)).finish(); - verify(mockEmitter, times(0)).emitFuture(any()); - verify(bundleProgressListener, times(0)).onBundleFinished(mockEmitter); - assertEquals( - "Expected the number of element in the current bundle to be 1", - 1L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected the pending bundle count to be 1", 1L, bundleManager.getPendingBundleCount()); - assertTrue("tryFinishBundle() closed the bundle", bundleManager.isBundleStarted()); - } - - @Test - public void testSignalFailureResetsTheBundleAndCollector() { - bundleManager.tryStartBundle(); - - bundleManager.signalFailure(mock(Throwable.class)); - verify(mockFutureCollector, times(1)).prepare(); - verify(mockFutureCollector, times(1)).discard(); - assertEquals( - "Expected the number of element in the current bundle to 0", - 0L, - bundleManager.getCurrentBundleElementCount()); - assertEquals( - "Expected pending bundle count to be 0", 0L, bundleManager.getPendingBundleCount()); - assertFalse("Error didn't reset the bundle as expected.", bundleManager.isBundleStarted()); - } - - /* - * We validate the following - * 1. Process watermark is held since there is a pending bundle. - * 2. Watermark propagates down stream once the output future is resolved. - * 3. The watermark propagated is the one that was held before closing the bundle - * 4. onBundleFinished and onWatermark callbacks are triggered - * 5. Pending bundle count is decremented once the future is resolved - */ - private void testWatermarkHoldPropagatesAfterFutureResolution( - OpEmitter mockEmitter, - ArgumentCaptor>>> captor, - CountDownLatch latch, - Instant sealedWatermark) { - Instant higherWatermark = Instant.now(); - - // Process watermark should result in watermark hold again since pending bundle count > 1 - bundleManager.processWatermark(higherWatermark, mockEmitter); - verify(bundleProgressListener, times(0)).onWatermark(higherWatermark, mockEmitter); - - // Resolving the process output futures should result in watermark propagation - latch.countDown(); - CompletionStage validationFuture = - captor - .getValue() - .thenAccept( - results -> { - verify(bundleProgressListener, times(1)).onBundleFinished(mockEmitter); - verify(bundleProgressListener, times(1)) - .onWatermark(sealedWatermark, mockEmitter); - assertEquals( - "Expected the pending bundle count to be 0", - 0L, - bundleManager.getPendingBundleCount()); - }); - - validationFuture.toCompletableFuture().join(); - } - - /* - * We validate the following - * 1. Watermark is held since there is a bundle in progress - * 2. Callbacks are not invoked when tryFinishBundle() is invoked since the future is unresolved - * 3. Watermark hold is sealed and output future is emitted - */ - private void testWatermarkHoldWhenPendingBundleInProgress( - OpEmitter mockEmitter, - ArgumentCaptor>>> captor, - Instant watermark) { - // Starts the bundle and reach the max bundle size so that tryFinishBundle() seals the current - // bundle - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - bundleManager.tryStartBundle(); - - bundleManager.processWatermark(watermark, mockEmitter); - verify(bundleProgressListener, times(0)).onWatermark(watermark, mockEmitter); - - // Bundle is still unresolved although sealed since count down the latch is not yet decremented. - bundleManager.tryFinishBundle(mockEmitter); - verify(mockFutureCollector, times(1)).finish(); - verify(mockEmitter, times(1)).emitFuture(captor.capture()); - assertFalse("tryFinishBundle() closed the bundle", bundleManager.isBundleStarted()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/FutureCollectorImplTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/FutureCollectorImplTest.java deleted file mode 100644 index b0b9b5450f23..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/FutureCollectorImplTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; -import org.apache.beam.sdk.values.WindowedValue; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -/** Unit tests for {@linkplain FutureCollectorImpl}. */ -public final class FutureCollectorImplTest { - private static final List RESULTS = ImmutableList.of("hello", "world"); - private FutureCollector futureCollector = new FutureCollectorImpl<>(); - - @Before - public void setup() { - futureCollector = new FutureCollectorImpl<>(); - } - - @Test(expected = IllegalStateException.class) - public void testAddWithoutPrepareCallThrowsException() { - futureCollector.add(mock(CompletionStage.class)); - } - - @Test - public void testFinishWithoutPrepareReturnsEmptyCollection() { - CompletionStage>> resultFuture = futureCollector.finish(); - CompletionStage validationFuture = - resultFuture.thenAccept( - result -> { - Assert.assertTrue("Expected the result to be empty", result.isEmpty()); - }); - validationFuture.toCompletableFuture().join(); - } - - @Test - public void testFinishReturnsExpectedResults() { - WindowedValue mockWindowedValue = mock(WindowedValue.class); - - when(mockWindowedValue.getValue()).thenReturn("hello").thenReturn("world"); - - futureCollector.prepare(); - futureCollector.add(CompletableFuture.completedFuture(mockWindowedValue)); - futureCollector.add(CompletableFuture.completedFuture(mockWindowedValue)); - - CompletionStage>> resultFuture = futureCollector.finish(); - CompletionStage validationFuture = - resultFuture.thenAccept( - results -> { - List actualResults = - results.stream().map(WindowedValue::getValue).collect(Collectors.toList()); - Assert.assertEquals( - "Expected the result to be {hello, world}", RESULTS, actualResults); - }); - validationFuture.toCompletableFuture().join(); - } - - @Test - public void testMultiplePrepareCallsWithoutFinishThrowsException() { - futureCollector.prepare(); - - try { - futureCollector.prepare(); - Assert.fail("Second invocation of prepare should throw IllegalStateException"); - } catch (IllegalStateException ex) { - } - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/GroupByKeyOpTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/GroupByKeyOpTest.java deleted file mode 100644 index 73454cc95421..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/GroupByKeyOpTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assume.assumeTrue; - -import java.io.Serializable; -import java.util.Arrays; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.coders.VarIntCoder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.testing.PAssert; -import org.apache.beam.sdk.testing.TestPipeline; -import org.apache.beam.sdk.testing.TestStream; -import org.apache.beam.sdk.transforms.Combine; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.transforms.windowing.FixedWindows; -import org.apache.beam.sdk.transforms.windowing.Window; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.TimestampedValue; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; - -/** Tests for GroupByKeyOp. */ -public class GroupByKeyOpTest implements Serializable { - - @BeforeClass - public static void beforeClass() { - // TODO(https://github.com/apache/beam/issues/32208) - assumeTrue(System.getProperty("java.version").startsWith("1.")); - } - - @Rule - public final transient TestPipeline pipeline = - TestPipeline.fromOptions( - PipelineOptionsFactory.fromArgs("--runner=TestSamzaRunner").create()); - - @Rule - public final transient TestPipeline dropLateDataPipeline = - TestPipeline.fromOptions( - PipelineOptionsFactory.fromArgs("--runner=TestSamzaRunner", "--dropLateData=true") - .create()); - - @Test - public void testDefaultGbk() { - TestStream.Builder testStream = - TestStream.create(VarIntCoder.of()) - .addElements(TimestampedValue.of(1, new Instant(1000))) - .addElements(TimestampedValue.of(2, new Instant(2000))) - .advanceWatermarkTo(new Instant(3000)) - .addElements(TimestampedValue.of(10, new Instant(1000))) - .advanceWatermarkTo(new Instant(10000)); - - PCollection aggregated = - pipeline - .apply(testStream.advanceWatermarkToInfinity()) - .apply( - Window.into(FixedWindows.of(Duration.standardSeconds(3))) - .accumulatingFiredPanes()) - .apply(Combine.globally(Sum.ofIntegers()).withoutDefaults()); - - PAssert.that(aggregated).containsInAnyOrder(Arrays.asList(3, 10)); - - pipeline.run().waitUntilFinish(); - } - - @Test - public void testDropLateDataNonKeyed() { - TestStream.Builder testStream = - TestStream.create(VarIntCoder.of()) - .addElements(TimestampedValue.of(1, new Instant(1000))) - .addElements(TimestampedValue.of(2, new Instant(2000))) - .advanceWatermarkTo(new Instant(3000)) - .addElements(TimestampedValue.of(10, new Instant(1000))) - .advanceWatermarkTo(new Instant(10000)); - - PCollection aggregated = - dropLateDataPipeline - .apply(testStream.advanceWatermarkToInfinity()) - .apply( - Window.into(FixedWindows.of(Duration.standardSeconds(3))) - .accumulatingFiredPanes()) - .apply(Combine.globally(Sum.ofIntegers()).withoutDefaults()); - - PAssert.that(aggregated).containsInAnyOrder(3); - - dropLateDataPipeline.run().waitUntilFinish(); - } - - @Test - public void testDropLateDataKeyed() { - TestStream.Builder> testStream = - TestStream.create(KvCoder.of(StringUtf8Coder.of(), VarIntCoder.of())) - .addElements(TimestampedValue.of(KV.of("a", 1), new Instant(1000))) - .addElements(TimestampedValue.of(KV.of("b", 2), new Instant(2000))) - .addElements(TimestampedValue.of(KV.of("a", 3), new Instant(2500))) - .advanceWatermarkTo(new Instant(3000)) - .addElements(TimestampedValue.of(KV.of("a", 10), new Instant(1000))) - .advanceWatermarkTo(new Instant(10000)); - - PCollection> aggregated = - dropLateDataPipeline - .apply(testStream.advanceWatermarkToInfinity()) - .apply( - Window.>into(FixedWindows.of(Duration.standardSeconds(3))) - .accumulatingFiredPanes()) - .apply(Sum.integersPerKey()); - - PAssert.that(aggregated).containsInAnyOrder(Arrays.asList(KV.of("a", 4), KV.of("b", 2))); - - dropLateDataPipeline.run().waitUntilFinish(); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerDataTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerDataTest.java deleted file mode 100644 index 1a2f82b1a0d7..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/KeyedTimerDataTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.io.ByteArrayOutputStream; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.testing.CoderProperties; -import org.apache.beam.sdk.transforms.windowing.GlobalWindow; -import org.apache.beam.sdk.values.CausedByDrain; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.Instant; -import org.junit.Test; - -/** Tests for {@link KeyedTimerData}. */ -public class KeyedTimerDataTest { - private static final Coder STRING_CODER = StringUtf8Coder.of(); - private static final Instant TIMESTAMP = - new DateTime(2020, 8, 11, 13, 42, 9, DateTimeZone.UTC).toInstant(); - // TODO: LISAMZA-19205 Test OUTPUT_TIMESTAMP after outputTimestamp is encoded - // private static final Instant OUTPUT_TIMESTAMP = TIMESTAMP.plus(Duration.standardSeconds(30)); - - @Test - public void testCoder() throws Exception { - final TimerInternals.TimerData td = - TimerInternals.TimerData.of( - "timer", - StateNamespaces.global(), - TIMESTAMP, - TIMESTAMP, - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - - final String key = "timer-key"; - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - STRING_CODER.encode(key, baos); - final byte[] keyBytes = baos.toByteArray(); - final KeyedTimerData ktd = new KeyedTimerData<>(keyBytes, key, td); - - final KeyedTimerData.KeyedTimerDataCoder ktdCoder = - new KeyedTimerData.KeyedTimerDataCoder<>(STRING_CODER, GlobalWindow.Coder.INSTANCE); - - // TODO: LISAMZA-19205: use CoderProperties.coderDecodeEncodeEqual - CoderProperties.coderDecodeEncodeEqualInContext(ktdCoder, Coder.Context.OUTER, ktd); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/PortableBundleManagerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/PortableBundleManagerTest.java deleted file mode 100644 index 522e146d21e6..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/PortableBundleManagerTest.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.apache.beam.runners.core.TimerInternals; -import org.apache.samza.operators.Scheduler; -import org.joda.time.Instant; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -public class PortableBundleManagerTest { - - @Mock BundleManager.BundleProgressListener bundleProgressListener; - @Mock Scheduler> bundleTimerScheduler; - @Mock OpEmitter emitter; - - PortableBundleManager portableBundleManager; - - private static final String TIMER_ID = "timerId"; - - private static final long MAX_BUNDLE_TIME_MS = 100000; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void test() { - portableBundleManager = - new PortableBundleManager<>( - bundleProgressListener, 1, MAX_BUNDLE_TIME_MS, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - } - - @Test - public void testWhen() { - portableBundleManager = - new PortableBundleManager<>( - bundleProgressListener, 4, MAX_BUNDLE_TIME_MS, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - } - - @Test - public void testWhenElementCountNotReachedTHenBundleDoesntFinish() { - portableBundleManager = - new PortableBundleManager<>( - bundleProgressListener, 4, MAX_BUNDLE_TIME_MS, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryFinishBundle(emitter); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - verify(bundleProgressListener, times(0)).onBundleFinished(any()); - } - - @Test - public void testWhenElementCountReachedThenFinishBundle() { - portableBundleManager = - new PortableBundleManager<>( - bundleProgressListener, 4, MAX_BUNDLE_TIME_MS, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryStartBundle(); - portableBundleManager.tryFinishBundle(emitter); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - verify(bundleProgressListener, times(1)).onBundleFinished(any()); - } - - @Test - public void testWhenBundleTimeReachedThenFinishBundle() throws Exception { - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 4, 1, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - Thread.sleep(2); - portableBundleManager.tryFinishBundle(emitter); - - verify(bundleProgressListener, times(1)).onBundleStarted(); - verify(bundleProgressListener, times(1)).onBundleFinished(any()); - } - - @Test - public void testWhenSignalFailureThenResetBundle() throws Exception { - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 4, 1, bundleTimerScheduler, TIMER_ID); - portableBundleManager.tryStartBundle(); - portableBundleManager.signalFailure(new Exception()); - portableBundleManager.tryStartBundle(); - - verify(bundleProgressListener, times(2)).onBundleStarted(); - } - - @Test - public void testProcessWatermarkWhenBundleNotStarted() { - Instant watermark = new Instant(); - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 4, 1, bundleTimerScheduler, TIMER_ID); - portableBundleManager.processWatermark(watermark, emitter); - verify(bundleProgressListener, times(1)).onWatermark(eq(watermark), eq(emitter)); - } - - @Test - public void testQueueWatermarkWhenBundleStarted() { - Instant watermark = new Instant(); - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 1, 1, bundleTimerScheduler, TIMER_ID); - - portableBundleManager.tryStartBundle(); - portableBundleManager.processWatermark(watermark, emitter); - verify(bundleProgressListener, times(0)).onWatermark(eq(watermark), eq(emitter)); - - portableBundleManager.tryFinishBundle(emitter); - verify(bundleProgressListener, times(1)).onWatermark(eq(watermark), eq(emitter)); - } - - @Test - public void testProcessTimerTriesFinishBundle() { - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 1, 1, bundleTimerScheduler, TIMER_ID); - - portableBundleManager.tryStartBundle(); - KeyedTimerData keyedTimerData = mock(KeyedTimerData.class); - TimerInternals.TimerData timerData = mock(TimerInternals.TimerData.class); - when(keyedTimerData.getTimerData()).thenReturn(timerData); - when(timerData.getTimerId()).thenReturn(TIMER_ID); - - portableBundleManager.processTimer(keyedTimerData, emitter); - verify(bundleProgressListener, times(1)).onBundleFinished(any()); - verify(bundleTimerScheduler).schedule(any(KeyedTimerData.class), anyLong()); - } - - @Test - public void testDifferentTimerIdIsIgnored() { - portableBundleManager = - new PortableBundleManager<>(bundleProgressListener, 1, 1, bundleTimerScheduler, TIMER_ID); - - portableBundleManager.tryStartBundle(); - KeyedTimerData keyedTimerData = mock(KeyedTimerData.class); - TimerInternals.TimerData timerData = mock(TimerInternals.TimerData.class); - when(keyedTimerData.getTimerData()).thenReturn(timerData); - when(timerData.getTimerId()).thenReturn("NOT_TIMER_ID"); - - portableBundleManager.processTimer(keyedTimerData, emitter); - verify(bundleProgressListener, times(0)).onBundleFinished(any()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandlerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandlerTest.java deleted file mode 100644 index 7fde83c02e5f..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaMetricsBundleProgressHandlerTest.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.DISTRIBUTION_INT64_TYPE; -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.LATEST_INT64_TYPE; -import static org.apache.beam.runners.core.metrics.MonitoringInfoConstants.TypeUrns.SUM_INT64_TYPE; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; -import org.apache.beam.model.fnexecution.v1.BeamFnApi; -import org.apache.beam.model.pipeline.v1.MetricsApi; -import org.apache.beam.runners.core.metrics.CounterCell; -import org.apache.beam.runners.core.metrics.DistributionCell; -import org.apache.beam.runners.core.metrics.GaugeCell; -import org.apache.beam.runners.core.metrics.MonitoringInfoConstants; -import org.apache.beam.runners.samza.metrics.SamzaMetricsContainer; -import org.apache.beam.sdk.metrics.MetricName; -import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.junit.Before; -import org.junit.Test; - -public class SamzaMetricsBundleProgressHandlerTest { - - public static final String EXPECTED_NAMESPACE = "namespace"; - public static final String EXPECTED_COUNTER_NAME = "counterName"; - private MetricsRegistryMap metricsRegistryMap; - private SamzaMetricsContainer samzaMetricsContainer; - - private SamzaMetricsBundleProgressHandler samzaMetricsBundleProgressHandler; - private String stepName = "stepName"; - Map transformIdToUniqueName = new HashMap<>(); - - @Before - public void setup() { - metricsRegistryMap = new MetricsRegistryMap(); - samzaMetricsContainer = new SamzaMetricsContainer(metricsRegistryMap); - samzaMetricsBundleProgressHandler = - new SamzaMetricsBundleProgressHandler( - stepName, samzaMetricsContainer, transformIdToUniqueName); - } - - @Test - public void testCounter() { - // Hex for 123 - byte[] payload = "\173".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo monitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(SUM_INT64_TYPE) - .setPayload(ByteString.copyFrom(payload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - BeamFnApi.ProcessBundleResponse response = - BeamFnApi.ProcessBundleResponse.newBuilder().addMonitoringInfos(monitoringInfo).build(); - - // Execute - samzaMetricsBundleProgressHandler.onCompleted(response); - - // Verify - MetricName metricName = MetricName.named(EXPECTED_NAMESPACE, EXPECTED_COUNTER_NAME); - CounterCell counter = - (CounterCell) samzaMetricsContainer.getContainer(stepName).getCounter(metricName); - - assertEquals(counter.getCumulative(), (Long) 123L); - } - - @Test - public void testGauge() { - // TimeStamp = 0, Value = 123 - byte[] payload = "\000\173".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo monitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(LATEST_INT64_TYPE) - .setPayload(ByteString.copyFrom(payload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - BeamFnApi.ProcessBundleResponse response = - BeamFnApi.ProcessBundleResponse.newBuilder().addMonitoringInfos(monitoringInfo).build(); - - // Execute - samzaMetricsBundleProgressHandler.onCompleted(response); - - // Verify - MetricName metricName = MetricName.named(EXPECTED_NAMESPACE, EXPECTED_COUNTER_NAME); - GaugeCell gauge = (GaugeCell) samzaMetricsContainer.getContainer(stepName).getGauge(metricName); - - assertEquals(123L, gauge.getCumulative().value()); - assertTrue( - gauge.getCumulative().timestamp().isBefore(Instant.now().plus(Duration.millis(500)))); - assertTrue( - gauge.getCumulative().timestamp().isAfter(Instant.now().minus(Duration.millis(500)))); - } - - @Test - public void testDistribution() { - // Count = 123, sum = 124, min = 125, max = 126 - byte[] payload = "\173\174\175\176".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo monitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(DISTRIBUTION_INT64_TYPE) - .setPayload(ByteString.copyFrom(payload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - BeamFnApi.ProcessBundleResponse response = - BeamFnApi.ProcessBundleResponse.newBuilder().addMonitoringInfos(monitoringInfo).build(); - - // Execute - samzaMetricsBundleProgressHandler.onCompleted(response); - - // Verify - MetricName metricName = MetricName.named(EXPECTED_NAMESPACE, EXPECTED_COUNTER_NAME); - DistributionCell gauge = - (DistributionCell) samzaMetricsContainer.getContainer(stepName).getDistribution(metricName); - - assertEquals(123L, gauge.getCumulative().count()); - assertEquals(124L, gauge.getCumulative().sum()); - assertEquals(125L, gauge.getCumulative().min()); - assertEquals(126L, gauge.getCumulative().max()); - } - - @Test - public void testEmptyPayload() { - - byte[] emptyPayload = "".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo emptyMonitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(SUM_INT64_TYPE) - .setPayload(ByteString.copyFrom(emptyPayload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - // Hex for 123 - byte[] payload = "\173".getBytes(Charset.defaultCharset()); - - MetricsApi.MonitoringInfo monitoringInfo = - MetricsApi.MonitoringInfo.newBuilder() - .setType(SUM_INT64_TYPE) - .setPayload(ByteString.copyFrom(payload)) - .putLabels(MonitoringInfoConstants.Labels.NAMESPACE, EXPECTED_NAMESPACE) - .putLabels(MonitoringInfoConstants.Labels.NAME, EXPECTED_COUNTER_NAME) - .build(); - BeamFnApi.ProcessBundleResponse response = - BeamFnApi.ProcessBundleResponse.newBuilder() - .addMonitoringInfos(emptyMonitoringInfo) - .addMonitoringInfos(monitoringInfo) - .addMonitoringInfos(emptyMonitoringInfo) - .build(); - - // Execute - samzaMetricsBundleProgressHandler.onCompleted(response); - - // Verify - MetricName metricName = MetricName.named(EXPECTED_NAMESPACE, EXPECTED_COUNTER_NAME); - CounterCell counter = - (CounterCell) samzaMetricsContainer.getContainer(stepName).getCounter(metricName); - - assertEquals(counter.getCumulative(), (Long) 123L); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternalsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternalsTest.java deleted file mode 100644 index 9409efbcf394..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaStoreStateInternalsTest.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; - -import java.io.File; -import java.io.IOException; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.TestSamzaRunner; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.StateValue; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.StateValueSerdeFactory; -import org.apache.beam.runners.samza.state.SamzaMapState; -import org.apache.beam.runners.samza.state.SamzaSetState; -import org.apache.beam.runners.samza.translation.ConfigBuilder; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.coders.VarIntCoder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.state.CombiningState; -import org.apache.beam.sdk.state.MapState; -import org.apache.beam.sdk.state.ReadableState; -import org.apache.beam.sdk.state.SetState; -import org.apache.beam.sdk.state.StateSpec; -import org.apache.beam.sdk.state.StateSpecs; -import org.apache.beam.sdk.state.ValueState; -import org.apache.beam.sdk.testing.PAssert; -import org.apache.beam.sdk.testing.TestPipeline; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterators; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; -import org.apache.samza.context.ContainerContext; -import org.apache.samza.context.JobContext; -import org.apache.samza.metrics.MetricsRegistry; -import org.apache.samza.serializers.Serde; -import org.apache.samza.storage.StorageEngineFactory; -import org.apache.samza.storage.kv.Entry; -import org.apache.samza.storage.kv.KeyValueIterator; -import org.apache.samza.storage.kv.KeyValueStore; -import org.apache.samza.storage.kv.KeyValueStoreMetrics; -import org.apache.samza.storage.kv.inmemory.InMemoryKeyValueStorageEngineFactory; -import org.apache.samza.storage.kv.inmemory.InMemoryKeyValueStore; -import org.apache.samza.system.SystemStreamPartition; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; - -/** Tests for SamzaStoreStateInternals. */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - // TODO(https://github.com/apache/beam/issues/21230): Remove when new version of - // errorprone is released (2.11.0) - "unused" -}) -public class SamzaStoreStateInternalsTest implements Serializable { - @Rule - public final transient TestPipeline pipeline = - TestPipeline.fromOptions( - PipelineOptionsFactory.fromArgs("--runner=TestSamzaRunner").create()); - - @BeforeClass - public static void beforeClass() { - // TODO(https://github.com/apache/beam/issues/32208) - assumeTrue(System.getProperty("java.version").startsWith("1.")); - } - - @Test - public void testMapStateIterator() { - final String stateId = "foo"; - final String countStateId = "count"; - - DoFn>, KV> fn = - new DoFn>, KV>() { - - @StateId(stateId) - private final StateSpec> mapState = - StateSpecs.map(StringUtf8Coder.of(), VarIntCoder.of()); - - @StateId(countStateId) - private final StateSpec> countState = - StateSpecs.combiningFromInputInternal(VarIntCoder.of(), Sum.ofIntegers()); - - @ProcessElement - public void processElement( - ProcessContext c, - @StateId(stateId) MapState mapState, - @StateId(countStateId) CombiningState count) { - SamzaMapState state = (SamzaMapState) mapState; - KV value = c.element().getValue(); - state.put(value.getKey(), value.getValue()); - count.add(1); - if (count.read() >= 4) { - final List> content = new ArrayList<>(); - final Iterator> iterator = state.readIterator().read(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - content.add(KV.of(entry.getKey(), entry.getValue())); - c.output(KV.of(entry.getKey(), entry.getValue())); - } - - assertEquals( - content, ImmutableList.of(KV.of("a", 97), KV.of("b", 42), KV.of("c", 12))); - } - } - }; - - PCollection> output = - pipeline - .apply( - Create.of( - KV.of("hello", KV.of("a", 97)), - KV.of("hello", KV.of("b", 42)), - KV.of("hello", KV.of("b", 42)), - KV.of("hello", KV.of("c", 12)))) - .apply(ParDo.of(fn)); - - PAssert.that(output).containsInAnyOrder(KV.of("a", 97), KV.of("b", 42), KV.of("c", 12)); - - pipeline.run(); - } - - @Test - public void testSetStateIterator() { - final String stateId = "foo"; - final String countStateId = "count"; - - DoFn, Set> fn = - new DoFn, Set>() { - - @StateId(stateId) - private final StateSpec> setState = StateSpecs.set(VarIntCoder.of()); - - @StateId(countStateId) - private final StateSpec> countState = - StateSpecs.combiningFromInputInternal(VarIntCoder.of(), Sum.ofIntegers()); - - @ProcessElement - public void processElement( - ProcessContext c, - @StateId(stateId) SetState setState, - @StateId(countStateId) CombiningState count) { - SamzaSetState state = (SamzaSetState) setState; - ReadableState isEmpty = state.isEmpty(); - state.add(c.element().getValue()); - assertFalse(isEmpty.read()); - count.add(1); - if (count.read() >= 4) { - final Set content = new HashSet<>(); - final Iterator iterator = state.readIterator().read(); - while (iterator.hasNext()) { - Integer value = iterator.next(); - content.add(value); - } - c.output(content); - - assertEquals(content, Sets.newHashSet(97, 42, 12)); - } - } - }; - - PCollection> output = - pipeline - .apply( - Create.of( - KV.of("hello", 97), KV.of("hello", 42), KV.of("hello", 42), KV.of("hello", 12))) - .apply(ParDo.of(fn)); - - PAssert.that(output).containsInAnyOrder(Sets.newHashSet(97, 42, 12)); - - pipeline.run(); - } - - @Test - public void testValueStateSameIdAcrossParDo() { - final String stateId = "foo"; - - DoFn, KV> fn = - new DoFn, KV>() { - - @StateId(stateId) - private final StateSpec> intState = - StateSpecs.value(VarIntCoder.of()); - - @ProcessElement - public void processElement( - @StateId(stateId) ValueState state, OutputReceiver> r) { - Integer currentValue = MoreObjects.firstNonNull(state.read(), 0); - r.output(KV.of("sizzle", currentValue)); - state.write(currentValue + 1); - } - }; - - DoFn, Integer> fn2 = - new DoFn, Integer>() { - - @StateId(stateId) - private final StateSpec> intState = - StateSpecs.value(VarIntCoder.of()); - - @ProcessElement - public void processElement( - @StateId(stateId) ValueState state, OutputReceiver r) { - Integer currentValue = MoreObjects.firstNonNull(state.read(), 13); - r.output(currentValue); - state.write(currentValue + 13); - } - }; - - PCollection> intermediate = - pipeline - .apply(Create.of(KV.of("hello", 42), KV.of("hello", 97), KV.of("hello", 84))) - .apply("First stateful ParDo", ParDo.of(fn)); - - PCollection output = intermediate.apply("Second stateful ParDo", ParDo.of(fn2)); - - PAssert.that(intermediate) - .containsInAnyOrder(KV.of("sizzle", 0), KV.of("sizzle", 1), KV.of("sizzle", 2)); - PAssert.that(output).containsInAnyOrder(13, 26, 39); - pipeline.run(); - } - - @Test - public void testValueStateSameIdAcrossParDoWithSameName() { - final String stateId = "foo"; - - DoFn, KV> fn = - new DoFn, KV>() { - - @StateId(stateId) - private final StateSpec> intState = - StateSpecs.value(VarIntCoder.of()); - - @ProcessElement - public void processElement( - @StateId(stateId) ValueState state, OutputReceiver> r) { - Integer currentValue = MoreObjects.firstNonNull(state.read(), 0); - r.output(KV.of("hello", currentValue)); - state.write(currentValue + 1); - } - }; - - DoFn, Integer> fn2 = - new DoFn, Integer>() { - - @StateId(stateId) - private final StateSpec> intState = - StateSpecs.value(VarIntCoder.of()); - - @ProcessElement - public void processElement( - @StateId(stateId) ValueState state, OutputReceiver r) { - Integer currentValue = MoreObjects.firstNonNull(state.read(), 13); - r.output(currentValue); - state.write(currentValue + 13); - } - }; - - PCollection> intermediate = - pipeline - .apply(Create.of(KV.of("hello", 42), KV.of("hello", 97), KV.of("hello", 84))) - .apply("Stateful ParDo with Same Name", ParDo.of(fn)); - - PCollection output = - intermediate.apply("Stateful ParDo with Same Name", ParDo.of(fn2)); - - PAssert.that(intermediate) - .containsInAnyOrder(KV.of("hello", 0), KV.of("hello", 1), KV.of("hello", 2)); - - PAssert.that(output).containsInAnyOrder(13, 26, 39); - pipeline.run(); - } - - /** A storage engine to create test stores. */ - public static class TestStorageEngine extends InMemoryKeyValueStorageEngineFactory { - - @Override - public KeyValueStore getKVStore( - String storeName, - File storeDir, - MetricsRegistry registry, - SystemStreamPartition changeLogSystemStreamPartition, - JobContext jobContext, - ContainerContext containerContext, - StorageEngineFactory.StoreMode readWrite) { - KeyValueStoreMetrics metrics = new KeyValueStoreMetrics(storeName, registry); - return new TestStore(metrics); - } - } - - /** A test store based on InMemoryKeyValueStore. */ - public static class TestStore extends InMemoryKeyValueStore { - static List iterators = Collections.synchronizedList(new ArrayList<>()); - - public TestStore(KeyValueStoreMetrics metrics) { - super(metrics); - } - - @Override - public KeyValueIterator range(byte[] from, byte[] to) { - TestKeyValueIteraor iter = new TestKeyValueIteraor(super.range(from, to)); - iterators.add(iter); - return iter; - } - - static class TestKeyValueIteraor implements KeyValueIterator { - private final KeyValueIterator iter; - boolean closed = false; - - TestKeyValueIteraor(KeyValueIterator iter) { - this.iter = iter; - } - - @Override - public void close() { - iter.close(); - closed = true; - } - - @Override - public boolean hasNext() { - return iter.hasNext(); - } - - @Override - public Entry next() { - return iter.next(); - } - } - } - - @Test - public void testIteratorClosed() { - final String stateId = "foo"; - - DoFn, Set> fn = - new DoFn, Set>() { - - @StateId(stateId) - private final StateSpec> setState = StateSpecs.set(VarIntCoder.of()); - - @ProcessElement - public void processElement( - ProcessContext c, @StateId(stateId) SetState setState) { - SamzaSetState state = (SamzaSetState) setState; - state.add(c.element().getValue()); - - // the iterator for size needs to be closed - int size = Iterators.size(state.readIterator().read()); - - if (size > 1) { - final Iterator iterator = state.readIterator().read(); - assertTrue(iterator.hasNext()); - // this iterator should be closed too - iterator.next(); - } - } - }; - - pipeline - .apply( - Create.of( - KV.of("hello", 97), KV.of("hello", 42), KV.of("hello", 42), KV.of("hello", 12))) - .apply(ParDo.of(fn)); - - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setRunner(TestSamzaRunner.class); - Map configs = new HashMap<>(ConfigBuilder.localRunConfig()); - configs.put("stores.foo.factory", TestStorageEngine.class.getName()); - pipeline.getOptions().as(SamzaPipelineOptions.class).setConfigOverride(configs); - pipeline.run(); - - // The test code creates 7 underlying iterators, and 1 more is created during state.clear() - // Verify all of them are closed - assertEquals(8, TestStore.iterators.size()); - TestStore.iterators.forEach(iter -> assertTrue(iter.closed)); - } - - @Test - public void testStateValueSerde() throws IOException { - StateValueSerdeFactory stateValueSerdeFactory = new StateValueSerdeFactory(); - Serde> serde = (Serde) stateValueSerdeFactory.getSerde("Test", null); - int value = 123; - Coder coder = VarIntCoder.of(); - - byte[] valueBytes = serde.toBytes(StateValue.of(value, coder)); - StateValue stateValue1 = serde.fromBytes(valueBytes); - StateValue stateValue2 = StateValue.of(valueBytes); - assertEquals(stateValue1.getValue(coder).intValue(), value); - assertEquals(stateValue2.getValue(coder).intValue(), value); - - Integer nullValue = null; - byte[] nullBytes = serde.toBytes(StateValue.of(nullValue, coder)); - StateValue nullStateValue = serde.fromBytes(nullBytes); - assertNull(nullBytes); - assertNull(nullStateValue.getValue(coder)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactoryTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactoryTest.java deleted file mode 100644 index ecedd7ae79f1..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SamzaTimerInternalsFactoryTest.java +++ /dev/null @@ -1,752 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.apache.beam.runners.core.StateNamespace; -import org.apache.beam.runners.core.StateNamespaces; -import org.apache.beam.runners.core.TimerInternals; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.ByteArray; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.ByteArraySerdeFactory; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.StateValue; -import org.apache.beam.runners.samza.runtime.SamzaStoreStateInternals.StateValueSerdeFactory; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.state.TimeDomain; -import org.apache.beam.sdk.values.CausedByDrain; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.apache.samza.config.MapConfig; -import org.apache.samza.context.TaskContext; -import org.apache.samza.metrics.MetricsRegistryMap; -import org.apache.samza.operators.Scheduler; -import org.apache.samza.serializers.Serde; -import org.apache.samza.storage.kv.KeyValueStore; -import org.apache.samza.storage.kv.KeyValueStoreMetrics; -import org.apache.samza.storage.kv.RocksDbKeyValueStore; -import org.apache.samza.storage.kv.SerializedKeyValueStore; -import org.apache.samza.storage.kv.SerializedKeyValueStoreMetrics; -import org.joda.time.Instant; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.rocksdb.FlushOptions; -import org.rocksdb.Options; -import org.rocksdb.WriteOptions; - -/** - * Tests for {@link SamzaTimerInternalsFactory}. Covers both event-time timers and processing-timer - * timers. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) -public class SamzaTimerInternalsFactoryTest { - @Rule public transient TemporaryFolder temporaryFolder = new TemporaryFolder(); - - private KeyValueStore> createStore() { - final Options options = new Options(); - options.setCreateIfMissing(true); - - RocksDbKeyValueStore rocksStore = - new RocksDbKeyValueStore( - temporaryFolder.getRoot(), - options, - new MapConfig(), - false, - "beamStore", - new WriteOptions(), - new FlushOptions(), - new KeyValueStoreMetrics("beamStore", new MetricsRegistryMap())); - - return new SerializedKeyValueStore<>( - rocksStore, - new ByteArraySerdeFactory.ByteArraySerde(), - new StateValueSerdeFactory.StateValueSerde(), - new SerializedKeyValueStoreMetrics("beamStore", new MetricsRegistryMap())); - } - - private static SamzaStoreStateInternals.Factory createNonKeyedStateInternalsFactory( - SamzaPipelineOptions pipelineOptions, KeyValueStore> store) { - final TaskContext context = mock(TaskContext.class); - when(context.getStore(anyString())).thenReturn((KeyValueStore) store); - - return SamzaStoreStateInternals.createNonKeyedStateInternalsFactory( - "42", context, pipelineOptions); - } - - private static SamzaTimerInternalsFactory createTimerInternalsFactory( - Scheduler> timerRegistry, - String timerStateId, - SamzaPipelineOptions pipelineOptions, - KeyValueStore> store) { - - final SamzaStoreStateInternals.Factory nonKeyedStateInternalsFactory = - createNonKeyedStateInternalsFactory(pipelineOptions, store); - - return SamzaTimerInternalsFactory.createTimerInternalFactory( - StringUtf8Coder.of(), - timerRegistry, - timerStateId, - nonKeyedStateInternalsFactory, - (WindowingStrategy) WindowingStrategy.globalDefault(), - PCollection.IsBounded.BOUNDED, - pipelineOptions); - } - - private static class TestTimerRegistry implements Scheduler> { - private final List> timers = new ArrayList<>(); - - @Override - public void schedule(KeyedTimerData key, long timestamp) { - timers.add(key); - } - - @Override - public void delete(KeyedTimerData key) { - timers.remove(key); - } - } - - @Test - public void testEventTimeTimers() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timer1", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timer2", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer2); - - timerInternalsFactory.setInputWatermark(new Instant(5)); - Collection> readyTimers = timerInternalsFactory.removeReadyTimers(); - assertTrue(readyTimers.isEmpty()); - - timerInternalsFactory.setInputWatermark(new Instant(20)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - assertEquals(timer1, readyTimers.iterator().next().getTimerData()); - - timerInternalsFactory.setInputWatermark(new Instant(150)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - assertEquals(timer2, readyTimers.iterator().next().getTimerData()); - - store.close(); - } - - @Test - public void testRestoreEventBufferSize() throws Exception { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final String key = "testKey"; - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey(key); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timer1", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - store.close(); - - // restore by creating a new instance - store = createStore(); - - final SamzaTimerInternalsFactory restoredFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - assertEquals(1, restoredFactory.getEventTimeBuffer().size()); - - restoredFactory.setInputWatermark(new Instant(150)); - Collection> readyTimers = restoredFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - - // Timer 1 should be evicted from buffer - assertTrue(restoredFactory.getEventTimeBuffer().isEmpty()); - final TimerInternals restoredTimerInternals = restoredFactory.timerInternalsForKey(key); - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timer2", - nameSpace, - new Instant(200), - new Instant(200), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - restoredTimerInternals.setTimer(timer2); - - // Timer 2 should be added to the Event buffer - assertEquals(1, restoredFactory.getEventTimeBuffer().size()); - // Timer 2 should not be ready - readyTimers = restoredFactory.removeReadyTimers(); - assertEquals(0, readyTimers.size()); - - restoredFactory.setInputWatermark(new Instant(250)); - - // Timer 2 should be ready - readyTimers = restoredFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - StringUtf8Coder.of().encode(key, baos); - byte[] keyBytes = baos.toByteArray(); - assertEquals( - new ArrayList<>(readyTimers), Arrays.asList(new KeyedTimerData<>(keyBytes, key, timer2))); - - store.close(); - } - - @Test - public void testRestore() throws Exception { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final String key = "testKey"; - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey(key); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timer1", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timer2", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer2); - - store.close(); - - // restore by creating a new instance - store = createStore(); - final SamzaTimerInternalsFactory restoredFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - restoredFactory.setInputWatermark(new Instant(150)); - Collection> readyTimers = restoredFactory.removeReadyTimers(); - assertEquals(2, readyTimers.size()); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - StringUtf8Coder.of().encode(key, baos); - byte[] keyBytes = baos.toByteArray(); - assertEquals( - new ArrayList<>(readyTimers), - Arrays.asList( - new KeyedTimerData<>(keyBytes, key, timer1), - new KeyedTimerData<>(keyBytes, key, timer2))); - - store.close(); - } - - @Test - public void testProcessingTimeTimers() throws IOException { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - KeyValueStore> store = createStore(); - TestTimerRegistry timerRegistry = new TestTimerRegistry(); - - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(timerRegistry, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timer1", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.PROCESSING_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timer2", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.PROCESSING_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer2); - - final TimerInternals.TimerData timer3 = - TimerInternals.TimerData.of( - "timer3", - "timerFamilyId3", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.PROCESSING_TIME); - timerInternals.setTimer(timer3); - assertEquals(3, timerRegistry.timers.size()); - - store.close(); - - // restore by creating a new instance - store = createStore(); - TestTimerRegistry restoredRegistry = new TestTimerRegistry(); - final SamzaTimerInternalsFactory restoredFactory = - createTimerInternalsFactory(restoredRegistry, "timer", pipelineOptions, store); - - assertEquals(3, restoredRegistry.timers.size()); - - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - StringUtf8Coder.of().encode("testKey", baos); - final byte[] keyBytes = baos.toByteArray(); - restoredFactory.removeProcessingTimer(new KeyedTimerData<>(keyBytes, "testKey", timer1)); - restoredFactory.removeProcessingTimer(new KeyedTimerData<>(keyBytes, "testKey", timer2)); - restoredFactory.removeProcessingTimer(new KeyedTimerData<>(keyBytes, "testKey", timer3)); - store.close(); - } - - @Test - public void testOverride() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - - KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - final TimerInternals.TimerData timer1 = - TimerInternals.TimerData.of( - "timerId", - nameSpace, - new Instant(10), - new Instant(10), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer1); - - // this timer should override the first timer - final TimerInternals.TimerData timer2 = - TimerInternals.TimerData.of( - "timerId", - nameSpace, - new Instant(100), - new Instant(100), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer2); - - final TimerInternals.TimerData timer3 = - TimerInternals.TimerData.of( - "timerId2", - nameSpace, - new Instant(200), - new Instant(200), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer3); - - // this timer shouldn't override since it has a different id - timerInternalsFactory.setInputWatermark(new Instant(50)); - Collection> readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(0, readyTimers.size()); - - timerInternalsFactory.setInputWatermark(new Instant(150)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - - timerInternalsFactory.setInputWatermark(new Instant(250)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(1, readyTimers.size()); - - store.close(); - } - - /** - * Test the number of expired event timers for each watermark does not exceed the predefined - * limit. - */ - @Test - public void testMaxExpiredEventTimersProcessAtOnce() { - // If maxExpiredTimersToProcessOnce <= the number of expired timers, then load - // "maxExpiredTimersToProcessOnce" timers. - testMaxExpiredEventTimersProcessAtOnce(10, 10, 5, 5); - testMaxExpiredEventTimersProcessAtOnce(10, 10, 10, 10); - - // If maxExpiredTimersToProcessOnce > the number of expired timers, then load all the ready - // timers. - testMaxExpiredEventTimersProcessAtOnce(10, 10, 20, 10); - } - - private void testMaxExpiredEventTimersProcessAtOnce( - int totalNumberOfTimersInStore, - int totalNumberOfExpiredTimers, - int maxExpiredTimersToProcessOnce, - int expectedExpiredTimersToProcess) { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setMaxReadyTimersToProcessOnce(maxExpiredTimersToProcessOnce); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - TimerInternals.TimerData timer; - for (int i = 0; i < totalNumberOfTimersInStore; i++) { - timer = - TimerInternals.TimerData.of( - "timer" + i, - nameSpace, - new Instant(i), - new Instant(i), - TimeDomain.EVENT_TIME, - CausedByDrain.NORMAL); - timerInternals.setTimer(timer); - } - - // Set the timestamp of the input watermark to be the value of totalNumberOfExpiredTimers - // so that totalNumberOfExpiredTimers timers are expected be expired with respect to this - // watermark. - final Instant inputWatermark = new Instant(totalNumberOfExpiredTimers); - timerInternalsFactory.setInputWatermark(inputWatermark); - final Collection> readyTimers = - timerInternalsFactory.removeReadyTimers(); - assertEquals(expectedExpiredTimersToProcess, readyTimers.size()); - store.close(); - } - - @Test - public void testBufferSizeNotExceedingPipelineOptionValue() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(2); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare 5 timers. - // timers in memory are then timestamped from 0 - 1; - // timers in store are then timestamped from 0 - 4. - for (int i = 0; i < 5; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // only two timers are supposed to be in event time buffer - assertEquals(2, timerInternalsFactory.getEventTimeBuffer().size()); - - store.close(); - } - - @Test - public void testAllTimersAreFiredWithReload() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(2); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare 3 timers. - // timers in memory now are timestamped from 0 - 1; - // timers in store now are timestamped from 0 - 2. - for (int i = 0; i < 3; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // total number of event time timers to fire equals to the number of timers in store - Collection> readyTimers; - timerInternalsFactory.setInputWatermark(new Instant(3)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - // buffer should reload from store and all timers are supposed to be fired. - assertEquals(3, readyTimers.size()); - - store.close(); - } - - /** - * Test the total number of event time timers reloaded into memory is aligned with the number of - * the event time timers written to the store. Moreover, event time timers reloaded into memory is - * maintained in order. - */ - @Test - public void testAllTimersAreFiredInOrder() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(5); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare 8 timers. - // timers in memory now are timestamped from 0 - 4; - // timers in store now are timestamped from 0 - 7. - for (int i = 0; i < 8; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // fire the first 2 timers. - // timers in memory now are timestamped from 2 - 4; - // timers in store now are timestamped from 2 - 7. - Collection> readyTimers; - timerInternalsFactory.setInputWatermark(new Instant(1)); - long lastTimestamp = 0; - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(2, readyTimers.size()); - - // add another 12 timers. - // timers in memory (reloaded for three times) now are timestamped from 2 - 4; 5 - 9; 10 - 14; - // 15 - 19. - // timers in store now are timestamped from 2 - 19. - // the total number of timers to fire is 18. - for (int i = 8; i < 20; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - timerInternalsFactory.setInputWatermark(new Instant(20)); - lastTimestamp = 0; - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(18, readyTimers.size()); - - store.close(); - } - - @Test - public void testNewTimersAreInsertedInOrder() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(5); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare 10 timers. - // timers in memory now are timestamped from 0 - 4; - // timers in store now are timestamped from 0 - 9. - for (int i = 0; i < 10; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // fire the first 2 timers. - // timers in memory now are timestamped from 2 - 4; - // timers in store now are timestamped from 2 - 9. - Collection> readyTimers; - timerInternalsFactory.setInputWatermark(new Instant(1)); - long lastTimestamp = 0; - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(2, readyTimers.size()); - - // add 3 timers but timer 2 has duplicate so drop. - // timers in memory now are timestamped from 0 to 2 prefixed with lateTimer, and 2 to - // 4 prefixed with timer, timestamp is in order; - // timers in store now are timestamped from 0 to 2 prefixed with lateTimer, and 2 to 9 - // prefixed with timer, timestamp is in order; - for (int i = 0; i < 3; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // there are 11 timers in state now. - // watermark 5 comes, so 6 timers will be evicted because their timestamp is less than 5. - // memory will be reloaded once to have 5 to 8 left (reload to have 4 to 8, but 4 is evicted), 5 - // to 9 left in store. - // all of them are in order for firing. - timerInternalsFactory.setInputWatermark(new Instant(5)); - lastTimestamp = 0; - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(6, readyTimers.size()); - assertEquals(4, timerInternalsFactory.getEventTimeBuffer().size()); - - // watermark 10 comes, so all timers will be evicted in order. - timerInternalsFactory.setInputWatermark(new Instant(10)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(4, readyTimers.size()); - assertEquals(0, timerInternalsFactory.getEventTimeBuffer().size()); - - store.close(); - } - - @Test - public void testBufferRefilledAfterRestoreToNonFullState() { - final SamzaPipelineOptions pipelineOptions = - PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - pipelineOptions.setEventTimerBufferSize(5); - - final KeyValueStore> store = createStore(); - final SamzaTimerInternalsFactory timerInternalsFactory = - createTimerInternalsFactory(null, "timer", pipelineOptions, store); - - final StateNamespace nameSpace = StateNamespaces.global(); - final TimerInternals timerInternals = timerInternalsFactory.timerInternalsForKey("testKey"); - - // prepare (buffer capacity + 1) 6 timers. - // timers in memory now are timestamped from 0 - 4; - // timer in store now is timestamped 6. - for (int i = 0; i < 6; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - - // total number of event time timers to fire equals to the number of timers in store - Collection> readyTimers; - timerInternalsFactory.setInputWatermark(new Instant(4)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - assertEquals(5, readyTimers.size()); - // reloaded timer5 - assertEquals(1, timerInternalsFactory.getEventTimeBuffer().size()); - - for (int i = 6; i < 13; i++) { - timerInternals.setTimer( - nameSpace, "timer" + i, "", new Instant(i), new Instant(i), TimeDomain.EVENT_TIME); - } - // timers should go into buffer not state - assertEquals(5, timerInternalsFactory.getEventTimeBuffer().size()); - - // watermark 10 comes,6 timers will be evicted in order and 2 still in buffer. - timerInternalsFactory.setInputWatermark(new Instant(10)); - readyTimers = timerInternalsFactory.removeReadyTimers(); - long lastTimestamp = 0; - for (KeyedTimerData keyedTimerData : readyTimers) { - final long currentTimeStamp = keyedTimerData.getTimerData().getTimestamp().getMillis(); - assertTrue(lastTimestamp <= currentTimeStamp); - lastTimestamp = currentTimeStamp; - } - assertEquals(6, readyTimers.size()); - assertEquals(2, timerInternalsFactory.getEventTimeBuffer().size()); - - store.close(); - } - - @Test - public void testByteArray() { - ByteArray key1 = ByteArray.of("hello world".getBytes(StandardCharsets.UTF_8)); - Serde serde = new ByteArraySerdeFactory().getSerde("", null); - byte[] keyBytes = serde.toBytes(key1); - ByteArray key2 = serde.fromBytes(keyBytes); - assertEquals(key1, key2); - - Map map = new HashMap<>(); - map.put(key1, "found it"); - assertEquals("found it", map.get(key2)); - - map.remove(key1); - assertTrue(!map.containsKey(key2)); - assertTrue(map.isEmpty()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SdkHarnessDoFnRunnerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SdkHarnessDoFnRunnerTest.java deleted file mode 100644 index e6029beb93b0..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/SdkHarnessDoFnRunnerTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.runtime; - -import java.util.concurrent.TimeoutException; -import org.junit.Test; - -public class SdkHarnessDoFnRunnerTest { - - @Test(expected = TimeoutException.class) - public void testRunWithTimeoutOccurred() throws Exception { - SamzaDoFnRunners.SdkHarnessDoFnRunner.runWithTimeout( - 100, - () -> { - try { - Thread.sleep(500); - } catch (InterruptedException ignored) { - } - }); - } - - @Test - public void testRunWithTimeoutDisabled() throws Exception { - SamzaDoFnRunners.SdkHarnessDoFnRunner.runWithTimeout( - -1, - () -> { - try { - Thread.sleep(500); - } catch (InterruptedException ignored) { - } - }); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/ConfigGeneratorTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/ConfigGeneratorTest.java deleted file mode 100644 index 9fbc515979b0..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/ConfigGeneratorTest.java +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import org.apache.beam.runners.samza.SamzaExecutionEnvironment; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.SamzaRunner; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.state.StateSpec; -import org.apache.beam.sdk.state.StateSpecs; -import org.apache.beam.sdk.state.ValueState; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.Filter; -import org.apache.beam.sdk.transforms.Impulse; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.sdk.values.TypeDescriptors; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; -import org.apache.samza.config.Config; -import org.apache.samza.config.JobConfig; -import org.apache.samza.config.JobCoordinatorConfig; -import org.apache.samza.config.TaskConfig; -import org.apache.samza.config.ZkConfig; -import org.apache.samza.job.yarn.YarnJobFactory; -import org.apache.samza.runtime.LocalApplicationRunner; -import org.apache.samza.runtime.RemoteApplicationRunner; -import org.apache.samza.storage.kv.RocksDbKeyValueStorageEngineFactory; -import org.apache.samza.storage.kv.inmemory.InMemoryKeyValueStorageEngineFactory; -import org.apache.samza.zk.ZkJobCoordinatorFactory; -import org.junit.Test; - -/** Test config generations for {@link org.apache.beam.runners.samza.SamzaRunner}. */ -// TODO(https://github.com/apache/beam/issues/21230): Remove when new version of errorprone is -// released (2.11.0) -@SuppressWarnings("unused") -public class ConfigGeneratorTest { - private static final String APP_RUNNER_CLASS = "app.runner.class"; - private static final String JOB_FACTORY_CLASS = "job.factory.class"; - - @Test - public void testStatefulBeamStoreConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Create.of(1, 2, 3)).apply(Sum.integersGlobally()); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.beamStore.factory")); - assertEquals("byteArraySerde", config.get("stores.beamStore.key.serde")); - assertEquals("stateValueSerde", config.get("stores.beamStore.msg.serde")); - assertNull(config.get("stores.beamStore.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - assertEquals( - "TestStoreConfig-1-beamStore-changelog", config2.get("stores.beamStore.changelog")); - } - - @Test - public void testStatelessBeamStoreConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Impulse.create()).apply(Filter.by(Objects::nonNull)); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - InMemoryKeyValueStorageEngineFactory.class.getName(), - config.get("stores.beamStore.factory")); - assertEquals("byteArraySerde", config.get("stores.beamStore.key.serde")); - assertEquals("stateValueSerde", config.get("stores.beamStore.msg.serde")); - assertNull(config.get("stores.beamStore.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - // For stateless jobs, ignore state durable pipeline option. - assertNull(config2.get("stores.beamStore.changelog")); - } - - @Test - public void testSamzaLocalExecutionEnvironmentConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestEnvConfig"); - options.setRunner(SamzaRunner.class); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.LOCAL); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Create.of(1, 2, 3)).apply(Sum.integersGlobally()); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertTrue( - Maps.difference(config, ConfigBuilder.localRunConfig()).entriesOnlyOnRight().isEmpty()); - } - - @Test - public void testSamzaYarnExecutionEnvironmentConfig() { - final String yarnPackagePath = "yarn.package.path"; - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestEnvConfig"); - options.setRunner(SamzaRunner.class); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.YARN); - options.setConfigOverride( - ImmutableMap.builder() - .put( - yarnPackagePath, - "file://${basedir}/target/${project.artifactId}-${pom.version}-dist.tar.gz") - .build()); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Create.of(1, 2, 3)).apply(Sum.integersGlobally()); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - try { - Config config = configBuilder.build(); - assertEquals(config.get(APP_RUNNER_CLASS), RemoteApplicationRunner.class.getName()); - assertEquals(config.get(JOB_FACTORY_CLASS), YarnJobFactory.class.getName()); - } catch (IllegalArgumentException e) { - throw new AssertionError( - String.format( - "Failed to validate correct configs for %s samza execution environment", - SamzaExecutionEnvironment.YARN), - e); - } - } - - @Test - public void testSamzaStandAloneExecutionEnvironmentConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestEnvConfig"); - options.setRunner(SamzaRunner.class); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.STANDALONE); - options.setConfigOverride( - ImmutableMap.builder().put(ZkConfig.ZK_CONNECT, "localhost:2181").build()); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Create.of(1, 2, 3)).apply(Sum.integersGlobally()); - - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - try { - Config config = configBuilder.build(); - assertEquals(config.get(APP_RUNNER_CLASS), LocalApplicationRunner.class.getName()); - assertEquals( - config.get(JobCoordinatorConfig.JOB_COORDINATOR_FACTORY), - ZkJobCoordinatorFactory.class.getName()); - } catch (IllegalArgumentException e) { - throw new AssertionError( - String.format( - "Failed to validate correct configs for %s samza execution environment", - SamzaExecutionEnvironment.STANDALONE), - e); - } - } - - @Test - public void testUserStoreConfig() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline - .apply( - Create.empty(TypeDescriptors.kvs(TypeDescriptors.strings(), TypeDescriptors.strings()))) - .apply( - ParDo.of( - new DoFn, Void>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) {} - })); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState.factory")); - assertEquals("byteArraySerde", config.get("stores.testState.key.serde")); - assertEquals("stateValueSerde", config.get("stores.testState.msg.serde")); - assertNull(config.get("stores.testState.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - assertEquals( - "TestStoreConfig-1-testState-changelog", config2.get("stores.testState.changelog")); - } - - @Test - public void testUserStoreConfigSameStateIdAcrossParDo() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline - .apply( - Create.empty(TypeDescriptors.kvs(TypeDescriptors.strings(), TypeDescriptors.strings()))) - .apply( - "First stateful ParDo", - ParDo.of( - new DoFn, KV>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) { - context.output(context.element()); - } - })) - .apply( - "Second stateful ParDo", - ParDo.of( - new DoFn, Void>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) {} - })); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState-First_stateful_ParDo.factory")); - assertEquals("byteArraySerde", config.get("stores.testState-First_stateful_ParDo.key.serde")); - assertEquals("stateValueSerde", config.get("stores.testState-First_stateful_ParDo.msg.serde")); - assertNull(config.get("stores.testState-First_stateful_ParDo.changelog")); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState-Second_stateful_ParDo.factory")); - assertEquals("byteArraySerde", config.get("stores.testState-Second_stateful_ParDo.key.serde")); - assertEquals("stateValueSerde", config.get("stores.testState-Second_stateful_ParDo.msg.serde")); - assertNull(config.get("stores.testState-Second_stateful_ParDo.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - assertEquals( - "TestStoreConfig-1-testState-First_stateful_ParDo-changelog", - config2.get("stores.testState-First_stateful_ParDo.changelog")); - assertEquals( - "TestStoreConfig-1-testState-Second_stateful_ParDo-changelog", - config2.get("stores.testState-Second_stateful_ParDo.changelog")); - } - - @Test - public void testUserStoreConfigSameStateIdAndPTransformName() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestStoreConfig"); - options.setRunner(SamzaRunner.class); - - Pipeline pipeline = Pipeline.create(options); - pipeline - .apply( - Create.empty(TypeDescriptors.kvs(TypeDescriptors.strings(), TypeDescriptors.strings()))) - .apply( - "Same stateful ParDo Name", - ParDo.of( - new DoFn, KV>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) { - context.output(context.element()); - } - })) - .apply( - "Same stateful ParDo Name", - ParDo.of( - new DoFn, Void>() { - private static final String testState = "testState"; - - @StateId(testState) - private final StateSpec> state = StateSpecs.value(); - - @ProcessElement - public void processElement( - ProcessContext context, @StateId(testState) ValueState state) {} - })); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config = configBuilder.build(); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState-Same_stateful_ParDo_Name.factory")); - assertEquals( - "byteArraySerde", config.get("stores.testState-Same_stateful_ParDo_Name.key.serde")); - assertEquals( - "stateValueSerde", config.get("stores.testState-Same_stateful_ParDo_Name.msg.serde")); - assertNull(config.get("stores.testState-Same_stateful_ParDo_Name.changelog")); - - assertEquals( - RocksDbKeyValueStorageEngineFactory.class.getName(), - config.get("stores.testState-Same_stateful_ParDo_Name2.factory")); - assertEquals( - "byteArraySerde", config.get("stores.testState-Same_stateful_ParDo_Name2.key.serde")); - assertEquals( - "stateValueSerde", config.get("stores.testState-Same_stateful_ParDo_Name2.msg.serde")); - assertNull(config.get("stores.testState-Same_stateful_ParDo_Name2.changelog")); - - options.setStateDurable(true); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Config config2 = configBuilder.build(); - assertEquals( - "TestStoreConfig-1-testState-Same_stateful_ParDo_Name-changelog", - config2.get("stores.testState-Same_stateful_ParDo_Name.changelog")); - assertEquals( - "TestStoreConfig-1-testState-Same_stateful_ParDo_Name2-changelog", - config2.get("stores.testState-Same_stateful_ParDo_Name2.changelog")); - } - - @Test - public void testCreateBundleConfig() { - // autosizing = 0: disabled - // autosizing = 1: enabled - for (int autosizing = 0; autosizing < 2; autosizing++) { - final SamzaPipelineOptions options = PipelineOptionsFactory.as(SamzaPipelineOptions.class); - final Map config = new HashMap<>(); - - // bundle size = 1 - options.setMaxBundleSize(1); - config.put(JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE, "5"); - if (autosizing != 0) { - // Test autosizing enabled, the output should be the same - config.put(JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE, "5"); - } - - Map bundleConfig = ConfigBuilder.createBundleConfig(options, config); - - assertEquals("1", bundleConfig.get(TaskConfig.MAX_CONCURRENCY)); - assertNull(bundleConfig.get(JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE)); - assertNull(bundleConfig.get(JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE)); - assertEquals(1, options.getNumThreadsForProcessElement()); - - // bundle size = 3, NumThreadsForProcessElement = 10 - options.setMaxBundleSize(3); - options.setNumThreadsForProcessElement(10); - bundleConfig = ConfigBuilder.createBundleConfig(options, config); - - assertEquals("3", bundleConfig.get(TaskConfig.MAX_CONCURRENCY)); - assertEquals("0", bundleConfig.get(JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE)); - assertEquals("0", bundleConfig.get(JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE)); - assertEquals(10, options.getNumThreadsForProcessElement()); - - // bundle size = 3, NumThreadsForProcessElement = 1 (default), threadPoolSize = 5 - options.setNumThreadsForProcessElement(1); - bundleConfig = ConfigBuilder.createBundleConfig(options, config); - - assertEquals("3", bundleConfig.get(TaskConfig.MAX_CONCURRENCY)); - assertEquals("0", bundleConfig.get(JobConfig.JOB_CONTAINER_THREAD_POOL_SIZE)); - assertEquals("0", bundleConfig.get(JobConfig.JOB_AUTOSIZING_CONTAINER_THREAD_POOL_SIZE)); - assertEquals(5, options.getNumThreadsForProcessElement()); - } - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemTest.java deleted file mode 100644 index 9cc0cc10894a..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/SamzaImpulseSystemTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.samza.Partition; -import org.apache.samza.config.MapConfig; -import org.apache.samza.system.IncomingMessageEnvelope; -import org.apache.samza.system.SystemConsumer; -import org.apache.samza.system.SystemStreamPartition; -import org.apache.samza.system.WatermarkMessage; -import org.junit.Assert; -import org.junit.Test; - -/** - * Tests for {@link - * org.apache.beam.runners.samza.translation.SamzaImpulseSystemFactory.SamzaImpulseSystemConsumer}. - */ -@SuppressWarnings({ - "rawtypes", // TODO(https://github.com/apache/beam/issues/20447) -}) -public class SamzaImpulseSystemTest { - @Test - public void testSamzaImpulseSystemConsumer() throws Exception { - SystemConsumer consumer = - new SamzaImpulseSystemFactory().getConsumer("default-system", new MapConfig(), null); - Map> result = - consumer.poll(Collections.singleton(sspForPartition(0)), 100); - Assert.assertEquals(1, result.size()); - Assert.assertTrue(result.containsKey(sspForPartition(0))); - - List messageEnvelopes = result.get(sspForPartition(0)); - Assert.assertEquals(3, messageEnvelopes.size()); - - Assert.assertTrue(messageEnvelopes.get(0).getMessage() instanceof OpMessage); - OpMessage impulseEvent = (OpMessage) messageEnvelopes.get(0).getMessage(); - Assert.assertEquals(OpMessage.Type.ELEMENT, impulseEvent.getType()); - - Assert.assertTrue(messageEnvelopes.get(1).getMessage() instanceof WatermarkMessage); - - Assert.assertTrue(messageEnvelopes.get(2).isEndOfStream()); - } - - private SystemStreamPartition sspForPartition(int i) { - return new SystemStreamPartition("default-system", "default-stream", new Partition(i)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/TranslationContextTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/TranslationContextTest.java deleted file mode 100644 index bb357dd6aced..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/translation/TranslationContextTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.translation; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.mock; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.runtime.OpMessage; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PValue; -import org.apache.samza.application.descriptors.StreamApplicationDescriptor; -import org.apache.samza.application.descriptors.StreamApplicationDescriptorImpl; -import org.apache.samza.config.Config; -import org.apache.samza.config.MapConfig; -import org.apache.samza.operators.KV; -import org.apache.samza.operators.MessageStream; -import org.apache.samza.operators.functions.MapFunction; -import org.apache.samza.serializers.KVSerde; -import org.apache.samza.serializers.NoOpSerde; -import org.apache.samza.serializers.Serde; -import org.apache.samza.system.descriptors.GenericInputDescriptor; -import org.apache.samza.system.descriptors.GenericSystemDescriptor; -import org.junit.Test; - -@SuppressWarnings({"rawtypes"}) -public class TranslationContextTest { - private final GenericInputDescriptor testInputDescriptor = - new GenericSystemDescriptor("mockSystem", "mockFactoryClassName") - .getInputDescriptor("test-input-1", mock(Serde.class)); - MapFunction keyFn = m -> m.toString(); - MapFunction valueFn = m -> m; - private final String streamName = "testStream"; - KVSerde serde = KVSerde.of(new NoOpSerde<>(), new NoOpSerde<>()); - StreamApplicationDescriptor streamApplicationDescriptor = - new StreamApplicationDescriptorImpl( - appDesc -> { - MessageStream inputStream = appDesc.getInputStream(testInputDescriptor); - inputStream.partitionBy(keyFn, valueFn, serde, streamName); - }, - getConfig()); - Map idMap = new HashMap<>(); - Set nonUniqueStateIds = new HashSet<>(); - TranslationContext translationContext = - new TranslationContext( - streamApplicationDescriptor, idMap, nonUniqueStateIds, mock(SamzaPipelineOptions.class)); - - @Test - public void testRegisterInputMessageStreams() { - final PCollection output = mock(PCollection.class); - List topics = Arrays.asList("stream1", "stream2"); - List inputDescriptors = - topics.stream() - .map(topicName -> createSamzaInputDescriptor(topicName, topicName)) - .collect(Collectors.toList()); - - translationContext.registerInputMessageStreams(output, inputDescriptors); - - assertNotNull(translationContext.getMessageStream(output)); - } - - public GenericInputDescriptor>> createSamzaInputDescriptor( - String systemName, String streamId) { - final Serde>> kvSerde = - KVSerde.of(new NoOpSerde<>(), new NoOpSerde<>()); - return new GenericSystemDescriptor(systemName, "factoryClass") - .getInputDescriptor(streamId, kvSerde); - } - - private static Config getConfig() { - HashMap configMap = new HashMap<>(); - configMap.put("job.name", "testJobName"); - configMap.put("job.id", "testJobId"); - return new MapConfig(configMap); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/DoFnUtilsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/DoFnUtilsTest.java deleted file mode 100644 index cef1a463ff96..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/DoFnUtilsTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.junit.Assert.assertEquals; - -import java.io.Serializable; -import java.util.Objects; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.Filter; -import org.apache.beam.sdk.transforms.GroupByKey; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.util.construction.PipelineTranslation; -import org.apache.beam.sdk.util.construction.graph.ExecutableStage; -import org.apache.beam.sdk.util.construction.graph.GreedyPipelineFuser; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PDone; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.junit.Test; - -public class DoFnUtilsTest implements Serializable { - private final Pipeline pipeline = Pipeline.create(); - - @Test - public void testExecutableStageWithoutOutput() { - pipeline.apply(Create.of(KV.of(1L, "1"))); - - assertEquals("[Create.Values-]", DoFnUtils.toStepName(getOnlyExecutableStage(pipeline))); - } - - @Test - public void testExecutableStageWithCustomizedName() { - pipeline.apply("MyCreateOf", Create.of(KV.of(1L, "1"))); - assertEquals("[MyCreateOf-]", DoFnUtils.toStepName(getOnlyExecutableStage(pipeline))); - } - - @Test - public void testExecutableStageWithOutput() { - pipeline - .apply("MyCreateOf", Create.of(KV.of(1L, "1"))) - .apply("MyFilterBy", Filter.by(Objects::nonNull)) - .apply(GroupByKey.create()); - - assertEquals("[MyCreateOf-MyFilterBy]", DoFnUtils.toStepName(getOnlyExecutableStage(pipeline))); - } - - @Test - public void testExecutableStageWithPDone() { - pipeline - .apply("MyCreateOf", Create.of("1")) - .apply( - "PDoneTransform", - new PTransform, PDone>() { - @Override - public PDone expand(PCollection input) { - return PDone.in(pipeline); - } - }); - - assertEquals("[MyCreateOf-]", DoFnUtils.toStepName(getOnlyExecutableStage(pipeline))); - } - - private static ExecutableStage getOnlyExecutableStage(Pipeline p) { - return Iterables.getOnlyElement( - GreedyPipelineFuser.fuse(PipelineTranslation.toProto(p)).getFusedStages()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/FutureUtilsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/FutureUtilsTest.java deleted file mode 100644 index c261fc623af3..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/FutureUtilsTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; -import org.junit.Assert; -import org.junit.Test; - -/** Unit tests for {@linkplain FutureUtils}. */ -public final class FutureUtilsTest { - private static final List RESULTS = ImmutableList.of("hello", "world"); - - @Test - public void testFlattenFuturesForCollection() { - CompletionStage> resultFuture = - FutureUtils.flattenFutures( - ImmutableList.of( - CompletableFuture.completedFuture("hello"), - CompletableFuture.completedFuture("world"))); - - CompletionStage validationFuture = - resultFuture.thenAccept( - actualResults -> { - Assert.assertEquals( - "Expected flattened results to contain {hello, world}", - RESULTS, - Lists.newArrayList(actualResults)); - }); - - validationFuture.toCompletableFuture().join(); - } - - @Test - public void testFlattenFuturesForFailedFuture() { - CompletionStage> resultFuture = - FutureUtils.flattenFutures( - ImmutableList.of( - CompletableFuture.completedFuture("hello"), - createFailedFuture(new RuntimeException()))); - - CompletionStage validationFuture = - resultFuture.handle( - (results, ex) -> { - Assert.assertTrue( - "Expected exception to be of RuntimeException", ex instanceof RuntimeException); - return null; - }); - - validationFuture.toCompletableFuture().join(); - } - - @Test - public void testWaitForAllFutures() { - CountDownLatch latch = new CountDownLatch(1); - CompletionStage> resultFuture = - FutureUtils.flattenFutures( - ImmutableList.of( - CompletableFuture.supplyAsync( - () -> { - try { - latch.await(); - } catch (InterruptedException e) { - return ""; - } - - return "hello"; - }), - CompletableFuture.supplyAsync( - () -> { - latch.countDown(); - return "world"; - }))); - - CompletionStage validationFuture = - resultFuture.thenAccept( - actualResults -> { - Assert.assertEquals( - "Expected flattened results to contain {hello, world}", - RESULTS, - Lists.newArrayList(actualResults)); - }); - - validationFuture.toCompletableFuture().join(); - } - - private static CompletionStage createFailedFuture(Throwable t) { - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(t); - return future; - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/InMemoryMetricsReporter.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/InMemoryMetricsReporter.java deleted file mode 100644 index fed43430a440..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/InMemoryMetricsReporter.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import java.util.HashMap; -import java.util.Map; -import org.apache.samza.metrics.MetricsReporter; -import org.apache.samza.metrics.ReadableMetricsRegistry; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** An in-memory {@link MetricsReporter} for testing. */ -public class InMemoryMetricsReporter implements MetricsReporter { - private Map registries; - - public InMemoryMetricsReporter() { - registries = new HashMap<>(); - } - - @Override - public void start() {} - - @Override - public void register(String source, ReadableMetricsRegistry registry) { - registries.put(source, registry); - } - - @Override - public void stop() {} - - public @Nullable ReadableMetricsRegistry getMetricsRegistry(@NonNull String source) { - return registries.get(source); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PipelineJsonRendererTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PipelineJsonRendererTest.java deleted file mode 100644 index 0a4f532808b1..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PipelineJsonRendererTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.junit.Assert.assertEquals; - -import com.google.gson.JsonParser; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import org.apache.beam.runners.samza.SamzaExecutionEnvironment; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.apache.beam.runners.samza.SamzaRunner; -import org.apache.beam.runners.samza.translation.ConfigBuilder; -import org.apache.beam.runners.samza.translation.ConfigContext; -import org.apache.beam.runners.samza.translation.PViewToIdMapper; -import org.apache.beam.runners.samza.translation.SamzaPipelineTranslator; -import org.apache.beam.runners.samza.translation.SamzaTransformOverrides; -import org.apache.beam.runners.samza.translation.StateIdParser; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.options.PipelineOptionsFactory; -import org.apache.beam.sdk.transforms.Create; -import org.apache.beam.sdk.transforms.Filter; -import org.apache.beam.sdk.transforms.Impulse; -import org.apache.beam.sdk.transforms.Sum; -import org.apache.beam.sdk.transforms.windowing.FixedWindows; -import org.apache.beam.sdk.transforms.windowing.Window; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PValue; -import org.apache.beam.sdk.values.TimestampedValue; -import org.joda.time.Duration; -import org.joda.time.Instant; -import org.junit.Test; - -/** Tests for {@link org.apache.beam.runners.samza.util.PipelineJsonRenderer}. */ -public class PipelineJsonRendererTest { - - @Test - public void testEmptyPipeline() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setRunner(SamzaRunner.class); - - Pipeline p = Pipeline.create(options); - final Map idMap = PViewToIdMapper.buildIdMap(p); - final Set nonUniqueStateIds = StateIdParser.scan(p); - final ConfigContext ctx = new ConfigContext(idMap, nonUniqueStateIds, options); - - String jsonDag = - "{ \"RootNode\": [" - + " { \"fullName\":\"OuterMostNode\"," - + " \"ChildNodes\":[ ]}],\"graphLinks\": [],\"transformIOInfo\": []" - + "}"; - - assertEquals( - JsonParser.parseString(jsonDag), - JsonParser.parseString( - PipelineJsonRenderer.toJsonString(p, ctx).replaceAll(System.lineSeparator(), ""))); - } - - @Test - public void testCompositePipeline() throws IOException { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setRunner(SamzaRunner.class); - options.setJobName("TestEnvConfig"); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.LOCAL); - - Pipeline p = Pipeline.create(options); - - p.apply( - Create.timestamped( - TimestampedValue.of(KV.of(1, 1), new Instant(1)), - TimestampedValue.of(KV.of(2, 2), new Instant(2)))) - .apply(Window.into(FixedWindows.of(Duration.millis(10)))) - .apply(Sum.integersPerKey()); - - p.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(p); - final Set nonUniqueStateIds = StateIdParser.scan(p); - final ConfigContext ctx = new ConfigContext(idMap, nonUniqueStateIds, options); - - String jsonDagFileName = "src/test/resources/ExpectedDag.json"; - String jsonDag = - new String(Files.readAllBytes(Paths.get(jsonDagFileName)), StandardCharsets.UTF_8); - String renderedDag = PipelineJsonRenderer.toJsonString(p, ctx); - - assertEquals( - JsonParser.parseString(jsonDag), - JsonParser.parseString(renderedDag.replaceAll(System.lineSeparator(), ""))); - } - - @Test - public void testBeamTransformIOConfigGen() { - SamzaPipelineOptions options = PipelineOptionsFactory.create().as(SamzaPipelineOptions.class); - options.setJobName("TestEnvConfig"); - options.setRunner(SamzaRunner.class); - options.setSamzaExecutionEnvironment(SamzaExecutionEnvironment.LOCAL); - - Pipeline pipeline = Pipeline.create(options); - pipeline.apply(Impulse.create()).apply(Filter.by(Objects::nonNull)); - pipeline.replaceAll(SamzaTransformOverrides.getDefaultOverrides()); - - final Map idMap = PViewToIdMapper.buildIdMap(pipeline); - final Set nonUniqueStateIds = StateIdParser.scan(pipeline); - final ConfigContext configCtx = new ConfigContext(idMap, nonUniqueStateIds, options); - - final ConfigBuilder configBuilder = new ConfigBuilder(options); - SamzaPipelineTranslator.createConfig(pipeline, configCtx, configBuilder); - final Map> transformInputOutput = - PipelineJsonRenderer.buildTransformIOMap(pipeline, configCtx); - - assertEquals(2, transformInputOutput.size()); - assertEquals("", transformInputOutput.get("Impulse").getKey()); // no input to impulse - assertEquals( - "Impulse.out", - transformInputOutput.get("Impulse").getValue()); // PValue for to Impulse.output - - // Input to Filter is PValue Output from Impulse - assertEquals( - "Impulse.out", - transformInputOutput.get("Filter/ParDo(Anonymous)/ParMultiDo(Anonymous)").getKey()); - // output PValue of filter - assertEquals( - "Filter/ParDo(Anonymous)/ParMultiDo(Anonymous).output", - transformInputOutput.get("Filter/ParDo(Anonymous)/ParMultiDo(Anonymous)").getValue()); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PortableConfigUtilsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PortableConfigUtilsTest.java deleted file mode 100644 index 14ab5b1af5fb..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/PortableConfigUtilsTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -import java.util.HashMap; -import java.util.Map; -import org.apache.beam.runners.samza.SamzaPipelineOptions; -import org.junit.Assert; -import org.junit.Test; - -public class PortableConfigUtilsTest { - - @Test - public void testNonPortableMode() { - SamzaPipelineOptions mockOptions = mock(SamzaPipelineOptions.class); - Map config = new HashMap<>(); - config.put(PortableConfigUtils.BEAM_PORTABLE_MODE, "false"); - doReturn(config).when(mockOptions).getConfigOverride(); - Assert.assertFalse( - "Expected false for portable mode ", PortableConfigUtils.isPortable(mockOptions)); - } - - @Test - public void testNonPortableModeNullConfig() { - SamzaPipelineOptions mockOptions = mock(SamzaPipelineOptions.class); - doReturn(null).when(mockOptions).getConfigOverride(); - Assert.assertFalse( - "Expected false for portable mode ", PortableConfigUtils.isPortable(mockOptions)); - } - - @Test - public void testPortableMode() { - SamzaPipelineOptions mockOptions = mock(SamzaPipelineOptions.class); - Map config = new HashMap<>(); - config.put(PortableConfigUtils.BEAM_PORTABLE_MODE, "true"); - doReturn(config).when(mockOptions).getConfigOverride(); - Assert.assertTrue( - "Expected true for portable runner", PortableConfigUtils.isPortable(mockOptions)); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/TestHashIdGenerator.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/TestHashIdGenerator.java deleted file mode 100644 index cf765e3db221..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/TestHashIdGenerator.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.mockito.Mockito.mock; - -import java.util.Set; -import org.apache.beam.sdk.transforms.Combine; -import org.apache.beam.sdk.transforms.Count; -import org.apache.beam.sdk.transforms.MapElements; -import org.apache.beam.sdk.transforms.Max; -import org.apache.beam.sdk.transforms.Min; -import org.apache.beam.sdk.transforms.SerializableFunction; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; -import org.junit.Assert; -import org.junit.Test; - -/** Test class for {@link HashIdGenerator}. */ -public class TestHashIdGenerator { - - @Test - public void testGetId() { - final HashIdGenerator idGenerator = new HashIdGenerator(); - final Set ids = - ImmutableSet.of( - idGenerator.getId(Count.perKey().getName()), - idGenerator.getId(MapElements.into(null).getName()), - idGenerator.getId(Count.globally().getName()), - idGenerator.getId(Combine.perKey(mock(SerializableFunction.class)).getName()), - idGenerator.getId(Min.perKey().getName()), - idGenerator.getId(Max.globally().getName())); - Assert.assertEquals(6, ids.size()); - } - - @Test - public void testGetShortId() { - final HashIdGenerator idGenerator = new HashIdGenerator(); - String id = idGenerator.getId("abcd"); - Assert.assertEquals("abcd", id); - } - - @Test - public void testSameNames() { - final HashIdGenerator idGenerator = new HashIdGenerator(); - String id1 = idGenerator.getId(Count.perKey().getName()); - String id2 = idGenerator.getId(Count.perKey().getName()); - Assert.assertNotEquals(id1, id2); - } - - @Test - public void testSameShortNames() { - final HashIdGenerator idGenerator = new HashIdGenerator(); - String id = idGenerator.getId("abcd"); - Assert.assertEquals("abcd", id); - String id2 = idGenerator.getId("abcd"); - Assert.assertNotEquals("abcd", id2); - } - - @Test - public void testLongHash() { - final HashIdGenerator idGenerator = new HashIdGenerator(10); - String id1 = idGenerator.getId(Count.perKey().getName()); - String id2 = idGenerator.getId(Count.perKey().getName()); - String id3 = idGenerator.getId(Count.perKey().getName()); - String id4 = idGenerator.getId(Count.perKey().getName()); - Assert.assertNotEquals(id1, id2); - Assert.assertNotEquals(id3, id2); - Assert.assertNotEquals(id3, id4); - } -} diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/WindowUtilsTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/util/WindowUtilsTest.java deleted file mode 100644 index 9c614175e5de..000000000000 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/util/WindowUtilsTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.samza.util; - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; -import org.apache.beam.sdk.Pipeline; -import org.apache.beam.sdk.coders.Coder; -import org.apache.beam.sdk.coders.KvCoder; -import org.apache.beam.sdk.coders.StringUtf8Coder; -import org.apache.beam.sdk.coders.VarLongCoder; -import org.apache.beam.sdk.coders.VoidCoder; -import org.apache.beam.sdk.transforms.windowing.FixedWindows; -import org.apache.beam.sdk.transforms.windowing.IntervalWindow; -import org.apache.beam.sdk.transforms.windowing.TimestampCombiner; -import org.apache.beam.sdk.util.construction.Environments; -import org.apache.beam.sdk.util.construction.SdkComponents; -import org.apache.beam.sdk.values.KV; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.WindowingStrategy; -import org.joda.time.Duration; -import org.junit.Test; - -/** Unit tests for {@link WindowUtils}. */ -public class WindowUtilsTest { - - @Test - public void testGetWindowStrategy() throws IOException { - SdkComponents components = SdkComponents.create(); - String environmentId = - components.registerEnvironment(Environments.createDockerEnvironment("java")); - WindowingStrategy expected = - WindowingStrategy.of(FixedWindows.of(Duration.standardMinutes(1))) - .withMode(WindowingStrategy.AccumulationMode.DISCARDING_FIRED_PANES) - .withTimestampCombiner(TimestampCombiner.END_OF_WINDOW) - .withAllowedLateness(Duration.ZERO) - .withEnvironmentId(environmentId); - components.registerWindowingStrategy(expected); - String collectionId = - components.registerPCollection( - PCollection.createPrimitiveOutputInternal( - Pipeline.create(), expected, PCollection.IsBounded.BOUNDED, VoidCoder.of()) - .setName("name")); - - WindowingStrategy actual = - WindowUtils.getWindowStrategy(collectionId, components.toComponents()); - - assertEquals(expected, actual); - } - - @Test - public void testInstantiateWindowedCoder() throws IOException { - Coder> expectedValueCoder = - KvCoder.of(VarLongCoder.of(), StringUtf8Coder.of()); - SdkComponents components = SdkComponents.create(); - components.registerEnvironment(Environments.createDockerEnvironment("java")); - String collectionId = - components.registerPCollection( - PCollection.createPrimitiveOutputInternal( - Pipeline.create(), - WindowingStrategy.globalDefault(), - PCollection.IsBounded.BOUNDED, - expectedValueCoder) - .setName("name")); - - assertEquals( - expectedValueCoder, - WindowUtils.instantiateWindowedCoder(collectionId, components.toComponents()) - .getValueCoder()); - } -} diff --git a/runners/samza/src/test/resources/ExpectedDag.json b/runners/samza/src/test/resources/ExpectedDag.json deleted file mode 100644 index c61b80134d8a..000000000000 --- a/runners/samza/src/test/resources/ExpectedDag.json +++ /dev/null @@ -1,373 +0,0 @@ -{ - "RootNode": [ - { - "fullName": "OuterMostNode", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues", - "enclosingNode": "OuterMostNode", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values", - "enclosingNode": "Create.TimestampedValues", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)", - "enclosingNode": "Create.TimestampedValues/Create.Values", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)" - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map" - } - ] - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow/Window.Assign", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)" - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/GroupByKey", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle" - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable" - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)" - } - ] - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous)", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map" - } - ] - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/NaiveProcess", - "enclosingNode": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements" - } - ] - } - ] - } - ] - } - ] - } - ] - }, - { - "fullName": "Create.TimestampedValues/ParDo(ConvertTimestamps)", - "enclosingNode": "Create.TimestampedValues", - "ChildNodes": [ - { - "fullName": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps)", - "enclosingNode": "Create.TimestampedValues/ParDo(ConvertTimestamps)" - } - ] - } - ] - }, - { - "fullName": "Window.Into()", - "enclosingNode": "OuterMostNode", - "ChildNodes": [ - { - "fullName": "Window.Into()/Window.Assign", - "enclosingNode": "Window.Into()" - } - ] - }, - { - "fullName": "Combine.perKey(SumInteger)", - "enclosingNode": "OuterMostNode", - "ChildNodes": [ - { - "fullName": "Combine.perKey(SumInteger)/GroupByKey", - "enclosingNode": "Combine.perKey(SumInteger)" - }, - { - "fullName": "Combine.perKey(SumInteger)/Combine.GroupedValues", - "enclosingNode": "Combine.perKey(SumInteger)", - "ChildNodes": [ - { - "fullName": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)", - "enclosingNode": "Combine.perKey(SumInteger)/Combine.GroupedValues", - "ChildNodes": [ - { - "fullName": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "enclosingNode": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)" - } - ] - } - ] - } - ] - } - ] - } - ], - "graphLinks": [ - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow/Window.Assign" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/SetIdentityWindow/Window.Assign", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ReifyOriginalMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/GroupByKey" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/GroupByKey", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/ExpandIterable/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous)" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous)", - "to": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/NaiveProcess" - }, - { - "from": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/NaiveProcess", - "to": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps)" - }, - { - "from": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps)", - "to": "Window.Into()/Window.Assign" - }, - { - "from": "Window.Into()/Window.Assign", - "to": "Combine.perKey(SumInteger)/GroupByKey" - }, - { - "from": "Combine.perKey(SumInteger)/GroupByKey", - "to": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)/ParMultiDo(Anonymous)" - } - ], - "transformIOInfo": [ - { - "transformName": "Window.Into()/Window.Assign", - "inputs": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps).output", - "outputs": "Window.Into()/Window.Assign.out" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse.out", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/NaiveProcess", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Drop key/Values/Map/ParMultiDo(Anonymous).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper).output" - }, - { - "transformName": "Combine.perKey(SumInteger)", - "inputs": "Window.Into()/Window.Assign.out", - "outputs": "Combine.perKey(SumInteger)/Combine.GroupedValues/ParDo(Anonymous)/ParMultiDo(Anonymous).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(OutputSingleSource)/ParMultiDo(OutputSingleSource).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Pair with initial restriction/ParMultiDo(PairWithRestriction).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Assign unique key/AddKeys/Map/ParMultiDo(Anonymous).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/ProcessKeyedElements/Reshuffle/RestoreMetadata/ParDo(Anonymous)/ParMultiDo(Anonymous).output" - }, - { - "transformName": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper).output", - "outputs": "Create.TimestampedValues/ParDo(ConvertTimestamps)/ParMultiDo(ConvertTimestamps).output" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse", - "inputs": "", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/Impulse.out" - }, - { - "transformName": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows)", - "inputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Split restriction/ParMultiDo(SplitRestriction).output", - "outputs": "Create.TimestampedValues/Create.Values/Read(CreateSource)/ParDo(BoundedSourceAsSDFWrapper)/ParMultiDo(BoundedSourceAsSDFWrapper)/Explode windows/ParMultiDo(ExplodeWindows).output" - } - ] -} \ No newline at end of file diff --git a/runners/samza/src/test/resources/log4j-test.properties b/runners/samza/src/test/resources/log4j-test.properties deleted file mode 100644 index 8a95279413c2..000000000000 --- a/runners/samza/src/test/resources/log4j-test.properties +++ /dev/null @@ -1,26 +0,0 @@ -################################################################################ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -################################################################################ - -# Set root logger level to ERROR. -# set manually to INFO for debugging purposes. -log4j.rootLogger=INFO,testlogger - -# ConsoleAppender. -log4j.appender.testlogger=org.apache.log4j.ConsoleAppender -log4j.appender.testlogger.layout=org.apache.log4j.PatternLayout -log4j.appender.testlogger.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n diff --git a/runners/spark/4/README.md b/runners/spark/4/README.md new file mode 100644 index 000000000000..371a44512657 --- /dev/null +++ b/runners/spark/4/README.md @@ -0,0 +1,73 @@ + +# Apache Beam Spark 4 Runner + +Experimental Beam runner for Apache Spark 4 (batch-only). Built on the shared +`runners/spark` source base via `spark_runner.gradle`'s per-version +source-overrides mechanism: this module contributes the small set of files +under `src/main/java/.../structuredstreaming/` that diverge from the Spark 3 +implementation. See the parent `runners/spark/` module for the bulk of the +runner code. + +## Requirements + +* **Spark 4.0.2** (and other Spark 4.0.x patch releases) +* **Scala 2.13** +* **Java 17** — Spark 4 does not run on earlier JDKs + +## Status + +Batch only. Streaming is tracked in +[#36841](https://github.com/apache/beam/issues/36841). + +## Known issues + +### `StackOverflowError` from `slf4j-jdk14` on the runtime classpath + +Spark 4 ships `org.slf4j:jul-to-slf4j` to route `java.util.logging` records +into SLF4J. If `org.slf4j:slf4j-jdk14` is also resolved at runtime — it routes +the other direction (SLF4J → JUL) — the first log line creates an infinite +loop: + +``` +java.lang.StackOverflowError + at org.slf4j.bridge.SLF4JBridgeHandler.publish(...) + at java.util.logging.Logger.log(...) + at org.slf4j.impl.JDK14LoggerAdapter.log(...) + at org.slf4j.bridge.SLF4JBridgeHandler.publish(...) + ... +``` + +This is the same condition that broke the Spark 3 runner in +[#26985](https://github.com/apache/beam/issues/26985), fixed in +[#27001](https://github.com/apache/beam/pull/27001). + +The shared `spark_runner.gradle` already excludes `slf4j-jdk14` from the +runner module's own `configurations.all`, so in-tree builds are unaffected. +Downstream Gradle consumers that assemble a runtime classpath against +`beam-runners-spark-4` should mirror that exclude: + +```groovy +configurations.all { + exclude group: "org.slf4j", module: "slf4j-jdk14" +} +``` + +For Maven, exclude `org.slf4j:slf4j-jdk14` from any dependency that pulls it +transitively (commonly the Beam SDK harness and several IO connectors). diff --git a/runners/spark/4/build.gradle b/runners/spark/4/build.gradle new file mode 100644 index 000000000000..ec1af8df38a4 --- /dev/null +++ b/runners/spark/4/build.gradle @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def basePath = '..' +/* All properties required for loading the Spark build script */ +project.ext { + spark_major = '4' + // Spark 4 version as defined in BeamModulePlugin; requires Scala 2.13 and Java 17 + spark_version = spark4_version + spark_scala_version = '2.13' + archives_base_name = 'beam-runners-spark-4' +} + +// Load the main build script which contains all build logic. +// spark_runner.gradle handles the per-version source-overrides Copy: +// shared base (runners/spark/src/) + previous majors + this module's ./src/ are +// merged into build/source-overrides/src using DuplicatesStrategy.INCLUDE so the +// 11 files under runners/spark/4/src/.../structuredstreaming/ override the +// shared-base versions. +apply from: "$basePath/spark_runner.gradle" + +// Spark 4 always requires Java 17, so unconditionally add the --add-opens flags +// required by Kryo and other libraries that use reflection on JDK internals. +tasks.withType(Test).configureEach { + jvmArgs "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED" +} + +tasks.validatesStructuredStreamingRunnerBatch { + filter { + // TODO(https://github.com/apache/beam/issues/36841): currently failing with INTERNAL_ERROR] + // Couldn't find method fromByteArray with arguments + excludeTestsMatching 'org.apache.beam.sdk.transforms.CombineTest$BasicTests.testHotKeyCombining' + } +} + +tasks.validatesRunner.dependsOn(validatesStructuredStreamingRunnerBatch) + +// Exclude DStream-based streaming tests from the shared-base copy: the Spark 4 module +// supports only structured streaming (batch) and does not include legacy DStream support. +// Streaming test utilities also depend on kafka.server.KafkaServerStartable which was +// removed in Kafka 2.8.0 (the first Kafka version with a _2.13 artifact). +tasks.named("copyTestSourceOverrides") { + exclude "**/translation/streaming/**" +} + diff --git a/runners/flink/1.17/job-server/build.gradle b/runners/spark/4/job-server/build.gradle similarity index 81% rename from runners/flink/1.17/job-server/build.gradle rename to runners/spark/4/job-server/build.gradle index 89915349ae9a..598cf3b4913a 100644 --- a/runners/flink/1.17/job-server/build.gradle +++ b/runners/spark/4/job-server/build.gradle @@ -4,13 +4,13 @@ * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance + * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-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. @@ -24,8 +24,8 @@ 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-spark-4-job-server' } // Load the main build script which contains all build logic. -apply from: "$basePath/flink_job_server.gradle" +apply from: "$basePath/spark_job_server.gradle" diff --git a/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java new file mode 100644 index 000000000000..d32dc14eccc0 --- /dev/null +++ b/runners/spark/4/src/main/java/org/apache/beam/runners/spark/structuredstreaming/io/BoundedDatasetFactory.java @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.io; + +import static java.util.stream.Collectors.toList; +import static org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.emptyList; +import static org.apache.beam.sdk.values.WindowedValues.timestampedValueInGlobalWindow; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; +import static scala.collection.JavaConverters.asScalaIterator; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Serializable; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntSupplier; +import java.util.function.Supplier; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.beam.sdk.io.BoundedSource; +import org.apache.beam.sdk.io.BoundedSource.BoundedReader; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.AbstractIterator; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; +import org.apache.spark.InterruptibleIterator; +import org.apache.spark.Partition; +import org.apache.spark.SparkContext; +import org.apache.spark.TaskContext; +import org.apache.spark.rdd.RDD; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Encoder; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.catalyst.InternalRow; +import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder; +import org.apache.spark.sql.catalyst.encoders.ExpressionEncoder.Serializer; +import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan; +import org.apache.spark.sql.classic.Dataset$; +import org.apache.spark.sql.connector.catalog.SupportsRead; +import org.apache.spark.sql.connector.catalog.Table; +import org.apache.spark.sql.connector.catalog.TableCapability; +import org.apache.spark.sql.connector.read.Batch; +import org.apache.spark.sql.connector.read.InputPartition; +import org.apache.spark.sql.connector.read.PartitionReader; +import org.apache.spark.sql.connector.read.PartitionReaderFactory; +import org.apache.spark.sql.connector.read.Scan; +import org.apache.spark.sql.connector.read.ScanBuilder; +import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation; +import org.apache.spark.sql.types.StructType; +import org.apache.spark.sql.util.CaseInsensitiveStringMap; +import scala.Option; +import scala.collection.Iterator; +import scala.reflect.ClassTag; + +public class BoundedDatasetFactory { + private BoundedDatasetFactory() {} + + /** + * Create a {@link Dataset} for a {@link BoundedSource} via a Spark {@link Table}. + * + *

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

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

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

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

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

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

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

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

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

    Note: Using {@code collect_list} isn't any worse than using {@link ReduceFnRunner}. In the diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java index ceafc1642baa..793f1cb3e1f6 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderFactory.java @@ -31,15 +31,32 @@ import scala.reflect.ClassTag; public class EncoderFactory { - // default constructor to reflectively create static invoke expressions + // Resolve the Scala case-class primary constructor (the one with the most parameters). + // Constructor ordering returned by Class.getConstructors() is JVM-defined and not stable + // across Spark versions, so we pick the widest constructor explicitly and then dispatch on + // parameter count below to pick the right argument shape per Spark version. private static final Constructor STATIC_INVOKE_CONSTRUCTOR = - (Constructor) StaticInvoke.class.getConstructors()[0]; + primaryConstructor(StaticInvoke.class); - private static final Constructor INVOKE_CONSTRUCTOR = - (Constructor) Invoke.class.getConstructors()[0]; + private static final Constructor INVOKE_CONSTRUCTOR = primaryConstructor(Invoke.class); private static final Constructor NEW_INSTANCE_CONSTRUCTOR = - (Constructor) NewInstance.class.getConstructors()[0]; + primaryConstructor(NewInstance.class); + + @SuppressWarnings("unchecked") + private static Constructor primaryConstructor(Class cls) { + Constructor[] ctors = cls.getConstructors(); + if (ctors.length == 0) { + throw new IllegalStateException("No public constructors found for " + cls.getName()); + } + Constructor widest = ctors[0]; + for (int i = 1; i < ctors.length; i++) { + if (ctors[i].getParameterCount() > widest.getParameterCount()) { + widest = ctors[i]; + } + } + return (Constructor) widest; + } static ExpressionEncoder create( Expression serializer, Expression deserializer, Class clazz) { diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java index daf8451faac5..29f01c84c02e 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/helpers/EncoderHelpers.java @@ -48,7 +48,6 @@ import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; import org.apache.spark.sql.Encoder; import org.apache.spark.sql.Encoders; @@ -98,7 +97,7 @@ public class EncoderHelpers { private static final DataType LIST_TYPE = new ObjectType(List.class); // Collections / maps of these types can be (de)serialized without (de)serializing each member - private static final Set> PRIMITIV_TYPES = + private static final Set> PRIMITIVE_TYPES = ImmutableSet.of( Boolean.class, Byte.class, @@ -154,7 +153,7 @@ public class EncoderHelpers { public static Encoder encoderOf(Class cls) { Encoder enc = getOrCreateDefaultEncoder(cls); if (enc == null) { - throw new IllegalArgumentException("No default coder available for class " + cls); + throw new IllegalArgumentException("No default encoder available for class " + cls); } return enc; } @@ -481,7 +480,7 @@ private static Expression deserializeSeq( } private static boolean isPrimitiveEnc(Encoder enc) { - return PRIMITIV_TYPES.contains(enc.clsTag().runtimeClass()); + return PRIMITIVE_TYPES.contains(enc.clsTag().runtimeClass()); } private static Expression serialize(Expression input, Encoder enc) { @@ -548,9 +547,17 @@ public static byte[] paneInfoToBytes(PaneInfo paneInfo) { return CoderHelpers.toByteArray(paneInfo, PaneInfoCoder.of()); } - /** The end of the only window (max timestamp). */ - public static Instant maxTimestamp(Iterable windows) { - return Iterables.getOnlyElement(windows).maxTimestamp(); + /** The maximum {@code maxTimestamp} across all associated windows. */ + public static Instant maxTimestamp(Iterable windows) { + Instant maxTimestamp = null; + for (BoundedWindow window : windows) { + Instant timestamp = window.maxTimestamp(); + if (maxTimestamp == null || timestamp.isAfter(maxTimestamp)) { + maxTimestamp = timestamp; + } + } + return Preconditions.checkNotNull( + maxTimestamp, "WindowedValue must have at least one window"); } public static List copyToList(ArrayData arrayData, DataType type) { diff --git a/runners/spark/src/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/SparkStreamingPortablePipelineTranslator.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java index 4850f886241b..9975c81b56a4 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/SparkStreamingPortablePipelineTranslator.java @@ -330,7 +330,8 @@ private static void translateFlatten( } } // Unify streams into a single stream. - unifiedStreams = context.getStreamingContext().union(JavaConverters.asScalaBuffer(dStreams)); + unifiedStreams = + context.getStreamingContext().union(JavaConverters.asScalaBuffer(dStreams).toList()); } context.pushDataset( diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/ParDoStateUpdateFn.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/ParDoStateUpdateFn.java index 909624c23239..ed9299db4ee7 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/ParDoStateUpdateFn.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/ParDoStateUpdateFn.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -62,7 +63,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.sparkproject.guava.collect.Iterators; import scala.Option; import scala.Tuple2; import scala.runtime.AbstractFunction3; @@ -236,7 +236,7 @@ public TimerInternals timerInternals() { final byte[] byteValue = serializedValue.get(); @Nullable WindowedValue windowedValue; @Nullable WindowedValue> keyedWindowedValue; - Iterator>> iterator = Iterators.emptyIterator(); + Iterator>> iterator = Collections.emptyIterator(); if (byteValue.length > 0) { windowedValue = CoderHelpers.fromByteArray(byteValue, this.wvCoder); keyedWindowedValue = windowedValue.withValue(KV.of(key, windowedValue.getValue())); diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/StreamingTransformTranslator.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/StreamingTransformTranslator.java index 48697a3dbafc..4a96edceba31 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/StreamingTransformTranslator.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/translation/streaming/StreamingTransformTranslator.java @@ -306,7 +306,7 @@ public void evaluate(Flatten.PCollections transform, EvaluationContext contex } // start by unifying streams into a single stream. JavaDStream> unifiedStreams = - context.getStreamingContext().union(JavaConverters.asScalaBuffer(dStreams)); + context.getStreamingContext().union(JavaConverters.asScalaBuffer(dStreams).toList()); context.putDataset(transform, new UnboundedDataset<>(unifiedStreams, streamingSources)); } diff --git a/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java b/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java index 4bc01bd205a9..2111867d3851 100644 --- a/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java +++ b/runners/spark/src/main/java/org/apache/beam/runners/spark/util/TimerUtils.java @@ -17,6 +17,7 @@ */ package org.apache.beam.runners.spark.util; +import io.opentelemetry.context.Context; import java.io.Serializable; import java.util.Collection; import java.util.Collections; @@ -34,6 +35,7 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.values.CausedByDrain; import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowingStrategy; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; @@ -116,11 +118,21 @@ public PaneInfo getPaneInfo() { return null; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return null; + } + @Override public CausedByDrain causedByDrain() { return CausedByDrain.NORMAL; } + @Override + public ValueKind getValueKind() { + return ValueKind.INSERT; + } + @Override public @Nullable Long getRecordOffset() { return null; diff --git a/runners/spark/src/test/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistratorTest.java b/runners/spark/src/test/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistratorTest.java index ddd0e74d1c9e..56eb0dfea5f2 100644 --- a/runners/spark/src/test/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistratorTest.java +++ b/runners/spark/src/test/java/org/apache/beam/runners/spark/coders/SparkRunnerKryoRegistratorTest.java @@ -17,7 +17,10 @@ */ package org.apache.beam.runners.spark.coders; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import com.esotericsoftware.kryo.Kryo; @@ -73,6 +76,52 @@ public void testDefaultSerializerNotCallingKryo() { } } + /** Unit tests for the {@link SparkRunnerKryoRegistrator#findFirstAvailableClass} helper. */ + public static class FindFirstAvailableClassTest { + + @Test + public void returnsFirstWhenAvailable() { + Class result = + SparkRunnerKryoRegistrator.findFirstAvailableClass( + "java.lang.String", "java.lang.Integer"); + assertSame(String.class, result); + } + + @Test + public void fallsBackWhenFirstMissing() { + Class result = + SparkRunnerKryoRegistrator.findFirstAvailableClass("does.not.Exist", "java.lang.Integer"); + assertSame(Integer.class, result); + } + + @Test + public void returnsNullWhenNoneAvailable() { + Class result = + SparkRunnerKryoRegistrator.findFirstAvailableClass("does.not.Exist1", "does.not.Exist2"); + assertNull(result); + } + + @Test + public void returnsNullForEmptyInput() { + assertNull(SparkRunnerKryoRegistrator.findFirstAvailableClass()); + } + + @Test + public void resolvesScalaWrappedArrayClassOnRealClasspath() { + // On any supported Scala version (2.12 ArraySeq$ofRef does not exist; 2.13 it does), at + // least one of the two wrapped-array class names must resolve. This is the production call + // the registrator makes. + Class result = + SparkRunnerKryoRegistrator.findFirstAvailableClass( + "scala.collection.mutable.ArraySeq$ofRef", + "scala.collection.mutable.WrappedArray$ofRef"); + assertEquals( + "expected one of the Scala wrapped-array classes to be on the classpath", + true, + result != null); + } + } + // Hide TestKryoRegistrator from the Enclosed JUnit runner interface Others { class TestKryoRegistrator extends SparkRunnerKryoRegistrator { diff --git a/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContextTest.java b/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContextTest.java new file mode 100644 index 000000000000..d1669e45c182 --- /dev/null +++ b/runners/spark/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/EvaluationContextTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +import org.apache.spark.sql.Dataset; +import org.junit.Test; + +/** + * Unit tests for the static error-path branches of {@link EvaluationContext}. The happy-path + * branches are covered end-to-end by the structured-streaming translation tests; these tests + * specifically exercise the {@code catch} blocks that wrap and rethrow underlying Spark failures. + */ +public class EvaluationContextTest { + + @Test + public void evaluateWrapsAndRethrowsRuntimeException() { + @SuppressWarnings("unchecked") + Dataset ds = mock(Dataset.class); + RuntimeException underlying = new RuntimeException("boom"); + doThrow(underlying).when(ds).write(); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> EvaluationContext.evaluate("test-ds", ds)); + assertSame(underlying, thrown.getCause()); + } + + @Test + public void evaluateHandlesNullExceptionMessage() { + // Reproduces the original NPE motivation for the String.valueOf wrap: a RuntimeException + // whose root cause carries a null message must not crash the error logger. + @SuppressWarnings("unchecked") + Dataset ds = mock(Dataset.class); + RuntimeException underlying = new RuntimeException((String) null); + doThrow(underlying).when(ds).write(); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> EvaluationContext.evaluate("test-ds", ds)); + assertSame(underlying, thrown.getCause()); + } + + @Test + public void collectWrapsAndRethrowsException() { + @SuppressWarnings("unchecked") + Dataset ds = mock(Dataset.class); + RuntimeException underlying = new RuntimeException("boom"); + doThrow(underlying).when(ds).collect(); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> EvaluationContext.collect("test-ds", ds)); + assertSame(underlying, thrown.getCause()); + } + + @Test + public void collectHandlesNullExceptionMessage() { + @SuppressWarnings("unchecked") + Dataset ds = mock(Dataset.class); + RuntimeException underlying = new RuntimeException((String) null); + doThrow(underlying).when(ds).collect(); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> EvaluationContext.collect("test-ds", ds)); + assertSame(underlying, thrown.getCause()); + } +} diff --git a/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/scripts/beam-sql.sh b/scripts/beam-sql.sh index 38907b0d2d26..906812eb1597 100755 --- a/scripts/beam-sql.sh +++ b/scripts/beam-sql.sh @@ -22,7 +22,7 @@ set -e # Exit immediately if a command exits with a non-zero status. # --- Configuration --- -DEFAULT_BEAM_VERSION="2.74.0" +DEFAULT_BEAM_VERSION="2.76.0" MAIN_CLASS="org.apache.beam.sdk.extensions.sql.jdbc.BeamSqlLine" # Directory to store cached executable JAR files CACHE_DIR="${HOME}/.beam/cache" @@ -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)" @@ -197,7 +232,6 @@ function list_runners() { echo " dataflow - DataflowRunner (runs on Google Cloud Dataflow)" echo " flink - FlinkRunner (runs on Apache Flink)" echo " spark - SparkRunner (runs on Apache Spark)" - echo " samza - SamzaRunner (runs on Apache Samza)" echo " jet - JetRunner (runs on Hazelcast Jet)" echo " twister2 - Twister2Runner (runs on Twister2)" echo "" @@ -209,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 @@ -218,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-*) @@ -239,10 +273,6 @@ function list_runners() { echo " spark-3 - SparkRunner (Spark 3.x)" echo " Runs on Apache Spark 3.x clusters." ;; - "samza") - echo " samza - SamzaRunner" - echo " Runs on Apache Samza." - ;; "jet") echo " jet - JetRunner" echo " Runs on Hazelcast Jet." @@ -278,7 +308,7 @@ function list_runners() { ;; esac done <<< "$runners" - + echo "" echo "💡 Usage: ./beam-sql.sh --runner " echo " Default: direct" @@ -296,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 ;; @@ -368,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 @@ -377,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} @@ -400,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 8989783e9d7c..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": "^7.0.11" + "nodemailer": "^9.0.1" } }, "node_modules/@octokit/auth-token": { @@ -207,9 +207,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "version": "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": "7.0.11", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==" + "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 5e365333f42d..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": "^7.0.11", + "nodemailer": "^9.0.1", "node-fetch": "^2.6.1" }, "type": "module" diff --git a/scripts/ci/pr-bot/package-lock.json b/scripts/ci/pr-bot/package-lock.json index d1b77b0a26da..7f32478d08cd 100644 --- a/scripts/ci/pr-bot/package-lock.json +++ b/scripts/ci/pr-bot/package-lock.json @@ -292,11 +292,10 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -689,13 +688,12 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -876,11 +874,10 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1050,9 +1047,9 @@ } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "engines": { "node": ">=8.6" @@ -1827,9 +1824,9 @@ "dev": true }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -2088,12 +2085,12 @@ }, "dependencies": { "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" } } } @@ -2222,9 +2219,9 @@ "dev": true }, "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -2339,9 +2336,9 @@ } }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "prettier": { diff --git a/scripts/ci/release/test/resources/jenkins_trigger_phrases.txt b/scripts/ci/release/test/resources/jenkins_trigger_phrases.txt deleted file mode 100644 index 4f59885de8b4..000000000000 --- a/scripts/ci/release/test/resources/jenkins_trigger_phrases.txt +++ /dev/null @@ -1,19 +0,0 @@ -Run Release Gradle Build -Run CommunityMetrics PreCommit -Run Direct ValidatesRunner Java 11 -Run Direct ValidatesRunner Java 17 -Run Java 11 Examples on Dataflow Runner V2 -Run Java 17 Examples on Dataflow Runner V2 -Run Java PreCommit -Run Java examples on Dataflow Java 11 -Run Java examples on Dataflow Java 17 -Run Javadoc PostCommit -Run Jpms Dataflow Java 17 PostCommit -Run Jpms Direct Java 17 PostCommit -Run Python Flink ValidatesRunner -Run Python PreCommit -Run Python Samza ValidatesRunner -Run Python Spark ValidatesRunner -Run SQL_Java17 PreCommit -Run XVR_Flink PostCommit -Run XVR_GoUsingJava_Dataflow PostCommit diff --git a/scripts/ci/release/test/resources/mass_comment.txt b/scripts/ci/release/test/resources/mass_comment.txt deleted file mode 100644 index 1f6f340eb0b7..000000000000 --- a/scripts/ci/release/test/resources/mass_comment.txt +++ /dev/null @@ -1,106 +0,0 @@ -Run Release Gradle Build -Run CommunityMetrics PreCommit -Run Dataflow Runner Nexmark Tests -Run Dataflow Runner V2 Java 11 Nexmark Tests -Run Dataflow Runner V2 Java 17 Nexmark Tests -Run Dataflow Runner V2 Nexmark Tests -Run Dataflow Streaming ValidatesRunner -Run Dataflow ValidatesRunner Java 11 -Run Dataflow ValidatesRunner Java 17 -Run Dataflow ValidatesRunner -Run Direct Runner Nexmark Tests -Run Direct ValidatesRunner Java 11 -Run Direct ValidatesRunner Java 17 -Run Direct ValidatesRunner in Java 11 -Run Direct ValidatesRunner -Run Flink Runner Nexmark Tests -Run Flink ValidatesRunner Java 11 -Run Flink ValidatesRunner -Run Go Flink ValidatesRunner -Run Go PostCommit -Run Go PreCommit -Run Go Samza ValidatesRunner -Run Go Spark ValidatesRunner -Run GoPortable PreCommit -Run Java 11 Examples on Dataflow Runner V2 -Run Java 17 Examples on Dataflow Runner V2 -Run Java Dataflow V2 ValidatesRunner Streaming -Run Java Dataflow V2 ValidatesRunner -Run Java Examples on Dataflow Runner V2 -Run Java Examples_Direct -Run Java Examples_Flink -Run Java Examples_Spark -Run Java Flink PortableValidatesRunner Streaming -Run Java Portability examples on Dataflow with Java 11 -Run Java PostCommit -Run Java PreCommit -Run Java Samza PortableValidatesRunner -Run Java Spark PortableValidatesRunner Batch -Run Java Spark v2 PortableValidatesRunner Streaming -Run Java Spark v3 PortableValidatesRunner Streaming -Run Java examples on Dataflow Java 11 -Run Java examples on Dataflow Java 17 -Run Java examples on Dataflow with Java 11 -Run Java_Examples_Dataflow PreCommit -Run Java_Examples_Dataflow_Java11 PreCommit -Run Java_Examples_Dataflow_Java17 PreCommit -Run Java_PVR_Flink_Batch PreCommit -Run Java_PVR_Flink_Docker PreCommit -Run Javadoc PostCommit -Run Jpms Dataflow Java 11 PostCommit -Run Jpms Dataflow Java 17 PostCommit -Run Jpms Direct Java 11 PostCommit -Run Jpms Direct Java 17 PostCommit -Run Jpms Flink Java 11 PostCommit -Run Jpms Spark Java 11 PostCommit -Run PortableJar_Flink PostCommit -Run PortableJar_Spark PostCommit -Run Portable_Python PreCommit -Run PostCommit_Java_Dataflow -Run PostCommit_Java_DataflowV2 -Run PostCommit_Java_Hadoop_Versions -Run Python 3.8 PostCommit -Run Python 3.9 PostCommit -Run Python 3.10 PostCommit -Run Python 3.11 PostCommit -Run Python Dataflow V2 ValidatesRunner -Run Python Dataflow ValidatesContainer -Run Python Dataflow ValidatesRunner -Run Python Examples_Dataflow -Run Python Examples_Direct -Run Python Examples_Flink -Run Python Examples_Spark -Run Python Flink ValidatesRunner -Run Python PreCommit -Run Python Samza ValidatesRunner -Run Python Spark ValidatesRunner -Run PythonDocker PreCommit -Run PythonDocs PreCommit -Run PythonFormatter PreCommit -Run PythonLint PreCommit -Run Python_PVR_Flink PreCommit -Run Python_Xlang_Gcp_Direct PostCommit -Run RAT PreCommit -Run SQL PostCommit -Run SQL PreCommit -Run SQL_Java11 PreCommit -Run SQL_Java17 PreCommit -Run Samza ValidatesRunner -Run Spark Runner Nexmark Tests -Run Spark StructuredStreaming ValidatesRunner -Run Spark ValidatesRunner Java 11 -Run Spark ValidatesRunner -Run Spotless PreCommit -Run Twister2 ValidatesRunner -Run Typescript PreCommit -Run ULR Loopback ValidatesRunner -Run Whitespace PreCommit -Run Xlang_Generated_Transforms PreCommit -Run XVR_Direct PostCommit -Run XVR_Flink PostCommit -Run XVR_JavaUsingPython_Dataflow PostCommit -Run XVR_PythonUsingJavaSQL_Dataflow PostCommit -Run XVR_PythonUsingJava_Dataflow PostCommit -Run XVR_Samza PostCommit -Run XVR_Spark PostCommit -Run XVR_Spark3 PostCommit diff --git a/scripts/tools/bomupgrader.py b/scripts/tools/bomupgrader.py index bf6e7dfdd31f..4697c46a5568 100644 --- a/scripts/tools/bomupgrader.py +++ b/scripts/tools/bomupgrader.py @@ -27,7 +27,8 @@ 1. preprocessing BeamModulePlugin.groovy to decide the dependencies need to sync 2. generate an empty Maven project to fetch the exact target versions to change 3. Write back to BeamModulePlugin.groovy -4. Update libraries-bom version on sdks/java/container/license_scripts/dep_urls_java.yaml +4. Update libraries-bom and opentelemetry-bom entries on + sdks/java/container/license_scripts/dep_urls_java.yaml There are few reasons we need to declare the version numbers: 1. Sync the dependency that not included in GCP-BOM with those included with BOM @@ -254,17 +255,51 @@ def write_back(self): for line in self.target_lines: fout.write(line) + def _get_opentelemetry_version(self): + for line in self.target_lines: + match = self.VERSION_STRING.match(line) + if match and match.group(1) == 'opentelemetry': + return match.group(2) + logging.warning('opentelemetry_version not found in BeamModulePlugin') + return None + def write_license_script(self): logging.info("-----Update dep_urls_java.yaml-----") - with open(os.path.join(self.project_root, self.LICENSE_SC_PATH), - 'r') as fin: + license_path = os.path.join(self.project_root, self.LICENSE_SC_PATH) + with open(license_path, 'r') as fin: lines = fin.readlines() - with open(os.path.join(self.project_root, self.LICENSE_SC_PATH), - 'w') as fout: - for idx, line in enumerate(lines): - if line.strip() == 'libraries-bom:': - lines[idx + 1] = re.sub( - r'[\'"]\d[\.\d]+[\'"]', f"'{self.bom_version}'", lines[idx + 1]) + + otel_version = self._get_opentelemetry_version() + otel_license_url = ( + 'https://raw.githubusercontent.com/open-telemetry/' + 'opentelemetry-java/v{}/LICENSE'.format(otel_version) + if otel_version else None) + otel_license_line = ' license: "{}"\n'.format(otel_license_url) + + for idx, line in enumerate(lines): + stripped = line.strip() + if stripped == 'libraries-bom:': + lines[idx + 1] = re.sub( + r'[\'"]\d[\d\.]+[\'"]', "'{}'".format(self.bom_version), + lines[idx + 1]) + continue + if otel_version and stripped == 'opentelemetry-bom:': + lines[idx + 1] = re.sub( + r"'[\d\.]+'", "'{}'".format(otel_version), lines[idx + 1]) + lines[idx + 2] = otel_license_line + continue + if otel_version and stripped == 'opentelemetry-bom-alpha:': + lines[idx + 1] = re.sub( + r"'[\d\.]+-alpha'", "'{}-alpha'".format(otel_version), + lines[idx + 1]) + lines[idx + 2] = otel_license_line + + if otel_version: + logging.info( + 'Updated opentelemetry-bom license entries to %s', otel_version) + + with open(license_path, 'w') as fout: + for line in lines: fout.write(line) def run(self): diff --git a/sdks/go.mod b/sdks/go.mod index 0ebac1a6ca1c..272f91451db8 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.72.0 - cloud.google.com/go/bigtable v1.41.0 - cloud.google.com/go/datastore v1.21.0 - cloud.google.com/go/profiler v0.4.3 - cloud.google.com/go/pubsub v1.50.1 - cloud.google.com/go/spanner v1.87.0 - cloud.google.com/go/storage v1.59.2 - github.com/aws/aws-sdk-go-v2 v1.41.1 - github.com/aws/aws-sdk-go-v2/config v1.32.7 - github.com/aws/aws-sdk-go-v2/credentials v1.19.7 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 - github.com/aws/smithy-go v1.24.2 - github.com/docker/go-connections v0.6.0 + cloud.google.com/go/bigquery v1.77.0 + cloud.google.com/go/bigtable v1.50.0 + cloud.google.com/go/datastore v1.24.0 + cloud.google.com/go/profiler v0.6.0 + cloud.google.com/go/pubsub v1.50.2 + cloud.google.com/go/spanner v1.92.0 + cloud.google.com/go/storage v1.62.3 + github.com/aws/aws-sdk-go-v2 v1.42.0 + github.com/aws/aws-sdk-go-v2/config v1.32.25 + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28 + github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 + github.com/aws/smithy-go v1.27.2 + github.com/docker/go-connections v0.7.0 // indirect github.com/dustin/go-humanize v1.0.1 - github.com/go-sql-driver/mysql v1.9.3 + github.com/go-sql-driver/mysql v1.10.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/johannesboyne/gofakes3 v0.0.0-20250106100439-5c39aecd6999 - github.com/lib/pq v1.11.1 + github.com/lib/pq v1.12.3 github.com/linkedin/goavro/v2 v2.15.0 - github.com/nats-io/nats-server/v2 v2.12.6 - github.com/nats-io/nats.go v1.49.0 + github.com/nats-io/nats-server/v2 v2.14.2 + github.com/nats-io/nats.go v1.52.0 github.com/proullon/ramsql v0.1.4 github.com/spf13/cobra v1.10.2 - github.com/testcontainers/testcontainers-go v0.40.0 - github.com/tetratelabs/wazero v1.11.0 + github.com/testcontainers/testcontainers-go v0.42.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.52.0 - golang.org/x/oauth2 v0.35.0 - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.42.0 - golang.org/x/text v0.35.0 - google.golang.org/api v0.257.0 - google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 - google.golang.org/grpc v1.80.0 + golang.org/x/net v0.56.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.21.0 + golang.org/x/sys v0.46.0 + golang.org/x/text v0.38.0 + google.golang.org/api v0.286.0 + google.golang.org/genproto v0.0.0-20260523011958-0a33c5d7ca68 + google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -71,141 +71,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/golang-cz/devslog v0.0.17 + github.com/moby/moby/api v1.55.0 + github.com/moby/moby/client v0.5.0 + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a ) require ( - cel.dev/expr v0.25.1 // indirect - cloud.google.com/go/auth v0.17.0 // indirect + cel.dev/expr v0.25.2 // indirect + cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/monitoring v1.24.3 // indirect - cloud.google.com/go/pubsub/v2 v2.0.0 // indirect + cloud.google.com/go/monitoring v1.29.0 // indirect + cloud.google.com/go/pubsub/v2 v2.6.0 // indirect dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.1.1 // indirect - github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect - github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // 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.32.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect + github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/ebitengine/purego v0.8.4 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect 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/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // 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.1.0 // indirect + github.com/moby/go-archive v0.2.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect - github.com/nats-io/jwt/v2 v2.8.1 // indirect - github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/jwt/v2 v2.8.2 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/shirou/gopsutil/v4 v4.26.4 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/tklauser/go-sysconf v0.3.14 // indirect - github.com/tklauser/numcpus v0.9.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.73.0 // indirect + go.einride.tech/aip v0.83.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/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.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.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-20260209163413-e7419c687ee4 // indirect + golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 // indirect golang.org/x/time v0.15.0 // indirect ) require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/longrunning v0.7.0 // indirect + cloud.google.com/go/iam v1.11.0 // indirect + cloud.google.com/go/longrunning v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect - github.com/apache/thrift v0.21.0 // indirect + github.com/apache/thrift v0.23.0 // indirect github.com/aws/aws-sdk-go v1.55.5 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/docker/docker v28.5.2+incompatible // but required to resolve issue docker has with go1.20 github.com/docker/go-units v0.5.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/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-20250602020802-c6617b811d0e // 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.7 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.18.4 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/magiconair/properties v1.8.10 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect - github.com/morikuni/aec v1.0.0 // indirect + github.com/montanaflynn/stats v0.9.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pkg/xattr v0.4.10 // indirect github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/sirupsen/logrus v1.9.4 // 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.49.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/tools v0.45.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect ) diff --git a/sdks/go.sum b/sdks/go.sum index be5abe6383d9..a8692d22a9e9 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -1,11 +1,10 @@ -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= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -19,7 +18,6 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -34,628 +32,88 @@ cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Ud cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= -cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= -cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= -cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= -cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= -cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= -cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= -cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= -cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= -cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= -cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= -cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= -cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= -cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= -cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= -cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= -cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= -cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= -cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= -cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= -cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= -cloud.google.com/go/bigquery v1.72.0 h1:D/yLju+3Ens2IXx7ou1DJ62juBm+/coBInn4VVOg5Cw= -cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ= -cloud.google.com/go/bigtable v1.41.0 h1:99KOWShm/MUyuIbXBeVscdWJFV7GdgiYwFUrB5Iu4BI= -cloud.google.com/go/bigtable v1.41.0/go.mod h1:JlaltP06LEFXaxQdZiarGR9tKsX/II0IkNAKMDrWspI= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= -cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= -cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= -cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= -cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= -cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= -cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/bigquery v1.77.0 h1:L5AW3jhzEKpFVg4i0mVHxKpxogrqT7dczWBSr4m9MKU= +cloud.google.com/go/bigquery v1.77.0/go.mod h1:J4wuqka/1hEpdJxH2oBrUR0vjTD+r7drGkpcA3yqERM= +cloud.google.com/go/bigtable v1.50.0 h1:lihc3U/eVrlIjK55i93K8sol+pKtFozIJ9vooEuNd4I= +cloud.google.com/go/bigtable v1.50.0/go.mod h1:RTannV5mvoJM8KscLTfRYMPo84u9/j+C3PSyYJGf5Ic= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= -cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= -cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= -cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= -cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= -cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= -cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k= -cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= -cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= -cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= -cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/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.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/datastore v1.21.0 h1:dUrYq47ysCA4nM7u8kRT0WnbfXc6TzX49cP3TCwIiA0= -cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= -cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= -cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= -cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= -cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= -cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= -cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= -cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= -cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/datastore v1.24.0 h1:auNUPJTT9gFcHNj2iKOEeE23nrjf7dE7VA6TO3jw8h0= +cloud.google.com/go/datastore v1.24.0/go.mod h1:cEkLhU6Ti/gauQ7DFrUrG8bQjiMIxi++b5ePiThi5So= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= -cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= -cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= -cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= -cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= -cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= -cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/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.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= -cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= -cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw= -cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= -cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= -cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE= +cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= +cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k= +cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= +cloud.google.com/go/longrunning v1.0.0 h1:lwzWEYD8+NkYV7dhexOz6kmlvajZA70+bW/xMhRVVdY= +cloud.google.com/go/longrunning v1.0.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= -cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= -cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= -cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= -cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= -cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= -cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= -cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= -cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= -cloud.google.com/go/profiler v0.4.3 h1:IY3QNKlr8VbXwGWHcZbJQsMA/83ZTH6uAHf8jYyj7OI= -cloud.google.com/go/profiler v0.4.3/go.mod h1:3xFodugWfPIQZWFcXdUmfa+yTiiyQ8fWrdT+d2Sg4J0= +cloud.google.com/go/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= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= -cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= -cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM= -cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= -cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0= -cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= -cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= -cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= -cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= -cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= -cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/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.6.0 h1:8pjR0id+GTB+krKx5G6AGJoYrHog58w2Q89PCOrfM64= +cloud.google.com/go/pubsub/v2 v2.6.0/go.mod h1:4anqvV/w8Pcgu2tO0qr2XgsF3GXHowzryfQ5gOnVmWY= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= -cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= -cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= -cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= -cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= -cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= -cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= -cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= -cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/spanner v1.87.0 h1:M9RGcj/4gJk6yY1lRLOz1Ze+5ufoWhbIiurzXLOOfcw= -cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= -cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= -cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/spanner v1.92.0 h1:cfeMNmtFjz+OYzQVCIuGBw4Cik4CbF2ptXMuRQcUar0= +cloud.google.com/go/spanner v1.92.0/go.mod h1:rCDPfWXNX0h+t484r+crCEaaMKbJfoWkHRDKU3H3+oY= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storage v1.59.2 h1:gmOAuG1opU8YvycMNpP+DvHfT9BfzzK5Cy+arP+Nocw= -cloud.google.com/go/storage v1.59.2/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= -cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/storage v1.62.3 h1:SZq1t23NCI+e96dH77Dg3PEfsNNEjqO8zE5AnD8gVD0= +cloud.google.com/go/storage v1.62.3/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= -cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= -cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= -cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= -cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= -cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= -cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= -cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +cloud.google.com/go/trace v1.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= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= @@ -703,41 +161,33 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDm github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +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.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.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/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= -github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/apache/arrow/go/arrow v0.0.0-20200730104253-651201b0f516/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= -github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= -github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/apache/thrift v0.0.0-20181112125854-24918abba929/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.14.2/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= -github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= -github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/apache/thrift v0.23.0 h1:wKR6YnefQSEnxpEfmgTPuJibNG4bF0p2TK34tHLWi3s= +github.com/apache/thrift v0.23.0/go.mod h1:zPt6WxgvTOM6hF92y8C+MkEM5LMxZuk4JcQOiU4Esvs= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= @@ -749,99 +199,92 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= -github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= github.com/aws/aws-sdk-go-v2/config v1.25.3/go.mod h1:tAByZy03nH5jcq0vZmkcVoo6tRzRHEwSFx3QW4NmDw8= -github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= -github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.16.2/go.mod h1:sDdvGhXrSVT5yzBDR7qXz+rhbpiMpUYfF3vJ01QSdrc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.4/go.mod h1:t4i+yGHMCcUNIX1x7YVYa6bH/Do7civ5I6cG/6PMfyA= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.0/go.mod h1:UcgIwJ9KHquYxs6Q5skC9qXjhYMK+JASDYcXQ4X7JZE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 h1:1hWFp+52Vq8Fevy/KUhbW/1MEApMz7uitCF/PQXRJpk= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1/go.mod h1:sIec8j802/rCkCKgZV678HFR0s7lhQUYXT77tIvlaa4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28 h1:ez4y5o7sa0uaRI8BquYOXtZpioUPhbQEh7Igm88oV9U= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28/go.mod h1:TpmZOrQA12XKEpVypgBGZSQBsm1WUTndCiSnbDsbvug= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.3/go.mod h1:5yzAuE9i2RkVAttBl8yxZgQr5OCq4D5yDnG7j9x2L0U= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.3/go.mod h1:R+/S1O4TYpcktbVwddeOYg+uwUfLhADP2S/x4QwsCTM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.3/go.mod h1:Owv1I59vaghv1Ax8zz8ELY8DN7/Y0rGS+WWAmjgi950= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.3/go.mod h1:KZgs2ny8HsxRIRbDwgvJcHHBZPOzQr/+NtGwnP+w2ec= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= github.com/aws/aws-sdk-go-v2/service/s3 v1.43.0/go.mod h1:NXRKkiRF+erX2hnybnVU660cYT5/KChRD4iUgJ97cI8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 h1:ta8csKy5vN91F3i5gGR85lFV0srBqySEji7Jroes6rE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= github.com/aws/aws-sdk-go-v2/service/sso v1.17.2/go.mod h1:/pE21vno3q1h4bbhUOEi+6Zu/aT26UK2WKkDXd+TssQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.0/go.mod h1:dWqm5G767qwKPuayKfzm4rjzFmVjiBFbOJrpSPnAMDs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= github.com/aws/aws-sdk-go-v2/service/sts v1.25.3/go.mod h1:4EqRHDCKP78hq3zOnmFXu5k0j4bXbRFfCh/zQ6KnEfQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= +github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc= @@ -854,17 +297,12 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= -github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/colinmarc/hdfs/v2 v2.1.1/go.mod h1:M3x+k8UKKmxtFu++uAZ0OtDU8jR3jnaZIAc6yK4Ue0c= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -882,9 +320,8 @@ github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GK github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -897,18 +334,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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -917,21 +351,15 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= -github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= -github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= -github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= -github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= @@ -948,7 +376,6 @@ github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -962,7 +389,6 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -972,8 +398,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -981,19 +405,18 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/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= @@ -1004,8 +427,6 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1041,23 +462,21 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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 v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= -github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/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= @@ -1072,8 +491,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= @@ -1100,15 +517,14 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e h1:FJta/0WsADCe1r9vQjdHbd3KuiLPu7Y9WlyLGwMUNyE= -github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/pprof v0.0.0-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= @@ -1122,45 +538,26 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw= +github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 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.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= 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= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -1227,28 +624,23 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -1264,15 +656,12 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI= -github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/linkedin/goavro/v2 v2.15.0 h1:pDj1UrjUOO62iXhgBiE7jQkpNIc5/tA5eZsgolMjgVI= github.com/linkedin/goavro/v2 v2.15.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= -github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= -github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/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= @@ -1281,14 +670,10 @@ github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= -github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= -github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.34/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= @@ -1300,12 +685,14 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -1321,18 +708,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.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8= -github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo= -github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= -github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= -github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= -github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +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.2 h1:Q7dRhCY03Y00rETFW3KV+KGaCIajlDfWgWUVgbMxyuk= +github.com/nats-io/nats-server/v2 v2.14.2/go.mod h1:lWpb1bSpRELZfRdlMkdz8E7lbXKKyNe8RIn0vvepIHs= +github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= +github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nkeys v0.4.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= @@ -1345,20 +730,14 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1Gsh github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -1369,15 +748,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/proullon/ramsql v0.1.4 h1:yTFRTn46gFH/kPbzCx+mGjuFlyTBUeDr3h2ldwxddl0= github.com/proullon/ramsql v0.1.4/go.mod h1:CFGqeQHQpdRfWqYmWD3yXqPTEaHkF4zgXy1C6qDWc9E= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -1388,29 +762,26 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY= +github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1418,8 +789,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -1431,25 +802,24 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= -github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= -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/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.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= -github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= -github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= -github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/tklauser/go-sysconf v0.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= @@ -1466,17 +836,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps= -go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ= +go.einride.tech/aip v0.83.0 h1:TI21IdeOnLTwZEJ3BxtImIZk6bsN2Q+sd0x99SLiQ+M= +go.einride.tech/aip v0.83.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= @@ -1492,33 +861,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.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= -go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/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.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= -go.opentelemetry.io/otel/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.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/metric v1.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 v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8= 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= @@ -1553,19 +916,17 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1580,9 +941,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-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/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= @@ -1592,10 +952,6 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1620,14 +976,11 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1662,7 +1015,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= @@ -1670,34 +1022,22 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1718,19 +1058,8 @@ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1742,13 +1071,10 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1793,14 +1119,12 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1810,12 +1134,10 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1828,45 +1150,30 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= +golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1878,21 +1185,16 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1950,47 +1252,36 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -2035,30 +1326,8 @@ google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7 google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= -google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= -google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= -google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= +google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI= +google.golang.org/api v0.286.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2104,13 +1373,10 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= @@ -2154,75 +1420,13 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto 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-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -2250,23 +1454,10 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2282,9 +1473,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -2323,42 +1511,9 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= -modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= -modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/sdks/go/container/Dockerfile b/sdks/go/container/Dockerfile index b3b2fbbec3a9..3d70e2834b70 100644 --- a/sdks/go/container/Dockerfile +++ b/sdks/go/container/Dockerfile @@ -16,7 +16,12 @@ # limitations under the License. ############################################################################### -FROM gcr.io/distroless/base-nossl-debian12:latest +FROM gcr.io/distroless/base-nossl-debian12:latest AS base_image + +# Distroless base image has many layers. Squash into single layer +FROM scratch +COPY --from=base_image / / + LABEL Author "Apache Beam " ARG TARGETOS @@ -24,8 +29,7 @@ ARG TARGETARCH ADD target/${TARGETOS}_${TARGETARCH}/boot /opt/apache/beam/ -COPY target/LICENSE /opt/apache/beam/ -COPY target/NOTICE /opt/apache/beam/ +COPY target/LICENSE target/NOTICE /opt/apache/beam/ # Add Go licenses. COPY target/go-licenses/* /opt/apache/beam/third_party_licenses/golang/ diff --git a/sdks/go/container/boot.go b/sdks/go/container/boot.go index b75201520f39..469285821f7e 100644 --- a/sdks/go/container/boot.go +++ b/sdks/go/container/boot.go @@ -83,13 +83,13 @@ func configureGoogleCloudProfilerEnvVars(ctx context.Context, logger *tools.Logg } // Fallback to job_name from metadata - if profilerServiceName == "" { - if jobName, jobNameExists := metadata["job_name"]; jobNameExists { - profilerServiceName = jobName - } else { - return errors.New("required job_name missing from metadata, profiling will not be enabled without it") - } - } + if profilerServiceName == "" { + if jobName, jobNameExists := metadata["job_name"]; jobNameExists { + profilerServiceName = jobName + } else { + return errors.New("required job_name missing from metadata, profiling will not be enabled without it") + } + } jobID, idExists := metadata["job_id"] if !idExists { @@ -149,6 +149,7 @@ func main() { log.Fatalf("Endpoint not set: %v", err) } logger := &tools.Logger{Endpoint: *loggingEndpoint} + log.SetOutput(tools.NewBufferedLoggerWithFlushInterval(ctx, logger, 0)) logger.Printf(ctx, "Initializing Go harness: %v", strings.Join(os.Args, " ")) // (1) Obtain the pipeline options @@ -158,6 +159,9 @@ func main() { logger.Fatalf(ctx, "Failed to convert pipeline options: %v", err) } + // Inject artifact validation enabled state into context + ctx = artifact.WithArtifactValidation(ctx, !artifact.HasExperiment(info.GetPipelineOptions(), "disable_staged_file_integrity_checks")) + // (2) Retrieve the staged files. // // The Go SDK harness downloads the worker binary and invokes diff --git a/sdks/go/container/boot_test.go b/sdks/go/container/boot_test.go index 244f91fe42e7..bb94aca36be3 100644 --- a/sdks/go/container/boot_test.go +++ b/sdks/go/container/boot_test.go @@ -267,19 +267,19 @@ func TestConfigureGoogleCloudProfilerEnvVars(t *testing.T) { expectingError: true, }, { - name: "Missing profiler name and job_name", - options: `{ + name: "Missing profiler name and job_name", + options: `{ "beam:option:go_options:v1": { "options": { "dataflow_service_options": "enable_google_cloud_profiler" } } }`, - metadata: map[string]string{ - "job_id": "job-789", - }, - expectingError: true, - }, + metadata: map[string]string{ + "job_id": "job-789", + }, + expectingError: true, + }, } for _, tt := range tests { diff --git a/sdks/go/examples/large_wordcount/large_wordcount.go b/sdks/go/examples/large_wordcount/large_wordcount.go index eb9cf3010e75..63e6b3b88efb 100644 --- a/sdks/go/examples/large_wordcount/large_wordcount.go +++ b/sdks/go/examples/large_wordcount/large_wordcount.go @@ -74,7 +74,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dot" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/prism" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/universal" ) diff --git a/sdks/go/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/materialize.go b/sdks/go/pkg/beam/artifact/materialize.go index 624e30efcd2b..db624f3776af 100644 --- a/sdks/go/pkg/beam/artifact/materialize.go +++ b/sdks/go/pkg/beam/artifact/materialize.go @@ -51,6 +51,23 @@ const ( NoArtifactsStaged = "__no_artifacts_staged__" ) +type validationKey string + +const artifactValidationKey validationKey = "artifact_validation_enabled" + +// WithArtifactValidation returns a new context carrying the artifact validation enabled state. +func WithArtifactValidation(ctx context.Context, enabled bool) context.Context { + return context.WithValue(ctx, artifactValidationKey, enabled) +} + +// isArtifactValidationEnabled parses pipeline options to check if "disable_integrity_checks" is enabled. +func isArtifactValidationEnabled(ctx context.Context) bool { + if val, ok := ctx.Value(artifactValidationKey).(bool); ok { + return val + } + return true +} + // Materialize is a convenience helper for ensuring that all artifacts are // present and uncorrupted. It interprets each artifact name as a relative // path under the dest directory. It does not retrieve valid artifacts already @@ -131,6 +148,7 @@ func newMaterializeWithClient(ctx context.Context, client jobpb.ArtifactRetrieva RoleUrn: URNStagingTo, RolePayload: rolePayload, }, + expectedSha256: filePayload.Sha256, }) } @@ -183,8 +201,9 @@ func MustExtractFilePayload(artifact *pipepb.ArtifactInformation) (string, strin } type artifact struct { - client jobpb.ArtifactRetrievalServiceClient - dep *pipepb.ArtifactInformation + client jobpb.ArtifactRetrievalServiceClient + dep *pipepb.ArtifactInformation + expectedSha256 string } func (a artifact) retrieve(ctx context.Context, dest string) error { @@ -231,7 +250,19 @@ func (a artifact) retrieve(ctx context.Context, dest string) error { stat, _ := fd.Stat() log.Printf("Downloaded: %v (sha256: %v, size: %v)", filename, sha256Hash, stat.Size()) - return fd.Close() + if err := fd.Close(); err != nil { + return err + } + + if isArtifactValidationEnabled(ctx) { + if a.expectedSha256 == "" { + log.Printf("WARN: Artifact validation skipped for file: %v", filename) + } else if sha256Hash != a.expectedSha256 { + return errors.Errorf("bad SHA256 for %v: %v, want %v", filename, sha256Hash, a.expectedSha256) + } + } + + return nil } func writeChunks(stream jobpb.ArtifactRetrievalService_GetArtifactClient, w io.Writer) (string, error) { @@ -442,8 +473,12 @@ func retrieve(ctx context.Context, client jobpb.LegacyArtifactRetrievalServiceCl } // Artifact Sha256 hash is an optional field in metadata so we should only validate when its present. - if a.Sha256 != "" && sha256Hash != a.Sha256 { - return errors.Errorf("bad SHA256 for %v: %v, want %v", filename, sha256Hash, a.Sha256) + if isArtifactValidationEnabled(ctx) { + if a.Sha256 == "" { + log.Printf("WARN: Artifact validation skipped for file: %v", filename) + } else if sha256Hash != a.Sha256 { + return errors.Errorf("bad SHA256 for %v: %v, want %v", filename, sha256Hash, a.Sha256) + } } return nil } diff --git a/sdks/go/pkg/beam/artifact/materialize_test.go b/sdks/go/pkg/beam/artifact/materialize_test.go index 31890ed045cc..bf27e13e8a89 100644 --- a/sdks/go/pkg/beam/artifact/materialize_test.go +++ b/sdks/go/pkg/beam/artifact/materialize_test.go @@ -82,6 +82,52 @@ func TestMultiRetrieve(t *testing.T) { } } +func TestRetrieveWithBadShaFails(t *testing.T) { + cc := startServer(t) + defer cc.Close() + + ctx := grpcx.WriteWorkerID(context.Background(), "idA") + keys := []string{"foo"} + st := "whatever" + rt, artifacts := populate(ctx, cc, t, keys, 300, st) + + dst := makeTempDir(t) + defer os.RemoveAll(dst) + + client := jobpb.NewLegacyArtifactRetrievalServiceClient(cc) + for _, a := range artifacts { + a.Sha256 = "badhash" // mutate hash + if err := Retrieve(ctx, client, a, rt, dst); err == nil { + t.Errorf("expected materialization to fail due to bad sha256 mismatch") + } + } +} + +func TestRetrieveWithBadShaAndExperimentSucceeds(t *testing.T) { + cc := startServer(t) + defer cc.Close() + + ctx := WithArtifactValidation(grpcx.WriteWorkerID(context.Background(), "idA"), false) + keys := []string{"foo"} + st := "whatever" + rt, artifacts := populate(ctx, cc, t, keys, 300, st) + + dst := makeTempDir(t) + defer os.RemoveAll(dst) + + client := jobpb.NewLegacyArtifactRetrievalServiceClient(cc) + for _, a := range artifacts { + originalHash := a.Sha256 + a.Sha256 = "badhash" // mutate hash + filename := makeFilename(dst, a.Name) + if err := Retrieve(ctx, client, a, rt, dst); err != nil { + t.Errorf("materialize failed but should have succeeded because validation was disabled via experiment: %v", err) + continue + } + verifySHA256(t, filename, originalHash) + } +} + // populate stages a set of artifacts with the given keys, each with // slightly different sizes and chucksizes. func populate(ctx context.Context, cc *grpc.ClientConn, t *testing.T, keys []string, size int, st string) (string, []*jobpb.ArtifactMetadata) { @@ -266,6 +312,55 @@ func TestNewRetrieveWithResolution(t *testing.T) { checkStagedFiles(mds, dest, expected, t) } +func TestIsArtifactValidationEnabled(t *testing.T) { + ctx := context.Background() + if !isArtifactValidationEnabled(ctx) { + t.Errorf("empty context should have validation enabled") + } + + ctx2 := WithArtifactValidation(ctx, false) + if isArtifactValidationEnabled(ctx2) { + t.Errorf("context with validation disabled should have validation disabled") + } +} + +func TestNewRetrieveWithBadShaFails(t *testing.T) { + expected := map[string]string{"a.txt": "a"} + client := &fakeRetrievalService{artifacts: expected} + dest := makeTempDir(t) + defer os.RemoveAll(dest) + ctx := grpcx.WriteWorkerID(context.Background(), "worker") + + _, err := newMaterializeWithClient(ctx, client, client.fileArtifactsWithBadSha(), dest) + if err == nil { + t.Fatalf("expected materialization to fail due to bad sha256 mismatch") + } +} + +func TestNewRetrieveWithBadShaAndExperimentSucceeds(t *testing.T) { + expected := map[string]string{"a.txt": "a"} + client := &fakeRetrievalService{artifacts: expected} + dest := makeTempDir(t) + defer os.RemoveAll(dest) + + ctx := WithArtifactValidation(grpcx.WriteWorkerID(context.Background(), "worker"), false) + + mds, err := newMaterializeWithClient(ctx, client, client.fileArtifactsWithBadSha(), dest) + if err != nil { + t.Fatalf("materialize failed but should have succeeded because validation was disabled via experiment: %v", err) + } + + generated := make(map[string]string) + for _, md := range mds { + name, _ := MustExtractFilePayload(md) + payload, _ := proto.Marshal(&pipepb.ArtifactStagingToRolePayload{ + StagedName: name}) + generated[name] = string(payload) + } + + checkStagedFiles(mds, dest, generated, t) +} + func checkStagedFiles(mds []*pipepb.ArtifactInformation, dest string, expected map[string]string, t *testing.T) { if len(mds) != len(expected) { t.Errorf("wrong number of artifacts staged %v vs %v", len(mds), len(expected)) @@ -323,6 +418,21 @@ func (fake *fakeRetrievalService) fileArtifactsWithoutStagingTo() []*pipepb.Arti return artifacts } +func (fake *fakeRetrievalService) fileArtifactsWithBadSha() []*pipepb.ArtifactInformation { + var artifacts []*pipepb.ArtifactInformation + for name := range fake.artifacts { + payload, _ := proto.Marshal(&pipepb.ArtifactFilePayload{ + Path: filepath.Join("/tmp", name), + Sha256: "badhash", + }) + artifacts = append(artifacts, &pipepb.ArtifactInformation{ + TypeUrn: URNFileArtifact, + TypePayload: payload, + }) + } + return artifacts +} + func (fake *fakeRetrievalService) urlArtifactsWithoutStagingTo() []*pipepb.ArtifactInformation { var artifacts []*pipepb.ArtifactInformation for name := range fake.artifacts { diff --git a/sdks/go/pkg/beam/artifact/options.go b/sdks/go/pkg/beam/artifact/options.go new file mode 100644 index 000000000000..47356433161c --- /dev/null +++ b/sdks/go/pkg/beam/artifact/options.go @@ -0,0 +1,48 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package artifact + +import ( + structpb "google.golang.org/protobuf/types/known/structpb" +) + +// GetExperiments extracts a list of experiments from the pipeline options. +func GetExperiments(options *structpb.Struct) []string { + if options == nil { + return nil + } + + var exps []string + // Try legacy style + for _, v := range options.GetFields()["options"].GetStructValue().GetFields()["experiments"].GetListValue().GetValues() { + exps = append(exps, v.GetStringValue()) + } + // Try URN style + for _, v := range options.GetFields()["beam:option:experiments:v1"].GetListValue().GetValues() { + exps = append(exps, v.GetStringValue()) + } + return exps +} + +// HasExperiment checks if a specific experiment is enabled in the pipeline options. +func HasExperiment(options *structpb.Struct, experiment string) bool { + for _, exp := range GetExperiments(options) { + if exp == experiment { + return true + } + } + return false +} diff --git a/sdks/go/pkg/beam/artifact/options_test.go b/sdks/go/pkg/beam/artifact/options_test.go new file mode 100644 index 000000000000..a9f0e4bb7e35 --- /dev/null +++ b/sdks/go/pkg/beam/artifact/options_test.go @@ -0,0 +1,78 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package artifact + +import ( + "testing" + + structpb "google.golang.org/protobuf/types/known/structpb" +) + +func TestGetExperiments_Nil(t *testing.T) { + if got := GetExperiments(nil); got != nil { + t.Errorf("GetExperiments(nil) = %v, want nil", got) + } +} + +func TestGetExperiments_Legacy(t *testing.T) { + options, _ := structpb.NewStruct(map[string]interface{}{ + "options": map[string]interface{}{ + "experiments": []interface{}{"exp1", "exp2"}, + }, + }) + exps := GetExperiments(options) + if len(exps) != 2 || exps[0] != "exp1" || exps[1] != "exp2" { + t.Errorf("GetExperiments() = %v, want [exp1 exp2]", exps) + } +} + +func TestGetExperiments_URN(t *testing.T) { + urnOptions, _ := structpb.NewStruct(map[string]interface{}{ + "beam:option:experiments:v1": []interface{}{"expA", "expB"}, + }) + expsURN := GetExperiments(urnOptions) + if len(expsURN) != 2 || expsURN[0] != "expA" || expsURN[1] != "expB" { + t.Errorf("GetExperiments() = %v, want [expA expB]", expsURN) + } +} + +func TestHasExperiment(t *testing.T) { + options, _ := structpb.NewStruct(map[string]interface{}{ + "options": map[string]interface{}{ + "experiments": []interface{}{"exp1", "exp2"}, + }, + }) + + if !HasExperiment(options, "exp1") { + t.Errorf("HasExperiment(exp1) = false, want true") + } + if HasExperiment(options, "exp3") { + t.Errorf("HasExperiment(exp3) = true, want false") + } +} + +func TestGetExperiments_Combined(t *testing.T) { + options, _ := structpb.NewStruct(map[string]interface{}{ + "options": map[string]interface{}{ + "experiments": []interface{}{"exp1", "exp2"}, + }, + "beam:option:experiments:v1": []interface{}{"expA", "expB"}, + }) + exps := GetExperiments(options) + if len(exps) != 4 || exps[0] != "exp1" || exps[1] != "exp2" || exps[2] != "expA" || exps[3] != "expB" { + t.Errorf("GetExperiments() = %v, want [exp1 exp2 expA expB]", exps) + } +} diff --git a/sdks/go/pkg/beam/core/core.go b/sdks/go/pkg/beam/core/core.go index a277d1efdd96..c9c26a7311cd 100644 --- a/sdks/go/pkg/beam/core/core.go +++ b/sdks/go/pkg/beam/core/core.go @@ -27,7 +27,7 @@ const ( // SdkName is the human readable name of the SDK for UserAgents. SdkName = "Apache Beam SDK for Go" // SdkVersion is the current version of the SDK. - SdkVersion = "2.74.0.dev" + SdkVersion = "2.76.0.dev" // DefaultDockerImage represents the associated image for this release. DefaultDockerImage = "apache/beam_go_sdk:" + SdkVersion diff --git a/sdks/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/io/fhirio/common.go b/sdks/go/pkg/beam/io/fhirio/common.go index 75781f7ba0cc..9f7e39bf0efb 100644 --- a/sdks/go/pkg/beam/io/fhirio/common.go +++ b/sdks/go/pkg/beam/io/fhirio/common.go @@ -127,11 +127,12 @@ func (c *fhirStoreClientImpl) search(storePath, resourceType string, queries map queryParams = append(queryParams, googleapi.QueryParameter(pageTokenParameterKey, pageToken)) } - searchRequest := &healthcare.SearchResourcesRequest{} + // Pass an empty reader as the body because search parameters are passed via queryParams, + // and the new API expects an io.Reader instead of a specific struct. if resourceType == "" { - return c.fhirService().Search(storePath, searchRequest).Do(queryParams...) + return c.fhirService().Search(storePath, strings.NewReader("")).Do(queryParams...) } - return c.fhirService().SearchType(storePath, resourceType, searchRequest).Do(queryParams...) + return c.fhirService().SearchType(storePath, resourceType, strings.NewReader("")).Do(queryParams...) } func (c *fhirStoreClientImpl) deidentify(srcStorePath, dstStorePath string, deidConfig *healthcare.DeidentifyConfig) (operationResults, error) { 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/runners/dataflow/dataflow.go b/sdks/go/pkg/beam/runners/dataflow/dataflow.go index 101441dbcb56..ce5e99b0a8de 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflow.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflow.go @@ -24,6 +24,8 @@ package dataflow import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "flag" "fmt" @@ -40,6 +42,7 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/graphx" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/pipelinex" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/hooks" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/protox" "github.com/apache/beam/sdks/v2/go/pkg/beam/internal/errors" "github.com/apache/beam/sdks/v2/go/pkg/beam/log" "github.com/apache/beam/sdks/v2/go/pkg/beam/options/gcpopts" @@ -52,32 +55,34 @@ import ( // TODO(herohde) 5/16/2017: the Dataflow flags should match the other SDKs. var ( - endpoint = flag.String("dataflow_endpoint", "", "Dataflow endpoint (optional).") - stagingLocation = flag.String("staging_location", "", "GCS staging location (required).") - workerHarnessImage = flag.String("worker_harness_container_image", "", "Worker harness container image (optional). Deprecated in favor of the sdk_container_image flag.") - image = flag.String("sdk_container_image", "", "Worker harness container image (optional).") - labels = flag.String("labels", "", "JSON-formatted map[string]string of job labels (optional).") - serviceAccountEmail = flag.String("service_account_email", "", "Service account email (optional).") - numWorkers = flag.Int64("num_workers", 0, "Number of workers (optional).") - workerHarnessThreads = flag.Int64("number_of_worker_harness_threads", 0, "The number of threads per each worker harness process (optional).") - maxNumWorkers = flag.Int64("max_num_workers", 0, "Maximum number of workers during scaling (optional).") - diskSizeGb = flag.Int64("disk_size_gb", 0, "Size of root disk for VMs, in GB (optional).") - diskType = flag.String("disk_type", "", "Type of root disk for VMs (optional).") - autoscalingAlgorithm = flag.String("autoscaling_algorithm", "", "Autoscaling mode to use (optional).") - zone = flag.String("zone", "", "GCP zone (optional)") - kmsKey = flag.String("dataflow_kms_key", "", "The Cloud KMS key identifier used to encrypt data at rest (optional).") - network = flag.String("network", "", "GCP network (optional)") - subnetwork = flag.String("subnetwork", "", "GCP subnetwork (optional)") - noUsePublicIPs = flag.Bool("no_use_public_ips", false, "Workers must not use public IP addresses (optional)") - usePublicIPs = flag.Bool("use_public_ips", true, "Workers must use public IP addresses (optional)") - tempLocation = flag.String("temp_location", "", "Temp location (optional)") - workerMachineType = flag.String("worker_machine_type", "", "GCE machine type (optional)") - machineType = flag.String("machine_type", "", "alias of worker_machine_type (optional)") - minCPUPlatform = flag.String("min_cpu_platform", "", "GCE minimum cpu platform (optional)") - workerRegion = flag.String("worker_region", "", "Dataflow worker region (optional)") - workerZone = flag.String("worker_zone", "", "Dataflow worker zone (optional)") - dataflowServiceOptions = flag.String("dataflow_service_options", "", "Comma separated list of additional job modes and configurations (optional)") - flexRSGoal = flag.String("flexrs_goal", "", "Which Flexible Resource Scheduling mode to run in (optional)") + endpoint = flag.String("dataflow_endpoint", "", "Dataflow endpoint (optional).") + stagingLocation = flag.String("staging_location", "", "GCS staging location (required).") + workerHarnessImage = flag.String("worker_harness_container_image", "", "Worker harness container image (optional). Deprecated in favor of the sdk_container_image flag.") + image = flag.String("sdk_container_image", "", "Worker harness container image (optional).") + labels = flag.String("labels", "", "JSON-formatted map[string]string of job labels (optional).") + serviceAccountEmail = flag.String("service_account_email", "", "Service account email (optional).") + numWorkers = flag.Int64("num_workers", 0, "Number of workers (optional).") + workerHarnessThreads = flag.Int64("number_of_worker_harness_threads", 0, "The number of threads per each worker harness process (optional).") + maxNumWorkers = flag.Int64("max_num_workers", 0, "Maximum number of workers during scaling (optional).") + diskSizeGb = flag.Int64("disk_size_gb", 0, "Size of root disk for VMs, in GB (optional).") + diskType = flag.String("disk_type", "", "Type of root disk for VMs (optional).") + diskProvisionedIops = flag.Int64("disk_provisioned_iops", 0, "Provisioned IOPS for the worker disk (optional).") + diskProvisionedThroughputMibps = flag.Int64("disk_provisioned_throughput_mibps", 0, "Provisioned throughput in MiB/s for the worker disk (optional).") + autoscalingAlgorithm = flag.String("autoscaling_algorithm", "", "Autoscaling mode to use (optional).") + zone = flag.String("zone", "", "GCP zone (optional)") + kmsKey = flag.String("dataflow_kms_key", "", "The Cloud KMS key identifier used to encrypt data at rest (optional).") + network = flag.String("network", "", "GCP network (optional)") + subnetwork = flag.String("subnetwork", "", "GCP subnetwork (optional)") + noUsePublicIPs = flag.Bool("no_use_public_ips", false, "Workers must not use public IP addresses (optional)") + usePublicIPs = flag.Bool("use_public_ips", true, "Workers must use public IP addresses (optional)") + tempLocation = flag.String("temp_location", "", "Temp location (optional)") + workerMachineType = flag.String("worker_machine_type", "", "GCE machine type (optional)") + machineType = flag.String("machine_type", "", "alias of worker_machine_type (optional)") + minCPUPlatform = flag.String("min_cpu_platform", "", "GCE minimum cpu platform (optional)") + workerRegion = flag.String("worker_region", "", "Dataflow worker region (optional)") + workerZone = flag.String("worker_zone", "", "Dataflow worker zone (optional)") + dataflowServiceOptions = flag.String("dataflow_service_options", "", "Comma separated list of additional job modes and configurations (optional)") + flexRSGoal = flag.String("flexrs_goal", "", "Which Flexible Resource Scheduling mode to run in (optional)") // TODO(https://github.com/apache/beam/issues/21604) Turn this on once TO_STRING is implemented // enableHotKeyLogging = flag.Bool("enable_hot_key_logging", false, "Specifies that when a hot key is detected in the pipeline, the literal, human-readable key is printed in the user's Cloud Logging project (optional).") @@ -105,33 +110,35 @@ func init() { // should be added to this map. // Don't filter temp_location since we need this included in PipelineOptions to correctly upload heap dumps. var flagFilter = map[string]bool{ - "dataflow_endpoint": true, - "staging_location": true, - "worker_harness_container_image": true, - "sdk_container_image": true, - "labels": true, - "service_account_email": true, - "num_workers": true, - "max_num_workers": true, - "disk_size_gb": true, - "disk_type": true, - "autoscaling_algorithm": true, - "zone": true, - "network": true, - "subnetwork": true, - "no_use_public_ips": true, - "template_location": true, - "worker_machine_type": true, - "machine_type": true, - "min_cpu_platform": true, - "dataflow_worker_jar": true, - "worker_region": true, - "worker_zone": true, - "teardown_policy": true, - "cpu_profiling": true, - "session_recording": true, - "update": true, - "transform_name_mapping": true, + "dataflow_endpoint": true, + "staging_location": true, + "worker_harness_container_image": true, + "sdk_container_image": true, + "labels": true, + "service_account_email": true, + "num_workers": true, + "max_num_workers": true, + "disk_size_gb": true, + "disk_type": true, + "disk_provisioned_iops": true, + "disk_provisioned_throughput_mibps": true, + "autoscaling_algorithm": true, + "zone": true, + "network": true, + "subnetwork": true, + "no_use_public_ips": true, + "template_location": true, + "worker_machine_type": true, + "machine_type": true, + "min_cpu_platform": true, + "dataflow_worker_jar": true, + "worker_region": true, + "worker_zone": true, + "teardown_policy": true, + "cpu_profiling": true, + "session_recording": true, + "update": true, + "transform_name_mapping": true, // Job Options flags "endpoint": true, @@ -235,7 +242,10 @@ func Execute(ctx context.Context, p *beam.Pipeline) (beam.PipelineResult, error) log.Info(ctx, "Dry-run: not submitting job!") log.Info(ctx, model.String()) - job, err := dataflowlib.Translate(ctx, model, opts, workerURL, modelURL) + modelBytes := protox.MustEncode(model) + hash := sha256.Sum256(modelBytes) + pipelineProtoHash := hex.EncodeToString(hash[:]) + job, err := dataflowlib.Translate(ctx, model, opts, workerURL, modelURL, pipelineProtoHash) if err != nil { return nil, err } @@ -324,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") { @@ -339,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. @@ -358,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") @@ -379,37 +390,39 @@ func getJobOptions(ctx context.Context, streaming bool) (*dataflowlib.JobOptions beam.PipelineOptions.LoadOptionsFromFlags(flagFilter) opts := &dataflowlib.JobOptions{ - Name: jobopts.GetJobName(), - Streaming: streaming, - Experiments: experiments, - DataflowServiceOptions: dfServiceOptions, - Options: beam.PipelineOptions.Export(), - Project: project, - Region: region, - Zone: *zone, - KmsKey: *kmsKey, - Network: *network, - Subnetwork: *subnetwork, - NoUsePublicIPs: *noUsePublicIPs, - NumWorkers: *numWorkers, - MaxNumWorkers: *maxNumWorkers, - WorkerHarnessThreads: *workerHarnessThreads, - DiskSizeGb: *diskSizeGb, - DiskType: *diskType, - Algorithm: *autoscalingAlgorithm, - FlexRSGoal: *flexRSGoal, - MachineType: *firstNonEmpty(workerMachineType, machineType), - Labels: jobLabels, - ServiceAccountEmail: *serviceAccountEmail, - TempLocation: *tempLocation, - TemplateLocation: *templateLocation, - Worker: *jobopts.WorkerBinary, - WorkerRegion: *workerRegion, - WorkerZone: *workerZone, - TeardownPolicy: *teardownPolicy, - ContainerImage: getContainerImage(ctx), - Update: *update, - TransformNameMapping: updateTransformMapping, + Name: jobopts.GetJobName(), + Streaming: streaming, + Experiments: experiments, + DataflowServiceOptions: dfServiceOptions, + Options: beam.PipelineOptions.Export(), + Project: project, + Region: region, + Zone: *zone, + KmsKey: *kmsKey, + Network: *network, + Subnetwork: *subnetwork, + NoUsePublicIPs: *noUsePublicIPs, + NumWorkers: *numWorkers, + MaxNumWorkers: *maxNumWorkers, + WorkerHarnessThreads: *workerHarnessThreads, + DiskSizeGb: *diskSizeGb, + DiskType: *diskType, + DiskProvisionedIops: *diskProvisionedIops, + DiskProvisionedThroughputMibps: *diskProvisionedThroughputMibps, + Algorithm: *autoscalingAlgorithm, + FlexRSGoal: *flexRSGoal, + MachineType: *firstNonEmpty(workerMachineType, machineType), + Labels: jobLabels, + ServiceAccountEmail: *serviceAccountEmail, + TempLocation: *tempLocation, + TemplateLocation: *templateLocation, + Worker: *jobopts.WorkerBinary, + WorkerRegion: *workerRegion, + WorkerZone: *workerZone, + TeardownPolicy: *teardownPolicy, + ContainerImage: getContainerImage(ctx), + Update: *update, + TransformNameMapping: updateTransformMapping, } if opts.TempLocation == "" { opts.TempLocation = gcsx.Join(*stagingLocation, "tmp") diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflow_test.go index fa418ebbe6a0..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) @@ -516,6 +557,26 @@ func getFieldFromOpt(fieldName string, opts *dataflowlib.JobOptions) string { return reflect.ValueOf(opts).Elem().FieldByName(fieldName).String() } +func TestGetJobOptions_DiskProvisionedOptions(t *testing.T) { + resetGlobals() + *stagingLocation = "gs://testStagingLocation" + *gcpopts.Project = "testProject" + *gcpopts.Region = "testRegion" + *diskProvisionedIops = 1000 + *diskProvisionedThroughputMibps = 100 + + opts, err := getJobOptions(context.Background(), false) + if err != nil { + t.Fatalf("getJobOptions() returned error %q, want %q", err, "nil") + } + if got, want := opts.DiskProvisionedIops, int64(1000); got != want { + t.Errorf("opts.DiskProvisionedIops = %d, want %d", got, want) + } + if got, want := opts.DiskProvisionedThroughputMibps, int64(100); got != want { + t.Errorf("opts.DiskProvisionedThroughputMibps = %d, want %d", got, want) + } +} + func resetGlobals() { *autoscalingAlgorithm = "" *dataflowServiceOptions = "" @@ -537,4 +598,6 @@ func resetGlobals() { *workerHarnessImage = "" *workerMachineType = "" *machineType = "" + *diskProvisionedIops = 0 + *diskProvisionedThroughputMibps = 0 } diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go index 806b8940ae99..c5cb8a636f33 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/execute.go @@ -19,6 +19,8 @@ package dataflowlib import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "os" "strings" @@ -69,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( @@ -83,14 +86,17 @@ func Execute(ctx context.Context, raw *pipepb.Pipeline, opts *JobOptions, worker // (2) Upload model to GCS log.Info(ctx, raw.String()) - if err := StageModel(ctx, opts.Project, modelURL, protox.MustEncode(raw)); err != nil { + modelBytes := protox.MustEncode(raw) + modelHash := sha256.Sum256(modelBytes) + pipelineProtoHash := hex.EncodeToString(modelHash[:]) + if err := StageModel(ctx, opts.Project, modelURL, modelBytes); err != nil { return presult, err } log.Infof(ctx, "Staged model pipeline: %v", modelURL) // (3) Translate to v1b3 and submit - job, err := Translate(ctx, raw, opts, workerURL, modelURL) + job, err := Translate(ctx, raw, opts, workerURL, modelURL, pipelineProtoHash) if err != nil { return presult, err } diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go index db83992fbebc..27a0a0d0f224 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job.go @@ -47,26 +47,28 @@ type JobOptions struct { // Pipeline options Options runtime.RawOptions - Streaming bool - Project string - Region string - Zone string - KmsKey string - Network string - Subnetwork string - NoUsePublicIPs bool - NumWorkers int64 - DiskSizeGb int64 - DiskType string - MachineType string - Labels map[string]string - ServiceAccountEmail string - WorkerRegion string - WorkerZone string - ContainerImage string - ArtifactURLs []string // Additional packages for workers. - FlexRSGoal string - EnableHotKeyLogging bool + Streaming bool + Project string + Region string + Zone string + KmsKey string + Network string + Subnetwork string + NoUsePublicIPs bool + NumWorkers int64 + DiskSizeGb int64 + DiskType string + DiskProvisionedIops int64 + DiskProvisionedThroughputMibps int64 + MachineType string + Labels map[string]string + ServiceAccountEmail string + WorkerRegion string + WorkerZone string + ContainerImage string + ArtifactURLs []string // Additional packages for workers. + FlexRSGoal string + EnableHotKeyLogging bool // Streaming update settings Update bool @@ -83,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 @@ -115,7 +120,7 @@ func containerImages(p *pipepb.Pipeline) ([]*df.SdkHarnessContainerImage, []stri } // Translate translates a pipeline to a Dataflow job. -func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, workerURL, modelURL string) (*df.Job, error) { +func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, workerURL, modelURL string, pipelineProtoHash string) (*df.Job, error) { // (1) Translate pipeline to v1b3 speak. jobType := "JOB_TYPE_BATCH" @@ -134,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 { @@ -154,6 +160,9 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker return nil, err } + if opts.Options.Options == nil { + opts.Options.Options = make(map[string]string) + } opts.Options.Options["experiments"] = strings.Join(opts.Experiments, ",") job := &df.Job{ ProjectId: opts.Project, @@ -176,10 +185,11 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker SdkPipelineOptions: newMsg(pipelineOptions{ DisplayData: printOptions(opts, images), Options: dataflowOptions{ - PipelineURL: modelURL, - Region: opts.Region, - Experiments: opts.Experiments, - TempLocation: opts.TempLocation, + PipelineURL: modelURL, + PipelineProtoHash: pipelineProtoHash, + Region: opts.Region, + Experiments: opts.Experiments, + TempLocation: opts.TempLocation, }, GoOptions: opts.Options, }), @@ -189,18 +199,20 @@ func Translate(ctx context.Context, p *pipepb.Pipeline, opts *JobOptions, worker AutoscalingSettings: &df.AutoscalingSettings{ MaxNumWorkers: opts.MaxNumWorkers, }, - DiskSizeGb: opts.DiskSizeGb, - DiskType: opts.DiskType, - IpConfiguration: ipConfiguration, - Kind: "harness", - Packages: packages, - WorkerHarnessContainerImage: opts.ContainerImage, - SdkHarnessContainerImages: dfImages, - NumWorkers: 1, - MachineType: opts.MachineType, - Network: opts.Network, - Subnetwork: opts.Subnetwork, - Zone: opts.Zone, + DiskSizeGb: opts.DiskSizeGb, + DiskType: opts.DiskType, + DiskProvisionedIops: opts.DiskProvisionedIops, + DiskProvisionedThroughputMibps: opts.DiskProvisionedThroughputMibps, + IpConfiguration: ipConfiguration, + Kind: "harness", + Packages: packages, + WorkerHarnessContainerImage: opts.ContainerImage, + SdkHarnessContainerImages: dfImages, + NumWorkers: 1, + MachineType: opts.MachineType, + Network: opts.Network, + Subnetwork: opts.Subnetwork, + Zone: opts.Zone, }}, WorkerRegion: opts.WorkerRegion, WorkerZone: opts.WorkerZone, @@ -350,10 +362,13 @@ func GetMetrics(ctx context.Context, client *df.Service, project, region, jobID // pipeline options that are communicated to cross-language SDK harnesses, so any pipeline options // needed for cross-language transforms in Dataflow must be declared here. type dataflowOptions struct { - Experiments []string `json:"experiments,omitempty"` - PipelineURL string `json:"pipelineUrl"` - Region string `json:"region"` - TempLocation string `json:"tempLocation"` + Experiments []string `json:"experiments,omitempty"` + PipelineURL string `json:"pipelineUrl"` + PipelineProtoHash string `json:"pipelineProtoHash,omitempty"` + Region string `json:"region"` + TempLocation string `json:"tempLocation"` + DiskProvisionedIops int64 `json:"diskProvisionedIops"` + DiskProvisionedThroughputMibps int64 `json:"diskProvisionedThroughputMibps"` } func printOptions(opts *JobOptions, images []string) []*displayData { diff --git a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go index fb44ff9c0133..07f2d39e552c 100644 --- a/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go +++ b/sdks/go/pkg/beam/runners/dataflow/dataflowlib/job_test.go @@ -21,6 +21,9 @@ import ( "reflect" "testing" + "encoding/json" + + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/graphx" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/protox" pipepb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/pipeline_v1" @@ -280,3 +283,95 @@ func Test_containerImages(t *testing.T) { } } + +func TestTranslate(t *testing.T) { + ctx := context.Background() + p := &pipepb.Pipeline{} + opts := &JobOptions{ + Project: "test-project", + Name: "test-job", + DiskProvisionedIops: 4000, + DiskProvisionedThroughputMibps: 200, + WorkerHash: "worker-sha256-hash", + } + workerURL := "gs://any-location/temp" + modelURL := "gs://any-location/temp" + + job, err := Translate(ctx, p, opts, workerURL, modelURL, "dummy-hash-12345") + if err != nil { + t.Fatalf("Translate(...) error = %v, want nil", err) + } + + if len(job.Environment.WorkerPools) == 0 { + t.Fatal("Translate(...) returned job with no worker pools") + } + + wp := job.Environment.WorkerPools[0] + if wp.DiskProvisionedIops != 4000 { + t.Errorf("DiskProvisionedIops = %v, want 4000", wp.DiskProvisionedIops) + } + if wp.DiskProvisionedThroughputMibps != 200 { + t.Errorf("DiskProvisionedThroughputMibps = %v, want 200", wp.DiskProvisionedThroughputMibps) + } + + // 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) { + p := &pipepb.Pipeline{ + Components: &pipepb.Components{ + Environments: map[string]*pipepb.Environment{ + "env1": { + Payload: protox.MustEncode(&pipepb.DockerPayload{ + ContainerImage: "dummy_image", + }), + }, + }, + }, + } + opts := &JobOptions{ + Name: "test-job", + Project: "test-project", + Region: "test-region", + Options: runtime.RawOptions{ + Options: make(map[string]string), + }, + } + + expectedHashStr := "dummy-hash-12345" + + job, err := Translate(context.Background(), p, opts, "worker-url", "model-url", expectedHashStr) + if err != nil { + t.Fatalf("Translate failed: %v", err) + } + + // Verify PipelineProtoHash + var recoveredOptions struct { + Options struct { + PipelineURL string `json:"pipelineUrl"` + PipelineProtoHash string `json:"pipelineProtoHash"` + } `json:"options"` + } + + rawOpts := job.Environment.SdkPipelineOptions + if err := json.Unmarshal(rawOpts, &recoveredOptions); err != nil { + t.Fatalf("Failed to unmarshal SdkPipelineOptions: %v", err) + } + + if recoveredOptions.Options.PipelineProtoHash != expectedHashStr { + t.Errorf("Expected PipelineProtoHash %v, got %v", expectedHashStr, recoveredOptions.Options.PipelineProtoHash) + } +} diff --git a/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go b/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go index f720be20e375..3949d1af3248 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go +++ b/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go @@ -387,8 +387,11 @@ func (em *ElementManager) Bundles(ctx context.Context, upstreamCancelFn context. em.pendingElements.Wait() slog.Debug("no more pending elements: terminating pipeline") cancelFn(fmt.Errorf("elementManager out of elements, cleaning up")) - // Ensure the watermark evaluation goroutine exits. + // Ensure the watermark evaluation goroutine exits by locking the mutex + // before broadcasting, preventing a lost wake-up signal. + em.refreshCond.L.Lock() em.refreshCond.Broadcast() + em.refreshCond.L.Unlock() }() // Watermark evaluation goroutine. go func() { @@ -2496,7 +2499,9 @@ func (em *ElementManager) wakeUpAt(t mtime.Time) { // only create this goroutine if we have real-time clock enabled (also implying the pipeline does not have TestStream). go func(fireAt time.Time) { time.AfterFunc(time.Until(fireAt), func() { + em.refreshCond.L.Lock() em.refreshCond.Broadcast() + em.refreshCond.L.Unlock() }) }(t.ToTime()) } 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.go b/sdks/go/pkg/beam/runners/prism/internal/execute.go index 853b7974479d..f6e148f9f3f6 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/execute.go +++ b/sdks/go/pkg/beam/runners/prism/internal/execute.go @@ -376,7 +376,11 @@ func executePipeline(ctx context.Context, wks map[string]*worker.W, j *jobservic eg.Go(func() error { s := stages[rb.StageID] wk := wks[s.envID] - if err := s.Execute(ctx, j, wk, comps, em, rb); err != nil { + // Pass egctx instead of the parent ctx so that when any bundle fails, + // the errgroup cancels egctx and all other concurrent bundle execution + // goroutines immediately detect cancellation and abort. This prevents + // eg.Wait() from blocking indefinitely and allows prompt error reporting. + if err := s.Execute(egctx, j, wk, comps, em, rb); err != nil { // Ensure we clean up on bundle failure j.Logger.Error("Bundle Failed.", slog.Any("error", err)) em.FailBundle(rb) diff --git a/sdks/go/pkg/beam/runners/prism/internal/execute_test.go b/sdks/go/pkg/beam/runners/prism/internal/execute_test.go index 29fccaeb238e..96c90678be30 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/execute_test.go +++ b/sdks/go/pkg/beam/runners/prism/internal/execute_test.go @@ -519,6 +519,49 @@ func TestFailure(t *testing.T) { } } +func TestFailureHang(t *testing.T) { + initRunner(t) + + p, s := beam.NewPipelineWithRoot() + imp := beam.Impulse(s) + col1 := beam.ParDo(s, doFnBlock, imp) + col2 := beam.ParDo(s, doFnFail, imp) + beam.ParDo(s, &int64Check{Name: "block", Want: []int{}}, col1) + beam.ParDo(s, &int64Check{Name: "fail", Want: []int{}}, col2) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + _, err := executeWithT(ctx, t, p) + if err == nil { + t.Fatalf("expected pipeline failure, but got a success") + } + if want := "doFnFail: failing as intended"; !strings.Contains(err.Error(), want) { + t.Fatalf("expected pipeline failure with %q, but was %v", want, err) + } +} + +func 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 334d74fcae1d..903805cf659b 100644 --- a/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go +++ b/sdks/go/pkg/beam/runners/prism/internal/testdofns_test.go @@ -59,10 +59,12 @@ func init() { register.Function3x0(dofn1Counter) register.Function2x0(dofnSink) register.Function3x1(doFnFail) + register.Function3x0(doFnBlock) 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]() } @@ -283,6 +285,10 @@ func doFnFail(ctx context.Context, _ []byte, emit func(int64)) error { return fmt.Errorf("doFnFail: failing as intended") } +func doFnBlock(ctx context.Context, _ []byte, emit func(int64)) { + <-ctx.Done() +} + func combineIntSum(a, b int64) int64 { return a + b } @@ -399,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/runners/samza/samza.go b/sdks/go/pkg/beam/runners/samza/samza.go deleted file mode 100644 index 01a7c5233af2..000000000000 --- a/sdks/go/pkg/beam/runners/samza/samza.go +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one or more -// contributor license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright ownership. -// The ASF licenses this file to You under the Apache License, Version 2.0 -// (the "License"); you may not use this file except in compliance with -// the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package samza contains the Samza runner. -package samza - -import ( - "context" - - "github.com/apache/beam/sdks/v2/go/pkg/beam" - "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/universal" -) - -func init() { - beam.RegisterRunner("samza", Execute) - beam.RegisterRunner("SamzaRunner", Execute) -} - -// Execute runs the given pipeline on Samza. Convenience wrapper over the -// universal runner. -func Execute(ctx context.Context, p *beam.Pipeline) (beam.PipelineResult, error) { - return universal.Execute(ctx, p) -} diff --git a/sdks/go/pkg/beam/x/beamx/run.go b/sdks/go/pkg/beam/x/beamx/run.go index 0be42561b658..ff3583917b7a 100644 --- a/sdks/go/pkg/beam/x/beamx/run.go +++ b/sdks/go/pkg/beam/x/beamx/run.go @@ -33,7 +33,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dot" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/prism" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/universal" ) diff --git a/sdks/go/test/build.gradle b/sdks/go/test/build.gradle index 9302f25f8fce..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", @@ -79,8 +82,7 @@ task dataflowValidatesRunnerARM64() { task flinkValidatesRunner { group = "Verification" - // TODO(https://github.com/apache/beam/issues/37600) use project.ext.latestFlinkVersion after resolved - def flinkVersion = '1.20' + def flinkVersion = project.ext.latestFlinkVersion dependsOn ":sdks:go:test:goBuild" dependsOn ":sdks:go:container:docker" @@ -117,35 +119,6 @@ task flinkValidatesRunner { } } -// ValidatesRunner tests for Samza. Runs tests in the integration directory -// with Samza to validate that the runner behaves as expected. -task samzaValidatesRunner { - dependsOn ":sdks:go:test:goBuild" - dependsOn ":sdks:go:container:docker" - dependsOn ":sdks:java:container:${project.ext.currentJavaVersion}:docker" - dependsOn ":runners:samza:job-server:shadowJar" - dependsOn ":sdks:java:testing:expansion-service:buildTestExpansionServiceJar" - - doLast { - def pipelineOptions = [ // Pipeline options piped directly to Go SDK flags. - "--expansion_jar=test:${project(":sdks:java:testing:expansion-service").buildTestExpansionServiceJar.archivePath}", - ] - def options = [ - "--runner samza", - "--samza_job_server_jar ${project(":runners:samza:job-server").shadowJar.archivePath}", - "--pipeline_opts \"${pipelineOptions.join(' ')}\"", - "--prebuild_go_docker_tag ${project(":sdks:go:container").containerImageTags().first()}", - ] - exec { - if (fork_java_home) { - environment "JAVA_HOME_JOB_SERVER", fork_java_home - } - executable "sh" - args "-c", "./run_validatesrunner_tests.sh ${options.join(' ')}" - } - } -} - // ValidatesRunner tests for Spark. Runs tests in the integration directory // with Spark to validate that the runner behaves as expected. task sparkValidatesRunner { diff --git a/sdks/go/test/integration/expansions.go b/sdks/go/test/integration/expansions.go index 633f88d02930..2becce67333d 100644 --- a/sdks/go/test/integration/expansions.go +++ b/sdks/go/test/integration/expansions.go @@ -105,7 +105,7 @@ func (es *ExpansionServices) GetAddr(label string) (string, error) { } addr := "localhost:" + portStr - + // Use different wait strategies for test mode vs production if es.testMode { // In test mode, use simple wait time for compatibility with mock processes diff --git a/sdks/go/test/integration/expansions_test.go b/sdks/go/test/integration/expansions_test.go index 3afa2470157c..6e47f5d303b0 100644 --- a/sdks/go/test/integration/expansions_test.go +++ b/sdks/go/test/integration/expansions_test.go @@ -22,7 +22,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/test/integration/internal/jars" "github.com/google/go-cmp/cmp" diff --git a/sdks/go/test/integration/integration.go b/sdks/go/test/integration/integration.go index 9d04f08de4fb..a0eef7d10189 100644 --- a/sdks/go/test/integration/integration.go +++ b/sdks/go/test/integration/integration.go @@ -199,60 +199,13 @@ var flinkFilters = []string{ "TestTestStreamToGBK", "TestTestStreamTimersEventTime", - "TestTimers_EventTime_Unbounded", // (failure when comparing on side inputs (NPE on window lookup)) + "TestTimers_EventTime_WithNoOutputTimestamp", // Encounter error: TimestampCombiner moved element from TIMESTAMP_MAX_VALUE to earlier time (end of global window) for window GlobalWindow "TestTimers_ProcessingTime.*", // Flink doesn't support processing time timers. // no support for BundleFinalizer "TestParDoBundleFinalizer.*", } -var samzaFilters = []string{ - // TODO(https://github.com/apache/beam/issues/20987): Samza tests invalid encoding. - "TestReshuffle", - "TestReshuffleKV", - // The Samza runner does not support the TestStream primitive - "TestTestStream.*", - // The trigger and pane tests uses TestStream - "TestTrigger.*", - "TestPanes", - // TODO(https://github.com/apache/beam/issues/21244): Samza doesn't yet support post job metrics, used by WordCount - "TestWordCount.*", - // TODO(BEAM-13215): GCP IOs currently do not work in non-Dataflow portable runners. - "TestBigQueryIO.*", - "TestBigtableIO.*", - "TestSpannerIO.*", - // The Samza runner does not support self-checkpointing - "TestCheckpointing", - // The samza runner does not support pipeline drain for SDF. - "TestDrain", - // FhirIO currently only supports Dataflow runner - "TestFhirIO.*", - // OOMs currently only lead to heap dumps on Dataflow runner - "TestOomParDo", - // The samza runner does not support user state. - "TestValueState", - "TestValueStateWindowed", - "TestValueStateClear", - "TestBagState", - "TestBagStateClear", - "TestCombiningState", - "TestMapState", - "TestMapStateClear", - "TestSetState", - "TestSetStateClear", - // TODO(https://github.com/apache/beam/issues/26126): Java runner issue (AcitveBundle has no regsitered handler) - "TestDebeziumIO_BasicRead", - - "TestOrderedListState", - - // Samza does not support state. - "TestTimers.*", - "TestBagStateBlindWrite", - - // no support for BundleFinalizer - "TestParDoBundleFinalizer.*", -} - var sparkFilters = []string{ // TODO(BEAM-11498): XLang tests broken with Spark runner. "TestXLang.*", @@ -375,8 +328,6 @@ func CheckFilters(t *testing.T) { filters = portableFilters case "flink", "FlinkRunner": filters = flinkFilters - case "samza", "SamzaRunner": - filters = samzaFilters case "spark", "SparkRunner": filters = sparkFilters case "dataflow", "DataflowRunner": diff --git a/sdks/go/test/integration/internal/containers/containers.go b/sdks/go/test/integration/internal/containers/containers.go index 0b8ab6e45076..e0473f0416e5 100644 --- a/sdks/go/test/integration/internal/containers/containers.go +++ b/sdks/go/test/integration/internal/containers/containers.go @@ -22,7 +22,6 @@ import ( "time" retry "github.com/avast/retry-go/v4" - "github.com/docker/go-connections/nat" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) @@ -106,7 +105,7 @@ func Port( ctx context.Context, t *testing.T, container testcontainers.Container, - port nat.Port, + port string, ) string { t.Helper() diff --git a/sdks/go/test/integration/io/mongodbio/mongodbio_test.go b/sdks/go/test/integration/io/mongodbio/mongodbio_test.go index b8885e7c728d..481729c9eb98 100644 --- a/sdks/go/test/integration/io/mongodbio/mongodbio_test.go +++ b/sdks/go/test/integration/io/mongodbio/mongodbio_test.go @@ -26,7 +26,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/io/mongodbio" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" diff --git a/sdks/go/test/integration/io/spannerio/test_helpers.go b/sdks/go/test/integration/io/spannerio/test_helpers.go index 47a27f1a97be..8c170a548991 100644 --- a/sdks/go/test/integration/io/spannerio/test_helpers.go +++ b/sdks/go/test/integration/io/spannerio/test_helpers.go @@ -27,7 +27,6 @@ import ( adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" instance "cloud.google.com/go/spanner/admin/instance/apiv1" instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" - "github.com/docker/go-connections/nat" "github.com/testcontainers/testcontainers-go/wait" "google.golang.org/api/option" "google.golang.org/grpc" @@ -59,7 +58,7 @@ func setUpTestContainer(ctx context.Context, t *testing.T) string { containers.WithWaitStrategy(wait.ForLog("Cloud Spanner emulator running")), ) - mappedPort := containers.Port(ctx, t, container, nat.Port(spannerPorts[0])) + mappedPort := containers.Port(ctx, t, container, spannerPorts[0]) hostIP, err := container.Host(ctx) if err != nil { diff --git a/sdks/go/test/integration/io/xlang/debezium/debezium_test.go b/sdks/go/test/integration/io/xlang/debezium/debezium_test.go index a4850d4a3a33..8ccb64cae209 100644 --- a/sdks/go/test/integration/io/xlang/debezium/debezium_test.go +++ b/sdks/go/test/integration/io/xlang/debezium/debezium_test.go @@ -25,7 +25,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/io/xlang/debeziumio" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" diff --git a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go index 0eddc3e788d2..2ea84d427987 100644 --- a/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go +++ b/sdks/go/test/integration/io/xlang/jdbc/jdbc_test.go @@ -26,14 +26,13 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" "github.com/apache/beam/sdks/v2/go/test/integration/internal/containers" - "github.com/docker/go-connections/nat" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" + "strings" "github.com/testcontainers/testcontainers-go/wait" ) @@ -61,8 +60,9 @@ func setupTestContainer(ctx context.Context, t *testing.T, dbname, username, pas } hostname := "localhost" - dbURL := func(host string, port nat.Port) string { - return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", username, password, host, port.Port(), dbname) + dbURL := func(host string, port string) string { + cleanPort := strings.Split(port, "/")[0] + return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", username, password, host, cleanPort, dbname) } waitStrategy := wait.ForSQL(postgresPort, "postgres", dbURL).WithStartupTimeout(time.Second * 5) diff --git a/sdks/go/test/integration/io/xlang/kafka/kafka_test.go b/sdks/go/test/integration/io/xlang/kafka/kafka_test.go index e1cdc2e935db..2a682ea5cd2c 100644 --- a/sdks/go/test/integration/io/xlang/kafka/kafka_test.go +++ b/sdks/go/test/integration/io/xlang/kafka/kafka_test.go @@ -24,7 +24,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" diff --git a/sdks/go/test/integration/primitives/primitives_test.go b/sdks/go/test/integration/primitives/primitives_test.go index ef8a265b8bfa..ceaac4532f3c 100644 --- a/sdks/go/test/integration/primitives/primitives_test.go +++ b/sdks/go/test/integration/primitives/primitives_test.go @@ -20,7 +20,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" ) diff --git a/sdks/go/test/integration/synthetic/synthetic_test.go b/sdks/go/test/integration/synthetic/synthetic_test.go index 3161012975bd..f55fc7d7a302 100644 --- a/sdks/go/test/integration/synthetic/synthetic_test.go +++ b/sdks/go/test/integration/synthetic/synthetic_test.go @@ -22,7 +22,7 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/io/synthetic" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" + _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" diff --git a/sdks/go/test/integration/wordcount/wordcount_test.go b/sdks/go/test/integration/wordcount/wordcount_test.go index 09c6683cd14e..3c18a799f752 100644 --- a/sdks/go/test/integration/wordcount/wordcount_test.go +++ b/sdks/go/test/integration/wordcount/wordcount_test.go @@ -23,7 +23,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam/core/metrics" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" "github.com/apache/beam/sdks/v2/go/test/integration" diff --git a/sdks/go/test/integration/xlang/xlang_test.go b/sdks/go/test/integration/xlang/xlang_test.go index f1473f199057..091967a9d03c 100644 --- a/sdks/go/test/integration/xlang/xlang_test.go +++ b/sdks/go/test/integration/xlang/xlang_test.go @@ -27,7 +27,6 @@ import ( "github.com/apache/beam/sdks/v2/go/pkg/beam" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert" "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" diff --git a/sdks/go/test/regression/lperror_test.go b/sdks/go/test/regression/lperror_test.go index 7e99e73c9f80..0dd251980b3d 100644 --- a/sdks/go/test/regression/lperror_test.go +++ b/sdks/go/test/regression/lperror_test.go @@ -26,7 +26,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" ) diff --git a/sdks/go/test/regression/pardo_test.go b/sdks/go/test/regression/pardo_test.go index fc6d240fbf5a..e193739de452 100644 --- a/sdks/go/test/regression/pardo_test.go +++ b/sdks/go/test/regression/pardo_test.go @@ -23,7 +23,6 @@ import ( _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/flink" - _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/samza" _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/spark" ) diff --git a/sdks/go/test/run_validatesrunner_tests.sh b/sdks/go/test/run_validatesrunner_tests.sh index daae322dec85..9ea89fd22cfc 100755 --- a/sdks/go/test/run_validatesrunner_tests.sh +++ b/sdks/go/test/run_validatesrunner_tests.sh @@ -36,7 +36,7 @@ # --timeout -> Timeout for the go test command, on a per-package level. # --simultaneous -> Number of simultaneous packages to test. # Controls the -p flag for the go test command. -# Not used for Flink, Spark, or Samza runners. Defaults to 3 otherwise. +# Not used for Flink or Spark runners. Defaults to 3 otherwise. # --endpoint -> An endpoint for an existing job server outside the script. # If present, job server jar flags are ignored. # --test_expansion_jar -> Filepath to jar for an expansion service, for @@ -170,11 +170,6 @@ case $key in shift # past argument shift # past value ;; - --samza_job_server_jar) - SAMZA_JOB_SERVER_JAR="$2" - shift # past argument - shift # past value - ;; --spark_job_server_jar) SPARK_JOB_SERVER_JAR="$2" shift # past argument @@ -272,7 +267,7 @@ else fi # Set up environment based on runner. -if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "samza" || "$RUNNER" == "portable" || "$RUNNER" == "prism" ]]; then +if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "portable" || "$RUNNER" == "prism" ]]; then if [[ -z "$ENDPOINT" ]]; then JOB_PORT=$(python3 -c "$SOCKET_SCRIPT") ENDPOINT="localhost:$JOB_PORT" @@ -289,12 +284,6 @@ if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "samza" || "$ --job-port $JOB_PORT \ --expansion-port 0 \ --artifact-port 0 & - elif [[ "$RUNNER" == "samza" ]]; then - "$JAVA_CMD" \ - -jar $SAMZA_JOB_SERVER_JAR \ - --job-port $JOB_PORT \ - --expansion-port 0 \ - --artifact-port 0 & elif [[ "$RUNNER" == "spark" ]]; then "$JAVA_CMD" \ -jar $SPARK_JOB_SERVER_JAR \ @@ -354,7 +343,7 @@ if [[ "$RUNNER" != "direct" ]]; then fi # Disable parallelism on runners that don't support it. -if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" || "$RUNNER" == "samza" ]]; then +if [[ "$RUNNER" == "flink" || "$RUNNER" == "spark" ]]; then SIMULTANEOUS=1 fi diff --git a/sdks/java/build-tools/src/main/resources/beam/beam-codestyle.xml b/sdks/java/build-tools/src/main/resources/beam/beam-codestyle.xml index f722a0e2e525..3f913778ab71 100644 --- a/sdks/java/build-tools/src/main/resources/beam/beam-codestyle.xml +++ b/sdks/java/build-tools/src/main/resources/beam/beam-codestyle.xml @@ -192,7 +192,7 @@ - + @@ -214,7 +214,7 @@ - + @@ -284,7 +284,7 @@ - + diff --git a/sdks/java/container/Dockerfile b/sdks/java/container/Dockerfile index c43eb0cb8c02..7a8c9e7e7d7e 100644 --- a/sdks/java/container/Dockerfile +++ b/sdks/java/container/Dockerfile @@ -24,22 +24,21 @@ ARG TARGETARCH ARG pull_licenses -ADD target/slf4j-api.jar /opt/apache/beam/jars/ -ADD target/slf4j-jdk14.jar /opt/apache/beam/jars/ -ADD target/jcl-over-slf4j.jar /opt/apache/beam/jars/ -ADD target/log4j-over-slf4j.jar /opt/apache/beam/jars/ -ADD target/log4j-to-slf4j.jar /opt/apache/beam/jars/ -ADD target/beam-sdks-java-harness.jar /opt/apache/beam/jars/ - -# Required to use jamm as a javaagent to get accurate object size measuring -# COPY fails if file is not found, so use a wildcard for open-module-agent.jar -# since it is only included in Java 9+ containers -COPY target/jamm.jar target/open-module-agent.jar /opt/apache/beam/jars/ - -COPY target/${TARGETOS}_${TARGETARCH}/boot /opt/apache/beam/ - -COPY target/LICENSE /opt/apache/beam/ -COPY target/NOTICE /opt/apache/beam/ +# Dependency jars +COPY target/slf4j-api.jar \ + target/slf4j-jdk14.jar \ + target/jcl-over-slf4j.jar \ + target/log4j-over-slf4j.jar \ + target/log4j-to-slf4j.jar \ + # Required to use jamm as a javaagent to get accurate object size measuring + target/jamm.jar \ + /opt/apache/beam/jars/ + +# Built jars +COPY target/open-module-agent.jar target/beam-sdks-java-harness.jar /opt/apache/beam/jars/ + +# Built binary with licenses +COPY target/${TARGETOS}_${TARGETARCH}/boot target/LICENSE target/NOTICE /opt/apache/beam/ # copy third party licenses ADD target/third_party_licenses /opt/apache/beam/third_party_licenses/ diff --git a/sdks/java/container/boot.go b/sdks/java/container/boot.go index 8c918f231797..ad29f8d940ac 100644 --- a/sdks/java/container/boot.go +++ b/sdks/java/container/boot.go @@ -105,6 +105,9 @@ func main() { logger.Fatalf(ctx, "Failed to convert pipeline options: %v", err) } + // Inject artifact validation enabled state into context + ctx = artifact.WithArtifactValidation(ctx, !artifact.HasExperiment(info.GetPipelineOptions(), "disable_staged_file_integrity_checks")) + // (2) Retrieve the staged user jars. We ignore any disk limit, // because the staged jars are mandatory. @@ -192,9 +195,6 @@ func main() { "-XX:+UseParallelGC", "-XX:+AlwaysActAsServerClassMachine", "-XX:-OmitStackTraceInFastThrow", - // Crash and restart instead of throwing OutOfMemoryError which may be caught by user or - // framework code and leave things in a degraded state. - "-XX:+ExitOnOutOfMemoryError", } enableGoogleCloudProfiler := strings.Contains(options, enableGoogleCloudProfilerOption) @@ -224,18 +224,16 @@ func main() { args = append(args, jammAgentArgs) } - enableHeapDumpsOnOom := false // If heap dumping is enabled, configure the JVM to dump it on oom events. if pipelineOptions, ok := info.GetPipelineOptions().GetFields()["options"]; ok { if heapDumpOption, ok := pipelineOptions.GetStructValue().GetFields()["enableHeapDumps"]; ok { - enableHeapDumpsOnOom = heapDumpOption.GetBoolValue() + if heapDumpOption.GetBoolValue() { + args = append(args, "-XX:+HeapDumpOnOutOfMemoryError", + "-Dbeam.fn.heap_dump_dir="+filepath.Join(dir, "heapdumps"), + "-XX:HeapDumpPath="+filepath.Join(dir, "heapdumps", "heap_dump.hprof")) + } } } - if enableHeapDumpsOnOom { - args = append(args, "-XX:+HeapDumpOnOutOfMemoryError", - "-Dbeam.fn.heap_dump_dir="+filepath.Join(dir, "heapdumps"), - "-XX:HeapDumpPath="+filepath.Join(dir, "heapdumps", "heap_dump.hprof")) - } // Apply meta options const metaDir = "/opt/apache/beam/options" diff --git a/sdks/java/container/license_scripts/dep_urls_java.yaml b/sdks/java/container/license_scripts/dep_urls_java.yaml index 8b021e0347d4..589f7ee2d125 100644 --- a/sdks/java/container/license_scripts/dep_urls_java.yaml +++ b/sdks/java/container/license_scripts/dep_urls_java.yaml @@ -46,7 +46,7 @@ jaxen: '1.1.6': type: "3-Clause BSD" libraries-bom: - '26.80.0': + '26.83.0': license: "https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-opensource-java/master/LICENSE" type: "Apache License 2.0" paranamer: @@ -58,7 +58,7 @@ xz: '1.5': # The original repo is down. This license is taken from https://tukaani.org/xz/java.html. license: "file://{}/xz/COPYING" jackson-bom: - '2.15.4': + '2.18.6': license: "https://raw.githubusercontent.com/FasterXML/jackson-bom/master/LICENSE" type: "Apache License 2.0" org.eclipse.jgit: @@ -66,12 +66,12 @@ org.eclipse.jgit: license: "https://www.eclipse.org/org/documents/edl-v10.html" type: "Eclipse Distribution License - v1.0" opentelemetry-bom: - '1.51.0': - license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.51.0/LICENSE" + '1.57.0': + license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.57.0/LICENSE" type: "Apache License 2.0" opentelemetry-bom-alpha: - '1.51.0-alpha': - license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.51.0/LICENSE" + '1.57.0-alpha': + license: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/v1.57.0/LICENSE" type: "Apache License 2.0" zstd-jni: '1.5.2-5': diff --git a/sdks/java/core/build.gradle b/sdks/java/core/build.gradle index 3b2cbc81f08a..f532e9d14166 100644 --- a/sdks/java/core/build.gradle +++ b/sdks/java/core/build.gradle @@ -98,6 +98,7 @@ dependencies { shadow library.java.jackson_databind shadow platform(library.java.opentelemetry_bom) shadow library.java.opentelemetry_api + shadow library.java.opentelemetry_context shadow library.java.slf4j_api shadow library.java.snappy_java shadow library.java.joda_time diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/PipelineResult.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/PipelineResult.java index 91313f3924aa..46cca7833e52 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/PipelineResult.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/PipelineResult.java @@ -43,6 +43,19 @@ public interface PipelineResult { */ State cancel() throws IOException; + /** + * Drains the pipeline execution. + * + *

    Draining requests that the runner stop accepting new input and finish processing data that + * has already entered the pipeline. + * + * @throws IOException if there is a problem executing the drain request. + * @throws UnsupportedOperationException if the runner does not support draining. + */ + default State drain() throws IOException { + throw new UnsupportedOperationException("Runner does not support draining."); + } + /** * Waits until the pipeline finishes and returns the final status. It times out after the given * duration. 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/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/options/ExperimentalOptions.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/ExperimentalOptions.java index c0e2e1dcb48f..ee883ead5bb9 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/ExperimentalOptions.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/ExperimentalOptions.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.options; +import java.util.ArrayList; import java.util.List; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.checkerframework.checker.nullness.qual.Nullable; @@ -57,9 +58,7 @@ static boolean hasExperiment(PipelineOptions options, String experiment) { /** Adds experiment to options if not already present. */ static void addExperiment(ExperimentalOptions options, String experiment) { List experiments = options.getExperiments(); - if (experiments == null) { - experiments = Lists.newArrayList(); - } + experiments = (experiments == null) ? Lists.newArrayList() : new ArrayList<>(experiments); if (!experiments.contains(experiment)) { experiments.add(experiment); } 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 831dd69ec95f..7267dda9ed0b 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/options/SdkHarnessOptions.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/options/SdkHarnessOptions.java @@ -481,4 +481,10 @@ public OpenTelemetry create(PipelineOptions options) { return GlobalOpenTelemetry.get(); } } + + /** The hex-encoded SHA256 hash of the staged portable pipeline proto. */ + @Description("The hex-encoded SHA256 hash of the staged portable pipeline proto") + String getPipelineProtoHash(); + + void setPipelineProtoHash(String hash); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FieldValueTypeInformation.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FieldValueTypeInformation.java index 43aac6a5e20c..95030eda0988 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FieldValueTypeInformation.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FieldValueTypeInformation.java @@ -105,7 +105,11 @@ public abstract static class Builder { public abstract Builder setDescription(@Nullable String fieldDescription); - abstract FieldValueTypeInformation build(); + public abstract FieldValueTypeInformation build(); + } + + public static Builder builder() { + return new AutoValue_FieldValueTypeInformation.Builder(); } public static FieldValueTypeInformation forOneOf( @@ -311,7 +315,8 @@ public FieldValueTypeInformation withName(String name) { return toBuilder().setName(name).build(); } - static @Nullable FieldValueTypeInformation getIterableComponentType(TypeDescriptor valueType) { + public static @Nullable FieldValueTypeInformation getIterableComponentType( + TypeDescriptor valueType) { // TODO: Figure out nullable elements. TypeDescriptor componentType = ReflectUtils.getIterableComponentType(valueType); if (componentType == null) { @@ -331,13 +336,13 @@ public FieldValueTypeInformation withName(String name) { } // If the type is a map type, returns the key type, otherwise returns a null reference. - private static @Nullable FieldValueTypeInformation getMapKeyType( + public static @Nullable FieldValueTypeInformation getMapKeyType( TypeDescriptor typeDescriptor) { return getMapType(typeDescriptor, 0); } // If the type is a map type, returns the value type, otherwise returns a null reference. - private static @Nullable FieldValueTypeInformation getMapValueType( + public static @Nullable FieldValueTypeInformation getMapValueType( TypeDescriptor typeDescriptor) { return getMapType(typeDescriptor, 1); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FromRowUsingCreator.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FromRowUsingCreator.java index 464dc00cec7d..0f5c47f72591 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FromRowUsingCreator.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/FromRowUsingCreator.java @@ -82,10 +82,10 @@ public T apply(Row row) { return null; } if (row instanceof RowWithGetters) { - Object target = ((RowWithGetters) row).getGetterTarget(); - if (target.getClass().equals(typeDescriptor.getRawType())) { + RowWithGetters rowWithGetters = (RowWithGetters) row; + if (rowWithGetters.getGetterTargetType().equals(typeDescriptor)) { // Efficient path: simply extract the underlying object instead of creating a new one. - return (T) target; + return (T) rowWithGetters.getGetterTarget(); } } if (fieldConverters == null) { diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/GetterBasedSchemaProvider.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/GetterBasedSchemaProvider.java index e08f193d4072..7fb9e5a5dee5 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/GetterBasedSchemaProvider.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/GetterBasedSchemaProvider.java @@ -20,16 +20,20 @@ import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.Schema.LogicalType; import org.apache.beam.sdk.schemas.Schema.TypeName; import org.apache.beam.sdk.schemas.logicaltypes.EnumerationType; import org.apache.beam.sdk.schemas.logicaltypes.OneOfType; +import org.apache.beam.sdk.schemas.utils.ReflectUtils; import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.values.Row; import org.apache.beam.sdk.values.TypeDescriptor; @@ -117,9 +121,11 @@ private class ToRowWithValueGetters implements SerializableFunction { private final Schema schema; private final Factory>> getterFactory; + private final TypeDescriptor getterTargetType; - public ToRowWithValueGetters(Schema schema) { + public ToRowWithValueGetters(Schema schema, TypeDescriptor getterTargetType) { this.schema = schema; + this.getterTargetType = getterTargetType; // Since we know that this factory is always called from inside the lambda with the same // schema, return a caching factory that caches the first value seen for each class. This // prevents having to lookup the getter list each time createGetters is called. @@ -128,13 +134,13 @@ public ToRowWithValueGetters(Schema schema) { (Factory>>) (typeDescriptor, schema1) -> (List) - GetterBasedSchemaProvider.this.fieldValueGetters( - typeDescriptor, schema1)); + GetterBasedSchemaProvider.this.fieldValueGetters(typeDescriptor, schema1), + GetterBasedSchemaProvider.this::fieldValueTypeInformations); } @Override public Row apply(T input) { - return Row.withSchema(schema).withFieldValueGetters(getterFactory, input); + return Row.withSchema(schema).withFieldValueGetters(getterFactory, input, getterTargetType); } private GetterBasedSchemaProvider getOuter() { @@ -172,7 +178,7 @@ public SerializableFunction toRowFunction(TypeDescriptor typeDesc Verify.verifyNotNull( schemaFor(typeDescriptor), "can't create a ToRowFunction with null schema"); - return new ToRowWithValueGetters<>(schema); + return new ToRowWithValueGetters<>(schema, typeDescriptor); } @Override @@ -194,16 +200,21 @@ public boolean equals(@Nullable Object obj) { private static class RowValueGettersFactory implements Factory>> { private final Factory>> gettersFactory; + private final Factory> typeInfoFactory; private final @NotOnlyInitialized Factory>> cachingGettersFactory; static Factory>> of( - Factory>> gettersFactory) { - return new RowValueGettersFactory<>(gettersFactory).cachingGettersFactory; + Factory>> gettersFactory, + Factory> typeInfoFactory) { + return new RowValueGettersFactory(gettersFactory, typeInfoFactory).cachingGettersFactory; } - RowValueGettersFactory(Factory>> gettersFactory) { + RowValueGettersFactory( + Factory>> gettersFactory, + Factory> typeInfoFactory) { this.gettersFactory = gettersFactory; + this.typeInfoFactory = typeInfoFactory; this.cachingGettersFactory = new CachingFactory<>(this); } @@ -211,9 +222,17 @@ private static class RowValueGettersFactory public List> create( TypeDescriptor typeDescriptor, Schema schema) { List> getters = gettersFactory.create(typeDescriptor, schema); + Map typeInfoByName = + typeInfoFactory.create(typeDescriptor, schema).stream() + .collect(Collectors.toMap(FieldValueTypeInformation::getName, Function.identity())); List> rowGetters = new ArrayList<>(getters.size()); for (int i = 0; i < getters.size(); i++) { - rowGetters.add(rowValueGetter(getters.get(i), schema.getField(i).getType())); + FieldValueGetter getter = Verify.verifyNotNull(getters.get(i)); + rowGetters.add( + rowValueGetter( + getter, + schema.getField(i).getType(), + Verify.verifyNotNull(typeInfoByName.get(getter.name())).getType())); } return rowGetters; } @@ -229,26 +248,49 @@ && needsConversion(Verify.verifyNotNull(type.getCollectionElementType()))) || needsConversion(Verify.verifyNotNull(type.getMapValueType())))); } - FieldValueGetter rowValueGetter(FieldValueGetter base, FieldType type) { + FieldValueGetter rowValueGetter( + FieldValueGetter base, FieldType type, @Nullable TypeDescriptor getterReturnType) { TypeName typeName = type.getTypeName(); if (!needsConversion(type)) { return base; } if (typeName.equals(TypeName.ROW)) { - return new GetRow(base, Verify.verifyNotNull(type.getRowSchema()), cachingGettersFactory); - } else if (typeName.equals(TypeName.ARRAY)) { + return new GetRow( + base, + getterReturnType, + Verify.verifyNotNull(type.getRowSchema()), + cachingGettersFactory); + } else if (typeName.equals(TypeName.ARRAY) || typeName.equals(TypeName.ITERABLE)) { FieldType elementType = Verify.verifyNotNull(type.getCollectionElementType()); - return elementType.getTypeName().equals(TypeName.ROW) - ? new GetEagerCollection(base, converter(elementType)) - : new GetCollection(base, converter(elementType)); - } else if (typeName.equals(TypeName.ITERABLE)) { - return new GetIterable( - base, converter(Verify.verifyNotNull(type.getCollectionElementType()))); + TypeDescriptor elementTypeDescriptor = + Optional.ofNullable(getterReturnType) + .map(ReflectUtils::getIterableComponentType) + .orElse(null); + if (TypeName.ARRAY == typeName) { + return TypeName.ROW == elementType.getTypeName() + ? new GetEagerCollection(base, converter(elementType, elementTypeDescriptor)) + : new GetCollection(base, converter(elementType, elementTypeDescriptor)); + } else { // TypeName.ITERABLE + return new GetIterable(base, converter(elementType, elementTypeDescriptor)); + } } else if (typeName.equals(TypeName.MAP)) { + @Nullable + TypeDescriptor[] resolvedKeyValueTypes = + Optional.ofNullable(getterReturnType) + .<@Nullable TypeDescriptor[]>map( + getterType -> + Arrays.stream(Map.class.getTypeParameters()) + .<@Nullable TypeDescriptor>map( + typeVar -> { + TypeDescriptor resolved = getterType.resolveType(typeVar); + return resolved.hasUnresolvedParameters() ? null : resolved; + }) + .<@Nullable TypeDescriptor>toArray(TypeDescriptor[]::new)) + .orElse(new TypeDescriptor[] {null, null}); return new GetMap( base, - converter(Verify.verifyNotNull(type.getMapKeyType())), - converter(Verify.verifyNotNull(type.getMapValueType()))); + converter(Verify.verifyNotNull(type.getMapKeyType()), resolvedKeyValueTypes[0]), + converter(Verify.verifyNotNull(type.getMapValueType()), resolvedKeyValueTypes[1])); } else if (type.isLogicalType(OneOfType.IDENTIFIER)) { OneOfType oneOfType = type.getLogicalType(OneOfType.class); Schema oneOfSchema = oneOfType.getOneOfSchema(); @@ -258,7 +300,7 @@ FieldValueGetter rowValueGetter(FieldValueGetter base, FieldType type Maps.newHashMapWithExpectedSize(values.size()); for (Map.Entry kv : values.entrySet()) { FieldType fieldType = oneOfSchema.getField(kv.getKey()).getType(); - FieldValueGetter converter = converter(fieldType); + FieldValueGetter converter = converter(fieldType, null); converters.put(kv.getValue(), converter); } @@ -269,27 +311,35 @@ FieldValueGetter rowValueGetter(FieldValueGetter base, FieldType type return base; } - FieldValueGetter converter(FieldType type) { - return rowValueGetter(IDENTITY, type); + FieldValueGetter converter(FieldType type, @Nullable TypeDescriptor getterReturnType) { + return rowValueGetter(IDENTITY, type, getterReturnType); } static class GetRow extends Converter { final Schema schema; final Factory>> factory; + final @Nullable TypeDescriptor valueType; GetRow( FieldValueGetter getter, + @Nullable TypeDescriptor getterReturnType, Schema schema, Factory>> factory) { super(getter); this.schema = schema; this.factory = factory; + this.valueType = getterReturnType; } @Override Object convert(V value) { - return Row.withSchema(schema).withFieldValueGetters(factory, value); + return Row.withSchema(schema) + .withFieldValueGetters( + factory, + value, + Optional.ofNullable(valueType) + .orElse((TypeDescriptor) TypeDescriptor.of(value.getClass()))); } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AutoValueUtils.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AutoValueUtils.java index 78808fdc10c8..a9353bcaaacb 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AutoValueUtils.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AutoValueUtils.java @@ -162,7 +162,7 @@ private static String getAutoValueGeneratedName(String baseClass) { Optional> constructor = Arrays.stream(generatedTypeDescriptor.getRawType().getDeclaredConstructors()) .filter(c -> !Modifier.isPrivate(c.getModifiers())) - .filter(c -> matchConstructor(c, schemaTypes)) + .filter(c -> matchConstructor(generatedTypeDescriptor, c, schemaTypes)) .findAny(); return constructor .map( @@ -177,7 +177,9 @@ private static String getAutoValueGeneratedName(String baseClass) { } private static boolean matchConstructor( - Constructor constructor, List getterTypes) { + TypeDescriptor typeDescriptor, + Constructor constructor, + List getterTypes) { if (constructor.getParameters().length != getterTypes.size()) { return false; } @@ -197,7 +199,8 @@ private static boolean matchConstructor( // Verify that constructor parameters match (name and type) the inferred schema. for (Parameter parameter : constructor.getParameters()) { FieldValueTypeInformation type = typeMap.get(parameter.getName()); - if (type == null || type.getRawType() != parameter.getType()) { + if (type == null + || !type.getType().equals(typeDescriptor.resolveType(parameter.getParameterizedType()))) { valid = false; break; } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/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/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/testing/UsesSideInputsInTimer.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/UsesSideInputsInTimer.java new file mode 100644 index 000000000000..8320c1451d17 --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/UsesSideInputsInTimer.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.testing; + +import org.apache.beam.sdk.annotations.Internal; + +/** + * Category tag for validation tests which use sideinputs in OnTimer and OnWindowExpiration. Tests + * tagged with {@link UsesSideInputsInTimer} should be run for runners which support sideinputs. + */ +@Internal +public class UsesSideInputsInTimer {} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/AsyncWrapper.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/AsyncWrapper.java new file mode 100644 index 000000000000..f6272ba4fe67 --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/AsyncWrapper.java @@ -0,0 +1,779 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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); + if (inFlight.future.isDone()) { + try { + if (!inFlight.future.isCancelled()) { + toReturn.add(inFlight.future.get()); + } + + finishedElementIds.add(elementId); + activeElements.remove(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 new file mode 100644 index 000000000000..d410c5be6007 --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/BatchElements.java @@ -0,0 +1,608 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 java.util.Collections.singleton; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import javax.annotation.Nullable; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.GlobalWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.WindowingStrategy; + +/** + * A {@link PTransform} that batches elements for amortized processing. + * + *

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

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

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

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

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

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

    Example

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

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

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

    This represents the desired ratio: + * + *

    fixed_cost / total_time + * + *

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

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

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

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

    Maintains separate batches for each active window and emits batches when they reach the + * target size or when windows are evicted. + */ + @SuppressWarnings("initialization") + static class WindowAwareBatchingDoFn extends DoFn> { + private transient BatchSizeEstimator estimator; + private final BatchConfig config; + private transient Map> batches; + private int targetBatchSize; + + private static final int MAX_LIVE_WINDOWS = 10; + + private static class SizedBatch implements Serializable { + List elements = new ArrayList<>(); + int size = 0; + } + + private WindowAwareBatchingDoFn(BatchConfig config) { + this.config = config; + } + + @Setup + public void setup() { + estimator = new BatchSizeEstimator(config); + } + + @StartBundle + public void startBundle() { + batches = new HashMap<>(); + targetBatchSize = estimator.nextBatchSize(); + } + + @ProcessElement + public void processElement( + @Element T element, BoundedWindow window, OutputReceiver> receiver) { + + // get or create batch for this window + SizedBatch batch = batches.computeIfAbsent(window, w -> new SizedBatch<>()); + + int elementSize = 1; + + // emit if this window's batch is full + if (batch.size + elementSize > targetBatchSize) { + try (BatchSizeEstimator.Stopwatch sw = estimator.recordTime(batch.size)) { + receiver.output(batch.elements); + } + batches.remove(window); + targetBatchSize = estimator.nextBatchSize(); + // create fresh batch for this window after emit + batch = batches.computeIfAbsent(window, w -> new SizedBatch<>()); + } + + batch.elements.add(element); + batch.size += elementSize; + + // evict largest window if too many live windows + if (batches.size() > MAX_LIVE_WINDOWS) { + Map.Entry> largest = + batches.entrySet().stream().max(Comparator.comparingInt(e -> e.getValue().size)).get(); + + BoundedWindow targetWindow = largest.getKey(); + SizedBatch targetBatch = largest.getValue(); + + try (BatchSizeEstimator.Stopwatch sw = estimator.recordTime(targetBatch.size)) { + + receiver.outputWindowedValue( + targetBatch.elements, + targetWindow.maxTimestamp(), + singleton(targetWindow), + PaneInfo.NO_FIRING); + } + + batches.remove(targetWindow); + targetBatchSize = estimator.nextBatchSize(); + } + } + + @FinishBundle + public void finishBundle(FinishBundleContext context) { + for (Map.Entry> entry : batches.entrySet()) { + BoundedWindow window = entry.getKey(); + SizedBatch batch = entry.getValue(); + if (!batch.elements.isEmpty()) { + try (BatchSizeEstimator.Stopwatch sw = estimator.recordTime(batch.size)) { + context.output(batch.elements, window.maxTimestamp(), window); + } + } + } + } + } + + @Override + public PCollection> expand(PCollection input) { + if (input.getWindowingStrategy().equals(WindowingStrategy.globalDefault())) { + return input.apply(ParDo.of(new GlobalWindowsBatchingDoFn<>(config))); + } else { + return input.apply(ParDo.of(new WindowAwareBatchingDoFn<>(config))); + } + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFn.java index 0d892ab12d33..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. */ @@ -595,8 +629,10 @@ public interface MultiOutputReceiver { * *

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

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

  • If one of its arguments is tagged with the {@link Element} annotation, then it will be * passed the current element being processed. The argument type must match the input type * of this DoFn exactly, or both types must have equivalent schemas registered. + *
  • If one of its arguments is tagged with the {@link CurrentRecordId} annotation, then it + * will be passed the record id of the current element being processed; the argument must be + * of type {@link String}. + *
  • If one of its arguments is tagged with the {@link CurrentRecordOffset} annotation, then + * it will be passed the record offset of the current element being processed; the argument + * must be of type {@link Long}. *
  • If one of its arguments is tagged with the {@link Timestamp} annotation, then it will be * passed the timestamp of the current element being processed; the argument must be of type * {@link Instant}. @@ -795,6 +840,12 @@ public interface MultiOutputReceiver { *
  • If one of its arguments is tagged with the {@link Timestamp} annotation, then it will be * passed the timestamp of the current element being processed; the argument must be of type * {@link Instant}. + *
  • If one of its arguments is tagged with the {@link CurrentRecordId} annotation, then it + * will be passed the record ID of the current element being processed; the argument must be + * of type {@link String}. + *
  • If one of its arguments is tagged with the {@link CurrentRecordOffset} annotation, then + * it will be passed the record offset of the current element being processed; the argument + * must be of type {@link Long}. *
  • If one of its arguments is of the type {@link WatermarkEstimator}, then it will be passed * the watermark estimator. *
  • If one of its arguments is of the type {@link ManualWatermarkEstimator}, then it will be @@ -838,6 +889,18 @@ public interface MultiOutputReceiver { @Target(ElementType.PARAMETER) public @interface Element {} + /** Parameter annotation for the input element record id for {@link ProcessElement}. */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface CurrentRecordId {} + + /** Parameter annotation for the input element record offset for {@link ProcessElement}. */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface CurrentRecordOffset {} + /** * Parameter annotation for the restriction for {@link GetSize}, {@link SplitRestriction}, {@link * GetInitialWatermarkEstimatorState}, {@link NewWatermarkEstimator}, and {@link NewTracker} @@ -860,6 +923,12 @@ public interface MultiOutputReceiver { @Target(ElementType.PARAMETER) public @interface Timestamp {} + /** Parameter annotation for the firing timestamp for {@link OnTimer}. */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface FireTimestamp {} + /** Parameter annotation for the SideInput for a {@link ProcessElement} method. */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/DoFnTester.java index 4de2c3d2c9c0..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; @@ -502,7 +503,9 @@ public void output(TupleTag tag, T output, Instant timestamp, BoundedWind PaneInfo.NO_FIRING, null, null, - CausedByDrain.NORMAL)); + CausedByDrain.NORMAL, + null, + ValueKind.INSERT)); } }; } @@ -606,6 +609,11 @@ public CausedByDrain causedByDrain() { return element.getCausedByDrain(); } + @Override + public ValueKind valueKind() { + return element.getValueKind(); + } + @Override public PipelineOptions getPipelineOptions() { return options; @@ -646,7 +654,9 @@ public void outputWithTimestamp(TupleTag tag, T output, Instant timestamp element.getPaneInfo(), null, null, - CausedByDrain.NORMAL)); + CausedByDrain.NORMAL, + element.getOpenTelemetryContext(), + ValueKind.INSERT)); } @Override @@ -666,7 +676,9 @@ public void outputWindowedValue(TupleTag tag, WindowedValue windowedVa windowedValue.getPaneInfo(), windowedValue.getRecordId(), windowedValue.getRecordOffset(), - windowedValue.causedByDrain())); + windowedValue.causedByDrain(), + windowedValue.getOpenTelemetryContext(), + windowedValue.getValueKind())); } } @@ -681,7 +693,15 @@ public void outputWindowedValue( getMutableOutput(tag) .add( ValueInSingleWindow.of( - output, timestamp, w, paneInfo, null, null, CausedByDrain.NORMAL)); + output, + timestamp, + w, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + 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..5463365e4c6e 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Redistribute.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Redistribute.java @@ -188,6 +188,7 @@ public void processElement( .setWindow(kv.getValue().getWindow()) .setPaneInfo(kv.getValue().getPaneInfo()) .setCausedByDrain(kv.getValue().getCausedByDrain()) + .setValueKind(kv.getValue().getValueKind()) .output(); } })); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java index 9974d29bfa0f..b1288c054142 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reify.java @@ -30,6 +30,7 @@ import org.apache.beam.sdk.values.TimestampedValue; import org.apache.beam.sdk.values.TimestampedValue.TimestampedValueCoder; import org.apache.beam.sdk.values.ValueInSingleWindow; +import org.apache.beam.sdk.values.ValueKind; import org.joda.time.Duration; import org.joda.time.Instant; @@ -148,6 +149,7 @@ public void processElement( BoundedWindow window, PaneInfo paneInfo, CausedByDrain causedByDrain, + ValueKind valueKind, OutputReceiver>> r) { r.output( KV.of( @@ -159,7 +161,9 @@ public void processElement( paneInfo, pc.currentRecordId(), pc.currentRecordOffset(), - causedByDrain))); + causedByDrain, + null, + valueKind))); } })) .setCoder( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reshuffle.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reshuffle.java index 0a8d058107b8..594bdfc85039 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reshuffle.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/Reshuffle.java @@ -190,6 +190,7 @@ public void processElement( .setTimestamp(kv.getValue().getTimestamp()) .setWindow(kv.getValue().getWindow()) .setPaneInfo(kv.getValue().getPaneInfo()) + .setValueKind(kv.getValue().getValueKind()) .output(); } })); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java index 54d630d92fe4..c08243fda5c0 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/ByteBuddyDoFnInvokerFactory.java @@ -77,8 +77,11 @@ import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.BundleFinalizerParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.Cases; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CausedByDrainParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CurrentRecordIdParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CurrentRecordOffsetParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.ElementParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.FinishBundleContextParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.FireTimestampParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.OnTimerContextParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.OutputReceiverParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.PaneInfoParameter; @@ -94,6 +97,7 @@ import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.TimerFamilyParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.TimerParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.TimestampParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.ValueKindParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.WatermarkEstimatorParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.WatermarkEstimatorStateParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.WindowParameter; @@ -128,12 +132,18 @@ class ByteBuddyDoFnInvokerFactory implements DoFnInvokerFactory { public static final String SCHEMA_ELEMENT_PARAMETER_METHOD = "schemaElement"; public static final String TIMESTAMP_PARAMETER_METHOD = "timestamp"; public static final String CAUSED_BY_DRAIN_PARAMETER_METHOD = "causedByDrain"; + public static final String CURRENT_RECORD_ID_PARAMETER_METHOD = "currentRecordId"; + public static final String CURRENT_RECORD_OFFSET_PARAMETER_METHOD = "currentRecordOffset"; + public static final String FIRE_TIMESTAMP_PARAMETER_METHOD = "fireTimestamp"; + public static final String 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"; @@ -1111,6 +1121,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); @@ -1153,6 +1172,16 @@ public StackManipulation dispatch(OnTimerContextParameter p) { ON_TIMER_CONTEXT_PARAMETER_METHOD, DoFn.class))); } + @Override + public StackManipulation dispatch( + DoFnSignature.Parameter.OnWindowExpirationContextParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + ON_WINDOW_EXPIRATION_CONTEXT_PARAMETER_METHOD, DoFn.class))); + } + @Override public StackManipulation dispatch(WindowParameter p) { return new StackManipulation.Compound( @@ -1265,6 +1294,33 @@ public StackManipulation dispatch(DoFnSignature.Parameter.TimerIdParameter p) { TIMER_ID_PARAMETER_METHOD, DoFn.class))); } + @Override + public StackManipulation dispatch(CurrentRecordIdParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + CURRENT_RECORD_ID_PARAMETER_METHOD, DoFn.class))); + } + + @Override + public StackManipulation dispatch(CurrentRecordOffsetParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + CURRENT_RECORD_OFFSET_PARAMETER_METHOD, DoFn.class))); + } + + @Override + public StackManipulation dispatch(FireTimestampParameter p) { + return new StackManipulation.Compound( + pushDelegate, + MethodInvocation.invoke( + getExtraContextFactoryMethodDescription( + FIRE_TIMESTAMP_PARAMETER_METHOD, DoFn.class))); + } + @Override public StackManipulation dispatch(DoFnSignature.Parameter.KeyParameter p) { return new StackManipulation.Compound( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnInvoker.java index a615761292aa..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); @@ -218,9 +223,23 @@ interface ArgumentProvider { /** Provide a reference to the input element timestamp. */ Instant timestamp(DoFn doFn); + /** Provide a reference to the record id of the current element. */ + @Nullable + String currentRecordId(DoFn doFn); + + /** Provide a reference to the record offset of the current element. */ + @Nullable + Long currentRecordOffset(DoFn doFn); + + /** Provide a reference to the firing timestamp of the current timer. */ + Instant fireTimestamp(DoFn doFn); + /** Provide a reference to the caused by drain. */ CausedByDrain causedByDrain(DoFn doFn); + /** 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); @@ -329,12 +348,36 @@ public Instant timestamp(DoFn doFn) { String.format("Timestamp unsupported in %s", getErrorContext())); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + throw new UnsupportedOperationException( + String.format("RecordId unsupported in %s", getErrorContext())); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + throw new UnsupportedOperationException( + String.format("RecordOffset unsupported in %s", getErrorContext())); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new UnsupportedOperationException( + String.format("FireTimestamp unsupported in %s", getErrorContext())); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { throw new UnsupportedOperationException( 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( @@ -408,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( @@ -499,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); @@ -524,11 +580,31 @@ public Instant timestamp(DoFn doFn) { return delegate.timestamp(doFn); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + return delegate.currentRecordId(doFn); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + return delegate.currentRecordOffset(doFn); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + return delegate.fireTimestamp(doFn); + } + @Override public CausedByDrain causedByDrain(DoFn doFn) { return delegate.causedByDrain(doFn); } + @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 83c2a67655bd..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) { @@ -345,6 +347,14 @@ public ResultT match(Cases cases) { return cases.dispatch((BundleFinalizerParameter) this); } else if (this instanceof CausedByDrainParameter) { return cases.dispatch((CausedByDrainParameter) this); + } else if (this instanceof CurrentRecordIdParameter) { + return cases.dispatch((CurrentRecordIdParameter) this); + } else if (this instanceof CurrentRecordOffsetParameter) { + return cases.dispatch((CurrentRecordOffsetParameter) this); + } else if (this instanceof FireTimestampParameter) { + return cases.dispatch((FireTimestampParameter) this); + } else if (this instanceof ValueKindParameter) { + return cases.dispatch((ValueKindParameter) this); } else if (this instanceof KeyParameter) { return cases.dispatch((KeyParameter) this); } else { @@ -375,8 +385,16 @@ public interface Cases { ResultT dispatch(TaggedOutputReceiverParameter p); + ResultT dispatch(CurrentRecordIdParameter p); + + ResultT dispatch(CurrentRecordOffsetParameter p); + + ResultT dispatch(FireTimestampParameter p); + ResultT dispatch(OnTimerContextParameter p); + ResultT dispatch(OnWindowExpirationContextParameter p); + ResultT dispatch(WindowParameter p); ResultT dispatch(PaneInfoParameter p); @@ -405,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. */ @@ -462,11 +482,31 @@ public ResultT dispatch(TimeDomainParameter p) { return dispatchDefault(p); } + @Override + public ResultT dispatch(CurrentRecordIdParameter p) { + return dispatchDefault(p); + } + + @Override + public ResultT dispatch(CurrentRecordOffsetParameter p) { + return dispatchDefault(p); + } + + @Override + public ResultT dispatch(FireTimestampParameter p) { + return dispatchDefault(p); + } + @Override public ResultT dispatch(OnTimerContextParameter p) { return dispatchDefault(p); } + @Override + public ResultT dispatch(OnWindowExpirationContextParameter p) { + return dispatchDefault(p); + } + @Override public ResultT dispatch(WindowParameter p) { return dispatchDefault(p); @@ -507,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); @@ -564,8 +609,16 @@ 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 = + new AutoValue_DoFnSignature_Parameter_CurrentRecordIdParameter(); + private static final CurrentRecordOffsetParameter CURRENT_RECORD_OFFSET_PARAMETER = + new AutoValue_DoFnSignature_Parameter_CurrentRecordOffsetParameter(); + private static final FireTimestampParameter FIRE_TIMESTAMP_PARAMETER = + new AutoValue_DoFnSignature_Parameter_FireTimestampParameter(); /** Returns a {@link ProcessContextParameter}. */ public static ProcessContextParameter processContext() { @@ -592,6 +645,26 @@ 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; + } + + /** Returns a {@link CurrentRecordOffsetParameter}. */ + public static CurrentRecordOffsetParameter currentRecordOffsetParameter() { + return CURRENT_RECORD_OFFSET_PARAMETER; + } + + /** Returns a {@link FireTimestampParameter}. */ + public static FireTimestampParameter fireTimestampParameter() { + return FIRE_TIMESTAMP_PARAMETER; + } + public static ElementParameter elementParameter(TypeDescriptor elementT) { return new AutoValue_DoFnSignature_Parameter_ElementParameter(elementT); } @@ -754,6 +827,46 @@ 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}. + * + *

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

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

    All such descriptors are equal. + */ + @AutoValue + public abstract static class FireTimestampParameter extends Parameter { + FireTimestampParameter() {} + } + /** * Descriptor for a {@link Parameter} of type {@link DoFn.Element}. * diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java index 3dcf7ff1f9d0..9f3491bca7b9 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/transforms/reflect/DoFnSignatures.java @@ -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; @@ -140,7 +141,10 @@ private DoFnSignatures() {} Parameter.StateParameter.class, Parameter.SideInputParameter.class, Parameter.TimerFamilyParameter.class, + Parameter.CurrentRecordIdParameter.class, + Parameter.CurrentRecordOffsetParameter.class, Parameter.CausedByDrainParameter.class, + Parameter.ValueKindParameter.class, Parameter.BundleFinalizerParameter.class); private static final ImmutableList> @@ -157,7 +161,10 @@ private DoFnSignatures() {} Parameter.RestrictionTrackerParameter.class, Parameter.WatermarkEstimatorParameter.class, Parameter.SideInputParameter.class, + Parameter.CurrentRecordIdParameter.class, + Parameter.CurrentRecordOffsetParameter.class, Parameter.CausedByDrainParameter.class, + Parameter.ValueKindParameter.class, Parameter.BundleFinalizerParameter.class); private static final ImmutableList> ALLOWED_SETUP_PARAMETERS = @@ -188,8 +195,10 @@ private DoFnSignatures() {} Parameter.StateParameter.class, Parameter.TimerFamilyParameter.class, 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 = @@ -205,8 +214,10 @@ private DoFnSignatures() {} Parameter.StateParameter.class, Parameter.TimerFamilyParameter.class, 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 = @@ -217,7 +228,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 = @@ -1348,6 +1361,19 @@ private static Parameter analyzeExtraParameter( rawType.equals(Instant.class), "@Timestamp argument must have type org.joda.time.Instant."); return Parameter.timestampParameter(); + } else if (hasAnnotation(DoFn.CurrentRecordId.class, param.getAnnotations())) { + methodErrors.checkArgument( + rawType.equals(String.class), "@CurrentRecordId argument must have type String."); + return Parameter.currentRecordIdParameter(); + } else if (hasAnnotation(DoFn.CurrentRecordOffset.class, param.getAnnotations())) { + methodErrors.checkArgument( + rawType.equals(Long.class), "@CurrentRecordOffset argument must have type Long."); + return Parameter.currentRecordOffsetParameter(); + } else if (hasAnnotation(DoFn.FireTimestamp.class, param.getAnnotations())) { + methodErrors.checkArgument( + rawType.equals(Instant.class), + "@FireTimestamp argument must have type org.joda.time.Instant."); + return Parameter.fireTimestampParameter(); } else if (hasAnnotation(DoFn.Key.class, param.getAnnotations())) { methodErrors.checkArgument( KV.class.equals(inputT.getRawType()), @@ -1367,6 +1393,11 @@ private static Parameter analyzeExtraParameter( rawType.equals(CausedByDrain.class), "CausedByDrain argument must have type org.apache.beam.sdk.values.CausedByDrain."); return Parameter.causedByDrainParameter(); + } else if (ValueKind.class.isAssignableFrom(rawType)) { + methodErrors.checkArgument( + rawType.equals(ValueKind.class), + "ValueKind argument must have type org.apache.beam.sdk.values.ValueKind."); + return Parameter.valueKindParameter(); } else if (hasAnnotation(DoFn.SideInput.class, param.getAnnotations())) { String sideInputId = getSideInputId(param.getAnnotations()); paramErrors.checkArgument( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/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/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 5adf3d32a1a9..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(); @@ -688,6 +705,21 @@ public Long currentRecordOffset() { return outerContext.currentRecordOffset(); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + return outerContext.currentRecordId(); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + return outerContext.currentRecordOffset(); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new IllegalStateException(); + } + @Override public Object watermarkEstimatorState() { throw new UnsupportedOperationException( diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OpenTelemetryContextPropagator.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OpenTelemetryContextPropagator.java new file mode 100644 index 000000000000..20120bf2172d --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OpenTelemetryContextPropagator.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.values; + +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import org.apache.beam.model.fnexecution.v1.BeamFnApi; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.checkerframework.checker.nullness.qual.Nullable; + +class OpenTelemetryContextPropagator { + + private static final TextMapSetter SETTER = + (carrier, key, value) -> { + if (carrier == null) { + return; + } + if ("traceparent".equals(key)) { + carrier.setTraceparent(value); + } else if ("tracestate".equals(key)) { + carrier.setTracestate(value); + } + }; + + private static final TextMapGetter GETTER = + new TextMapGetter() { + @Override + public Iterable keys(BeamFnApi.Elements.ElementMetadata carrier) { + return Lists.newArrayList("traceparent", "tracestate"); + } + + @Override + public @Nullable String get( + BeamFnApi.Elements.@Nullable ElementMetadata carrier, String key) { + if (carrier == null) { + return null; + } + if ("traceparent".equals(key)) { + return carrier.getTraceparent(); + } else if ("tracestate".equals(key)) { + return carrier.getTracestate(); + } + return null; + } + }; + + static void set(Context from, BeamFnApi.Elements.ElementMetadata.Builder builder) { + W3CTraceContextPropagator.getInstance().inject(from, builder, SETTER); + } + + static Context read(BeamFnApi.Elements.ElementMetadata from) { + return W3CTraceContextPropagator.getInstance().extract(Context.root(), from, GETTER); + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java index 67473e567d63..c08942405721 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/OutputBuilder.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.values; +import io.opentelemetry.context.Context; import java.util.Collection; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; @@ -50,5 +51,9 @@ public interface OutputBuilder extends WindowedValue { OutputBuilder setCausedByDrain(CausedByDrain causedByDrain); + OutputBuilder setOpenTelemetryContext(@Nullable Context openTelemetryContext); + + OutputBuilder setValueKind(ValueKind valueKind); + void output(); } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/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/ValueInSingleWindow.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java index 0d8b2f7515e8..ef7b40aabe44 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueInSingleWindow.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.values; import com.google.auto.value.AutoValue; +import io.opentelemetry.context.Context; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -68,6 +69,10 @@ public T getValue() { public abstract CausedByDrain getCausedByDrain(); + public abstract @Nullable Context getOpenTelemetryContext(); + + public abstract ValueKind getValueKind(); + // todo #33176 specify additional metadata in the future public static ValueInSingleWindow of( T value, @@ -76,14 +81,54 @@ public static ValueInSingleWindow of( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext) { + return of( + value, + timestamp, + window, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + ValueKind.INSERT); + } + + public static ValueInSingleWindow of( + T value, + Instant timestamp, + BoundedWindow window, + PaneInfo paneInfo, + @Nullable String currentRecordId, + @Nullable Long currentRecordOffset, + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext, + ValueKind valueKind) { return new AutoValue_ValueInSingleWindow<>( - value, timestamp, window, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + timestamp, + window, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + valueKind); } public static ValueInSingleWindow of( T value, Instant timestamp, BoundedWindow window, PaneInfo paneInfo) { - return of(value, timestamp, window, paneInfo, null, null, CausedByDrain.NORMAL); + return of( + value, + timestamp, + window, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT); } /** A coder for {@link ValueInSingleWindow}. */ @@ -108,11 +153,14 @@ public static Coder of( @Override public void encode(ValueInSingleWindow windowedElem, OutputStream outStream) throws IOException { - encode(windowedElem, outStream, Context.NESTED); + encode(windowedElem, outStream, org.apache.beam.sdk.coders.Coder.Context.NESTED); } @Override - public void encode(ValueInSingleWindow windowedElem, OutputStream outStream, Context context) + public void encode( + ValueInSingleWindow windowedElem, + OutputStream outStream, + org.apache.beam.sdk.coders.Coder.Context context) throws IOException { InstantCoder.of().encode(windowedElem.getTimestamp(), outStream); windowCoder.encode(windowedElem.getWindow(), outStream); @@ -127,6 +175,12 @@ public void encode(ValueInSingleWindow windowedElem, OutputStream outStream, windowedElem.getCausedByDrain() == CausedByDrain.CAUSED_BY_DRAIN ? BeamFnApi.Elements.DrainMode.Enum.DRAINING : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING); + io.opentelemetry.context.Context openTelemetryContext = + windowedElem.getOpenTelemetryContext(); + if (openTelemetryContext != null) { + OpenTelemetryContextPropagator.set(openTelemetryContext, builder); + } + builder.setValueKind(ValueKindUtil.toProto(windowedElem.getValueKind())); BeamFnApi.Elements.ElementMetadata metadata = builder.build(); ByteArrayCoder.of().encode(metadata.toByteArray(), outStream); } @@ -136,16 +190,19 @@ public void encode(ValueInSingleWindow windowedElem, OutputStream outStream, @Override public ValueInSingleWindow decode(InputStream inStream) throws IOException { - return decode(inStream, Context.NESTED); + return decode(inStream, org.apache.beam.sdk.coders.Coder.Context.NESTED); } @Override @SuppressWarnings("IgnoredPureGetter") - public ValueInSingleWindow decode(InputStream inStream, Context context) throws IOException { + public ValueInSingleWindow decode( + InputStream inStream, org.apache.beam.sdk.coders.Coder.Context context) throws IOException { Instant timestamp = InstantCoder.of().decode(inStream); BoundedWindow window = windowCoder.decode(inStream); PaneInfo paneInfo = PaneInfo.PaneInfoCoder.INSTANCE.decode(inStream); CausedByDrain causedByDrain = CausedByDrain.NORMAL; + io.opentelemetry.context.@Nullable Context openTelemetryContext = null; + ValueKind valueKind = ValueKind.INSERT; if (WindowedValues.WindowedValueCoder.isMetadataSupported() && paneInfo.isElementMetadata()) { BeamFnApi.Elements.ElementMetadata elementMetadata = BeamFnApi.Elements.ElementMetadata.parseFrom(ByteArrayCoder.of().decode(inStream)); @@ -153,12 +210,22 @@ public ValueInSingleWindow decode(InputStream inStream, Context context) thro elementMetadata.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; + openTelemetryContext = OpenTelemetryContextPropagator.read(elementMetadata); + valueKind = ValueKindUtil.fromProto(elementMetadata.getValueKind()); } T value = valueCoder.decode(inStream, context); // todo #33176 specify additional metadata in the future return new AutoValue_ValueInSingleWindow<>( - value, timestamp, window, paneInfo, null, null, causedByDrain); + value, + timestamp, + window, + paneInfo, + null, + null, + causedByDrain, + openTelemetryContext, + valueKind); } @Override diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/UpdatingCombineFn.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKind.java similarity index 53% rename from runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/UpdatingCombineFn.java rename to sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKind.java index f0a7e5e1ceaa..7a190496ca2c 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/transforms/UpdatingCombineFn.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKind.java @@ -15,21 +15,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.runners.samza.transforms; +package org.apache.beam.sdk.values; -import org.apache.beam.sdk.transforms.Combine; +/** The type of change operation represented by a Change Data Capture (CDC) record. */ +public enum ValueKind { + /** Indicates a new record was created in the source system. */ + INSERT, -/** - * Currently Beam only supports either throw away the accumulation or keep it after firing. This - * CombineFn allows more flexibility to update the accumulation. - */ -public abstract class UpdatingCombineFn - extends Combine.CombineFn { + /** + * Indicates the state of a record immediately before an update occurred. This is typically + * used to identify the previous values of modified columns or to locate the record via its + * primary key. + */ + UPDATE_BEFORE, /** - * Returns an updated accumulator from the given accumulator after firing a window pane. - * - *

    For efficiency, the input accumulator may be modified and returned. + * Indicates the state of a record immediately after an update occurred. Represents the + * current, valid state of the record following the change. */ - public abstract AccumT updateAfterFiring(AccumT accumulator); + UPDATE_AFTER, + + /** Indicates that an existing record was removed from the source system. */ + DELETE } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKindUtil.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKindUtil.java new file mode 100644 index 000000000000..46ceb8f6247a --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/ValueKindUtil.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.values; + +import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; + +/** Utility class for converting between {@link ValueKind} and {@link Elements.ValueKind.Enum}. */ +public final class ValueKindUtil { + public static Elements.ValueKind.Enum toProto(ValueKind valueKind) { + switch (valueKind) { + case INSERT: + return Elements.ValueKind.Enum.INSERT; + case UPDATE_BEFORE: + return Elements.ValueKind.Enum.UPDATE_BEFORE; + case UPDATE_AFTER: + return Elements.ValueKind.Enum.UPDATE_AFTER; + case DELETE: + return Elements.ValueKind.Enum.DELETE; + default: + throw new IllegalArgumentException("Unknown ValueKind: " + valueKind); + } + } + + public static ValueKind fromProto(Elements.ValueKind.Enum proto) { + switch (proto) { + case VALUE_KIND_UNSPECIFIED: + case INSERT: + return ValueKind.INSERT; + case UPDATE_BEFORE: + return ValueKind.UPDATE_BEFORE; + case UPDATE_AFTER: + return ValueKind.UPDATE_AFTER; + case DELETE: + return ValueKind.DELETE; + default: + throw new IllegalArgumentException("Unknown ValueKind: " + proto); + } + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java index daebeb31a39c..13796018d7aa 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValue.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.values; +import io.opentelemetry.context.Context; import java.util.Collection; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.PaneInfo; @@ -49,11 +50,18 @@ public interface WindowedValue { @Nullable String getRecordId(); + @Nullable + Context getOpenTelemetryContext(); + @Nullable Long getRecordOffset(); CausedByDrain causedByDrain(); + /** Returns the {@link ValueKind} associated with this WindowedValue. */ + @Pure + ValueKind getValueKind(); + /** * A representation of each of the actual values represented by this compressed {@link * WindowedValue}, one per window. diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java index ba2720f5e39b..43db94dffb61 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/values/WindowedValues.java @@ -22,6 +22,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; +import io.opentelemetry.context.Context; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -81,7 +82,9 @@ public static Builder builder(WindowedValue template) { .setPaneInfo(template.getPaneInfo()) .setRecordOffset(template.getRecordOffset()) .setRecordId(template.getRecordId()) - .setCausedByDrain(template.causedByDrain()); + .setCausedByDrain(template.causedByDrain()) + .setOpenTelemetryContext(template.getOpenTelemetryContext()) + .setValueKind(template.getValueKind()); } public static class Builder implements OutputBuilder { @@ -104,6 +107,8 @@ 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 public Builder setValue(T value) { @@ -154,6 +159,19 @@ public Builder setCausedByDrain(CausedByDrain causedByDrain) { return this; } + @Override + public Builder setOpenTelemetryContext(@Nullable Context openTelemetryContext) { + this.openTelemetryContext = openTelemetryContext; + return this; + } + + @Override + public Builder setValueKind(ValueKind valueKind) { + checkStateNotNull(valueKind, "ValueKind is null"); + this.valueKind = valueKind; + return this; + } + public Builder setReceiver(WindowedValueReceiver receiver) { this.receiver = receiver; return this; @@ -180,6 +198,11 @@ public Instant getTimestamp() { return timestamp; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return openTelemetryContext; + } + @Override public Collection getWindows() { checkStateNotNull(windows, "Windows not set"); @@ -208,6 +231,12 @@ public CausedByDrain causedByDrain() { return causedByDrain; } + @Override + public ValueKind getValueKind() { + checkStateNotNull(valueKind, "ValueKind not set"); + return valueKind; + } + @Override public Collection> explodeWindows() { throw new UnsupportedOperationException( @@ -243,7 +272,9 @@ public WindowedValue build() { getPaneInfo(), getRecordId(), getRecordOffset(), - causedByDrain()); + causedByDrain(), + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -254,6 +285,7 @@ public String toString() { .add("windows", getWindows()) .add("paneInfo", getPaneInfo()) .add("causedByDrain", causedByDrain()) + .add("valueKind", getValueKind()) .add("receiver", receiver) .toString(); } @@ -261,7 +293,16 @@ public String toString() { public static WindowedValue of( T value, Instant timestamp, Collection windows, PaneInfo paneInfo) { - return of(value, timestamp, windows, paneInfo, null, null, CausedByDrain.NORMAL); + return of( + value, + timestamp, + windows, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT); } /** Returns a {@code WindowedValue} with the given value, timestamp, and windows. */ @@ -272,10 +313,13 @@ public static WindowedValue of( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext, + ValueKind valueKind) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); checkArgument(windows.size() > 0, "WindowedValue requires windows, but there were none"); checkArgument(causedByDrain != null, "WindowedValue requires CausedByDrain, but it was null"); + checkArgument(valueKind != null, "WindowedValue requires ValueKind, but it was null"); if (windows.size() == 1) { return of( value, @@ -284,10 +328,20 @@ public static WindowedValue of( paneInfo, currentRecordId, currentRecordOffset, - causedByDrain); + causedByDrain, + openTelemetryContext, + valueKind); } else { return new TimestampedValueInMultipleWindows<>( - value, timestamp, windows, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + timestamp, + windows, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + valueKind); } } @@ -298,12 +352,31 @@ static WindowedValue createWithoutValidation( Instant timestamp, Collection windows, PaneInfo paneInfo, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext, + ValueKind valueKind) { if (windows.size() == 1) { - return of(value, timestamp, windows.iterator().next(), paneInfo, null, null, causedByDrain); + return of( + value, + timestamp, + windows.iterator().next(), + paneInfo, + null, + null, + causedByDrain, + openTelemetryContext, + valueKind); } else { return new TimestampedValueInMultipleWindows<>( - value, timestamp, windows, paneInfo, null, null, causedByDrain); + value, + timestamp, + windows, + paneInfo, + null, + null, + causedByDrain, + openTelemetryContext, + valueKind); } } @@ -312,10 +385,18 @@ public static WindowedValue of( T value, Instant timestamp, BoundedWindow window, PaneInfo paneInfo) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); - return of(value, timestamp, window, paneInfo, null, null, CausedByDrain.NORMAL); + return of( + value, + timestamp, + window, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT); } - /** Returns a {@code WindowedValue} with the given value, timestamp, and window. */ public static WindowedValue of( T value, Instant timestamp, @@ -323,20 +404,44 @@ public static WindowedValue of( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext, + ValueKind valueKind) { checkArgument(paneInfo != null, "WindowedValue requires PaneInfo, but it was null"); checkArgument(causedByDrain != null, "WindowedValue requires CausedByDrain, but it was null"); + checkArgument(valueKind != null, "WindowedValue requires ValueKind, but it was null"); boolean isGlobal = GlobalWindow.INSTANCE.equals(window); if (isGlobal && BoundedWindow.TIMESTAMP_MIN_VALUE.equals(timestamp)) { return new ValueInGlobalWindow<>( - value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + valueKind); } else if (isGlobal) { return new TimestampedValueInGlobalWindow<>( - value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + timestamp, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + valueKind); } else { return new TimestampedValueInSingleWindow<>( - value, timestamp, window, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + value, + timestamp, + window, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + valueKind); } } @@ -345,7 +450,8 @@ public static WindowedValue of( * default timestamp and pane. */ public static WindowedValue valueInGlobalWindow(T value) { - return new ValueInGlobalWindow<>(value, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL); + return new ValueInGlobalWindow<>( + value, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL, null, ValueKind.INSERT); } /** @@ -353,7 +459,8 @@ public static WindowedValue valueInGlobalWindow(T value) { * default timestamp and the specified pane. */ public static WindowedValue valueInGlobalWindow(T value, PaneInfo paneInfo) { - return new ValueInGlobalWindow<>(value, paneInfo, null, null, CausedByDrain.NORMAL); + return new ValueInGlobalWindow<>( + value, paneInfo, null, null, CausedByDrain.NORMAL, null, ValueKind.INSERT); } /** @@ -365,7 +472,14 @@ public static WindowedValue timestampedValueInGlobalWindow(T value, Insta return valueInGlobalWindow(value); } else { return new TimestampedValueInGlobalWindow<>( - value, timestamp, PaneInfo.NO_FIRING, null, null, CausedByDrain.NORMAL); + value, + timestamp, + PaneInfo.NO_FIRING, + null, + null, + CausedByDrain.NORMAL, + null, + ValueKind.INSERT); } } @@ -379,7 +493,7 @@ public static WindowedValue timestampedValueInGlobalWindow( return timestampedValueInGlobalWindow(value, timestamp); } else { return new TimestampedValueInGlobalWindow<>( - value, timestamp, paneInfo, null, null, CausedByDrain.NORMAL); + value, timestamp, paneInfo, null, null, CausedByDrain.NORMAL, null, ValueKind.INSERT); } } @@ -396,7 +510,9 @@ public static WindowedValue withValue( windowedValue.getPaneInfo(), windowedValue.getRecordId(), windowedValue.getRecordOffset(), - windowedValue.causedByDrain()); + windowedValue.causedByDrain(), + windowedValue.getOpenTelemetryContext(), + windowedValue.getValueKind()); } public static boolean equals( @@ -415,7 +531,8 @@ public static boolean equals( return left.getTimestamp().isEqual(right.getTimestamp()) && Objects.equals(left.getValue(), right.getValue()) && Iterables.elementsEqual(left.getWindows(), right.getWindows()) - && Objects.equals(left.getPaneInfo(), right.getPaneInfo()); + && Objects.equals(left.getPaneInfo(), right.getPaneInfo()) + && Objects.equals(left.getValueKind(), right.getValueKind()); } public static int hashCode(WindowedValue windowedValue) { @@ -424,7 +541,8 @@ public static int hashCode(WindowedValue windowedValue) { windowedValue.getValue(), windowedValue.getTimestamp().getMillis(), windowedValue.getWindows(), - windowedValue.getPaneInfo()); + windowedValue.getPaneInfo(), + windowedValue.getValueKind()); } private static final Collection GLOBAL_WINDOWS = @@ -448,6 +566,8 @@ private abstract static class SimpleWindowedValue implements WindowedValue private final @Nullable String currentRecordId; private final @Nullable Long currentRecordOffset; private final CausedByDrain causedByDrain; + private final @Nullable Context context; + private final ValueKind valueKind; @Override public @Nullable String getRecordId() { @@ -464,17 +584,26 @@ public CausedByDrain causedByDrain() { return causedByDrain; } + @Override + public ValueKind getValueKind() { + return valueKind; + } + protected SimpleWindowedValue( T value, PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { + CausedByDrain causedByDrain, + @Nullable Context context, + ValueKind valueKind) { this.value = value; this.paneInfo = checkNotNull(paneInfo); this.currentRecordId = currentRecordId; this.currentRecordOffset = currentRecordOffset; this.causedByDrain = causedByDrain; + this.context = context; + this.valueKind = checkNotNull(valueKind, "ValueKind is null"); } @Override @@ -487,6 +616,11 @@ public T getValue() { return value; } + @Override + public @Nullable Context getOpenTelemetryContext() { + return context; + } + @Override public Iterable> explodeWindows() { if (this.getWindows().size() == 1) { @@ -523,8 +657,10 @@ public MinTimestampWindowedValue( PaneInfo pane, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, pane, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context context, + ValueKind valueKind) { + super(value, pane, currentRecordId, currentRecordOffset, causedByDrain, context, valueKind); } @Override @@ -542,8 +678,11 @@ public ValueInGlobalWindow( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context context, + ValueKind valueKind) { + super( + value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context, valueKind); } @Override @@ -559,7 +698,13 @@ public BoundedWindow getWindow() { @Override public WindowedValue withValue(NewT newValue) { return new ValueInGlobalWindow<>( - newValue, getPaneInfo(), getRecordId(), getRecordOffset(), causedByDrain()); + newValue, + getPaneInfo(), + getRecordId(), + getRecordOffset(), + causedByDrain(), + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -598,8 +743,11 @@ public TimestampedWindowedValue( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context context, + ValueKind valueKind) { + super( + value, paneInfo, currentRecordId, currentRecordOffset, causedByDrain, context, valueKind); this.timestamp = checkNotNull(timestamp); } @@ -622,8 +770,18 @@ public TimestampedValueInGlobalWindow( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context context, + ValueKind valueKind) { + super( + value, + timestamp, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + context, + valueKind); } @Override @@ -644,7 +802,9 @@ public WindowedValue withValue(NewT newValue) { getPaneInfo(), getRecordId(), getRecordOffset(), - causedByDrain()); + causedByDrain(), + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -695,8 +855,18 @@ public TimestampedValueInSingleWindow( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext, + ValueKind valueKind) { + super( + value, + timestamp, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + valueKind); this.window = checkNotNull(window); } @@ -709,7 +879,9 @@ public WindowedValue withValue(NewT newValue) { getPaneInfo(), getRecordId(), getRecordOffset(), - causedByDrain()); + causedByDrain(), + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -767,8 +939,18 @@ public TimestampedValueInMultipleWindows( PaneInfo paneInfo, @Nullable String currentRecordId, @Nullable Long currentRecordOffset, - CausedByDrain causedByDrain) { - super(value, timestamp, paneInfo, currentRecordId, currentRecordOffset, causedByDrain); + CausedByDrain causedByDrain, + @Nullable Context openTelemetryContext, + ValueKind valueKind) { + super( + value, + timestamp, + paneInfo, + currentRecordId, + currentRecordOffset, + causedByDrain, + openTelemetryContext, + valueKind); this.windows = checkNotNull(windows); } @@ -786,7 +968,9 @@ public WindowedValue withValue(NewT newValue) { getPaneInfo(), getRecordId(), getRecordOffset(), - causedByDrain()); + causedByDrain(), + getOpenTelemetryContext(), + getValueKind()); } @Override @@ -860,12 +1044,16 @@ 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; } + public static void setMetadataNotSupported() { + metadataSupported = false; + } + public static boolean isMetadataSupported() { return metadataSupported; } @@ -927,11 +1115,11 @@ public WindowedValueCoder withValueCoder(Coder valueCoder) { @Override public void encode(WindowedValue windowedElem, OutputStream outStream) throws CoderException, IOException { - encode(windowedElem, outStream, Context.NESTED); + encode(windowedElem, outStream, Coder.Context.NESTED); } @Override - public void encode(WindowedValue windowedElem, OutputStream outStream, Context context) + public void encode(WindowedValue windowedElem, OutputStream outStream, Coder.Context context) throws CoderException, IOException { InstantCoder.of().encode(windowedElem.getTimestamp(), outStream); windowsCoder.encode(windowedElem.getWindows(), outStream); @@ -941,12 +1129,18 @@ public void encode(WindowedValue windowedElem, OutputStream outStream, Contex if (metadataSupported) { BeamFnApi.Elements.ElementMetadata.Builder builder = BeamFnApi.Elements.ElementMetadata.newBuilder(); + io.opentelemetry.context.Context openTelemetryContext = + windowedElem.getOpenTelemetryContext(); + if (openTelemetryContext != null) { + OpenTelemetryContextPropagator.set(openTelemetryContext, builder); + } BeamFnApi.Elements.ElementMetadata em = builder .setDrain( windowedElem.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN ? BeamFnApi.Elements.DrainMode.Enum.DRAINING : BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING) + .setValueKind(ValueKindUtil.toProto(windowedElem.getValueKind())) .build(); ByteArrayCoder.of().encode(em.toByteArray(), outStream); @@ -956,16 +1150,18 @@ public void encode(WindowedValue windowedElem, OutputStream outStream, Contex @Override public WindowedValue decode(InputStream inStream) throws CoderException, IOException { - return decode(inStream, Context.NESTED); + return decode(inStream, Coder.Context.NESTED); } @Override - public WindowedValue decode(InputStream inStream, Context context) + public WindowedValue decode(InputStream inStream, Coder.Context context) throws CoderException, IOException { Instant timestamp = InstantCoder.of().decode(inStream); Collection windows = windowsCoder.decode(inStream); PaneInfo paneInfo = PaneInfoCoder.INSTANCE.decode(inStream); CausedByDrain causedByDrain = CausedByDrain.NORMAL; + io.opentelemetry.context.Context openTelemetryContext = null; + ValueKind valueKind = ValueKind.INSERT; if (isMetadataSupported() && paneInfo.isElementMetadata()) { BeamFnApi.Elements.ElementMetadata elementMetadata = BeamFnApi.Elements.ElementMetadata.parseFrom(ByteArrayCoder.of().decode(inStream)); @@ -973,13 +1169,15 @@ public WindowedValue decode(InputStream inStream, Context context) elementMetadata.getDrain().equals(BeamFnApi.Elements.DrainMode.Enum.DRAINING) ? CausedByDrain.CAUSED_BY_DRAIN : CausedByDrain.NORMAL; + openTelemetryContext = OpenTelemetryContextPropagator.read(elementMetadata); + valueKind = ValueKindUtil.fromProto(elementMetadata.getValueKind()); } T value = valueCoder.decode(inStream, context); // Because there are some remaining (incorrect) uses of WindowedValue with no windows, // we call this deprecated no-validation path when decoding return WindowedValues.createWithoutValidation( - value, timestamp, windows, paneInfo, causedByDrain); + value, timestamp, windows, paneInfo, causedByDrain, openTelemetryContext, valueKind); } @Override @@ -1045,22 +1243,22 @@ public WindowedValueCoder withValueCoder(Coder valueCoder) { @Override public void encode(WindowedValue windowedElem, OutputStream outStream) throws CoderException, IOException { - encode(windowedElem, outStream, Context.NESTED); + encode(windowedElem, outStream, Coder.Context.NESTED); } @Override - public void encode(WindowedValue windowedElem, OutputStream outStream, Context context) + public void encode(WindowedValue windowedElem, OutputStream outStream, Coder.Context context) throws CoderException, IOException { valueCoder.encode(windowedElem.getValue(), outStream, context); } @Override public WindowedValue decode(InputStream inStream) throws CoderException, IOException { - return decode(inStream, Context.NESTED); + return decode(inStream, Coder.Context.NESTED); } @Override - public WindowedValue decode(InputStream inStream, Context context) + public WindowedValue decode(InputStream inStream, Coder.Context context) throws CoderException, IOException { T value = valueCoder.decode(inStream, context); return WindowedValues.valueInGlobalWindow(value); @@ -1109,7 +1307,18 @@ public static ParamWindowedValueCoder of( Instant timestamp, Collection windows, PaneInfo paneInfo) { - return new ParamWindowedValueCoder<>(valueCoder, windowCoder, timestamp, windows, paneInfo); + return of(valueCoder, windowCoder, timestamp, windows, paneInfo, ValueKind.INSERT); + } + + public static ParamWindowedValueCoder of( + Coder valueCoder, + Coder windowCoder, + Instant timestamp, + Collection windows, + PaneInfo paneInfo, + ValueKind valueKind) { + return new ParamWindowedValueCoder<>( + valueCoder, windowCoder, timestamp, windows, paneInfo, valueKind); } /** @@ -1124,7 +1333,8 @@ public static ParamWindowedValueCoder of( windowCoder, BoundedWindow.TIMESTAMP_MIN_VALUE, GLOBAL_WINDOWS, - PaneInfo.NO_FIRING); + PaneInfo.NO_FIRING, + ValueKind.INSERT); } /** @@ -1142,36 +1352,52 @@ public static ParamWindowedValueCoder of(Coder valueCoder) { Coder windowCoder, Instant timestamp, Collection windows, - PaneInfo paneInfo) { + PaneInfo paneInfo, + ValueKind valueKind) { super(valueCoder, windowCoder); - this.windowedValuePrototype = WindowedValues.of(EMPTY_BYTES, timestamp, windows, paneInfo); + this.windowedValuePrototype = + WindowedValues.of( + EMPTY_BYTES, + timestamp, + windows, + paneInfo, + null, + null, + CausedByDrain.NORMAL, + null, + valueKind); } @Override public WindowedValueCoder withValueCoder(Coder valueCoder) { return new ParamWindowedValueCoder<>( - valueCoder, getWindowCoder(), getTimestamp(), getWindows(), getPaneInfo()); + valueCoder, + getWindowCoder(), + getTimestamp(), + getWindows(), + getPaneInfo(), + getValueKind()); } @Override public void encode(WindowedValue windowedElem, OutputStream outStream) throws CoderException, IOException { - encode(windowedElem, outStream, Context.NESTED); + encode(windowedElem, outStream, Coder.Context.NESTED); } @Override - public void encode(WindowedValue windowedElem, OutputStream outStream, Context context) + public void encode(WindowedValue windowedElem, OutputStream outStream, Coder.Context context) throws CoderException, IOException { valueCoder.encode(windowedElem.getValue(), outStream, context); } @Override public WindowedValue decode(InputStream inStream) throws CoderException, IOException { - return decode(inStream, Context.NESTED); + return decode(inStream, Coder.Context.NESTED); } @Override - public WindowedValue decode(InputStream inStream, Context context) + public WindowedValue decode(InputStream inStream, Coder.Context context) throws CoderException, IOException { return WindowedValues.withValue(windowedValuePrototype, valueCoder.decode(inStream, context)); } @@ -1188,6 +1414,10 @@ public void registerByteSizeObserver(WindowedValue value, ElementByteSizeObse valueCoder.registerByteSizeObserver(value.getValue(), observer); } + public ValueKind getValueKind() { + return windowedValuePrototype.getValueKind(); + } + public Instant getTimestamp() { return windowedValuePrototype.getTimestamp(); } @@ -1207,7 +1437,15 @@ public static byte[] getPayload(ParamWindowedValueCoder from) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); WindowedValue windowedValue = WindowedValues.of( - EMPTY_BYTES, from.getTimestamp(), from.getWindows(), from.getPaneInfo()); + EMPTY_BYTES, + from.getTimestamp(), + from.getWindows(), + from.getPaneInfo(), + null, + null, + CausedByDrain.NORMAL, + null, + from.getValueKind()); WindowedValues.FullWindowedValueCoder windowedValueCoder = WindowedValues.FullWindowedValueCoder.of(ByteArrayCoder.of(), from.getWindowCoder()); try { @@ -1235,7 +1473,8 @@ public static WindowedValues.ParamWindowedValueCoder fromComponents( windowCoder, windowedValue.getTimestamp(), windowedValue.getWindows(), - windowedValue.getPaneInfo()); + windowedValue.getPaneInfo(), + windowedValue.getValueKind()); } catch (IOException e) { throw new RuntimeException( "Unable to decode constant members from payload for ParamWindowedValueCoder: ", e); diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/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/TextIOReadTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOReadTest.java index c475d6d203b8..e7d25608eb7b 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOReadTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOReadTest.java @@ -453,10 +453,12 @@ public void testReadLinesWithDefaultDelimiterOnSplittingSourceAndSlowReadChannel getTextSource(path.toString(), null, 0) .createForSubrangeOfFile(metadata, 0, metadata.sizeBytes()); - PipelineOptions options = PipelineOptionsFactory.create(); - // Check every possible split positions. for (int i = 0; i < line.length(); ++i) { + + PipelineOptions options = PipelineOptionsFactory.create(); + options.setJobName("textio-test-" + i + "-" + System.currentTimeMillis()); + double fraction = i * 1.0 / line.length(); FileBasedReader reader = source.createSingleFileReader(options); 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/transforms/ConvertTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/ConvertTest.java index 93d6984d47e2..c206bd8f61fd 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/ConvertTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/transforms/ConvertTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.schemas.transforms; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -24,6 +25,7 @@ import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; import org.apache.beam.sdk.testing.NeedsRunner; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; @@ -211,6 +213,103 @@ public void testFromRows() { pipeline.run(); } + /** Reproducer for #37524: a Row with a logical-type DateTime field should convert to a POJO. */ + @DefaultSchema(JavaFieldSchema.class) + public static class TimePOJO { + public LocalDateTime time; + + public TimePOJO() {} + + public TimePOJO(LocalDateTime time) { + this.time = time; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TimePOJO)) { + return false; + } + TimePOJO that = (TimePOJO) o; + return Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(time); + } + } + + @Test + @Category(NeedsRunner.class) + public void testFromRowsWithLogicalTypeDateTime() { + Schema icebergStyleSchema = + Schema.builder().addLogicalTypeField("time", SqlTypes.DATETIME).build(); + LocalDateTime expected = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + Row inputRow = Row.withSchema(icebergStyleSchema).addValue(expected).build(); + + PCollection pojos = + pipeline + .apply(Create.of(inputRow).withRowSchema(icebergStyleSchema)) + .apply(Convert.fromRows(TimePOJO.class)); + + PAssert.that(pojos).containsInAnyOrder(new TimePOJO(expected)); + pipeline.run(); + } + + /** POJO mixing a logical-type field with a primitive field. */ + @DefaultSchema(JavaFieldSchema.class) + public static class MixedTimePOJO { + public LocalDateTime time; + public String name; + + public MixedTimePOJO() {} + + public MixedTimePOJO(LocalDateTime time, String name) { + this.time = time; + this.name = name; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MixedTimePOJO)) { + return false; + } + MixedTimePOJO that = (MixedTimePOJO) o; + return Objects.equals(time, that.time) && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(time, name); + } + } + + @Test + @Category(NeedsRunner.class) + public void testFromRowsWithMixedLogicalAndPrimitiveTypes() { + Schema mixedSchema = + Schema.builder() + .addLogicalTypeField("time", SqlTypes.DATETIME) + .addStringField("name") + .build(); + LocalDateTime expectedTime = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + Row inputRow = Row.withSchema(mixedSchema).addValues(expectedTime, "hello").build(); + + PCollection pojos = + pipeline + .apply(Create.of(inputRow).withRowSchema(mixedSchema)) + .apply(Convert.fromRows(MixedTimePOJO.class)); + + PAssert.that(pojos).containsInAnyOrder(new MixedTimePOJO(expectedTime, "hello")); + pipeline.run(); + } + @Test @Category(NeedsRunner.class) public void testGeneralConvert() { diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/JavaBeanUtilsTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/JavaBeanUtilsTest.java index 7e9cf9a894b9..57d4f905e6ec 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/JavaBeanUtilsTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/JavaBeanUtilsTest.java @@ -19,6 +19,7 @@ import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.BEAN_WITH_BOXED_FIELDS_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.BEAN_WITH_BYTE_ARRAY_SCHEMA; +import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.JAVA_TIME_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_ARRAY_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_BEAN_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestJavaBeans.NESTED_COLLECTION_BEAN_SCHEMA; @@ -44,6 +45,7 @@ import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.DefaultTypeConversionsFactory; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithBoxedFields; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.BeanWithByteArray; +import org.apache.beam.sdk.schemas.utils.TestJavaBeans.JavaTimeBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedArrayBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedBean; import org.apache.beam.sdk.schemas.utils.TestJavaBeans.NestedCollectionBean; @@ -79,6 +81,14 @@ public void testSimpleBean() { SchemaTestUtils.assertSchemaEquivalent(SIMPLE_BEAN_SCHEMA, schema); } + @Test + public void testJavaTimeBean() { + Schema schema = + JavaBeanUtils.schemaFromJavaBeanClass( + new TypeDescriptor() {}, GetterTypeSupplier.INSTANCE); + SchemaTestUtils.assertSchemaEquivalent(JAVA_TIME_BEAN_SCHEMA, schema); + } + @Test public void testNestedBean() { Schema schema = diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/POJOUtilsTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/POJOUtilsTest.java index 6b9fbcd30a27..72407d965da4 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/POJOUtilsTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/POJOUtilsTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.schemas.utils; +import static org.apache.beam.sdk.schemas.utils.TestPOJOs.JAVA_TIME_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_ARRAY_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_COLLECTION_POJO_SCHEMA; import static org.apache.beam.sdk.schemas.utils.TestPOJOs.NESTED_MAP_POJO_SCHEMA; @@ -38,7 +39,11 @@ import org.apache.beam.sdk.schemas.FieldValueGetter; import org.apache.beam.sdk.schemas.JavaFieldSchema.JavaFieldTypeSupplier; import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.schemas.Schema.TypeName; +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.DefaultTypeConversionsFactory; +import org.apache.beam.sdk.schemas.utils.TestPOJOs.JavaTimePOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedArrayPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedCollectionPOJO; import org.apache.beam.sdk.schemas.utils.TestPOJOs.NestedMapPOJO; @@ -82,6 +87,33 @@ public void testSimplePOJO() { assertEquals(SIMPLE_POJO_SCHEMA, schema); } + @Test + public void testJavaTimePOJO() { + Schema schema = + POJOUtils.schemaFromPojoClass( + new TypeDescriptor() {}, JavaFieldTypeSupplier.INSTANCE); + assertEquals(JAVA_TIME_POJO_SCHEMA, schema); + } + + /** + * Regression test for #37524: Joda {@link Instant} must continue to map to {@link + * FieldType#DATETIME}, not the new {@code java.time.Instant} logical type. This guards the branch + * ordering in {@code StaticSchemaInference.fieldFromType}. + */ + @Test + public void testJodaInstantStillInfersAsDatetime() { + FieldType jodaInferred = + StaticSchemaInference.fieldFromType( + TypeDescriptor.of(Instant.class), JavaFieldTypeSupplier.INSTANCE); + assertEquals(FieldType.DATETIME, jodaInferred); + + FieldType javaTimeInferred = + StaticSchemaInference.fieldFromType( + TypeDescriptor.of(java.time.Instant.class), JavaFieldTypeSupplier.INSTANCE); + assertEquals(TypeName.LOGICAL_TYPE, javaTimeInferred.getTypeName()); + assertEquals(NanosInstant.IDENTIFIER, javaTimeInferred.getLogicalType().getIdentifier()); + } + @Test public void testNestedPOJO() { Schema schema = diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/utils/TestJavaBeans.java index 694db05b0918..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..183b1851459c --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/AsyncWrapperTest.java @@ -0,0 +1,877 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 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 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(); + } +} diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/BatchElementsTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/BatchElementsTest.java new file mode 100644 index 000000000000..70167b43faf1 --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/BatchElementsTest.java @@ -0,0 +1,598 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.transforms; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import org.apache.beam.sdk.testing.NeedsRunner; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.FixedWindows; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.Window; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.TimestampedValue; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.Timeout; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BatchElementsTest implements Serializable { + + @Rule public transient TestPipeline pipeline = TestPipeline.create(); + + @Rule public transient Timeout globalTimeout = Timeout.seconds(120); + + // Helpers + + private static BatchElements.BatchConfig constantConfig(int size) { + return BatchElements.BatchConfig.builder() + .withMinBatchSize(size) + .withMaxBatchSize(size) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + } + + // BatchConfig validation + + @Test + public void testBatchConfigDefaults() { + BatchElements.BatchConfig config = BatchElements.BatchConfig.defaults(); + assertEquals(1, config.minBatchSize); + assertEquals(10_000, config.maxBatchSize); + assertEquals(0.05, config.targetBatchOverhead, 1e-9); + assertEquals(10.0, config.targetBatchDurationSecs, 1e-9); + assertEquals(0.25, config.variance, 1e-9); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigMinGreaterThanMaxThrows() { + BatchElements.BatchConfig.builder().withMinBatchSize(100).withMaxBatchSize(10).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigZeroTargetDurationThrows() { + BatchElements.BatchConfig.builder().withTargetBatchDurationSecs(0.0).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigNegativeTargetDurationThrows() { + BatchElements.BatchConfig.builder().withTargetBatchDurationSecs(-5.0).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigZeroOverheadThrows() { + BatchElements.BatchConfig.builder().withTargetBatchOverhead(0.0).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigOverheadAboveOneThrows() { + BatchElements.BatchConfig.builder().withTargetBatchOverhead(1.5).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBatchConfigNegativeFixedCostDurationThrows() { + BatchElements.BatchConfig.builder().withTargetBatchDurationSecsWithFixedCost(-5.0).build(); + } + + // BatchSizeEstimator unit tests + + @Test + public void testEstimatorConstantBatchSize() { + BatchElements.BatchConfig config = constantConfig(42); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + // When min == max, always return that size + for (int i = 0; i < 10; i++) { + assertEquals(42, estimator.nextBatchSize()); + } + } + + @Test + public void testEstimatorColdStartReturnsMinBatchSize() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(5) + .withMaxBatchSize(500) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + // No timing data yet — should return minBatchSize + assertEquals(5, estimator.nextBatchSize()); + } + + @Test + public void testEstimatorGrowsAfterTimingData() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(500) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + + // Warm up with some fake recordings + int size = estimator.nextBatchSize(); + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(size)) { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + size = estimator.nextBatchSize(); + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(size)) { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + int grown = estimator.nextBatchSize(); + assertTrue("Estimator should grow beyond minBatchSize after data, got: " + grown, grown > 1); + } + + @Test + public void testEstimatorNeverExceedsMaxBatchSize() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(10) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + + for (int i = 0; i < 50; i++) { + int next = estimator.nextBatchSize(); + assertTrue("Batch size " + next + " exceeds max of 10", next <= 10); + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(next)) { + Thread.sleep(10 + i); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testEstimatorNeverGoesBelowMinBatchSize() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(7) + .withMaxBatchSize(500) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + + for (int i = 0; i < 20; i++) { + int next = estimator.nextBatchSize(); + assertTrue("Batch size " + next + " is below min of 7", next >= 7); + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(next)) { + Thread.sleep(10 + i); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testIgnoreNextTimingReplaysBatchSize() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(500) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + BatchElements.BatchSizeEstimator estimator = new BatchElements.BatchSizeEstimator(config); + + estimator.ignoreNextTiming(); + int first = estimator.nextBatchSize(); + + // After ignoreNextTiming, the stopwatch will set replayLastBatchSize + try (BatchElements.BatchSizeEstimator.Stopwatch sw = estimator.recordTime(first)) {} + + // Next call should replay the same size + int replayed = estimator.nextBatchSize(); + assertEquals( + "Expected replay of batch size " + first + " but got " + replayed, first, replayed); + } + + // GlobalWindows pipeline tests + @Test + @Category(NeedsRunner.class) + public void testConstantBatchInGlobalWindow() { + // Mirrors Python: test_constant_batch + // Runner bundle boundaries are not fixed, so partial batches may be emitted at bundle end. + PCollection output = + pipeline + .apply("Create", Create.of(range(35))) + .apply("Batch", BatchElements.withConfig(constantConfig(10))) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List batch) { + return batch.size(); + } + })); + + PAssert.that(output).satisfies(sizes -> assertBatchSizesWithinLimitAndTotal(sizes, 10, 35)); + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testPipelineRespectsMaxBatchSizeAndPreservesElements() { + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(15) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + + PCollection sizes = + pipeline + .apply("Create", Create.of(range(200))) + .apply("Batch", BatchElements.withConfig(config)) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes) + .satisfies( + s -> { + int total = 0; + for (int size : s) { + assertTrue("Batch size must be > 0", size > 0); + assertTrue("Batch size " + size + " exceeded maxBatchSize 15", size <= 15); + total += size; + } + assertEquals("All 200 elements must be present", 200, total); + return null; + }); + + pipeline.run().waitUntilFinish(); + } + + // WindowAware pipeline tests + + @Test + @Category(NeedsRunner.class) + public void testWindowedBatches() { + // 47 elements across FixedWindows(30s); runner bundle boundaries may split batches. + List> timestamped = new ArrayList<>(); + for (int i = 0; i < 47; i++) { + timestamped.add(TimestampedValue.of(i, new Instant((long) i * 1000))); + } + + PCollection sizes = + pipeline + .apply("Create", Create.timestamped(timestamped)) + .apply( + "Window", + Window.into(FixedWindows.of(Duration.standardSeconds(30))) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes()) + .apply("Batch", BatchElements.withConfig(constantConfig(10))) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes).satisfies(s -> assertBatchSizesWithinLimitAndTotal(s, 10, 47)); + + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testCrossWindowIsolation() { + // Elements from different windows must NEVER appear in the same batch + List> elements = new ArrayList<>(); + for (int i = 0; i < 2000; i++) { + // 4 windows of 2s each, 500 elements per window + long ts = (long) (i / 500) * 2000L + (i % 500); + elements.add(TimestampedValue.of("w" + (i / 500) + "-e" + i, new Instant(ts))); + } + + PCollection isolationChecks = + pipeline + .apply("Create", Create.timestamped(elements)) + .apply( + "Window", + Window.into(FixedWindows.of(Duration.standardSeconds(2))) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes()) + .apply("Batch", BatchElements.withConfig(constantConfig(50))) + .apply( + "CheckIsolation", + MapElements.via( + new SimpleFunction, Boolean>() { + @Override + public Boolean apply(List batch) { + // All elements in a batch must share the same window prefix + String firstWindow = batch.get(0).substring(0, 2); + for (String el : batch) { + if (!el.startsWith(firstWindow)) { + throw new AssertionError( + "Cross-window contamination: " + + el + + " in batch starting with " + + firstWindow); + } + } + return true; + } + })); + + PAssert.that(isolationChecks) + .satisfies( + checks -> { + int count = 0; + for (boolean ok : checks) { + assertTrue(ok); + count++; + } + assertTrue("Expected at least one batch", count > 0); + return null; + }); + + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testWindowedBatchesPreserveAllElements() { + // Total elements across all windows must equal input count + int numElements = 500; + List> elements = new ArrayList<>(); + for (int i = 0; i < numElements; i++) { + elements.add(TimestampedValue.of(i, new Instant((long) (i / 100) * 5000L + i))); + } + + PCollection sizes = + pipeline + .apply("Create", Create.timestamped(elements)) + .apply( + "Window", + Window.into(FixedWindows.of(Duration.standardSeconds(5))) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes()) + .apply("Batch", BatchElements.withConfig(constantConfig(30))) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes).satisfies(s -> assertBatchSizesWithinLimitAndTotal(s, 30, numElements)); + + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testEvictionPreservesWindowMetadata() { + // 1. Use 12 windows to trigger the MAX_LIVE_WINDOWS (10) eviction + List> elements = new ArrayList<>(); + for (int w = 0; w < 12; w++) { + long timestamp = w * 10000L; // Distinct windows + elements.add(TimestampedValue.of(w, new Instant(timestamp))); + } + + PCollection> batched = + pipeline + .apply(Create.timestamped(elements)) + .apply(Window.into(FixedWindows.of(Duration.standardSeconds(5)))) + .apply( + BatchElements.withConfig( + constantConfig(10))); // Large size so they don't flush naturally + + // 2. Use ParDo to capture the ACTUAL window from the context + PCollection>> windowCaptured = + batched.apply( + ParDo.of( + new DoFn, KV>>() { + @ProcessElement + public void process( + @Element List e, + BoundedWindow w, + OutputReceiver>> r) { + r.output(KV.of((IntervalWindow) w, e)); + } + })); + + // 3. Assert that the value inside the batch matches the window it was found in + PAssert.that(windowCaptured) + .satisfies( + items -> { + for (KV> item : items) { + int windowIndex = (int) (item.getKey().start().getMillis() / 10000L); + for (Integer val : item.getValue()) { + assertEquals( + "Element " + val + " found in wrong window!", (Integer) windowIndex, val); + } + } + return null; + }); + + pipeline.run(); + } + + @Test + @Category(NeedsRunner.class) + public void testWindowedBatchMaxSizeRespected() { + int maxBatch = 20; + List> elements = new ArrayList<>(); + for (int i = 0; i < 300; i++) { + elements.add(TimestampedValue.of(i, new Instant((long) (i / 150) * 3000L + i))); + } + + BatchElements.BatchConfig config = + BatchElements.BatchConfig.builder() + .withMinBatchSize(1) + .withMaxBatchSize(maxBatch) + .withTargetBatchDurationSecs(10.0) + .withTargetBatchOverhead(0.05) + .withVariance(0.0) + .build(); + + PCollection sizes = + pipeline + .apply("Create", Create.timestamped(elements)) + .apply( + "Window", + Window.into(FixedWindows.of(Duration.standardSeconds(3))) + .withAllowedLateness(Duration.ZERO) + .discardingFiredPanes()) + .apply("Batch", BatchElements.withConfig(config)) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes) + .satisfies( + s -> { + int total = 0; + for (int size : s) { + assertTrue("Batch size must be > 0", size > 0); + assertTrue("Batch size " + size + " exceeded max " + maxBatch, size <= maxBatch); + total += size; + } + assertEquals("All 300 elements must be present", 300, total); + return null; + }); + + pipeline.run().waitUntilFinish(); + } + + @Test + @Category(NeedsRunner.class) + public void testWindowRoutingGlobalWindowUsesGlobalDoFn() { + // A plain Create (no windowing) should route through GlobalWindowsBatchingDoFn. + // We validate this indirectly: output is correct and no windowing errors occur. + PCollection sizes = + pipeline + .apply("Create", Create.of(range(25))) + .apply("Batch", BatchElements.withConfig(constantConfig(10))) + .apply( + "Sizes", + MapElements.via( + new SimpleFunction, Integer>() { + @Override + public Integer apply(List input) { + return input.size(); + } + })); + + PAssert.that(sizes).satisfies(s -> assertBatchSizesWithinLimitAndTotal(s, 10, 25)); + + pipeline.run().waitUntilFinish(); + } + + // LinearRegression unit tests + + @Test + public void testLinearRegressionPerfectFit() throws Exception { + double[] ab = linearRegression(new double[] {1, 2, 3, 4, 5}, new double[] {3, 5, 7, 9, 11}); + + assertEquals(1.0, ab[0], 1e-9); + assertEquals(2.0, ab[1], 1e-9); + } + + @Test + public void testLinearRegressionRepeatedXsUsesMeanTimePerElement() throws Exception { + double[] ab = linearRegression(new double[] {5, 5, 5, 5}, new double[] {10, 15, 20, 25}); + + assertEquals(0.0, ab[0], 1e-9); + assertEquals(3.5, ab[1], 1e-9); + } + + // Utility + + private static Void assertBatchSizesWithinLimitAndTotal( + Iterable sizes, int maxBatchSize, int expectedTotal) { + int total = 0; + for (int size : sizes) { + assertTrue("Batch size must be > 0", size > 0); + assertTrue("Batch size " + size + " exceeded max " + maxBatchSize, size <= maxBatchSize); + total += size; + } + assertEquals("All elements must be present", expectedTotal, total); + return null; + } + + private static double[] linearRegression(double[] xs, double[] ys) throws Exception { + BatchElements.BatchSizeEstimator estimator = + new BatchElements.BatchSizeEstimator(BatchElements.BatchConfig.defaults()); + Method method = + BatchElements.BatchSizeEstimator.class.getDeclaredMethod( + "linearRegression", double[].class, double[].class); + method.setAccessible(true); + return (double[]) method.invoke(estimator, xs, ys); + } + + private static List range(int n) { + List list = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + list.add(i); + } + return list; + } +} 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/MetadataPropagationTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java index a2ff99905f6c..3e5ff77cb1c4 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/MetadataPropagationTest.java @@ -17,13 +17,22 @@ */ package org.apache.beam.sdk.transforms; +import org.apache.beam.sdk.coders.BooleanCoder; import org.apache.beam.sdk.testing.NeedsRunner; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.testing.TestStream; +import org.apache.beam.sdk.transforms.windowing.FixedWindows; +import org.apache.beam.sdk.transforms.windowing.IntervalWindow; +import org.apache.beam.sdk.transforms.windowing.Window; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.TimestampedValue; import org.apache.beam.sdk.values.WindowedValues; -import org.junit.Ignore; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -36,10 +45,14 @@ public class MetadataPropagationTest { /** Tests for metadata propagation. */ @Rule public final transient TestPipeline pipeline = TestPipeline.create(); - static class CausedByDrainSettingDoFn extends DoFn { + static class CausedByDrainSettingDoFn extends DoFn { @ProcessElement - public void process(OutputReceiver r) { - r.builder("value").setCausedByDrain(CausedByDrain.CAUSED_BY_DRAIN).output(); + public void process(@Element Boolean isDrain, OutputReceiver r) { + if (isDrain) { + r.builder("value").setCausedByDrain(CausedByDrain.CAUSED_BY_DRAIN).output(); + } else { + r.builder("value").setCausedByDrain(CausedByDrain.NORMAL).output(); + } } } @@ -52,12 +65,11 @@ public void process(ProcessContext pc, OutputReceiver r) { @Test @Category(NeedsRunner.class) - @Ignore public void testMetadataPropagationAcrossShuffleParameter() { WindowedValues.WindowedValueCoder.setMetadataSupported(); PCollection results = pipeline - .apply(Create.of(1)) + .apply(Create.of(true)) .apply(ParDo.of(new CausedByDrainSettingDoFn())) .apply(Redistribute.arbitrarily()) .apply(ParDo.of(new CausedByDrainExtractingDoFn())); @@ -67,12 +79,18 @@ public void testMetadataPropagationAcrossShuffleParameter() { pipeline.run(); } + /** + * Tests metadata propagation for a parameter. Note: PAssert internally introduces a GroupByKey. + * This test works only with DirectRunner and runners that support metadata propagation across + * GroupByKey. See {@link #testMetadataPropagationAcrossGBK} for more information. + */ @Test @Category(NeedsRunner.class) public void testMetadataPropagationParameter() { + WindowedValues.WindowedValueCoder.setMetadataSupported(); PCollection results = pipeline - .apply(Create.of(1)) + .apply(Create.of(true)) .apply(ParDo.of(new CausedByDrainSettingDoFn())) .apply(ParDo.of(new CausedByDrainExtractingDoFn())); @@ -80,4 +98,70 @@ public void testMetadataPropagationParameter() { pipeline.run(); } + + static class CausedByDrainExtracingFromGBKDoFn + extends DoFn>, String> { + @ProcessElement + public void process(ProcessContext pc, OutputReceiver r) { + r.output(pc.causedByDrain().toString()); + } + } + + /** + * Tests metadata propagation across GroupByKey. Note: This test works only with DirectRunner and + * runners that support metadata propagation (e.g. via a flag to enable metadata encoding in + * coders). It fails on portable runners like Prism because they do not have implementation for + * metadata propagation, leading to coder mismatches. + */ + @Test + @Category(NeedsRunner.class) + public void testMetadataPropagationAcrossGBK() { + WindowedValues.WindowedValueCoder.setMetadataSupported(); + Instant baseTime = new Instant(0); + TestStream stream = + TestStream.create(BooleanCoder.of()) + .advanceWatermarkTo(baseTime) + .addElements(TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(10)))) + .advanceWatermarkTo(baseTime.plus(Duration.standardMinutes(1))) + .addElements( + TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(71))), + TimestampedValue.of(true, baseTime.plus(Duration.standardSeconds(72)))) + .advanceWatermarkTo(baseTime.plus(Duration.standardMinutes(2))) + .addElements( + TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(130))), + TimestampedValue.of(true, baseTime.plus(Duration.standardSeconds(131))), // drain + TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(132)))) + .advanceWatermarkTo(baseTime.plus(Duration.standardMinutes(3))) + .addElements( + TimestampedValue.of(false, baseTime.plus(Duration.standardSeconds(181)))) // normal + .advanceWatermarkTo(baseTime.plus(Duration.standardMinutes(4))) + .advanceWatermarkToInfinity(); + + Duration windowDuration = Duration.standardMinutes(1); + IntervalWindow window1 = new IntervalWindow(baseTime, windowDuration); + IntervalWindow window2 = new IntervalWindow(window1.end(), windowDuration); + IntervalWindow window3 = new IntervalWindow(window2.end(), windowDuration); + IntervalWindow window4 = new IntervalWindow(window3.end(), windowDuration); + + PCollection results = + pipeline + .apply(stream) + .apply(ParDo.of(new CausedByDrainSettingDoFn())) + .apply(WithKeys.of("1")) + .apply(Window.into(FixedWindows.of(windowDuration))) + .apply(GroupByKey.create()) + .apply(ParDo.of(new CausedByDrainExtracingFromGBKDoFn())); + + PAssert.that(results).inWindow(window1).containsInAnyOrder("NORMAL"); + PAssert.that(results).inWindow(window2).containsInAnyOrder("CAUSED_BY_DRAIN"); + PAssert.that(results).inWindow(window3).containsInAnyOrder("CAUSED_BY_DRAIN"); + PAssert.that(results).inWindow(window4).containsInAnyOrder("NORMAL"); + + pipeline.run(); + } + + @After + public void tearDown() { + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } } 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/SplittableDoFnTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/SplittableDoFnTest.java index 8ce9330b9ab4..80d8728aa01b 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/SplittableDoFnTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/SplittableDoFnTest.java @@ -31,10 +31,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.coders.BigEndianIntegerCoder; @@ -958,16 +958,22 @@ public OffsetRange getInitialRestriction() { } /** - * While the finalization callback hasn't been invoked, this DoFn will keep requesting - * finalization, wait one second and then checkpoint upto MAX_ATTEMPTS amount of times. Once the - * callback has been invoked, the DoFn will output the element and stop. + * While the finalization callback hasn't been invoked, this DoFn repeatedly registers a + * finalization request, sleeps ~100ms, and checkpoints (via {@link ProcessContinuation#resume()}) + * up to {@link #MAX_ATTEMPTS} times. Once the callback runs and flips {@code WAS_FINALIZED}, the + * DoFn outputs the element and stops. + * + *

    Shared state must be thread-safe: {@link BundleFinalizer} callbacks can run on a different + * thread than {@code @ProcessElement}; use {@link ConcurrentHashMap} for {@code WAS_FINALIZED} so + * {@code computeIfAbsent} / updates are not racy (a plain {@link HashMap} can hang or corrupt + * under concurrent structural access). */ public static class BundleFinalizingSplittableDoFn extends DoFn { + /** Upper bound on {@link ProcessContinuation#resume()} iterations via restriction width. */ private static final long MAX_ATTEMPTS = 3000; - // We use the UUID to uniquely identify this DoFn in case this test is run with - // other tests in the same JVM. - private static final Map WAS_FINALIZED = new HashMap(); - private final UUID uuid = UUID.randomUUID(); + + private static final long FINALIZATION_CALLBACK_TIMEOUT_SECS = 300; + private static final Map WAS_FINALIZED = new ConcurrentHashMap<>(); @NewTracker public RestrictionTracker newTracker(@Restriction OffsetRange restriction) { @@ -987,23 +993,29 @@ public ProcessContinuation process( RestrictionTracker tracker, BundleFinalizer bundleFinalizer) throws InterruptedException { - if (WAS_FINALIZED.computeIfAbsent(uuid, (unused) -> new AtomicBoolean()).get()) { + AtomicBoolean wasFinalized = + WAS_FINALIZED.computeIfAbsent(element, (unused) -> new AtomicBoolean()); + if (wasFinalized.get()) { tracker.tryClaim(tracker.currentRestriction().getFrom() + 1); receiver.output(element); + WAS_FINALIZED.remove(element); // Claim beyond the end now that we know we have been finalized. tracker.tryClaim(Long.MAX_VALUE); return stop(); } if (tracker.tryClaim(tracker.currentRestriction().getFrom() + 1)) { bundleFinalizer.afterBundleCommit( - Instant.now().plus(Duration.standardSeconds(MAX_ATTEMPTS)), - () -> WAS_FINALIZED.computeIfAbsent(uuid, (unused) -> new AtomicBoolean()).set(true)); + Instant.now().plus(Duration.standardSeconds(FINALIZATION_CALLBACK_TIMEOUT_SECS)), + () -> wasFinalized.set(true)); // We sleep here instead of setting a resume time since the resume time doesn't need to // be honored. sleep(100L); return resume(); } - return stop(); + WAS_FINALIZED.remove(element); + throw new RuntimeException( + String.format( + "Bundle finalization callback was not observed after %d checkpoints.", MAX_ATTEMPTS)); } @GetInitialRestriction @@ -1017,9 +1029,10 @@ public OffsetRange getInitialRestriction() { public void testBundleFinalizationOccursOnBoundedSplittableDoFn() throws Exception { @BoundedPerElement class BoundedBundleFinalizingSplittableDoFn extends BundleFinalizingSplittableDoFn {} - PCollection foo = p.apply(Create.of("foo")); + String element = "foo-" + UUID.randomUUID(); + PCollection foo = p.apply(Create.of(element)); PCollection res = foo.apply(ParDo.of(new BoundedBundleFinalizingSplittableDoFn())); - PAssert.that(res).containsInAnyOrder("foo"); + PAssert.that(res).containsInAnyOrder(element); p.run(); } @@ -1028,9 +1041,10 @@ class BoundedBundleFinalizingSplittableDoFn extends BundleFinalizingSplittableDo public void testBundleFinalizationOccursOnUnboundedSplittableDoFn() throws Exception { @UnboundedPerElement class UnboundedBundleFinalizingSplittableDoFn extends BundleFinalizingSplittableDoFn {} - PCollection foo = p.apply(Create.of("foo")); + String element = "foo-" + UUID.randomUUID(); + PCollection foo = p.apply(Create.of(element)); PCollection res = foo.apply(ParDo.of(new UnboundedBundleFinalizingSplittableDoFn())); - PAssert.that(res).containsInAnyOrder("foo"); + PAssert.that(res).containsInAnyOrder(element); p.run(); } 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/DoFnSignaturesTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java index 3369e18519ba..330bb5b94418 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/reflect/DoFnSignaturesTest.java @@ -57,8 +57,11 @@ import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.BundleFinalizerParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CausedByDrainParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CurrentRecordIdParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.CurrentRecordOffsetParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.ElementParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.FinishBundleContextParameter; +import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.FireTimestampParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.OutputReceiverParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.PaneInfoParameter; import org.apache.beam.sdk.transforms.reflect.DoFnSignature.Parameter.PipelineOptionsParameter; @@ -101,6 +104,37 @@ @SuppressWarnings("unused") public class DoFnSignaturesTest { + @Test + public void testRecordIdOnTimerError() throws Exception { + final String timerId = "some-timer-id"; + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Illegal parameter type"); + thrown.expectMessage("CurrentRecordIdParameter"); + DoFnSignatures.getSignature( + new DoFn() { + @TimerId(timerId) + private final TimerSpec myfield1 = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @ProcessElement + public void process(ProcessContext c) {} + + @OnTimer(timerId) + public void onTimer(@DoFn.CurrentRecordId String id) {} + }.getClass()); + } + + @Test + public void testFireTimestampOnProcessElementError() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Illegal parameter type"); + thrown.expectMessage("FireTimestampParameter"); + DoFnSignatures.getSignature( + new DoFn() { + @ProcessElement + public void process(@FireTimestamp Instant ts) {} + }.getClass()); + } + @Rule public ExpectedException thrown = ExpectedException.none(); @Test @@ -153,6 +187,24 @@ public void process( sig.processElement().extraParameters().get(9), instanceOf(CausedByDrainParameter.class)); } + @Test + public void testProcessElementRecordIdAndOffset() throws Exception { + DoFnSignature sig = + DoFnSignatures.getSignature( + new DoFn() { + @ProcessElement + public void process( + @DoFn.CurrentRecordId String id, @DoFn.CurrentRecordOffset Long offset) {} + }.getClass()); + + assertThat(sig.processElement().extraParameters().size(), equalTo(2)); + assertThat( + sig.processElement().extraParameters().get(0), instanceOf(CurrentRecordIdParameter.class)); + assertThat( + sig.processElement().extraParameters().get(1), + instanceOf(CurrentRecordOffsetParameter.class)); + } + @Test public void testBasicDoFnMultiOutputReceiver() throws Exception { DoFnSignature sig = @@ -647,6 +699,30 @@ public void onTimer( instanceOf(WindowParameter.class)); } + @Test + public void testFireTimestampOnTimer() throws Exception { + final String timerId = "some-timer-id"; + final String timerDeclarationId = TimerDeclaration.PREFIX + timerId; + + DoFnSignature sig = + DoFnSignatures.getSignature( + new DoFn() { + @TimerId(timerId) + private final TimerSpec myfield1 = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @ProcessElement + public void process(ProcessContext c) {} + + @OnTimer(timerId) + public void onTimer(@FireTimestamp Instant fireTimestamp) {} + }.getClass()); + + assertThat(sig.onTimerMethods().get(timerDeclarationId).extraParameters().size(), equalTo(1)); + assertThat( + sig.onTimerMethods().get(timerDeclarationId).extraParameters().get(0), + instanceOf(FireTimestampParameter.class)); + } + @Test public void testPipelineOptionsParameter() throws Exception { DoFnSignature sig = @@ -1346,16 +1422,20 @@ public void bar( @StateId("foo") ValueState s, PipelineOptions p, OutputReceiver o, - MultiOutputReceiver m) {} + MultiOutputReceiver m, + OnWindowExpirationContext c) {} }.getClass()); List params = sig.onWindowExpiration().extraParameters(); - assertThat(params.size(), equalTo(5)); + assertThat(params.size(), equalTo(6)); assertThat(params.get(0), instanceOf(WindowParameter.class)); assertThat(params.get(1), instanceOf(StateParameter.class)); assertThat(params.get(2), instanceOf(PipelineOptionsParameter.class)); assertThat(params.get(3), instanceOf(OutputReceiverParameter.class)); assertThat(params.get(4), instanceOf(TaggedOutputReceiverParameter.class)); + assertThat( + params.get(5), + instanceOf(DoFnSignature.Parameter.OnWindowExpirationContextParameter.class)); } private interface FeatureTest { diff --git a/sdks/java/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/UnboundedScheduledExecutorServiceTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/UnboundedScheduledExecutorServiceTest.java index efbb03519789..bba10c84fab6 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/UnboundedScheduledExecutorServiceTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/UnboundedScheduledExecutorServiceTest.java @@ -625,7 +625,7 @@ public void testThreadsAreAddedOnlyAsNeededWithContention() throws Exception { LOG.info("Created {} threads to execute at most 100 parallel tasks", largestPool); // Ideally we would never create more than 100, however with contention it is still possible // some extra threads will be created. - assertTrue(largestPool <= 110); + assertTrue(largestPool <= 120); executorService.shutdown(); } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/WindowedValueTest.java index 97b99321e5c7..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 @@ -23,6 +23,11 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; import java.util.Arrays; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.CoderException; @@ -34,12 +39,14 @@ import org.apache.beam.sdk.transforms.windowing.PaneInfo; import org.apache.beam.sdk.transforms.windowing.PaneInfo.Timing; import org.apache.beam.sdk.values.CausedByDrain; +import org.apache.beam.sdk.values.ValueKind; import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; 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; @@ -51,6 +58,11 @@ @RunWith(JUnit4.class) public class WindowedValueTest { + @After + public void tearDown() { + WindowedValues.WindowedValueCoder.setMetadataNotSupported(); + } + @Rule public ExpectedException thrown = ExpectedException.none(); @Test @@ -81,6 +93,17 @@ public void testWindowedValueCoder() throws CoderException { @Test public void testWindowedValueWithElementMetadataCoder() throws CoderException { WindowedValues.WindowedValueCoder.setMetadataSupported(); + + Context context = + Context.current() + .with( + Span.wrap( + SpanContext.create( + "ff000000000000000000000000000041", + "ff00000000000041", + TraceFlags.getSampled(), + TraceState.builder().put("foo", "bar").put("bar", "baz").build()))); + Instant timestamp = new Instant(1234); WindowedValue value = WindowedValues.of( @@ -93,12 +116,15 @@ public void testWindowedValueWithElementMetadataCoder() throws CoderException { PaneInfo.NO_FIRING, null, null, - CausedByDrain.CAUSED_BY_DRAIN); // drain is persisted as part of metadata + CausedByDrain.CAUSED_BY_DRAIN, + context, + ValueKind.DELETE); // drain is persisted as part of metadata Coder> windowedValueCoder = WindowedValues.getFullCoder(StringUtf8Coder.of(), IntervalWindow.getCoder()); byte[] encodedValue = CoderUtils.encodeToByteArray(windowedValueCoder, value); + WindowedValue decodedValue = CoderUtils.decodeFromByteArray(windowedValueCoder, encodedValue); @@ -106,6 +132,35 @@ public void testWindowedValueWithElementMetadataCoder() throws CoderException { Assert.assertEquals(value.getTimestamp(), decodedValue.getTimestamp()); Assert.assertArrayEquals(value.getWindows().toArray(), decodedValue.getWindows().toArray()); Assert.assertEquals(CausedByDrain.CAUSED_BY_DRAIN, value.causedByDrain()); + Assert.assertNotNull(value.getOpenTelemetryContext()); + 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 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/extensions/arrow/src/main/java/org/apache/beam/sdk/extensions/arrow/ArrowConversion.java b/sdks/java/extensions/arrow/src/main/java/org/apache/beam/sdk/extensions/arrow/ArrowConversion.java index 3e22bad6a415..bf7078abc58e 100644 --- a/sdks/java/extensions/arrow/src/main/java/org/apache/beam/sdk/extensions/arrow/ArrowConversion.java +++ b/sdks/java/extensions/arrow/src/main/java/org/apache/beam/sdk/extensions/arrow/ArrowConversion.java @@ -533,7 +533,9 @@ public Row next() { throw new IllegalStateException("There are no more Rows."); } Row result = - Row.withSchema(schema).withFieldValueGetters(this.fieldValueGetters, this.currRowIndex); + Row.withSchema(schema) + .withFieldValueGetters( + this.fieldValueGetters, this.currRowIndex, TypeDescriptor.of(Integer.class)); this.currRowIndex += 1; return result; } diff --git a/sdks/java/extensions/avro/build.gradle b/sdks/java/extensions/avro/build.gradle index e3afb5ff52e4..718f07c03660 100644 --- a/sdks/java/extensions/avro/build.gradle +++ b/sdks/java/extensions/avro/build.gradle @@ -40,6 +40,7 @@ def avroVersions = [ '182' : "1.8.2", '192' : "1.9.2", '1102': "1.10.2", + '1113': "1.11.3", ] avroVersions.each { k, v -> @@ -72,7 +73,7 @@ dependencies { // Exclude Avro dependencies from "core" since Avro support moved to this extension exclude group: "org.apache.avro", module: "avro" } - testImplementation project(path: ":sdks:java:extensions:avro:vendored-test", configuration: "shadowTest") + testImplementation(library.java.avro + ':tests') testImplementation library.java.junit testImplementation "org.tukaani:xz:1.9" // marked as optional in avro testImplementation "com.esotericsoftware:kryo:5.6.2" // Used by Avro coder test diff --git a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java index 457265646420..854b0d3c8bb0 100644 --- a/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java +++ b/sdks/java/extensions/avro/src/main/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtils.java @@ -156,6 +156,8 @@ "rawtypes" }) public class AvroUtils { + public static final String VERSION_AVRO = + org.apache.avro.Schema.class.getPackage().getImplementationVersion(); private static final ForLoadedType BYTES = new ForLoadedType(byte[].class); private static final ForLoadedType JAVA_INSTANT = new ForLoadedType(java.time.Instant.class); private static final ForLoadedType JAVA_LOCALE_DATE = @@ -333,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); } } @@ -356,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 = @@ -406,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); } } @@ -421,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 = @@ -477,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); } } diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/AvroVersionVerificationTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/AvroVersionVerificationTest.java index f9e9a54b0531..ac17afadae2a 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/AvroVersionVerificationTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/AvroVersionVerificationTest.java @@ -17,9 +17,9 @@ */ package org.apache.beam.sdk.extensions.avro; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.junit.Assert.assertEquals; -import org.apache.avro.Schema; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Assume; @@ -33,7 +33,6 @@ public class AvroVersionVerificationTest { public void testAvroVersion() { @Nullable String targetVer = System.getProperty("beam.target.avro.version"); Assume.assumeTrue(!Strings.isNullOrEmpty(targetVer)); - String actualVer = Schema.class.getPackage().getImplementationVersion(); - assertEquals(targetVer, actualVer); + assertEquals(targetVer, VERSION_AVRO); } } diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/coders/AvroCoderTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/coders/AvroCoderTest.java index 69dfe71ee0b1..e1843d79eff5 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/coders/AvroCoderTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/coders/AvroCoderTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.extensions.avro.coders; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -131,8 +132,6 @@ public class AvroCoderTest { ImmutableList.of(AVRO_NESTED_SPECIFIC_RECORD, AVRO_NESTED_SPECIFIC_RECORD), ImmutableMap.of("k1", AVRO_NESTED_SPECIFIC_RECORD, "k2", AVRO_NESTED_SPECIFIC_RECORD)); - private static final String VERSION_AVRO = Schema.class.getPackage().getImplementationVersion(); - @DefaultCoder(AvroCoder.class) private static class Pojo { public String text; @@ -875,6 +874,7 @@ private static class StringableClass {} @Test public void testDeterminismCyclicClass() { + // Note: this test fails on Avro 1.12.1 due to https://issues.apache.org/jira/browse/AVRO-4209 assertNonDeterministic( AvroCoder.of(Cyclic.class), reasonField(Cyclic.class, "cyclicField", "appears recursively")); @@ -1164,6 +1164,7 @@ private static class NullableCyclicField { @Test public void testNullableNonDeterministicField() { + // Note: this test fails on Avro 1.12.1 due to https://issues.apache.org/jira/browse/AVRO-4209 assertNonDeterministic( AvroCoder.of(NullableCyclic.class), reasonField( diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroGeneratedUserFactory.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroGeneratedUserFactory.java index 65552b705dff..8f16141443e0 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroGeneratedUserFactory.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroGeneratedUserFactory.java @@ -17,15 +17,15 @@ */ package org.apache.beam.sdk.extensions.avro.io; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; + import java.lang.reflect.Constructor; -import org.apache.avro.Schema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Create a {@link AvroGeneratedUser} instance with different constructors. */ public class AvroGeneratedUserFactory { private static final Logger LOG = LoggerFactory.getLogger(AvroGeneratedUserFactory.class); - private static final String VERSION_AVRO = Schema.class.getPackage().getImplementationVersion(); public static AvroGeneratedUser newInstance( String name, Integer favoriteNumber, String favoriteColor) { diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroSourceTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroSourceTest.java index f4841f1cdc02..27d070d6920a 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroSourceTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/AvroSourceTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.extensions.avro.io; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasDisplayItem; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -74,9 +75,6 @@ /** Tests for AvroSource. */ @RunWith(JUnit4.class) public class AvroSourceTest { - private static final String VERSION_AVRO = - org.apache.avro.Schema.class.getPackage().getImplementationVersion(); - @Rule public TemporaryFolder tmpFolder = new TemporaryFolder(); @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -560,10 +558,11 @@ public void testDatumReaderFactoryWithGenericRecord() throws Exception { AvroSource.DatumReaderFactory factory = (writer, reader) -> - new GenericDatumReader(writer, reader) { + new GenericDatumReader<>(writer, reader) { @Override - protected Object readString(Object old, Decoder in) throws IOException { - return super.readString(old, in) + "_custom"; + protected Object readString(Object old, Schema schema, Decoder in) + throws IOException { + return super.readString(old, schema, in) + "_custom"; } }; diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/SerializableAvroCodecFactoryTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/SerializableAvroCodecFactoryTest.java index 52f7a6700275..19141a82148a 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/SerializableAvroCodecFactoryTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/io/SerializableAvroCodecFactoryTest.java @@ -22,6 +22,7 @@ import static org.apache.avro.file.DataFileConstants.NULL_CODEC; import static org.apache.avro.file.DataFileConstants.SNAPPY_CODEC; import static org.apache.avro.file.DataFileConstants.XZ_CODEC; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.junit.Assert.assertEquals; import java.io.ByteArrayInputStream; @@ -40,9 +41,6 @@ /** Tests of SerializableAvroCodecFactory. */ @RunWith(JUnit4.class) public class SerializableAvroCodecFactoryTest { - private static final String VERSION_AVRO = - org.apache.avro.Schema.class.getPackage().getImplementationVersion(); - private static final List avroCodecs = new ArrayList<>(); static { diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroConversionFactory.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroConversionFactory.java index d6d6aeaffa3f..cdf16f71a69b 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroConversionFactory.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroConversionFactory.java @@ -17,15 +17,14 @@ */ package org.apache.beam.sdk.extensions.avro.schemas; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; + import java.lang.reflect.Constructor; -import org.apache.avro.Schema; import org.joda.time.LocalDate; /** Create a {@link TestAvroConversion} instance with different constructors. */ public class TestAvroConversionFactory { - private static final String VERSION_AVRO = Schema.class.getPackage().getImplementationVersion(); - public static TestAvroConversion newInstance(LocalDate date) throws Exception { if (VERSION_AVRO.equals("1.8.2")) { Constructor constructor = TestAvroConversion.class.getDeclaredConstructor(LocalDate.class); diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroFactory.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroFactory.java index 4d52a35543e0..ab1fdee92691 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroFactory.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/TestAvroFactory.java @@ -17,11 +17,12 @@ */ package org.apache.beam.sdk.extensions.avro.schemas; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; + import java.lang.reflect.Constructor; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; -import org.apache.avro.Schema; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.slf4j.Logger; @@ -30,7 +31,6 @@ /** Create a {@link TestAvro} instance with different constructors. */ public class TestAvroFactory { private static final Logger LOG = LoggerFactory.getLogger(TestAvroFactory.class); - private static final String VERSION_AVRO = Schema.class.getPackage().getImplementationVersion(); public static TestAvro newInstance( Boolean boolNonNullable, diff --git a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java index 57da8e77bb1b..6db5206c3cf6 100644 --- a/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java +++ b/sdks/java/extensions/avro/src/test/java/org/apache/beam/sdk/extensions/avro/schemas/utils/AvroUtilsTest.java @@ -17,10 +17,12 @@ */ package org.apache.beam.sdk.extensions.avro.schemas.utils; +import static org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.VERSION_AVRO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import com.fasterxml.jackson.databind.ObjectMapper; import com.pholser.junit.quickcheck.From; import com.pholser.junit.quickcheck.Property; import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; @@ -85,9 +87,6 @@ public class AvroUtilsTest { private static final org.apache.avro.Schema NULL_SCHEMA = org.apache.avro.Schema.create(org.apache.avro.Schema.Type.NULL); - private static final String VERSION_AVRO = - org.apache.avro.Schema.class.getPackage().getImplementationVersion(); - private Iterable randomData(org.apache.avro.Schema schema, int maxLength) throws Exception { Iterable data; if (VERSION_AVRO.equals("1.8.2")) { @@ -284,10 +283,20 @@ public void avroToBeamRoundTrip( Iterable iterable = randomData(avroSchema, 10); List records = Lists.newArrayList((Iterable) iterable); + // AVRO-4139: GenericRecord.equals() throws "Can't compare maps!" for records with + // nested map types on Avro 1.12.0. Fall back to JSON tree comparison on that version + // only; keep direct equals for other versions. + boolean useJsonCompare = "1.12.0".equals(VERSION_AVRO); + ObjectMapper mapper = useJsonCompare ? new ObjectMapper() : null; + for (GenericRecord record : records) { Row row = AvroUtils.toBeamRowStrict(record, schema); GenericRecord out = AvroUtils.toGenericRecord(row, avroSchema); - assertEquals(record, out); + if (useJsonCompare) { + assertEquals(mapper.readTree(record.toString()), mapper.readTree(out.toString())); + } else { + assertEquals(record, out); + } } } diff --git a/sdks/java/extensions/avro/vendored-test/build.gradle b/sdks/java/extensions/avro/vendored-test/build.gradle deleted file mode 100644 index b0489c27a13b..000000000000 --- a/sdks/java/extensions/avro/vendored-test/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * License); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an AS IS BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -plugins { id 'org.apache.beam.module' } -applyJavaNature( - automaticModuleName: 'org.apache.beam.sdk.extensions.avro', - exportJavadoc: false, - shadowClosure: { - dependencies { - include(dependency("org.apache.avro:avro:1.11.3:tests")) - } - }, -) - -configurations.all { - resolutionStrategy.force "org.apache.avro:avro:1.11.3:tests" -} - -dependencies { - testRuntimeOnly "org.apache.avro:avro:1.11.3:tests" -} - -description = "Apache Beam :: SDKs :: Java :: Extensions :: Avro :: Vendored Tests" -ext.summary = "Vendor Avro 1.11.3 tests for Beam, a workaround of Avro 1.11.4 not release test jar" diff --git a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtil.java b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtil.java index e3f01dd85295..ed727d495cf8 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtil.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtil.java @@ -25,6 +25,8 @@ import com.google.cloud.storage.BucketInfo; import com.google.cloud.storage.Storage.BlobGetOption; import com.google.cloud.storage.Storage.BlobListOption; +import com.google.cloud.storage.Storage.BlobSourceOption; +import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.Storage.BucketGetOption; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; @@ -186,9 +188,19 @@ public Page listBlobs( } public SeekableByteChannel open(GcsPath path) throws IOException { + if (delegateV2 != null) { + return delegateV2.open(path); + } return delegate.open(path); } + public SeekableByteChannel openV2(GcsPath path, BlobSourceOption... options) throws IOException { + if (delegateV2 != null) { + return delegateV2.open(path, options); + } + throw new IOException("GcsUtil V2 not initialized."); + } + /** @deprecated Use {@link #create(GcsPath, CreateOptions)} instead. */ @Deprecated public WritableByteChannel create(GcsPath path, String type) throws IOException { @@ -254,9 +266,20 @@ public CreateOptions build() { } public WritableByteChannel create(GcsPath path, CreateOptions options) throws IOException { + if (delegateV2 != null) { + delegateV2.create(path, options.delegate); + } return delegate.create(path, options.delegate); } + public WritableByteChannel createV2( + GcsPath path, CreateOptions options, BlobWriteOption... writeOptions) throws IOException { + if (delegateV2 != null) { + return delegateV2.create(path, options.delegate, writeOptions); + } + throw new IOException("GcsUtil V2 not initialized."); + } + public void verifyBucketAccessible(GcsPath path) throws IOException { if (delegateV2 != null) { delegateV2.verifyBucketAccessible(path); diff --git a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java index 1ade4be6fdb5..1ad08f0ba1a2 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV1.java @@ -30,7 +30,6 @@ import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpStatusCodes; -import com.google.api.client.http.HttpTransport; import com.google.api.client.util.BackOff; import com.google.api.client.util.Sleeper; import com.google.api.services.storage.Storage; @@ -53,7 +52,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.FileNotFoundException; import java.io.IOException; -import java.lang.reflect.Method; import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.AccessDeniedException; @@ -73,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; @@ -186,6 +185,7 @@ public boolean shouldRetry(IOException e) { return RetryDeterminer.SOCKET_ERRORS.shouldRetry(e); } }; + private static final AtomicBoolean overwriteLog = new AtomicBoolean(false); ///////////////////////////////////////////////////////////////////////////// @@ -267,8 +267,12 @@ public boolean shouldRetry(IOException e) { .setReadChannelOptions(gcsReadOptions) .setGrpcEnabled(shouldUseGrpc) .build(); - googleCloudStorage = - createGoogleCloudStorage(googleCloudStorageOptions, storageClient, credentials); + try { + googleCloudStorage = + createGoogleCloudStorage(googleCloudStorageOptions, storageClient, credentials); + } catch (IOException e) { + throw new RuntimeException(e); + } this.batchRequestSupplier = () -> { // Capture reference to this so that the most recent storageClient and initializer @@ -724,49 +728,24 @@ 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) { - try { - return new GoogleCloudStorageImpl(options, storage, credentials); - } catch (NoSuchMethodError e) { - // gcs-connector 3.x drops the direct constructor and exclusively uses Builder - // TODO eliminate reflection once Beam drops Java 8 support and upgrades to gcsio 3.x - try { - final Method builderMethod = GoogleCloudStorageImpl.class.getMethod("builder"); - Object builder = builderMethod.invoke(null); - final Class builderClass = - Class.forName( - "com.google.cloud.hadoop.gcsio.AutoBuilder_GoogleCloudStorageImpl_Builder"); - - final Method setOptionsMethod = - builderClass.getMethod("setOptions", GoogleCloudStorageOptions.class); - setOptionsMethod.setAccessible(true); - builder = setOptionsMethod.invoke(builder, options); - - final Method setHttpTransportMethod = - builderClass.getMethod("setHttpTransport", HttpTransport.class); - setHttpTransportMethod.setAccessible(true); - builder = - setHttpTransportMethod.invoke(builder, storage.getRequestFactory().getTransport()); - - final Method setCredentialsMethod = - builderClass.getMethod("setCredentials", Credentials.class); - setCredentialsMethod.setAccessible(true); - builder = setCredentialsMethod.invoke(builder, credentials); - - final Method setHttpRequestInitializerMethod = - builderClass.getMethod("setHttpRequestInitializer", HttpRequestInitializer.class); - setHttpRequestInitializerMethod.setAccessible(true); - builder = setHttpRequestInitializerMethod.invoke(builder, httpRequestInitializer); - - final Method buildMethod = builderClass.getMethod("build"); - buildMethod.setAccessible(true); - return (GoogleCloudStorage) buildMethod.invoke(builder); - } catch (Exception reflectionError) { - throw new RuntimeException( - "Failed to construct GoogleCloudStorageImpl from gcsio 3.x Builder", reflectionError); - } + GoogleCloudStorageOptions options, Storage storage, Credentials credentials) + throws IOException { + // 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()) + .setCredentials(credentials) + // gcsio 3 expects httpRequestInitializer to be either absent or + // com.google.cloud.hadoop.util.RetryHttpInitializer when credentials not provided + .setHttpRequestInitializer(credentials != null ? httpRequestInitializer : null) + .build(); } /** diff --git a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV2.java b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV2.java index b00b7ce0d728..9119dd79652e 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV2.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/main/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilV2.java @@ -23,6 +23,8 @@ import com.google.api.gax.paging.Page; import com.google.auto.value.AutoValue; +import com.google.cloud.ReadChannel; +import com.google.cloud.WriteChannel; import com.google.cloud.storage.Blob; import com.google.cloud.storage.BlobId; import com.google.cloud.storage.BlobInfo; @@ -33,21 +35,29 @@ import com.google.cloud.storage.Storage.BlobField; import com.google.cloud.storage.Storage.BlobGetOption; import com.google.cloud.storage.Storage.BlobListOption; +import com.google.cloud.storage.Storage.BlobSourceOption; +import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.Storage.BucketField; import com.google.cloud.storage.Storage.BucketGetOption; import com.google.cloud.storage.Storage.CopyRequest; import com.google.cloud.storage.StorageBatch; import com.google.cloud.storage.StorageBatchResult; +import com.google.cloud.storage.StorageChannelUtils; import com.google.cloud.storage.StorageException; import com.google.cloud.storage.StorageOptions; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.file.AccessDeniedException; import java.nio.file.FileAlreadyExistsException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; +import org.apache.beam.sdk.extensions.gcp.options.GcsOptions; import org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath; import org.apache.beam.sdk.options.DefaultValueFactory; import org.apache.beam.sdk.options.PipelineOptions; @@ -70,6 +80,8 @@ public GcsUtilV2 create(PipelineOptions options) { private Storage storage; + private final @Nullable Integer uploadBufferSizeBytes; + /** Maximum number of items to retrieve per Objects.List request. */ private static final long MAX_LIST_BLOBS_PER_CALL = 1024; @@ -85,13 +97,14 @@ public GcsUtilV2 create(PipelineOptions options) { GcsUtilV2(PipelineOptions options) { String projectId = options.as(GcpOptions.class).getProject(); storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService(); + uploadBufferSizeBytes = options.as(GcsOptions.class).getGcsUploadBufferSizeBytes(); } @SuppressWarnings({ "nullness" // For Creating AccessDeniedException FileNotFoundException, and // FileAlreadyExistsException with null. }) - private IOException translateStorageException(GcsPath gcsPath, StorageException e) { + private static IOException translateStorageException(GcsPath gcsPath, StorageException e) { switch (e.getCode()) { case 403: return new AccessDeniedException(gcsPath.toString(), null, e.getMessage()); @@ -481,4 +494,152 @@ public void removeBucket(BucketInfo bucketInfo) throws IOException { throw translateStorageException(bucketInfo.getName(), null, e); } } + + /** A bridge that allows a GCS ReadChannel to behave as a SeekableByteChannel. */ + private static class GcsSeekableByteChannel implements SeekableByteChannel { + private final ReadChannel reader; + private final long size; + private long position = 0; + + GcsSeekableByteChannel(ReadChannel reader, long size) { + this.reader = reader; + this.size = size; + this.position = 0; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int count = StorageChannelUtils.blockingFillFrom(dst, reader); + if (count > 0) { + this.position += count; + } + return count; + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + checkArgument(newPosition >= 0, "Position must be non-negative: %s", newPosition); + reader.seek(newPosition); + this.position = newPosition; + return this; + } + + @Override + public long position() throws IOException { + return this.position; + } + + @Override + public long size() throws IOException { + return size; + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + throw new UnsupportedOperationException( + "GcsSeekableByteChannels are read-only and cannot be truncated."); + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw new UnsupportedOperationException( + "GcsSeekableByteChannel are read-only and does not support writing."); + } + + @Override + public boolean isOpen() { + return reader.isOpen(); + } + + @Override + public void close() throws IOException { + if (isOpen()) { + reader.close(); + } + } + } + + public SeekableByteChannel open(GcsPath path, BlobSourceOption... sourceOptions) + throws IOException { + Blob blob = getBlob(path, BlobGetOption.fields(BlobField.SIZE)); + ReadChannel reader = blob.getStorage().reader(blob.getBlobId(), sourceOptions); + // disable internal buffering, and make the channel non-blocking + reader.setChunkSize(0); + return new GcsSeekableByteChannel(reader, blob.getSize()); + } + + /** A bridge that allows a GCS WriteChannel to behave as a WritableByteChannel. */ + private static class GcsWritableByteChannel implements WritableByteChannel { + private final WriteChannel writer; + private final GcsPath gcsPath; + + GcsWritableByteChannel(WriteChannel writer, GcsPath gcsPath) { + this.writer = writer; + this.gcsPath = gcsPath; + } + + @Override + public int write(ByteBuffer src) throws IOException { + try { + return writer.write(src); + } catch (StorageException e) { + throw translateStorageException(gcsPath, e); + } + } + + @Override + public boolean isOpen() { + return writer.isOpen(); + } + + @Override + public void close() throws IOException { + writer.close(); + } + } + + public WritableByteChannel create( + GcsPath path, GcsUtilV1.CreateOptions options, BlobWriteOption... writeOptions) + throws IOException { + try { + // Define the metadata for the new object + BlobInfo.Builder builder = BlobInfo.newBuilder(path.getBucket(), path.getObject()); + String type = options.getContentType(); + if (type != null) { + builder.setContentType(type); + } + + BlobInfo blobInfo = builder.build(); + + List writeOptionList = new ArrayList<>(Arrays.asList(writeOptions)); + if (options.getExpectFileToNotExist()) { + writeOptionList.add(BlobWriteOption.doesNotExist()); + } else { + // We do not merge this check with the getExpectFileToNotExist() branch above + // because we don't want to always make the storage.get() RPC call. + Blob blob = storage.get(path.getBucket(), path.getObject()); + if (blob == null) { + writeOptionList.add(BlobWriteOption.doesNotExist()); + } else { + writeOptionList.add(BlobWriteOption.generationMatch(blob.getGeneration())); + } + } + // Open a WriteChannel from the storage service + WriteChannel writer = + storage.writer(blobInfo, writeOptionList.toArray(new BlobWriteOption[0])); + Integer uploadBufferSizeBytes = + options.getUploadBufferSizeBytes() != null + ? options.getUploadBufferSizeBytes() + : this.uploadBufferSizeBytes; + if (uploadBufferSizeBytes != null) { + writer.setChunkSize(uploadBufferSizeBytes); + } + + // Return the bridge wrapper + return new GcsWritableByteChannel(writer, path); + + } catch (StorageException e) { + throw translateStorageException(path, e); + } + } } diff --git a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilParameterizedIT.java b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilParameterizedIT.java index 80ffd72924fa..5759bb10a654 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilParameterizedIT.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilParameterizedIT.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.extensions.gcp.util; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -28,15 +29,25 @@ import com.google.api.services.storage.model.StorageObject; import com.google.cloud.storage.Blob; import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.StorageChannelUtils; +import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.AccessDeniedException; import java.nio.file.FileAlreadyExistsException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.apache.beam.sdk.extensions.gcp.options.GcsOptions; +import org.apache.beam.sdk.extensions.gcp.util.GcsUtil.CreateOptions; import org.apache.beam.sdk.extensions.gcp.util.GcsUtilV2.MissingStrategy; import org.apache.beam.sdk.extensions.gcp.util.GcsUtilV2.OverwriteStrategy; import org.apache.beam.sdk.extensions.gcp.util.gcsfs.GcsPath; @@ -301,7 +312,8 @@ public void testCreateAndRemoveBucket() throws IOException { } } - private List createTestBucketHelper(String bucketName) throws IOException { + private List createTestBucketHelper(String bucketName, boolean copyData) + throws IOException { final List originPaths = Arrays.asList( GcsPath.fromUri("gs://apache-beam-samples/shakespeare/kingrichardii.txt"), @@ -316,16 +328,24 @@ private List createTestBucketHelper(String bucketName) throws IOExcepti if (experiment.equals("use_gcsutil_v2")) { gcsUtil.createBucket(BucketInfo.of(bucketName)); - gcsUtil.copyV2(originPaths, testPaths); + if (copyData) { + gcsUtil.copyV2(originPaths, testPaths); + } else { + return Collections.emptyList(); + } } else { GcsOptions gcsOptions = options.as(GcsOptions.class); gcsUtil.createBucket(gcsOptions.getProject(), new Bucket().setName(bucketName)); - final List originList = - originPaths.stream().map(o -> o.toString()).collect(Collectors.toList()); - final List testList = - testPaths.stream().map(o -> o.toString()).collect(Collectors.toList()); - gcsUtil.copy(originList, testList); + if (copyData) { + final List originList = + originPaths.stream().map(o -> o.toString()).collect(Collectors.toList()); + final List testList = + testPaths.stream().map(o -> o.toString()).collect(Collectors.toList()); + gcsUtil.copy(originList, testList); + } else { + return Collections.emptyList(); + } } return testPaths; @@ -355,7 +375,7 @@ public void testCopy() throws IOException { final String nonExistentBucket = "my-random-test-bucket-12345"; try { - final List srcPaths = createTestBucketHelper(existingBucket); + final List srcPaths = createTestBucketHelper(existingBucket, true); final List dstPaths = srcPaths.stream() .map(o -> GcsPath.fromComponents(existingBucket, o.getObject() + ".bak")) @@ -423,7 +443,7 @@ public void testRemove() throws IOException { final String nonExistentBucket = "my-random-test-bucket-12345"; try { - final List srcPaths = createTestBucketHelper(existingBucket); + final List srcPaths = createTestBucketHelper(existingBucket, true); final List errPaths = srcPaths.stream() .map(o -> GcsPath.fromComponents(nonExistentBucket, o.getObject())) @@ -485,7 +505,7 @@ public void testRename() throws IOException { final String nonExistentBucket = "my-random-test-bucket-12345"; try { - final List srcPaths = createTestBucketHelper(existingBucket); + final List srcPaths = createTestBucketHelper(existingBucket, true); final List tmpPaths = srcPaths.stream() .map(o -> GcsPath.fromComponents(existingBucket, "tmp/" + o.getObject())) @@ -587,4 +607,80 @@ private void assertNotExists(GcsPath path) throws IOException { assertThrows(FileNotFoundException.class, () -> gcsUtil.getObject(path)); } } + + String computeHash(ByteBuffer buffer) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(buffer); + byte[] hashBytes = digest.digest(); + + // Convert bytes to Hex String + StringBuilder sb = new StringBuilder(); + for (byte b : hashBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + @Test + public void testRead() throws IOException, NoSuchAlgorithmException { + final GcsPath gcsPath = GcsPath.fromUri("gs://apache-beam-samples/shakespeare/kinglear.txt"); + final String expectedHash = "674a2725884307c96398440497c889ad8cecccedf5689df85e6b0faabe4e0fe8"; + final long expectedSize = 157283L; + + try (SeekableByteChannel channel = gcsUtil.open(gcsPath)) { + // Verify Size + assertEquals(expectedSize, channel.size()); + assertEquals(0, channel.position()); + + // Read content into ByteBuffer. + // Allocate a larger buffer to ensure we receive the EOF at the expected place. + ByteBuffer buffer = ByteBuffer.allocate((int) expectedSize + 1024); + int bytesRead = StorageChannelUtils.blockingFillFrom(buffer, channel); + + // Verify total bytes read and position + assertEquals(expectedSize, bytesRead); + assertEquals(expectedSize, channel.position()); + + // Flip the buffer to prepare it for reading (sets limit to current position, position to 0) + buffer.flip(); + + // Verify hash + String actualHash = computeHash(buffer); + assertEquals("Content hash should match", expectedHash, actualHash); + } + } + + @Test + public void testWriteAndRead() throws IOException { + final String bucketName = "apache-beam-temp-bucket-12345"; + final GcsPath targetPath = + GcsPath.fromComponents(bucketName, "test-object-" + java.util.UUID.randomUUID() + ".txt"); + final byte[] content = "Hello, GCS!".getBytes(StandardCharsets.UTF_8); + + try { + createTestBucketHelper(bucketName, false); + + // Write content to a GCS file + CreateOptions options = CreateOptions.builder().setExpectFileToNotExist(true).build(); + try (WritableByteChannel writer = gcsUtil.create(targetPath, options)) { + writer.write(ByteBuffer.wrap(content)); + } + + // Read content into a buffer + ByteArrayOutputStream readContent = new ByteArrayOutputStream(); + try (ReadableByteChannel reader = gcsUtil.open(targetPath)) { + ByteBuffer buffer = ByteBuffer.allocate(1024); + while (reader.read(buffer) != -1) { + buffer.flip(); + readContent.write(buffer.array(), 0, buffer.limit()); + buffer.clear(); + } + } + + // Verify content + assertArrayEquals(content, readContent.toByteArray()); + } finally { + tearDownTestBucketHelper(bucketName); + } + } } diff --git a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilTest.java b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilTest.java index a2b0e0af502b..d32ca162e3fd 100644 --- a/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilTest.java +++ b/sdks/java/extensions/google-cloud-platform-core/src/test/java/org/apache/beam/sdk/extensions/gcp/util/GcsUtilTest.java @@ -184,8 +184,8 @@ public void testCreationWithExplicitGoogleCloudStorageReadOptions() throws Excep GoogleCloudStorageReadOptions readOptions = GoogleCloudStorageReadOptions.builder() .setFadvise(GoogleCloudStorageReadOptions.Fadvise.AUTO) - .setSupportGzipEncoding(true) - .setFastFailOnNotFound(false) + .setGzipEncodingSupportEnabled(true) + .setFastFailOnNotFoundEnabled(false) .build(); GcsOptions pipelineOptions = PipelineOptionsFactory.as(GcsOptions.class); @@ -193,7 +193,10 @@ public void testCreationWithExplicitGoogleCloudStorageReadOptions() throws Excep GcsUtil gcsUtil = pipelineOptions.getGcsUtil(); GoogleCloudStorage googleCloudStorageMock = Mockito.spy(GoogleCloudStorage.class); - Mockito.when(googleCloudStorageMock.open(Mockito.any(), Mockito.any())) + Mockito.when( + googleCloudStorageMock.open( + Mockito.any(StorageResourceId.class), + Mockito.any(GoogleCloudStorageReadOptions.class))) .thenReturn(Mockito.mock(SeekableByteChannel.class)); gcsUtil.delegate.setCloudStorageImpl(googleCloudStorageMock); @@ -1006,7 +1009,7 @@ public void testGCSChannelCloseIdempotent() throws IOException { GcsOptions pipelineOptions = gcsOptionsWithTestCredential(); GcsUtil gcsUtil = pipelineOptions.getGcsUtil(); GoogleCloudStorageReadOptions readOptions = - GoogleCloudStorageReadOptions.builder().setFastFailOnNotFound(false).build(); + GoogleCloudStorageReadOptions.builder().setFastFailOnNotFoundEnabled(false).build(); gcsUtil.delegate.setCloudStorageImpl( GoogleCloudStorageOptions.builder() @@ -1026,7 +1029,7 @@ public void testGCSReadMetricsIsSet() { GcsOptions pipelineOptions = gcsOptionsWithTestCredential(); GcsUtil gcsUtil = pipelineOptions.getGcsUtil(); GoogleCloudStorageReadOptions readOptions = - GoogleCloudStorageReadOptions.builder().setFastFailOnNotFound(true).build(); + GoogleCloudStorageReadOptions.builder().setFastFailOnNotFoundEnabled(true).build(); gcsUtil.delegate.setCloudStorageImpl( GoogleCloudStorageOptions.builder() .setAppName("Beam") @@ -1673,8 +1676,10 @@ public static GcsUtilV1Mock createMockWithMockStorage( .thenReturn(Channels.newChannel(new ByteArrayOutputStream())); } else { SeekableByteChannel seekableByteChannel = new SeekableInMemoryByteChannel(readPayload); - Mockito.when(googleCloudStorageMock.open(Mockito.any())).thenReturn(seekableByteChannel); - Mockito.when(googleCloudStorageMock.open(Mockito.any(), Mockito.any())) + Mockito.when(googleCloudStorageMock.open(Mockito.any(StorageResourceId.class))) + .thenReturn(seekableByteChannel); + Mockito.when( + googleCloudStorageMock.open(Mockito.any(StorageResourceId.class), Mockito.any())) .thenReturn(seekableByteChannel); } return gcsUtilMock; 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/runners/samza/src/main/java/org/apache/beam/runners/samza/container/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 similarity index 84% rename from runners/samza/src/main/java/org/apache/beam/runners/samza/container/package-info.java rename to sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/package-info.java index 58a09e6023de..e389a1a7dc9e 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/container/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 @@ -16,5 +16,5 @@ * limitations under the License. */ -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.container; +/** 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 1e319c97a8e2..893a485e7d86 100644 --- a/sdks/java/extensions/sql/iceberg/build.gradle +++ b/sdks/java/extensions/sql/iceberg/build.gradle @@ -48,6 +48,7 @@ dependencies { testImplementation library.java.google_api_services_bigquery testImplementation "org.apache.iceberg:iceberg-api:1.9.2" testImplementation "org.apache.iceberg:iceberg-core:1.9.2" + testRuntimeOnly library.java.bigdataoss_util_hadoop testImplementation project(":sdks:java:io:google-cloud-platform") testImplementation project(":sdks:java:extensions:google-cloud-platform-core") } diff --git a/sdks/java/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/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/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/CalciteQueryPlanner.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/CalciteQueryPlanner.java index 606a3c5f71a2..aa6f4d121871 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; @@ -180,8 +185,8 @@ public SqlNode parse(String sqlStatement) throws ParseException { public BeamRelNode convertToBeamRel(String sqlStatement, QueryParameters queryParameters) throws ParseException, SqlConversionException { Preconditions.checkArgument( - queryParameters.getKind() == Kind.NONE, - "Beam SQL Calcite dialect does not yet support query parameters."); + queryParameters.getKind() == Kind.NONE || queryParameters.getKind() == Kind.POSITIONAL, + "Beam SQL Calcite dialect only supports positional query parameters."); BeamRelNode beamRelNode; try { SqlNode parsed = planner.parse(sqlStatement); @@ -191,28 +196,35 @@ public BeamRelNode convertToBeamRel(String sqlStatement, QueryParameters queryPa // root of original logical plan RelRoot root = planner.rel(validated); + RelNode relNode = root.rel; + if (queryParameters.getKind() == Kind.POSITIONAL) { + relNode = + bindParameters( + relNode, + new ParameterBinder(root.rel.getCluster().getRexBuilder(), queryParameters)); + } LOG.info("SQLPlan>\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 +237,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 +286,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/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/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/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/utils/CalciteUtils.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java index 3aaa91680999..d55c227e7b45 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java @@ -170,6 +170,25 @@ public static boolean isStringType(FieldType fieldType) { FieldType.DATETIME, SqlTypeName.TIMESTAMP, FieldType.STRING, SqlTypeName.VARCHAR); + private static final Map, SqlTypeName> JAVA_TO_SQL_TYPE_MAPPING = + ImmutableMap., SqlTypeName>builder() + .put(String.class, SqlTypeName.VARCHAR) + .put(Integer.class, SqlTypeName.INTEGER) + .put(int.class, SqlTypeName.INTEGER) + .put(Long.class, SqlTypeName.BIGINT) + .put(long.class, SqlTypeName.BIGINT) + .put(Double.class, SqlTypeName.DOUBLE) + .put(double.class, SqlTypeName.DOUBLE) + .put(Float.class, SqlTypeName.FLOAT) + .put(float.class, SqlTypeName.FLOAT) + .put(Short.class, SqlTypeName.SMALLINT) + .put(short.class, SqlTypeName.SMALLINT) + .put(Byte.class, SqlTypeName.TINYINT) + .put(byte.class, SqlTypeName.TINYINT) + .put(Boolean.class, SqlTypeName.BOOLEAN) + .put(boolean.class, SqlTypeName.BOOLEAN) + .build(); + // Associating FieldType to generated RelDataType objects for Beam logical types. Used for // recovering the original type in output schema after full Beam FieldType->Calcite Type->Beam // FieldType trip @@ -365,7 +384,9 @@ private static RelDataType toRelDataType( * SQL-Java type mapping, with specified Beam rules:
      * 1. redirect {@link AbstractInstant} to {@link Date} so Calcite can recognize it.
      * 2. For a list, the component type is needed to create a Sql array type.
      - * 3. For a Map, the component type is needed to create a Sql map type. + * 3. For a Map, the component type is needed to create a Sql map type.
      + * 4. For standard Java classes (String, Integer, etc.), map them to corresponding Calcite SQL + * type with appropriate nullability. * * @param type * @return Calcite RelDataType @@ -396,6 +417,14 @@ public static RelDataType sqlTypeWithAutoCast(RelDataTypeFactory typeFactory, Ty + ". This is currently unsupported, use List instead " + "of Array."); } + if (type instanceof Class) { + Class clazz = (Class) type; + SqlTypeName sqlTypeName = JAVA_TO_SQL_TYPE_MAPPING.get(clazz); + if (sqlTypeName != null) { + return typeFactory.createTypeWithNullability( + typeFactory.createSqlType(sqlTypeName), !clazz.isPrimitive()); + } + } return typeFactory.createJavaType((Class) type); } } diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java index cdee6c930224..68e80c2340fa 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java @@ -18,7 +18,6 @@ package org.apache.beam.sdk.extensions.sql.meta.catalog; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import java.util.Collection; import java.util.Collections; @@ -111,13 +110,20 @@ public Collection databases() { @Override public boolean dropDatabase(String database, boolean cascade) { - checkState(!cascade, "%s does not support CASCADE.", getClass().getSimpleName()); + MetaStore metaStore = metaStores.get(database); + if (!cascade && metaStore != null && !metaStore.getTables().isEmpty()) { + throw new IllegalStateException("Database '" + database + "' is not empty."); + } boolean removed = databases.remove(database); + if (!removed) { + return false; + } if (database.equals(currentDatabase)) { currentDatabase = null; } - return removed; + metaStores.remove(database); + return true; } @Override diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/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/LazyAggregateCombineFnTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/LazyAggregateCombineFnTest.java index 17636c628eb8..408118ff09e9 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/LazyAggregateCombineFnTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/LazyAggregateCombineFnTest.java @@ -34,6 +34,7 @@ import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.type.RelDataTypeSystem; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.AggregateFunction; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.FunctionParameter; +import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.type.SqlTypeName; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Rule; @@ -77,7 +78,9 @@ public void subclassGetUdafImpl() { LazyAggregateCombineFn combiner = new LazyAggregateCombineFn<>(aggregateFn); AggregateFunction aggregateFunction = combiner.getUdafImpl(); RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl(RelDataTypeSystem.DEFAULT); - RelDataType expectedType = typeFactory.createJavaType(Long.class); + RelDataType expectedType = + typeFactory.createTypeWithNullability( + typeFactory.createSqlType(SqlTypeName.BIGINT), true); List params = aggregateFunction.getParameters(); assertThat(params, hasSize(1)); diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/UdfImplTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/UdfImplTest.java new file mode 100644 index 000000000000..3746b9ac8471 --- /dev/null +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/UdfImplTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.extensions.sql.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.lang.reflect.Method; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link UdfImpl}. */ +@RunWith(JUnit4.class) +public class UdfImplTest { + + @Test + public void testFindMethod_overloaded_prioritizesMaxParams() { + Method method = UdfImpl.findMethod(OverloadedFn.class, "eval"); + assertNotNull(method); + assertEquals(3, method.getParameterTypes().length); + } + + @Test + public void testFindMethod_singleMethod() { + Method method = UdfImpl.findMethod(SingleFn.class, "eval"); + assertNotNull(method); + assertEquals(1, method.getParameterTypes().length); + } + + public static class OverloadedFn { + public String eval(String a) { + return a; + } + + public String eval(String a, String b) { + return a + b; + } + + public String eval(String a, String b, String c) { + return a + b + c; + } + } + + public static class SingleFn { + public String eval(String a) { + return a; + } + } +} diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java index dbe8be441ac6..0730f17d9c84 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java @@ -17,6 +17,8 @@ */ package org.apache.beam.sdk.extensions.sql.impl.rel; +import java.util.ArrayList; +import java.util.List; import org.apache.beam.sdk.extensions.sql.TestUtils; import org.apache.beam.sdk.extensions.sql.impl.planner.BeamRelMetadataQuery; import org.apache.beam.sdk.extensions.sql.impl.planner.NodeStats; @@ -24,6 +26,7 @@ import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.RelNode; @@ -316,4 +319,44 @@ public void testNodeStatsEstimation() { Assert.assertEquals(10., estimate.getRowCount(), 0.01); Assert.assertEquals(10., estimate.getWindow(), 0.01); } + + @Test + public void testOrderBy_noLimit() { + String sql = + "SELECT order_id, site_id, price " + + "FROM ORDER_DETAILS " + + "ORDER BY order_id asc, site_id desc"; + + PCollection rows = compilePipeline(sql, pipeline); + PAssert.that(rows).satisfies(new AssertSorted()); + pipeline.run().waitUntilFinish(); + } + + private static class AssertSorted implements SerializableFunction, Void> { + @Override + public Void apply(Iterable input) { + List list = new ArrayList<>(); + for (Row r : input) { + list.add(r); + } + Assert.assertEquals(10, list.size()); + for (int i = 0; i < list.size() - 1; i++) { + Row r1 = list.get(i); + Row r2 = list.get(i + 1); + Long id1 = r1.getInt64("order_id"); + Long id2 = r2.getInt64("order_id"); + int comp = id1.compareTo(id2); + if (comp > 0) { + Assert.fail("Rows not sorted by order_id asc: " + list); + } else if (comp == 0) { + Integer site1 = r1.getInt32("site_id"); + Integer site2 = r2.getInt32("site_id"); + if (site1 < site2) { + Assert.fail("Rows not sorted by site_id desc when order_id is equal: " + list); + } + } + } + return null; + } + } } diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/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 4d7819ce9b42..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 @@ -2070,6 +2089,22 @@ public Instant timestamp(DoFn doFn) { return timestamp(); } + @Override + public @Nullable String currentRecordId(DoFn doFn) { + return currentRecordId(); + } + + @Override + public @Nullable Long currentRecordOffset(DoFn doFn) { + return currentRecordOffset(); + } + + @Override + public Instant fireTimestamp(DoFn doFn) { + throw new UnsupportedOperationException( + "Cannot access fire timestamp outside of @OnTimer method."); + } + @Override public String timerId(DoFn doFn) { throw new UnsupportedOperationException( @@ -2215,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; @@ -2270,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(); + } } /** @@ -2317,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() @@ -2348,7 +2405,9 @@ public void output(TupleTag tag, T output) { currentTimer.getPaneInfo(), null, null, - currentTimer.causedByDrain())); + currentTimer.causedByDrain(), + null, + currentElement.getValueKind())); } @Override @@ -2417,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; @@ -2442,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; @@ -2602,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; @@ -2748,6 +2827,20 @@ public TimeDomain timeDomain(DoFn doFn) { return currentTimeDomain; } + @Override + 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..449afd6a0243 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/control/ProcessBundleHandler.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/control/ProcessBundleHandler.java @@ -224,7 +224,6 @@ public ProcessBundleHandler( private void addRunnerAndConsumersForPTransformRecursively( BeamFnStateClient beamFnStateClient, - BeamFnDataClient queueingClient, String pTransformId, PTransform pTransform, Supplier processBundleInstructionId, @@ -257,7 +256,6 @@ private void addRunnerAndConsumersForPTransformRecursively( for (String consumingPTransformId : pCollectionIdsToConsumingPTransforms.get(pCollectionId)) { addRunnerAndConsumersForPTransformRecursively( beamFnStateClient, - queueingClient, consumingPTransformId, processBundleDescriptor.getTransformsMap().get(consumingPTransformId), processBundleInstructionId, @@ -315,7 +313,7 @@ public ShortIdMap getShortIdMap() { @Override public BeamFnDataClient getBeamFnDataClient() { - return queueingClient; + return beamFnDataClient; } @Override @@ -378,9 +376,8 @@ public FnDataReceiver addOutgoingDataEndpoint( outboundAggregatorMap.computeIfAbsent( apiServiceDescriptor, asd -> - queueingClient.createOutboundAggregator( - asd, - processBundleInstructionId, + new BeamFnDataOutboundAggregator( + options, runnerCapabilities.contains( BeamUrns.getUrn( StandardRunnerProtocols.Enum @@ -391,21 +388,19 @@ public FnDataReceiver addOutgoingDataEndpoint( @Override public FnDataReceiver> addOutgoingTimersEndpoint( String timerFamilyId, org.apache.beam.sdk.coders.Coder> coder) { - BeamFnDataOutboundAggregator aggregator; if (!processBundleDescriptor.hasTimerApiServiceDescriptor()) { throw new IllegalStateException( String.format( - "Timers are unsupported because the " - + "ProcessBundleRequest %s does not provide a timer ApiServiceDescriptor.", + "Timers are unsupported because the ProcessBundleRequest %s does not" + + " provide a timer ApiServiceDescriptor.", processBundleInstructionId.get())); } - aggregator = + BeamFnDataOutboundAggregator aggregator = outboundAggregatorMap.computeIfAbsent( processBundleDescriptor.getTimerApiServiceDescriptor(), asd -> - queueingClient.createOutboundAggregator( - asd, - processBundleInstructionId, + new BeamFnDataOutboundAggregator( + options, runnerCapabilities.contains( BeamUrns.getUrn( StandardRunnerProtocols.Enum @@ -499,6 +494,8 @@ public BundleFinalizer getBundleFinalizer() { */ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest request) throws Exception { + String instructionId = request.getInstructionId(); + String dataStreamId = request.getProcessBundle().getDataStreamId(); @Nullable BundleProcessor bundleProcessor = null; try { bundleProcessor = @@ -515,13 +512,20 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re } })); + for (Map.Entry entry : + bundleProcessor.getOutboundAggregators().entrySet()) { + BeamFnDataOutboundAggregator aggregator = entry.getValue(); + aggregator.prepareForInstruction( + instructionId, beamFnDataClient.getOutboundObserver(entry.getKey(), dataStreamId)); + } + PTransformFunctionRegistry startFunctionRegistry = bundleProcessor.getStartFunctionRegistry(); PTransformFunctionRegistry finishFunctionRegistry = bundleProcessor.getFinishFunctionRegistry(); ExecutionStateTracker stateTracker = bundleProcessor.getStateTracker(); ProcessBundleResponse.Builder response = ProcessBundleResponse.newBuilder(); try (HandleStateCallsForBundle beamFnStateClient = bundleProcessor.getBeamFnStateClient()) { - stateTracker.start(request.getInstructionId()); + stateTracker.start(instructionId); try { // Already in reverse topological order so we don't need to do anything. for (ThrowingRunnable startFunction : startFunctionRegistry.getFunctions()) { @@ -545,12 +549,14 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re } else if (!bundleProcessor.getInboundEndpointApiServiceDescriptors().isEmpty()) { BeamFnDataInboundObserver observer = bundleProcessor.getInboundObserver(); beamFnDataClient.registerReceiver( - request.getInstructionId(), + instructionId, + dataStreamId, bundleProcessor.getInboundEndpointApiServiceDescriptors(), observer); observer.awaitCompletion(); beamFnDataClient.unregisterReceiver( - request.getInstructionId(), + instructionId, + dataStreamId, bundleProcessor.getInboundEndpointApiServiceDescriptors()); } @@ -581,7 +587,7 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re if (!bundleProcessor.getBundleFinalizationCallbackRegistrations().isEmpty()) { finalizeBundleHandler.registerCallbacks( - bundleProcessor.getInstructionId(), + instructionId, ImmutableList.copyOf(bundleProcessor.getBundleFinalizationCallbackRegistrations())); response.setRequiresFinalization(true); } @@ -599,7 +605,7 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re } catch (Exception e) { LOG.debug( "Error processing bundle {} with bundleProcessor for {} after exception", - request.getInstructionId(), + instructionId, request.getProcessBundle().getProcessBundleDescriptorId(), e); if (bundleProcessor != null) { @@ -607,7 +613,7 @@ public BeamFnApi.InstructionResponse.Builder processBundle(InstructionRequest re bundleProcessorCache.discard(bundleProcessor); } // Ensure that if more data arrives for the instruction it is discarded. - beamFnDataClient.poisonInstructionId(request.getInstructionId()); + beamFnDataClient.poisonInstructionId(instructionId); throw e; } } @@ -629,6 +635,10 @@ private void embedOutboundElementsIfApplicable( collectedElements.add(elements); } if (!hasFlushedAggregator) { + for (BeamFnDataOutboundAggregator aggregator : + bundleProcessor.getOutboundAggregators().values()) { + aggregator.finishInstruction(); + } Elements.Builder elementsToEmbed = Elements.newBuilder(); for (Elements collectedElement : collectedElements) { elementsToEmbed.mergeFrom(collectedElement); @@ -645,6 +655,7 @@ private void embedOutboundElementsIfApplicable( if (elements != null) { aggregator.sendElements(elements); } + aggregator.finishInstruction(); } } } @@ -875,7 +886,6 @@ public void afterBundleCommit(Instant callbackExpiry, Callback callback) { addRunnerAndConsumersForPTransformRecursively( beamFnStateClient, - beamFnDataClient, entry.getKey(), entry.getValue(), bundleProcessor::getInstructionId, diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataClient.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataClient.java index 94d59d0fcb62..1a50f5b448c5 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataClient.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataClient.java @@ -18,13 +18,12 @@ package org.apache.beam.fn.harness.data; import java.util.List; -import java.util.function.Supplier; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; import org.apache.beam.model.pipeline.v1.Endpoints; import org.apache.beam.model.pipeline.v1.Endpoints.ApiServiceDescriptor; -import org.apache.beam.sdk.fn.data.BeamFnDataOutboundAggregator; import org.apache.beam.sdk.fn.data.CloseableFnDataReceiver; import org.apache.beam.sdk.fn.data.FnDataReceiver; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.stub.StreamObserver; /** * The {@link BeamFnDataClient} is able to forward inbound elements to a {@link FnDataReceiver} and @@ -47,6 +46,7 @@ public interface BeamFnDataClient { */ void registerReceiver( String instructionId, + String dataStreamId, List apiServiceDescriptors, CloseableFnDataReceiver receiver); @@ -58,7 +58,8 @@ void registerReceiver( * to the {@link BeamFnDataClient} during a future {@link FnDataReceiver#accept} invocation or via * a call to {@link #poisonInstructionId}. */ - void unregisterReceiver(String instructionId, List apiServiceDescriptors); + void unregisterReceiver( + String instructionId, String dataStreamId, List apiServiceDescriptors); /** * Poisons the instruction id, indicating that future data arriving for it should be discarded. @@ -68,22 +69,7 @@ void registerReceiver( */ void poisonInstructionId(String instructionId); - /** - * Creates a {@link BeamFnDataOutboundAggregator} for buffering and sending outbound data and - * timers over the data plane. It is important that {@link - * BeamFnDataOutboundAggregator#sendOrCollectBufferedDataAndFinishOutboundStreams()} is called on - * the returned BeamFnDataOutboundAggregator at the end of each bundle. If - * collectElementsIfNoFlushes is set to true, {@link - * BeamFnDataOutboundAggregator#sendOrCollectBufferedDataAndFinishOutboundStreams()} returns the - * buffered elements instead of sending it through the outbound StreamObserver if there's no - * previous flush. - * - *

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

    The returned aggregator is not thread safe. - */ - BeamFnDataOutboundAggregator createOutboundAggregator( - Endpoints.ApiServiceDescriptor apiServiceDescriptor, - Supplier processBundleRequestIdSupplier, - boolean collectElementsIfNoFlushes); + /** Get the outbound observer for the specified apiServiceDescriptor and dataStreamId. */ + StreamObserver getOutboundObserver( + Endpoints.ApiServiceDescriptor apiServiceDescriptor, String dataStreamId); } diff --git a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClient.java b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClient.java index 499d816f8cc0..2f2a6b0fc660 100644 --- a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClient.java +++ b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClient.java @@ -18,20 +18,22 @@ package org.apache.beam.fn.harness.data; import java.util.List; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; -import java.util.function.Supplier; +import javax.annotation.Nullable; import org.apache.beam.model.fnexecution.v1.BeamFnApi.Elements; import org.apache.beam.model.fnexecution.v1.BeamFnDataGrpc; import org.apache.beam.model.pipeline.v1.Endpoints; import org.apache.beam.model.pipeline.v1.Endpoints.ApiServiceDescriptor; import org.apache.beam.sdk.fn.data.BeamFnDataGrpcMultiplexer; -import org.apache.beam.sdk.fn.data.BeamFnDataOutboundAggregator; import org.apache.beam.sdk.fn.data.CloseableFnDataReceiver; import org.apache.beam.sdk.fn.stream.OutboundObserverFactory; -import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.ManagedChannel; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.Metadata; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.stub.MetadataUtils; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.stub.StreamObserver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,17 +46,42 @@ public class BeamFnDataGrpcClient implements BeamFnDataClient { private static final Logger LOG = LoggerFactory.getLogger(BeamFnDataGrpcClient.class); - private final ConcurrentMap - multiplexerCache; + private static class MultiplexerKey { + private final Endpoints.ApiServiceDescriptor apiServiceDescriptor; + private final String dataStreamId; + + private MultiplexerKey( + Endpoints.ApiServiceDescriptor apiServiceDescriptor, String dataStreamId) { + this.apiServiceDescriptor = apiServiceDescriptor; + this.dataStreamId = dataStreamId; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MultiplexerKey)) { + return false; + } + MultiplexerKey that = (MultiplexerKey) o; + return Objects.equals(dataStreamId, that.dataStreamId) + && Objects.equals(apiServiceDescriptor, that.apiServiceDescriptor); + } + + @Override + public int hashCode() { + return Objects.hash(apiServiceDescriptor, dataStreamId); + } + } + + private final ConcurrentMap multiplexerCache; private final Function channelFactory; private final OutboundObserverFactory outboundObserverFactory; - private final PipelineOptions options; public BeamFnDataGrpcClient( - PipelineOptions options, Function channelFactory, OutboundObserverFactory outboundObserverFactory) { - this.options = options; this.channelFactory = channelFactory; this.outboundObserverFactory = outboundObserverFactory; this.multiplexerCache = new ConcurrentHashMap<>(); @@ -63,21 +90,22 @@ public BeamFnDataGrpcClient( @Override public void registerReceiver( String instructionId, + String dataStreamId, List apiServiceDescriptors, CloseableFnDataReceiver receiver) { LOG.debug("Registering consumer for {}", instructionId); for (int i = 0, size = apiServiceDescriptors.size(); i < size; i++) { - BeamFnDataGrpcMultiplexer client = getClientFor(apiServiceDescriptors.get(i)); + BeamFnDataGrpcMultiplexer client = getMultiplexer(apiServiceDescriptors.get(i), dataStreamId); client.registerConsumer(instructionId, receiver); } } @Override public void unregisterReceiver( - String instructionId, List apiServiceDescriptors) { + String instructionId, String dataStreamId, List apiServiceDescriptors) { LOG.debug("Unregistering consumer for {}", instructionId); for (int i = 0, size = apiServiceDescriptors.size(); i < size; i++) { - BeamFnDataGrpcMultiplexer client = getClientFor(apiServiceDescriptors.get(i)); + BeamFnDataGrpcMultiplexer client = getMultiplexer(apiServiceDescriptors.get(i), dataStreamId); client.unregisterConsumer(instructionId); } } @@ -91,25 +119,32 @@ public void poisonInstructionId(String instructionId) { } @Override - public BeamFnDataOutboundAggregator createOutboundAggregator( - ApiServiceDescriptor apiServiceDescriptor, - Supplier processBundleRequestIdSupplier, - boolean collectElementsIfNoFlushes) { - return new BeamFnDataOutboundAggregator( - options, - processBundleRequestIdSupplier, - getClientFor(apiServiceDescriptor).getOutboundObserver(), - collectElementsIfNoFlushes); + public StreamObserver getOutboundObserver( + ApiServiceDescriptor apiServiceDescriptor, String dataStreamId) { + return getMultiplexer(apiServiceDescriptor, dataStreamId).getOutboundObserver(); } - private BeamFnDataGrpcMultiplexer getClientFor( - Endpoints.ApiServiceDescriptor apiServiceDescriptor) { + private BeamFnDataGrpcMultiplexer getMultiplexer( + Endpoints.ApiServiceDescriptor apiServiceDescriptor, String dataStreamId) { + MultiplexerKey key = new MultiplexerKey(apiServiceDescriptor, dataStreamId); return multiplexerCache.computeIfAbsent( - apiServiceDescriptor, - (Endpoints.ApiServiceDescriptor descriptor) -> - new BeamFnDataGrpcMultiplexer( - descriptor, - outboundObserverFactory, - BeamFnDataGrpc.newStub(channelFactory.apply(apiServiceDescriptor))::data)); + key, + k -> { + OutboundObserverFactory.BasicFactory baseOutboundObserverFactory = + inboundObserver -> { + BeamFnDataGrpc.BeamFnDataStub stub = + BeamFnDataGrpc.newStub(channelFactory.apply(apiServiceDescriptor)); + if (dataStreamId != null && !dataStreamId.isEmpty()) { + Metadata headers = new Metadata(); + headers.put( + Metadata.Key.of("data_stream_id", Metadata.ASCII_STRING_MARSHALLER), + dataStreamId); + stub = stub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers)); + } + return stub.data(inboundObserver); + }; + return new BeamFnDataGrpcMultiplexer( + apiServiceDescriptor, outboundObserverFactory, baseOutboundObserverFactory); + }); } } diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/BeamFnDataWriteRunnerTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/BeamFnDataWriteRunnerTest.java index 70a894e7b375..2882b75a2593 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/BeamFnDataWriteRunnerTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/BeamFnDataWriteRunnerTest.java @@ -32,7 +32,6 @@ import java.util.Map; import java.util.ServiceLoader; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; import org.apache.beam.fn.harness.PTransformRunnerFactory.Registrar; import org.apache.beam.fn.harness.data.BeamFnDataClient; import org.apache.beam.model.fnexecution.v1.BeamFnApi; @@ -107,19 +106,29 @@ public void setUp() { MockitoAnnotations.initMocks(this); } - private BeamFnDataOutboundAggregator createRecordingAggregator( - Map>> output, Supplier bundleId) { + @Test + public void testReuseForMultipleBundles() throws Exception { + AtomicReference bundleId = new AtomicReference<>("0"); + String localInputId = "inputPC"; + RunnerApi.PTransform pTransform = + RemoteGrpcPortWrite.writeToPort(localInputId, PORT_SPEC).toPTransform(); + + List> output0 = new ArrayList<>(); + List> output1 = new ArrayList<>(); + Map aggregators = new HashMap<>(); + PipelineOptions options = PipelineOptionsFactory.create(); options.as(ExperimentalOptions.class).setExperiments(Arrays.asList("data_buffer_size_limit=0")); - return new BeamFnDataOutboundAggregator( - options, - bundleId, + BeamFnDataOutboundAggregator aggregator = new BeamFnDataOutboundAggregator(options, false); + + Map>> outputs = ImmutableMap.of("0", output0, "1", output1); + StreamObserver observer = new StreamObserver() { @Override public void onNext(Elements elements) { for (Data data : elements.getDataList()) { try { - output.get(bundleId.get()).add(WIRE_CODER.decode(data.getData().newInput())); + outputs.get(bundleId.get()).add(WIRE_CODER.decode(data.getData().newInput())); } catch (IOException e) { throw new RuntimeException("Failed to decode output."); } @@ -131,22 +140,9 @@ public void onError(Throwable throwable) {} @Override public void onCompleted() {} - }, - false); - } - - @Test - public void testReuseForMultipleBundles() throws Exception { - AtomicReference bundleId = new AtomicReference<>("0"); - String localInputId = "inputPC"; - RunnerApi.PTransform pTransform = - RemoteGrpcPortWrite.writeToPort(localInputId, PORT_SPEC).toPTransform(); + }; - List> output0 = new ArrayList<>(); - List> output1 = new ArrayList<>(); - Map aggregators = new HashMap<>(); - BeamFnDataOutboundAggregator aggregator = - createRecordingAggregator(ImmutableMap.of("0", output0, "1", output1), bundleId::get); + aggregator.prepareForInstruction(bundleId.get(), observer); aggregators.put(PORT_SPEC.getApiServiceDescriptor(), aggregator); PTransformRunnerFactoryTestContext context = @@ -172,18 +168,20 @@ public void testReuseForMultipleBundles() throws Exception { FnDataReceiver pCollectionConsumer = context.getPCollectionConsumer(localInputId); pCollectionConsumer.accept(valueInGlobalWindow("ABC")); pCollectionConsumer.accept(valueInGlobalWindow("DEF")); - assertThat(output0, contains(valueInGlobalWindow("ABC"), valueInGlobalWindow("DEF"))); + aggregator.finishInstruction(); output0.clear(); // Process for bundle id 1 bundleId.set("1"); + aggregator.prepareForInstruction(bundleId.get(), observer); pCollectionConsumer.accept(valueInGlobalWindow("GHI")); pCollectionConsumer.accept(valueInGlobalWindow("JKL")); assertThat(output1, contains(valueInGlobalWindow("GHI"), valueInGlobalWindow("JKL"))); + aggregator.finishInstruction(); verifyNoMoreInteractions(mockBeamFnDataClient); } diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java index 50a2fec0b5a2..2aa555e83cd5 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java @@ -839,7 +839,7 @@ private class TestBeamFnDataOutboundAggregator extends BeamFnDataOutboundAggrega private Supplier processBundleRequestIdSupplier; public TestBeamFnDataOutboundAggregator(Supplier bundleIdSupplier) { - super(PipelineOptionsFactory.create(), bundleIdSupplier, null, false); + super(PipelineOptionsFactory.create(), false); this.timers = new HashMap<>(); this.dataOutput = new HashMap<>(); this.processBundleRequestIdSupplier = bundleIdSupplier; diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/PTransformRunnerFactoryTestContext.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/PTransformRunnerFactoryTestContext.java index 7b4387738a4c..51e49953b406 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/PTransformRunnerFactoryTestContext.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/PTransformRunnerFactoryTestContext.java @@ -54,6 +54,7 @@ import org.apache.beam.sdk.transforms.DoFn.BundleFinalizer; import org.apache.beam.sdk.util.construction.Timer; import org.apache.beam.sdk.values.WindowedValue; +import org.apache.beam.vendor.grpc.v1p69p0.io.grpc.stub.StreamObserver; import org.joda.time.Instant; /** @@ -74,6 +75,7 @@ public static Builder builder(String pTransformId, RunnerApi.PTransform pTransfo @Override public void registerReceiver( String instructionId, + String dataStreamId, List apiServiceDescriptors, CloseableFnDataReceiver receiver) { throw new UnsupportedOperationException("Unexpected call during test."); @@ -81,15 +83,15 @@ public void registerReceiver( @Override public void unregisterReceiver( - String instructionId, List apiServiceDescriptors) { + String instructionId, + String dataStreamId, + List apiServiceDescriptors) { throw new UnsupportedOperationException("Unexpected call during test."); } @Override - public BeamFnDataOutboundAggregator createOutboundAggregator( - ApiServiceDescriptor apiServiceDescriptor, - Supplier processBundleRequestIdSupplier, - boolean collectElementsIfNoFlushes) { + public StreamObserver getOutboundObserver( + ApiServiceDescriptor apiServiceDescriptor, String dataStreamId) { throw new UnsupportedOperationException("Unexpected call during test."); } diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java index 47f85178b0a1..c03f82726740 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java @@ -37,7 +37,6 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; @@ -1071,28 +1070,20 @@ private ProcessBundleHandler setupProcessBundleHandlerForSimpleRecordingDoFn( dataOutput.add(input.getValue()); })); - Mockito.doAnswer( - (invocation) -> - new BeamFnDataOutboundAggregator( - PipelineOptionsFactory.create(), - invocation.getArgument(1), - new StreamObserver() { - @Override - public void onNext(Elements elements) { - for (Timers timer : elements.getTimersList()) { - timerOutput.addAll(elements.getTimersList()); - } - } + Mockito.when(beamFnDataClient.getOutboundObserver(any(), any())) + .thenReturn( + new StreamObserver() { + @Override + public void onNext(Elements elements) { + timerOutput.addAll(elements.getTimersList()); + } - @Override - public void onError(Throwable throwable) {} + @Override + public void onError(Throwable throwable) {} - @Override - public void onCompleted() {} - }, - invocation.getArgument(2))) - .when(beamFnDataClient) - .createOutboundAggregator(any(), any(), anyBoolean()); + @Override + public void onCompleted() {} + }); return new ProcessBundleHandler( PipelineOptionsFactory.create(), @@ -1409,7 +1400,7 @@ public void testInstructionIsUnregisteredFromBeamFnDataClientOnSuccess() throws (invocation) -> { String instructionId = invocation.getArgument(0, String.class); CloseableFnDataReceiver data = - invocation.getArgument(2, CloseableFnDataReceiver.class); + invocation.getArgument(3, CloseableFnDataReceiver.class); data.accept( BeamFnApi.Elements.newBuilder() .addData( @@ -1421,7 +1412,7 @@ public void testInstructionIsUnregisteredFromBeamFnDataClientOnSuccess() throws return null; }) .when(beamFnDataClient) - .registerReceiver(any(), any(), any()); + .registerReceiver(any(), any(), any(), any()); ProcessBundleHandler handler = new ProcessBundleHandler( @@ -1451,8 +1442,8 @@ public void testInstructionIsUnregisteredFromBeamFnDataClientOnSuccess() throws .build()); // Ensure that we unregister during successful processing - verify(beamFnDataClient).registerReceiver(eq("instructionId"), any(), any()); - verify(beamFnDataClient).unregisterReceiver(eq("instructionId"), any()); + verify(beamFnDataClient).registerReceiver(eq("instructionId"), any(), any(), any()); + verify(beamFnDataClient).unregisterReceiver(eq("instructionId"), any(), any()); verifyNoMoreInteractions(beamFnDataClient); } @@ -1475,7 +1466,7 @@ public void testDataProcessingExceptionsArePropagated() throws Exception { StringUtf8Coder.of().encode("A", encodedData); String instructionId = invocation.getArgument(0, String.class); CloseableFnDataReceiver data = - invocation.getArgument(2, CloseableFnDataReceiver.class); + invocation.getArgument(3, CloseableFnDataReceiver.class); data.accept( BeamFnApi.Elements.newBuilder() .addData( @@ -1489,7 +1480,7 @@ public void testDataProcessingExceptionsArePropagated() throws Exception { return null; }) .when(beamFnDataClient) - .registerReceiver(any(), any(), any()); + .registerReceiver(any(), any(), any(), any()); ProcessBundleHandler handler = new ProcessBundleHandler( @@ -1526,7 +1517,7 @@ public void testDataProcessingExceptionsArePropagated() throws Exception { .build())); // Ensure that we unregister during successful processing - verify(beamFnDataClient).registerReceiver(eq("instructionId"), any(), any()); + verify(beamFnDataClient).registerReceiver(eq("instructionId"), any(), any(), any()); verify(beamFnDataClient).poisonInstructionId(eq("instructionId")); verifyNoMoreInteractions(beamFnDataClient); } diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClientTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClientTest.java index 15f83f2582c7..9d9efa0b9c49 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClientTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/data/BeamFnDataGrpcClientTest.java @@ -23,8 +23,8 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import java.util.Arrays; import java.util.Collection; @@ -49,6 +49,7 @@ import org.apache.beam.sdk.fn.data.LogicalEndpoint; import org.apache.beam.sdk.fn.stream.OutboundObserverFactory; import org.apache.beam.sdk.fn.test.TestStreams; +import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.values.WindowedValue; @@ -169,7 +170,6 @@ public StreamObserver data( BeamFnDataGrpcClient clientFactory = new BeamFnDataGrpcClient( - PipelineOptionsFactory.create(), (Endpoints.ApiServiceDescriptor descriptor) -> channel, OutboundObserverFactory.trivial()); @@ -183,7 +183,7 @@ public StreamObserver data( Collections.emptyList()); clientFactory.registerReceiver( - INSTRUCTION_ID_A, Arrays.asList(apiServiceDescriptor), observerA); + INSTRUCTION_ID_A, "", Arrays.asList(apiServiceDescriptor), observerA); waitForClientToConnect.await(); outboundServerObserver.get().onNext(ELEMENTS_A_1); @@ -193,7 +193,7 @@ public StreamObserver data( Thread.sleep(100); clientFactory.registerReceiver( - INSTRUCTION_ID_B, Arrays.asList(apiServiceDescriptor), observerB); + INSTRUCTION_ID_B, "", Arrays.asList(apiServiceDescriptor), observerB); // Show that out of order stream completion can occur. observerB.awaitCompletion(); @@ -245,7 +245,6 @@ public StreamObserver data( BeamFnDataGrpcClient clientFactory = new BeamFnDataGrpcClient( - PipelineOptionsFactory.create(), (Endpoints.ApiServiceDescriptor descriptor) -> channel, OutboundObserverFactory.trivial()); @@ -262,7 +261,7 @@ public StreamObserver data( Collections.emptyList()); clientFactory.registerReceiver( - INSTRUCTION_ID_A, Arrays.asList(apiServiceDescriptor), observer); + INSTRUCTION_ID_A, "", Arrays.asList(apiServiceDescriptor), observer); waitForClientToConnect.await(); @@ -270,12 +269,8 @@ public StreamObserver data( outboundServerObserver.get().onNext(ELEMENTS_A_1); outboundServerObserver.get().onNext(ELEMENTS_A_2); - try { - observer.awaitCompletion(); - fail("Expected channel to fail"); - } catch (Exception e) { - assertEquals(exceptionToThrow, e); - } + Exception e = assertThrows(Exception.class, observer::awaitCompletion); + assertEquals(exceptionToThrow, e); // The server should not have received any values assertThat(inboundServerValues, empty()); // The consumer should have only been invoked once @@ -321,7 +316,6 @@ public StreamObserver data( BeamFnDataGrpcClient clientFactory = new BeamFnDataGrpcClient( - PipelineOptionsFactory.create(), (Endpoints.ApiServiceDescriptor descriptor) -> channel, OutboundObserverFactory.trivial()); @@ -347,7 +341,7 @@ public StreamObserver data( }); clientFactory.registerReceiver( - INSTRUCTION_ID_A, Arrays.asList(apiServiceDescriptor), observerA); + INSTRUCTION_ID_A, "", Arrays.asList(apiServiceDescriptor), observerA); waitForClientToConnect.await(); outboundServerObserver.get().onNext(ELEMENTS_B_1); @@ -358,11 +352,9 @@ public StreamObserver data( assertTrue(receivedAElement.await(5, TimeUnit.SECONDS)); clientFactory.poisonInstructionId(INSTRUCTION_ID_A); - try { - future.get(); - fail(); // We expect the awaitCompletion to fail due to closing. - } catch (Exception ignored) { - } + // We expect the awaitCompletion to fail due to closing. + // Expected. + assertThrows(Exception.class, future::get); outboundServerObserver.get().onNext(ELEMENTS_A_2); @@ -404,16 +396,15 @@ public StreamObserver data( ManagedChannel channel = InProcessChannelBuilder.forName(apiServiceDescriptor.getUrl()).build(); + PipelineOptions options = + PipelineOptionsFactory.fromArgs("--experiments=data_buffer_size_limit=20").create(); BeamFnDataGrpcClient clientFactory = new BeamFnDataGrpcClient( - PipelineOptionsFactory.fromArgs( - new String[] {"--experiments=data_buffer_size_limit=20"}) - .create(), (Endpoints.ApiServiceDescriptor descriptor) -> channel, OutboundObserverFactory.trivial()); - BeamFnDataOutboundAggregator aggregator = - clientFactory.createOutboundAggregator( - apiServiceDescriptor, () -> INSTRUCTION_ID_A, false); + BeamFnDataOutboundAggregator aggregator = new BeamFnDataOutboundAggregator(options, false); + aggregator.prepareForInstruction( + INSTRUCTION_ID_A, clientFactory.getOutboundObserver(apiServiceDescriptor, "")); FnDataReceiver> fnDataReceiver = aggregator.registerOutputDataLocation(TRANSFORM_ID_A, CODER); fnDataReceiver.accept(valueInGlobalWindow("ABC")); 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/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/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/delta/build.gradle b/sdks/java/io/delta/build.gradle new file mode 100644 index 000000000000..57b1cd8ad876 --- /dev/null +++ b/sdks/java/io/delta/build.gradle @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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" + + // For Avro conversions + testImplementation project(":sdks:java:extensions:avro") + + testImplementation library.java.avro + testImplementation library.java.junit + testImplementation library.java.hamcrest + testImplementation "org.apache.parquet:parquet-avro:$parquet_version" + testImplementation project(":sdks:java:io:parquet") + testImplementation project(":sdks:java:managed") + testRuntimeOnly "org.yaml:snakeyaml:2.0" + testImplementation project(path: ":runners:direct-java", configuration: "shadow") +} + +configurations.all { + // Exclude conflicting logging frameworks + exclude group: "org.apache.logging.log4j", module: "log4j-slf4j2-impl" + exclude group: "org.apache.logging.log4j", module: "log4j-slf4j-impl" + exclude group: "org.slf4j", module: "slf4j-reload4j" + + // Force overriding for all configurations + resolutionStrategy.force "com.google.cloud.bigdataoss:gcs-connector:$bigdataoss_gcs_connector_version" + resolutionStrategy.force "com.google.cloud.bigdataoss:util-hadoop:$bigdataoss_gcs_connector_version" + resolutionStrategy.force "com.google.cloud.bigdataoss:gcsio:$bigdataoss_gcs_connector_version" + resolutionStrategy.force "com.google.cloud.bigdataoss:util:$bigdataoss_gcs_connector_version" +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java new file mode 100644 index 000000000000..de82d8d01b81 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamEngine.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.engine.Engine; +import io.delta.kernel.engine.ExpressionHandler; +import io.delta.kernel.engine.FileSystemClient; +import io.delta.kernel.engine.JsonHandler; +import io.delta.kernel.engine.ParquetHandler; + +/** A Beam specific {@link Engine} wrapper that provides a custom {@link ParquetHandler}. */ +public class BeamEngine implements Engine { + private final Engine delegate; + private final ParquetHandler parquetHandler; + + public BeamEngine(Engine delegate, ParquetHandler parquetHandler) { + this.delegate = delegate; + this.parquetHandler = parquetHandler; + } + + @Override + public ExpressionHandler getExpressionHandler() { + return delegate.getExpressionHandler(); + } + + @Override + public JsonHandler getJsonHandler() { + return delegate.getJsonHandler(); + } + + @Override + public FileSystemClient getFileSystemClient() { + return delegate.getFileSystemClient(); + } + + @Override + public ParquetHandler getParquetHandler() { + return parquetHandler; + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java new file mode 100644 index 000000000000..0327bddac280 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/BeamParquetHandler.java @@ -0,0 +1,376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.defaults.internal.parquet.ParquetFileReader.BatchReadSupport; +import io.delta.kernel.engine.FileReadResult; +import io.delta.kernel.engine.ParquetHandler; +import io.delta.kernel.expressions.Column; +import io.delta.kernel.expressions.Predicate; +import io.delta.kernel.types.MetadataColumnSpec; +import io.delta.kernel.types.StructType; +import io.delta.kernel.utils.CloseableIterator; +import io.delta.kernel.utils.DataFileStatus; +import io.delta.kernel.utils.FileStatus; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.parquet.column.page.PageReadStore; +import org.apache.parquet.filter2.compat.FilterCompat; +import org.apache.parquet.format.converter.ParquetMetadataConverter; +import org.apache.parquet.hadoop.ParquetFileReader; +import org.apache.parquet.hadoop.api.InitContext; +import org.apache.parquet.hadoop.api.ReadSupport; +import org.apache.parquet.hadoop.metadata.FileMetaData; +import org.apache.parquet.hadoop.metadata.ParquetMetadata; +import org.apache.parquet.hadoop.util.HadoopInputFile; +import org.apache.parquet.io.ColumnIOFactory; +import org.apache.parquet.io.MessageColumnIO; +import org.apache.parquet.io.RecordReader; +import org.apache.parquet.io.api.RecordMaterializer; +import org.apache.parquet.schema.MessageType; + +/** + * A Beam specific {@link ParquetHandler} that delegates row group claiming to a {@link + * DeltaReadTaskTracker}. + */ +public class BeamParquetHandler implements ParquetHandler { + private final Configuration conf; + private final ParquetHandler delegate; + private final RestrictionTracker tracker; + private static final long DEFAULT_START_RG_INDEX = 0L; + + public BeamParquetHandler( + Configuration conf, ParquetHandler delegate, RestrictionTracker tracker) { + this.conf = conf; + this.delegate = delegate; + this.tracker = tracker; + } + + private boolean claimFailed = false; + + /** + * A method that is expected to be called after the first file processing is done. It returns + * whether the last file process resulted in a claim failure. This allows the caller to skip + * trying to read the remaining files of the task which would result in claim failures for each + * row group within them. + * + * @return true, if the last file process resulted in a claim failure. Returns false otherwise. + */ + public boolean hasClaimFailed() { + return claimFailed; + } + + @Override + public CloseableIterator readParquetFiles( + CloseableIterator fileIter, + StructType physicalSchema, + Optional predicate) + throws IOException { + return readParquetFiles(fileIter, physicalSchema, predicate, DEFAULT_START_RG_INDEX); + } + + /** + * Reads Parquet files starting from a given row group index. + * + *

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

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

    If {@code tryClaim} fails during reading, subsequent {@code hasClaimFailed} calls will + * return {@code true}, so the caller can skip reading subsequent files that are in the range + * being considered for reading. + */ + public CloseableIterator readParquetFiles( + CloseableIterator fileIter, + StructType physicalSchema, + Optional predicate, + long startRgIndex) + throws IOException { + + List> results = new ArrayList<>(); + boolean hasRowIndexCol = physicalSchema.contains(MetadataColumnSpec.ROW_INDEX); + + long currentRgIndex = startRgIndex; + + try { + while (fileIter.hasNext()) { + if (currentRgIndex >= tracker.currentRestriction().getTo()) { + // Skipping all blocks for the remaining files since they are located after the + // end index of the tracker. Since currentRgIndex is monotonically increasing, + // we can break the loop immediately to avoid extremely expensive network I/O. + break; + } + + FileStatus fileStatus = fileIter.next(); + Path hadoopPath = new Path(fileStatus.getPath()); + ParquetMetadata metadata = + ParquetFileReader.readFooter(conf, hadoopPath, ParquetMetadataConverter.NO_FILTER); + long fileBlocks = metadata.getBlocks().size(); + + if (currentRgIndex + fileBlocks <= tracker.currentRestriction().getFrom()) { + // Skipping all blocks for the current file since they are located before the + // start index of the tracker. + currentRgIndex += fileBlocks; + continue; + } + + results.add( + readParquetFileDirect( + fileStatus, + hadoopPath, + metadata, + physicalSchema, + hasRowIndexCol, + currentRgIndex, + fileBlocks)); + + currentRgIndex += fileBlocks; + } + } finally { + fileIter.close(); + } + + return combineResults(results); + } + + // Reads the correct set of blocks that belong to the given Parquet file that + // are within range for the current `RestrictionTracker`. If the current file + // has some blocks that are within the tracker's range and some that are + // outside, + // this will only read the blocks that are within the range. + private CloseableIterator readParquetFileDirect( + FileStatus fileStatus, + Path hadoopPath, + ParquetMetadata metadata, + StructType physicalSchema, + boolean hasRowIndexCol, + long startRgIndex, + long fileBlocks) { + + return new CloseableIterator() { + @javax.annotation.Nullable private ParquetFileReader reader = null; + @javax.annotation.Nullable private BatchReadSupport readSupport = null; + @javax.annotation.Nullable private RecordMaterializer recordConverter = null; + @javax.annotation.Nullable private MessageColumnIO columnIO = null; + + private long currentRgOffset = 0; + @javax.annotation.Nullable private RecordReader currentRecordReader = null; + private long currentRgTotalRows = 0; + private long currentRgRowOffset = 0; + private long currentRgStartingRowIndex = 0; + + @javax.annotation.Nullable private FileReadResult nextResult = null; + private boolean isDone = false; + + private void initReaderIfRequired() throws IOException { + if (reader != null) { + return; + } + HadoopInputFile inputFile = HadoopInputFile.fromPath(hadoopPath, conf); + ParquetFileReader localReader = ParquetFileReader.open(inputFile); + reader = localReader; + + FileMetaData fileMetaData = metadata.getFileMetaData(); + MessageType fileSchema = fileMetaData.getSchema(); + Map> keyValueMetadata = new HashMap<>(); + if (fileMetaData.getKeyValueMetaData() != null) { + for (Map.Entry entry : fileMetaData.getKeyValueMetaData().entrySet()) { + keyValueMetadata.put(entry.getKey(), Collections.singleton(entry.getValue())); + } + } + + BatchReadSupport localReadSupport = new BatchReadSupport(1024, physicalSchema); + readSupport = localReadSupport; + ReadSupport.ReadContext readContext = + localReadSupport.init(new InitContext(conf, keyValueMetadata, fileSchema)); + RecordMaterializer localRecordConverter = + localReadSupport.prepareForRead( + conf, fileMetaData.getKeyValueMetaData(), fileSchema, readContext); + recordConverter = localRecordConverter; + localReader.setRequestedSchema(readContext.getRequestedSchema()); + + ColumnIOFactory columnIOFactory = new ColumnIOFactory(fileMetaData.getCreatedBy()); + columnIO = columnIOFactory.getColumnIO(readContext.getRequestedSchema(), fileSchema, true); + } + + @Override + public boolean hasNext() { + if (isDone) { + return false; + } + if (nextResult != null) { + return true; + } + + try { + initReaderIfRequired(); + ParquetFileReader localReader = reader; + BatchReadSupport localReadSupport = readSupport; + MessageColumnIO localColumnIO = columnIO; + RecordMaterializer localRecordConverter = recordConverter; + if (localReader == null + || localReadSupport == null + || localColumnIO == null + || localRecordConverter == null) { + throw new IllegalStateException("Reader not initialized"); + } + + while (true) { + RecordReader localRecordReader = currentRecordReader; + if (localRecordReader != null && currentRgRowOffset < currentRgTotalRows) { + int batchSize = (int) Math.min(1024L, currentRgTotalRows - currentRgRowOffset); + for (int i = 0; i < batchSize; i++) { + localRecordReader.read(); + long rowIndex = + hasRowIndexCol ? (currentRgStartingRowIndex + currentRgRowOffset + i) : -1L; + localReadSupport.finalizeCurrentRow(rowIndex); + } + currentRgRowOffset += batchSize; + io.delta.kernel.data.ColumnarBatch batch = + localReadSupport.getDataAsColumnarBatch(batchSize); + nextResult = new FileReadResult(batch, fileStatus.getPath()); + return true; + } + + currentRecordReader = null; + if (currentRgOffset >= fileBlocks) { + isDone = true; + return false; + } + + // Checking the range for specific row groups. + long rgIndex = startRgIndex + currentRgOffset; + if (rgIndex < tracker.currentRestriction().getFrom()) { + // Skip till we get to the first block to read. + localReader.skipNextRowGroup(); + currentRgOffset++; + continue; + } else if (rgIndex >= tracker.currentRestriction().getTo()) { + // Once we are past the end index of the tracker we don't have to read any more + // blocks. + isDone = true; + return false; + } + + // We only read the row group if it's within the range for the + // RestrictionTracker. + if (tracker.tryClaim(rgIndex)) { + PageReadStore pages = localReader.readNextRowGroup(); + currentRecordReader = + localColumnIO.getRecordReader(pages, localRecordConverter, FilterCompat.NOOP); + currentRgTotalRows = pages.getRowCount(); + currentRgRowOffset = 0; + currentRgStartingRowIndex = pages.getRowIndexOffset().orElse(0L); + currentRgOffset++; + } else { + // Mark claim failed for the current row group, so we stop processing + // the remaining row groups in the source. + claimFailed = true; + isDone = true; + return false; + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public FileReadResult next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + FileReadResult res = nextResult; + if (res == null) { + throw new NoSuchElementException(); + } + nextResult = null; + return res; + } + + @Override + public void close() throws IOException { + if (reader != null) { + reader.close(); + } + } + }; + } + + @Override + public void writeParquetFileAtomically( + String filePath, CloseableIterator data) throws IOException { + delegate.writeParquetFileAtomically(filePath, data); + } + + @Override + public CloseableIterator writeParquetFiles( + String filePath, CloseableIterator data, List statsColumns) + throws IOException { + return delegate.writeParquetFiles(filePath, data, statsColumns); + } + + private static CloseableIterator combineResults( + List> iterators) { + return new CloseableIterator() { + private int currentIdx = 0; + + @Override + public boolean hasNext() { + while (currentIdx < iterators.size()) { + if (iterators.get(currentIdx).hasNext()) { + return true; + } + currentIdx++; + } + return false; + } + + @Override + public FileReadResult next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return iterators.get(currentIdx).next(); + } + + @Override + public void close() throws IOException { + for (CloseableIterator it : iterators) { + it.close(); + } + } + }; + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java new file mode 100644 index 000000000000..36c9a1a47f8c --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/CreateReadTasksDoFn.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.Scan; +import io.delta.kernel.Snapshot; +import io.delta.kernel.Table; +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.data.Row; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.internal.InternalScanFileUtils; +import io.delta.kernel.utils.CloseableIterator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.hadoop.conf.Configuration; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A DoFn that reads the Delta log and outputs a list of DeltaReadTask records to read. */ +class CreateReadTasksDoFn extends DoFn { + private static final long MAX_TASK_SIZE_BYTES = 1024L * 1024L * 1024L; // 1 GB + private final @Nullable Map hadoopConfig; + + public CreateReadTasksDoFn(@Nullable Map hadoopConfig) { + this.hadoopConfig = hadoopConfig; + } + + @ProcessElement + public void processElement(@Element String tablePath, OutputReceiver out) + throws Exception { + Configuration conf = new Configuration(); + if (hadoopConfig != null) { + for (Map.Entry entry : hadoopConfig.entrySet()) { + conf.set(entry.getKey(), entry.getValue()); + } + } + Engine engine = DefaultEngine.create(conf); + Table table = Table.forPath(engine, tablePath); + Snapshot snapshot = table.getLatestSnapshot(engine); + Scan scan = snapshot.getScanBuilder().build(); + Row scanState = scan.getScanState(engine); + SerializableRow serializableScanState = new SerializableRow(scanState); + + List currentGroup = new ArrayList<>(); + List> currentGroupRowGroupSizes = new ArrayList<>(); + long currentGroupSize = 0L; + + try (CloseableIterator scanFiles = scan.getScanFiles(engine)) { + while (scanFiles.hasNext()) { + FilteredColumnarBatch batch = scanFiles.next(); + try (CloseableIterator rows = batch.getRows()) { + while (rows.hasNext()) { + Row scanFileRow = rows.next(); + SerializableRow fileRow = new SerializableRow(scanFileRow); + long fileSize = InternalScanFileUtils.getAddFileStatus(fileRow).getSize(); + List fileRowGroupSizes = getRowGroupSizes(fileRow, conf); + + if (fileSize >= MAX_TASK_SIZE_BYTES) { + if (!currentGroup.isEmpty()) { + DeltaReadTask readTask = + new DeltaReadTask( + currentGroup, serializableScanState, currentGroupRowGroupSizes); + out.output(readTask); + currentGroup = new ArrayList<>(); + currentGroupRowGroupSizes = new ArrayList<>(); + currentGroupSize = 0L; + } + + DeltaReadTask readTask = + new DeltaReadTask( + Collections.singletonList(fileRow), + serializableScanState, + Collections.singletonList(fileRowGroupSizes)); + out.output(readTask); + } else { + if (currentGroupSize + fileSize > MAX_TASK_SIZE_BYTES) { + DeltaReadTask readTask = + new DeltaReadTask( + currentGroup, serializableScanState, currentGroupRowGroupSizes); + out.output(readTask); + currentGroup = new ArrayList<>(); + currentGroupRowGroupSizes = new ArrayList<>(); + currentGroup.add(fileRow); + currentGroupRowGroupSizes.add(fileRowGroupSizes); + currentGroupSize = fileSize; + } else { + currentGroup.add(fileRow); + currentGroupRowGroupSizes.add(fileRowGroupSizes); + currentGroupSize += fileSize; + } + } + } + } + } + } + + if (!currentGroup.isEmpty()) { + DeltaReadTask readTask = + new DeltaReadTask(currentGroup, serializableScanState, currentGroupRowGroupSizes); + out.output(readTask); + } + } + + private List getRowGroupSizes(SerializableRow scanFileRow, Configuration conf) { + List sizes = new ArrayList<>(); + String pathStr = InternalScanFileUtils.getAddFileStatus(scanFileRow).getPath(); + try { + org.apache.hadoop.fs.Path hadoopPath = new org.apache.hadoop.fs.Path(pathStr); + org.apache.parquet.hadoop.metadata.ParquetMetadata metadata = + org.apache.parquet.hadoop.ParquetFileReader.readFooter( + conf, + hadoopPath, + org.apache.parquet.format.converter.ParquetMetadataConverter.NO_FILTER); + for (org.apache.parquet.hadoop.metadata.BlockMetaData block : metadata.getBlocks()) { + sizes.add(block.getTotalByteSize()); + } + } catch (java.io.IOException e) { + throw new RuntimeException("Failed to read Parquet footer for " + pathStr, e); + } + return sizes; + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java new file mode 100644 index 000000000000..c511a7380dc1 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaIO.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.Table; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructField; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import java.util.Map; +import org.apache.beam.sdk.annotations.Internal; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PBegin; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.apache.hadoop.conf.Configuration; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A connector that reads from Delta Lake tables. + * + *

    This is work in progress. For more details and to track progress, please see Issue 21100. + */ +@Internal +public class DeltaIO { + + public static ReadRows readRows() { + return new AutoValue_DeltaIO_ReadRows.Builder().build(); + } + + @AutoValue + public abstract static class ReadRows extends PTransform> { + + public abstract @Nullable String getTablePath(); + + public abstract @Nullable Long getVersion(); + + public abstract @Nullable String getTimestamp(); + + public abstract @Nullable Map getHadoopConfig(); + + abstract Builder toBuilder(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setTablePath(String tablePath); + + abstract Builder setVersion(@Nullable Long version); + + abstract Builder setTimestamp(@Nullable String timestamp); + + abstract Builder setHadoopConfig(@Nullable Map hadoopConfig); + + abstract ReadRows build(); + } + + public ReadRows from(String tablePath) { + return toBuilder().setTablePath(tablePath).build(); + } + + public ReadRows withVersion(@Nullable Long version) { + return toBuilder().setVersion(version).build(); + } + + public ReadRows withTimestamp(@Nullable String timestamp) { + return toBuilder().setTimestamp(timestamp).build(); + } + + public ReadRows withConfig(Map config) { + return toBuilder().setHadoopConfig(config).build(); + } + + @Override + public PCollection expand(PBegin input) { + String path = getTablePath(); + if (path == null) { + throw new IllegalArgumentException("Table path must be set."); + } + + Configuration conf = new Configuration(); + Map hadoopConfig = getHadoopConfig(); + if (hadoopConfig != null) { + for (Map.Entry entry : hadoopConfig.entrySet()) { + conf.set(entry.getKey(), entry.getValue()); + } + } + Engine engine = DefaultEngine.create(conf); + Table table = Table.forPath(engine, path); + io.delta.kernel.Snapshot snapshot = table.getLatestSnapshot(engine); + StructType deltaSchema = snapshot.getSchema(); + if (deltaSchema == null) { + throw new IllegalStateException("Table schema is null."); + } + Schema beamSchema = convertToBeamSchema(deltaSchema); + + return input + .apply("Create Path", Create.of(path)) + .apply("Plan Files", ParDo.of(new CreateReadTasksDoFn(hadoopConfig))) + .apply("Read Logical Data", ParDo.of(new DeltaSourceDoFn(hadoopConfig))) + .setRowSchema(beamSchema); + } + + static Schema convertToBeamSchema(StructType deltaSchema) { + Schema.Builder builder = Schema.builder(); + for (StructField field : deltaSchema.fields()) { + builder.addField(field.getName(), convertToBeamFieldType(field.getDataType())); + } + return builder.build(); + } + + static Schema.FieldType convertToBeamFieldType(DataType deltaType) { + if (deltaType instanceof StringType) { + return Schema.FieldType.STRING; + } else if (deltaType instanceof IntegerType) { + return Schema.FieldType.INT32; + } else if (deltaType instanceof LongType) { + return Schema.FieldType.INT64; + } else if (deltaType instanceof FloatType) { + return Schema.FieldType.FLOAT; + } else if (deltaType instanceof DoubleType) { + return Schema.FieldType.DOUBLE; + } else if (deltaType instanceof BooleanType) { + return Schema.FieldType.BOOLEAN; + } else if (deltaType instanceof BinaryType) { + return Schema.FieldType.BYTES; + } else if (deltaType instanceof TimestampType) { + return Schema.FieldType.DATETIME; + } else if (deltaType instanceof DateType) { + return Schema.FieldType.DATETIME; + } else if (deltaType instanceof ArrayType) { + DataType elementType = ((ArrayType) deltaType).getElementType(); + return Schema.FieldType.iterable(convertToBeamFieldType(elementType)); + } else if (deltaType instanceof MapType) { + DataType keyType = ((MapType) deltaType).getKeyType(); + DataType valueType = ((MapType) deltaType).getValueType(); + return Schema.FieldType.map( + convertToBeamFieldType(keyType), convertToBeamFieldType(valueType)); + } else if (deltaType instanceof StructType) { + return Schema.FieldType.row(convertToBeamSchema((StructType) deltaType)); + } else { + throw new UnsupportedOperationException("Unsupported Delta type: " + deltaType.getClass()); + } + } + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProvider.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProvider.java new file mode 100644 index 000000000000..42ca3f24def9 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadSchemaTransformProvider.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import static org.apache.beam.sdk.io.delta.DeltaReadSchemaTransformProvider.Configuration; +import static org.apache.beam.sdk.util.construction.BeamUrns.getUrn; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.beam.model.pipeline.v1.ExternalTransforms; +import org.apache.beam.sdk.io.delta.DeltaReadSchemaTransformProvider.Configuration; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.NoSuchSchemaException; +import org.apache.beam.sdk.schemas.SchemaRegistry; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * SchemaTransform implementation for {@link DeltaIO#readRows}. Reads records from Delta Lake and + * outputs a {@link org.apache.beam.sdk.values.PCollection} of Beam {@link + * org.apache.beam.sdk.values.Row}s. + */ +@AutoService(SchemaTransformProvider.class) +public class DeltaReadSchemaTransformProvider extends TypedSchemaTransformProvider { + static final String OUTPUT_TAG = "output"; + + @Override + protected SchemaTransform from(Configuration configuration) { + return new DeltaReadSchemaTransform(configuration); + } + + @Override + public List outputCollectionNames() { + return Collections.singletonList(OUTPUT_TAG); + } + + @Override + public String identifier() { + return getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_READ); + } + + static class DeltaReadSchemaTransform extends SchemaTransform { + private final Configuration configuration; + + DeltaReadSchemaTransform(Configuration configuration) { + this.configuration = + java.util.Objects.requireNonNull(configuration, "configuration cannot be null"); + } + + Row getConfigurationRow() { + try { + return SchemaRegistry.createDefault() + .getToRowFunction(Configuration.class) + .apply(configuration) + .sorted() + .toSnakeCase(); + } catch (NoSuchSchemaException e) { + throw new RuntimeException(e); + } + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + DeltaIO.ReadRows read = DeltaIO.readRows().from(configuration.getTable()); + if (configuration.getVersion() != null) { + read = read.withVersion(configuration.getVersion()); + } + if (configuration.getTimestamp() != null) { + read = read.withTimestamp(configuration.getTimestamp()); + } + Map hadoopConfig = configuration.getHadoopConfig(); + if (hadoopConfig != null) { + read = read.withConfig(hadoopConfig); + } + + PCollection output = input.getPipeline().apply(read); + + return PCollectionRowTuple.of(OUTPUT_TAG, output); + } + } + + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class Configuration { + static Builder builder() { + return new AutoValue_DeltaReadSchemaTransformProvider_Configuration.Builder(); + } + + @SchemaFieldDescription("Identifier of the Delta Lake table.") + abstract String getTable(); + + @SchemaFieldDescription("Version of the Delta Lake table to read.") + @Nullable + abstract Long getVersion(); + + @SchemaFieldDescription("Timestamp of the Delta Lake table to read.") + @Nullable + abstract String getTimestamp(); + + @SchemaFieldDescription("Properties passed to the Hadoop Configuration.") + @Nullable + abstract Map getHadoopConfig(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setTable(String table); + + abstract Builder setVersion(@Nullable Long version); + + abstract Builder setTimestamp(@Nullable String timestamp); + + abstract Builder setHadoopConfig(@Nullable Map hadoopConfig); + + abstract Configuration build(); + } + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java new file mode 100644 index 000000000000..10dbce43a75b --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTask.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A serializable task containing the necessary metadata to read a group of files in a Delta table. + * Packs both the {@code scanFileRows} (representing the physical files and deletion vectors) and + * {@code scanStateRow} (containing snapshot-level read schemas, configuration, and options). + */ +public class DeltaReadTask implements Serializable { + private static final long serialVersionUID = 1L; + + private final List scanFileRows; + private final SerializableRow scanStateRow; + private final List> rowGroupSizesPerFile; + + public DeltaReadTask( + List scanFileRows, + SerializableRow scanStateRow, + List> rowGroupSizesPerFile) { + this.scanFileRows = scanFileRows; + this.scanStateRow = scanStateRow; + this.rowGroupSizesPerFile = rowGroupSizesPerFile; + } + + public List getScanFileRows() { + return scanFileRows; + } + + public SerializableRow getScanStateRow() { + return scanStateRow; + } + + public List> getRowGroupSizesPerFile() { + return rowGroupSizesPerFile; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DeltaReadTask)) { + return false; + } + DeltaReadTask that = (DeltaReadTask) o; + return Objects.equals(scanFileRows, that.scanFileRows) + && Objects.equals(scanStateRow, that.scanStateRow) + && Objects.equals(rowGroupSizesPerFile, that.rowGroupSizesPerFile); + } + + @Override + public int hashCode() { + return Objects.hash(scanFileRows, scanStateRow, rowGroupSizesPerFile); + } + + @Override + public String toString() { + return "DeltaReadTask{" + + "scanFileRows=" + + scanFileRows + + ", scanStateRow=" + + scanStateRow + + ", rowGroupSizesPerFile=" + + rowGroupSizesPerFile + + '}'; + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java new file mode 100644 index 000000000000..c81a2a231713 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaReadTaskTracker.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import java.util.List; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker; + +/** + * A {@link org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker} for tracking progress + * across Parquet row groups represented by a {@link DeltaReadTask}. + */ +public class DeltaReadTaskTracker extends OffsetRangeTracker { + private final List rowGroupSizes; + + public DeltaReadTaskTracker(OffsetRange restriction, List rowGroupSizes) { + super(restriction); + this.rowGroupSizes = rowGroupSizes; + } + + @Override + public Progress getProgress() { + long workCompleted = 0L; + long workRemaining = 0L; + long from = range.getFrom(); + long to = range.getTo(); + long attempted = lastAttemptedOffset == null ? (from - 1) : lastAttemptedOffset; + + for (int i = (int) from; i < (int) to; i++) { + // Upper bound of the range is the number of row groups. + if (i < rowGroupSizes.size()) { + if (i <= attempted) { + workCompleted += rowGroupSizes.get(i); + } else { + workRemaining += rowGroupSizes.get(i); + } + } + } + return Progress.from((double) workCompleted, (double) workRemaining); + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java new file mode 100644 index 000000000000..bd53c3c9d045 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/DeltaSourceDoFn.java @@ -0,0 +1,455 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.Scan; +import io.delta.kernel.data.ArrayValue; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.ColumnarBatch; +import io.delta.kernel.data.FilteredColumnarBatch; +import io.delta.kernel.data.MapValue; +import io.delta.kernel.defaults.engine.DefaultEngine; +import io.delta.kernel.engine.Engine; +import io.delta.kernel.engine.FileReadResult; +import io.delta.kernel.internal.InternalScanFileUtils; +import io.delta.kernel.internal.data.ScanStateRow; +import io.delta.kernel.internal.util.Utils; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.ByteType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.ShortType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructField; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import io.delta.kernel.utils.CloseableIterator; +import io.delta.kernel.utils.FileStatus; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.beam.sdk.values.Row; +import org.apache.hadoop.conf.Configuration; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A Splittable DoFn that processes {@link DeltaReadTask} elements, performs logical reads, and + * supports dynamic work rebalancing. + */ +@DoFn.BoundedPerElement +class DeltaSourceDoFn extends DoFn { + @Nullable Map hadoopConfig; + private transient @Nullable Engine engine; + private transient @Nullable Configuration conf; + + public DeltaSourceDoFn(@Nullable Map hadoopConfig) { + this.hadoopConfig = hadoopConfig; + } + + private synchronized Configuration getConfiguration() { + Configuration localConf = conf; + if (localConf == null) { + localConf = new Configuration(); + if (hadoopConfig != null) { + for (Map.Entry entry : hadoopConfig.entrySet()) { + localConf.set(entry.getKey(), entry.getValue()); + } + } + conf = localConf; + } + return localConf; + } + + // Returns the sizes of the row groups for a given DeltaReadTask. + private List getRowGroupSizes(DeltaReadTask task) { + List sizes = new ArrayList<>(); + for (List fileSizes : task.getRowGroupSizesPerFile()) { + sizes.addAll(fileSizes); + } + return sizes; + } + + @GetInitialRestriction + public OffsetRange getInitialRestriction(@Element DeltaReadTask task) { + List rowGroupSizes = getRowGroupSizes(task); + // Note that we use the number of row groups, `rowGroupSizes.size()`, here + // as the upper bound not the byte size of row groups. + return new OffsetRange(0L, rowGroupSizes.size()); + } + + @NewTracker + public DeltaReadTaskTracker newTracker( + @Restriction OffsetRange restriction, @Element DeltaReadTask task) { + return new DeltaReadTaskTracker(restriction, getRowGroupSizes(task)); + } + + @Setup + public void setUp() { + engine = DefaultEngine.create(getConfiguration()); + } + + @ProcessElement + public ProcessContinuation processElement( + @Element DeltaReadTask task, + RestrictionTracker tracker, + OutputReceiver out) + throws Exception { + + SerializableRow scanStateRow = task.getScanStateRow(); + StructType physicalSchema = ScanStateRow.getPhysicalDataReadSchema(scanStateRow); + StructType logicalSchema = ScanStateRow.getLogicalSchema(scanStateRow); + Schema beamSchema = DeltaIO.ReadRows.convertToBeamSchema(logicalSchema); + + Engine currentEngine = engine; + if (currentEngine == null) { + throw new IllegalArgumentException("Expected the engine to not be null"); + } + + // `BeamParquetHandler` takes a reference to the `RestrictionTracker` so that it + // can perform `getFrom`, `getTo`, `tryClaim` requests to return the correct set + // of row groups that map to the current restriction. + BeamParquetHandler parquetHandler = + new BeamParquetHandler(getConfiguration(), currentEngine.getParquetHandler(), tracker); + BeamEngine beamEngine = new BeamEngine(currentEngine, parquetHandler); + + long currentStartRgIndex = 0L; + + // We have to go through files in the `DeltaReadTask` in order so that the + // `RestrictionTracker` + // can correctly handle the range of the current split. + List scanFileRows = task.getScanFileRows(); + List> rowGroupSizesPerFile = task.getRowGroupSizesPerFile(); + for (int i = 0; i < scanFileRows.size(); i++) { + if (currentStartRgIndex >= tracker.currentRestriction().getTo()) { + // Breaking early to prevent metadata reads. + break; + } + SerializableRow scanFileRow = scanFileRows.get(i); + FileStatus fileStatus = InternalScanFileUtils.getAddFileStatus(scanFileRow); + + long fileBlocks = rowGroupSizesPerFile.get(i).size(); + + try (CloseableIterator fileReadResults = + parquetHandler.readParquetFiles( + Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + Optional.empty(), + currentStartRgIndex)) { + + // Get the correct set of physical data for the current file that are within the + // range for the current `RestrictionTracker`. + CloseableIterator physicalData = + new CloseableIterator() { + @Override + public void close() throws java.io.IOException {} + + @Override + public boolean hasNext() { + return fileReadResults.hasNext(); + } + + @Override + public ColumnarBatch next() { + return fileReadResults.next().getData(); + } + }; + + // Convert physical data to logical data. + try (CloseableIterator logicalBatches = + Scan.transformPhysicalData(beamEngine, scanStateRow, scanFileRow, physicalData)) { + + while (logicalBatches.hasNext()) { + FilteredColumnarBatch batch = logicalBatches.next(); + try (CloseableIterator logicalRows = batch.getRows()) { + while (logicalRows.hasNext()) { + io.delta.kernel.data.Row deltaRow = logicalRows.next(); + Row beamRow = toBeamRow(deltaRow, beamSchema); + out.output(beamRow); + } + } + } + } + } + + // Advance the total number of blocked handled so far. + currentStartRgIndex += fileBlocks; + + // If the tryClaim failed during processing of the current file, there's no need + // to look at the rest of the files within the task. + if (parquetHandler.hasClaimFailed()) { + break; + } + } + return ProcessContinuation.stop(); + } + + // Convert Delta `Row` to Beam `Row`. + private static Row toBeamRow(io.delta.kernel.data.Row deltaRow, Schema beamSchema) { + Row.Builder builder = Row.withSchema(beamSchema); + StructType deltaSchema = deltaRow.getSchema(); + List fields = deltaSchema.fields(); + for (int i = 0; i < fields.size(); i++) { + StructField field = fields.get(i); + builder.addValue(getFieldValue(deltaRow, i, field.getDataType())); + } + return builder.build(); + } + + // Returns the value at a specific index in a given row. + private static @Nullable Object getFieldValue( + io.delta.kernel.data.Row row, int index, DataType type) { + if (row.isNullAt(index)) { + return null; + } + if (type instanceof BooleanType) { + return row.getBoolean(index); + } else if (type instanceof ByteType) { + return (int) row.getByte(index); + } else if (type instanceof ShortType) { + return (int) row.getShort(index); + } else if (type instanceof IntegerType) { + return row.getInt(index); + } else if (type instanceof LongType) { + return row.getLong(index); + } else if (type instanceof FloatType) { + return row.getFloat(index); + } else if (type instanceof DoubleType) { + return row.getDouble(index); + } else if (type instanceof StringType) { + return row.getString(index); + } else if (type instanceof BinaryType) { + return row.getBinary(index); + } else if (type instanceof TimestampType) { + long microSeconds = row.getLong(index); + return new org.joda.time.Instant(microSeconds / 1000L); + } else if (type instanceof DateType) { + int daysSinceEpoch = row.getInt(index); + return new org.joda.time.Instant(daysSinceEpoch * 86400000L); + } else if (type instanceof ArrayType) { + ArrayValue arrayValue = row.getArray(index); + int size = arrayValue.getSize(); + ColumnVector elements = arrayValue.getElements(); + DataType elementType = ((ArrayType) type).getElementType(); + List<@Nullable Object> list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(getVectorValue(elements, i, elementType)); + } + return list; + } else if (type instanceof MapType) { + MapValue mapValue = row.getMap(index); + int size = mapValue.getSize(); + ColumnVector keys = mapValue.getKeys(); + ColumnVector values = mapValue.getValues(); + DataType keyType = ((MapType) type).getKeyType(); + DataType valueType = ((MapType) type).getValueType(); + Map map = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + Object key = getVectorValue(keys, i, keyType); + if (key != null) { + map.put(key, getVectorValue(values, i, valueType)); + } + } + return map; + } else if (type instanceof StructType) { + io.delta.kernel.data.Row nestedRow = row.getStruct(index); + Schema nestedBeamSchema = DeltaIO.ReadRows.convertToBeamSchema((StructType) type); + return toBeamRow(nestedRow, nestedBeamSchema); + } + throw new UnsupportedOperationException("Unsupported type: " + type.getClass()); + } + + // Returns the value at a specific index in a given column vector. + private static @Nullable Object getVectorValue(ColumnVector vector, int index, DataType type) { + if (vector.isNullAt(index)) { + return null; + } + if (type instanceof BooleanType) { + return vector.getBoolean(index); + } else if (type instanceof ByteType) { + return (int) vector.getByte(index); + } else if (type instanceof ShortType) { + return (int) vector.getShort(index); + } else if (type instanceof IntegerType) { + return vector.getInt(index); + } else if (type instanceof LongType) { + return vector.getLong(index); + } else if (type instanceof FloatType) { + return vector.getFloat(index); + } else if (type instanceof DoubleType) { + return vector.getDouble(index); + } else if (type instanceof StringType) { + return vector.getString(index); + } else if (type instanceof BinaryType) { + return vector.getBinary(index); + } else if (type instanceof TimestampType) { + long microSeconds = vector.getLong(index); + return new org.joda.time.Instant(microSeconds / 1000L); + } else if (type instanceof DateType) { + // Convert days since epoch to milliseconds since epoch. + int daysSinceEpoch = vector.getInt(index); + return new org.joda.time.Instant(daysSinceEpoch * 24L * 60L * 60L * 1000L); + } else if (type instanceof ArrayType) { + ArrayValue arrayValue = vector.getArray(index); + int size = arrayValue.getSize(); + ColumnVector elements = arrayValue.getElements(); + DataType elementType = ((ArrayType) type).getElementType(); + List<@Nullable Object> list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.add(getVectorValue(elements, i, elementType)); + } + return list; + } else if (type instanceof MapType) { + MapValue mapValue = vector.getMap(index); + int size = mapValue.getSize(); + ColumnVector keys = mapValue.getKeys(); + ColumnVector values = mapValue.getValues(); + DataType keyType = ((MapType) type).getKeyType(); + DataType valueType = ((MapType) type).getValueType(); + Map map = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + Object key = getVectorValue(keys, i, keyType); + if (key != null) { + map.put(key, getVectorValue(values, i, valueType)); + } + } + return map; + } else if (type instanceof StructType) { + StructType structType = (StructType) type; + int numFields = structType.fields().size(); + ColumnVector[] childVectors = new ColumnVector[numFields]; + for (int i = 0; i < numFields; i++) { + childVectors[i] = vector.getChild(i); + } + io.delta.kernel.data.Row nestedRow = new VectorRow(structType, childVectors, index); + Schema nestedBeamSchema = DeltaIO.ReadRows.convertToBeamSchema(structType); + return toBeamRow(nestedRow, nestedBeamSchema); + } + throw new UnsupportedOperationException("Unsupported vector type: " + type.getClass()); + } + + // A new new Delta Row type to efficiently convert columnar data from Delta Lake + // to Beam Rows. We use this to store columnar vectors without creating + // additinal memory copies for all fields but return values of the specific + // row index. + private static class VectorRow implements io.delta.kernel.data.Row { + private final StructType schema; + private final ColumnVector[] fields; + private final int rowIndex; + + VectorRow(StructType schema, ColumnVector[] fields, int rowIndex) { + this.schema = schema; + this.fields = fields; + this.rowIndex = rowIndex; + } + + @Override + public StructType getSchema() { + return schema; + } + + @Override + public boolean isNullAt(int ord) { + return fields[ord].isNullAt(rowIndex); + } + + @Override + public boolean getBoolean(int ord) { + return fields[ord].getBoolean(rowIndex); + } + + @Override + public byte getByte(int ord) { + return fields[ord].getByte(rowIndex); + } + + @Override + public short getShort(int ord) { + return fields[ord].getShort(rowIndex); + } + + @Override + public int getInt(int ord) { + return fields[ord].getInt(rowIndex); + } + + @Override + public long getLong(int ord) { + return fields[ord].getLong(rowIndex); + } + + @Override + public float getFloat(int ord) { + return fields[ord].getFloat(rowIndex); + } + + @Override + public double getDouble(int ord) { + return fields[ord].getDouble(rowIndex); + } + + @Override + public String getString(int ord) { + return fields[ord].getString(rowIndex); + } + + @Override + public byte[] getBinary(int ord) { + return fields[ord].getBinary(rowIndex); + } + + @Override + public BigDecimal getDecimal(int ord) { + return fields[ord].getDecimal(rowIndex); + } + + @Override + public io.delta.kernel.data.Row getStruct(int ord) { + StructType childSchema = (StructType) schema.fields().get(ord).getDataType(); + int numFields = childSchema.fields().size(); + ColumnVector[] childFields = new ColumnVector[numFields]; + for (int j = 0; j < numFields; j++) { + childFields[j] = fields[ord].getChild(j); + } + return new VectorRow(childSchema, childFields, rowIndex); + } + + @Override + public ArrayValue getArray(int ord) { + return fields[ord].getArray(rowIndex); + } + + @Override + public MapValue getMap(int ord) { + return fields[ord].getMap(rowIndex); + } + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java new file mode 100644 index 000000000000..308956383e79 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableRow.java @@ -0,0 +1,544 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.data.ArrayValue; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.MapValue; +import io.delta.kernel.data.Row; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.ByteType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DecimalType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.ShortType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A serializable wrapper for Delta {@link Row} that implements the {@link Row} interface itself, + * allowing worker nodes to access serialized Row objects using standard Delta Kernel APIs. + */ +public class SerializableRow implements Row, Serializable { + private static final long serialVersionUID = 1L; + + private final SerializableStructType schema; + private final @Nullable Object[] values; + + public SerializableRow(Row row) { + this.schema = new SerializableStructType(row.getSchema()); + StructType structType = row.getSchema(); + int numFields = structType.fields().size(); + this.values = new Object[numFields]; + for (int i = 0; i < numFields; i++) { + DataType type = structType.fields().get(i).getDataType(); + this.values[i] = getValue(row, i, type); + } + } + + @Override + public StructType getSchema() { + return schema.get(); + } + + @Override + public boolean isNullAt(int ord) { + return values == null || values[ord] == null; + } + + @Override + public boolean getBoolean(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Boolean) val : false; + } + + @Override + public byte getByte(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Byte) val : 0; + } + + @Override + public short getShort(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Short) val : 0; + } + + @Override + public int getInt(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Integer) val : 0; + } + + @Override + public long getLong(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Long) val : 0L; + } + + @Override + public float getFloat(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Float) val : 0.0f; + } + + @Override + public double getDouble(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + return val != null ? (Double) val : 0.0d; + } + + @Override + @SuppressWarnings("nullness") + public String getString(int ord) { + return (String) Objects.requireNonNull(values)[ord]; + } + + @Override + @SuppressWarnings("nullness") + public byte[] getBinary(int ord) { + return (byte[]) Objects.requireNonNull(values)[ord]; + } + + @Override + @SuppressWarnings("nullness") + public BigDecimal getDecimal(int ord) { + return (BigDecimal) Objects.requireNonNull(values)[ord]; + } + + @Override + @SuppressWarnings("nullness") + public Row getStruct(int ord) { + return (Row) Objects.requireNonNull(values)[ord]; + } + + @Override + @SuppressWarnings({"unchecked", "nullness"}) + public ArrayValue getArray(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + if (val == null) { + return null; + } + DataType elementType = + ((ArrayType) getSchema().fields().get(ord).getDataType()).getElementType(); + return new SerializableArrayValue((List<@Nullable Object>) val, elementType); + } + + @Override + @SuppressWarnings({"unchecked", "nullness"}) + public MapValue getMap(int ord) { + Object val = Objects.requireNonNull(values)[ord]; + if (val == null) { + return null; + } + MapType mapType = (MapType) getSchema().fields().get(ord).getDataType(); + return new SerializableMapValue( + (Map) val, mapType.getKeyType(), mapType.getValueType()); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SerializableRow)) { + return false; + } + SerializableRow that = (SerializableRow) o; + return Objects.equals(schema, that.schema) && java.util.Arrays.deepEquals(values, that.values); + } + + @Override + public int hashCode() { + return Objects.hash(schema, java.util.Arrays.deepHashCode(values)); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("SerializableRow{schema=").append(schema).append(", values=["); + if (values != null) { + for (int i = 0; i < values.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(values[i]); + } + } + sb.append("]}"); + return sb.toString(); + } + + private static @Nullable Object getValue(Row row, int index, DataType type) { + if (row.isNullAt(index)) { + return null; + } + if (type instanceof BooleanType) { + return row.getBoolean(index); + } else if (type instanceof ByteType) { + return row.getByte(index); + } else if (type instanceof ShortType) { + return row.getShort(index); + } else if (type instanceof IntegerType) { + return row.getInt(index); + } else if (type instanceof LongType) { + return row.getLong(index); + } else if (type instanceof FloatType) { + return row.getFloat(index); + } else if (type instanceof DoubleType) { + return row.getDouble(index); + } else if (type instanceof StringType) { + return row.getString(index); + } else if (type instanceof BinaryType) { + return row.getBinary(index); + } else if (type instanceof DecimalType) { + return row.getDecimal(index); + } else if (type instanceof StructType) { + return new SerializableRow(row.getStruct(index)); + } else if (type instanceof DateType) { + return row.getInt(index); + } else if (type instanceof TimestampType) { + return row.getLong(index); + } else if (type instanceof ArrayType) { + ArrayValue arr = row.getArray(index); + return convertArray(arr, (ArrayType) type); + } else if (type instanceof MapType) { + MapValue map = row.getMap(index); + return convertMap(map, (MapType) type); + } + throw new IllegalArgumentException("Unsupported type: " + type); + } + + private static @Nullable Object getVectorValue(ColumnVector vector, int index, DataType type) { + if (vector.isNullAt(index)) { + return null; + } + if (type instanceof BooleanType) { + return vector.getBoolean(index); + } else if (type instanceof ByteType) { + return vector.getByte(index); + } else if (type instanceof ShortType) { + return vector.getShort(index); + } else if (type instanceof IntegerType) { + return vector.getInt(index); + } else if (type instanceof LongType) { + return vector.getLong(index); + } else if (type instanceof FloatType) { + return vector.getFloat(index); + } else if (type instanceof DoubleType) { + return vector.getDouble(index); + } else if (type instanceof StringType) { + return vector.getString(index); + } else if (type instanceof BinaryType) { + return vector.getBinary(index); + } else if (type instanceof DecimalType) { + return vector.getDecimal(index); + } else if (type instanceof StructType) { + StructType structType = (StructType) type; + int numFields = structType.fields().size(); + ColumnVector[] childFields = new ColumnVector[numFields]; + for (int j = 0; j < numFields; j++) { + childFields[j] = vector.getChild(j); + } + return new SerializableRow(new VectorRow(structType, childFields, index)); + } else if (type instanceof DateType) { + return vector.getInt(index); + } else if (type instanceof TimestampType) { + return vector.getLong(index); + } else if (type instanceof ArrayType) { + ArrayValue arr = vector.getArray(index); + return convertArray(arr, (ArrayType) type); + } else if (type instanceof MapType) { + MapValue map = vector.getMap(index); + return convertMap(map, (MapType) type); + } + throw new IllegalArgumentException("Unsupported vector type: " + type); + } + + private static List<@Nullable Object> convertArray(ArrayValue arr, ArrayType arrayType) { + int size = arr.getSize(); + ColumnVector elements = arr.getElements(); + DataType elementType = arrayType.getElementType(); + List<@Nullable Object> result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + result.add(getVectorValue(elements, i, elementType)); + } + return result; + } + + private static Map convertMap(MapValue map, MapType mapType) { + int size = map.getSize(); + ColumnVector keys = map.getKeys(); + ColumnVector values = map.getValues(); + DataType keyType = mapType.getKeyType(); + DataType valueType = mapType.getValueType(); + Map result = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + Object key = getVectorValue(keys, i, keyType); + if (key != null) { + result.put(key, getVectorValue(values, i, valueType)); + } + } + return result; + } + + private static class VectorRow implements Row { + private final StructType schema; + private final ColumnVector[] fields; + private final int rowIndex; + + VectorRow(StructType schema, ColumnVector[] fields, int rowIndex) { + this.schema = schema; + this.fields = fields; + this.rowIndex = rowIndex; + } + + @Override + public StructType getSchema() { + return schema; + } + + @Override + public boolean isNullAt(int ord) { + return fields[ord].isNullAt(rowIndex); + } + + @Override + public boolean getBoolean(int ord) { + return fields[ord].getBoolean(rowIndex); + } + + @Override + public byte getByte(int ord) { + return fields[ord].getByte(rowIndex); + } + + @Override + public short getShort(int ord) { + return fields[ord].getShort(rowIndex); + } + + @Override + public int getInt(int ord) { + return fields[ord].getInt(rowIndex); + } + + @Override + public long getLong(int ord) { + return fields[ord].getLong(rowIndex); + } + + @Override + public float getFloat(int ord) { + return fields[ord].getFloat(rowIndex); + } + + @Override + public double getDouble(int ord) { + return fields[ord].getDouble(rowIndex); + } + + @Override + public String getString(int ord) { + return fields[ord].getString(rowIndex); + } + + @Override + public byte[] getBinary(int ord) { + return fields[ord].getBinary(rowIndex); + } + + @Override + public BigDecimal getDecimal(int ord) { + return fields[ord].getDecimal(rowIndex); + } + + @Override + public Row getStruct(int ord) { + StructType childSchema = (StructType) schema.fields().get(ord).getDataType(); + int numFields = childSchema.fields().size(); + ColumnVector[] childFields = new ColumnVector[numFields]; + for (int j = 0; j < numFields; j++) { + childFields[j] = fields[ord].getChild(j); + } + return new VectorRow(childSchema, childFields, rowIndex); + } + + @Override + public ArrayValue getArray(int ord) { + return fields[ord].getArray(rowIndex); + } + + @Override + public MapValue getMap(int ord) { + return fields[ord].getMap(rowIndex); + } + } + + private static class ListColumnVector implements ColumnVector { + private final DataType dataType; + private final List<@Nullable Object> list; + + @SuppressWarnings("unchecked") + ListColumnVector(DataType dataType, List list) { + this.dataType = dataType; + this.list = (List<@Nullable Object>) list; + } + + @Override + public DataType getDataType() { + return dataType; + } + + @Override + public int getSize() { + return list.size(); + } + + @Override + public boolean isNullAt(int rowId) { + return list.get(rowId) == null; + } + + @Override + public boolean getBoolean(int rowId) { + Object val = list.get(rowId); + return val != null ? (Boolean) val : false; + } + + @Override + public byte getByte(int rowId) { + Object val = list.get(rowId); + return val != null ? (Byte) val : 0; + } + + @Override + public short getShort(int rowId) { + Object val = list.get(rowId); + return val != null ? (Short) val : 0; + } + + @Override + public int getInt(int rowId) { + Object val = list.get(rowId); + return val != null ? (Integer) val : 0; + } + + @Override + public long getLong(int rowId) { + Object val = list.get(rowId); + return val != null ? (Long) val : 0L; + } + + @Override + public float getFloat(int rowId) { + Object val = list.get(rowId); + return val != null ? (Float) val : 0.0f; + } + + @Override + public double getDouble(int rowId) { + Object val = list.get(rowId); + return val != null ? (Double) val : 0.0d; + } + + @Override + @SuppressWarnings("nullness") + public String getString(int rowId) { + return (String) list.get(rowId); + } + + @Override + @SuppressWarnings("nullness") + public byte[] getBinary(int rowId) { + return (byte[]) list.get(rowId); + } + + @Override + @SuppressWarnings("nullness") + public BigDecimal getDecimal(int rowId) { + return (BigDecimal) list.get(rowId); + } + + @Override + public void close() {} + } + + private static class SerializableArrayValue implements ArrayValue { + private final List list; + private final DataType elementType; + + SerializableArrayValue(List list, DataType elementType) { + this.list = list; + this.elementType = elementType; + } + + @Override + public int getSize() { + return list.size(); + } + + @Override + public ColumnVector getElements() { + return new ListColumnVector(elementType, list); + } + } + + private static class SerializableMapValue implements MapValue { + private final Map map; + private final DataType keyType; + private final DataType valueType; + + SerializableMapValue(Map map, DataType keyType, DataType valueType) { + this.map = map; + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + public int getSize() { + return map.size(); + } + + @Override + public ColumnVector getKeys() { + return new ListColumnVector(keyType, new ArrayList<>(map.keySet())); + } + + @Override + public ColumnVector getValues() { + return new ListColumnVector(valueType, new ArrayList<>(map.values())); + } + } +} diff --git a/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java new file mode 100644 index 000000000000..08a27ac55303 --- /dev/null +++ b/sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/SerializableStructType.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.internal.types.DataTypeJsonSerDe; +import io.delta.kernel.types.StructType; +import java.io.Serializable; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A serializable wrapper for {@link StructType} using the Delta Kernel JSON + * serializer/deserializer. + */ +public class SerializableStructType implements Serializable { + private static final long serialVersionUID = 1L; + + private final String jsonSchema; + private transient StructType structType; + + public SerializableStructType(StructType structType) { + this.structType = Objects.requireNonNull(structType, "structType cannot be null"); + this.jsonSchema = DataTypeJsonSerDe.serializeStructType(structType); + } + + public StructType get() { + if (structType == null) { + structType = DataTypeJsonSerDe.deserializeStructType(jsonSchema); + } + return structType; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SerializableStructType)) { + return false; + } + SerializableStructType that = (SerializableStructType) o; + return Objects.equals(jsonSchema, that.jsonSchema); + } + + @Override + public int hashCode() { + return Objects.hash(jsonSchema); + } + + @Override + public String toString() { + return jsonSchema; + } +} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/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 runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/package-info.java rename to sdks/java/io/delta/src/main/java/org/apache/beam/sdk/io/delta/package-info.java index 97415846e310..c765a7fb7655 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/metrics/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. */ -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.metrics; +/** + * Transforms for reading from Delta Lake. + * + * @see org.apache.beam.sdk.io.delta.DeltaIO + */ +package org.apache.beam.sdk.io.delta; diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java new file mode 100644 index 000000000000..bd8bf8b3c8cc --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/DeltaIOTest.java @@ -0,0 +1,813 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructField; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import org.apache.avro.generic.GenericRecord; +import org.apache.beam.sdk.extensions.avro.coders.AvroCoder; +import org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils; +import org.apache.beam.sdk.io.Compression; +import org.apache.beam.sdk.io.FileIO; +import org.apache.beam.sdk.io.delta.DeltaIO.ReadRows; +import org.apache.beam.sdk.io.parquet.ParquetIO; +import org.apache.beam.sdk.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.ParDo; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +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(); + + io.delta.kernel.data.Row scanState = scan.getScanState(engine); + System.err.println("SCAN STATE SCHEMA: " + scanState.getSchema().toString()); + + try (io.delta.kernel.utils.CloseableIterator + scanFiles = scan.getScanFiles(engine)) { + while (scanFiles.hasNext()) { + io.delta.kernel.data.FilteredColumnarBatch batch = scanFiles.next(); + try (io.delta.kernel.utils.CloseableIterator rows = + batch.getRows()) { + while (rows.hasNext()) { + io.delta.kernel.data.Row row = rows.next(); + verifySerialization(row); + } + } + } + } + } + + private void verifySerialization(io.delta.kernel.data.Row row) throws Exception { + SerializableRow serializableRow = new SerializableRow(row); + + // Serialize using standard Java Serialization + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + try (java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos)) { + oos.writeObject(serializableRow); + } + + byte[] bytes = baos.toByteArray(); + + // Deserialize + SerializableRow deserializedRow; + java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(bytes); + try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(bais)) { + deserializedRow = (SerializableRow) ois.readObject(); + } + + // Assert equals + org.junit.Assert.assertEquals(serializableRow, deserializedRow); + org.junit.Assert.assertEquals( + row.getSchema().toString(), deserializedRow.getSchema().toString()); + + // Deep verify fields + io.delta.kernel.types.StructType schema = row.getSchema(); + for (int i = 0; i < schema.fields().size(); i++) { + org.junit.Assert.assertEquals(row.isNullAt(i), deserializedRow.isNullAt(i)); + if (!row.isNullAt(i)) { + io.delta.kernel.types.DataType type = schema.fields().get(i).getDataType(); + if (type instanceof io.delta.kernel.types.StringType) { + org.junit.Assert.assertEquals(row.getString(i), deserializedRow.getString(i)); + } else if (type instanceof io.delta.kernel.types.LongType) { + org.junit.Assert.assertEquals(row.getLong(i), deserializedRow.getLong(i)); + } + } + } + } + + @Test + public void testCreateReadTasksDoFn() throws Exception { + File tableDir = tempFolder.newFolder("delta-table"); + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":100,\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row dummyRow = Row.withSchema(schema).addValues("test-name").build(); + writeParquetFile(new File(tableDir, "part-00000.parquet"), dummyRow); + + PCollection output = + writePipeline + .apply(Create.of(tableDir.getAbsolutePath())) + .apply(ParDo.of(new CreateReadTasksDoFn(null))); + + PCollection paths = + output.apply( + org.apache.beam.sdk.transforms.MapElements.into( + org.apache.beam.sdk.values.TypeDescriptors.strings()) + .via( + task -> + io.delta.kernel.internal.InternalScanFileUtils.getAddFileStatus( + task.getScanFileRows().get(0)) + .getPath())); + + PAssert.that(paths) + .containsInAnyOrder("file:" + tableDir.getAbsolutePath() + "/part-00000.parquet"); + + writePipeline.run().waitUntilFinish(); + } + + @Test + public void testCreateReadTasksDoFnGrouping() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-grouping"); + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00001.parquet\",\"partitionValues\":{},\"size\":400000000,\"modificationTime\":123456789,\"dataChange\":true}}\n" + + "{\"add\":{\"path\":\"part-00002.parquet\",\"partitionValues\":{},\"size\":400000000,\"modificationTime\":123456789,\"dataChange\":true}}\n" + + "{\"add\":{\"path\":\"part-00003.parquet\",\"partitionValues\":{},\"size\":1200000000,\"modificationTime\":123456789,\"dataChange\":true}}\n" + + "{\"add\":{\"path\":\"part-00004.parquet\",\"partitionValues\":{},\"size\":100,\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row dummyRow = Row.withSchema(schema).addValues("test-name").build(); + writeParquetFile(new File(tableDir, "part-00001.parquet"), dummyRow); + writeParquetFile(new File(tableDir, "part-00002.parquet"), dummyRow); + writeParquetFile(new File(tableDir, "part-00003.parquet"), dummyRow); + writeParquetFile(new File(tableDir, "part-00004.parquet"), dummyRow); + + PCollection output = + writePipeline + .apply("Create Grouping Input", Create.of(tableDir.getAbsolutePath())) + .apply("Plan Grouped Files", ParDo.of(new CreateReadTasksDoFn(null))); + + PCollection taskDescriptions = + output.apply( + org.apache.beam.sdk.transforms.MapElements.into( + org.apache.beam.sdk.values.TypeDescriptors.strings()) + .via( + task -> { + StringBuilder sb = new StringBuilder(); + for (SerializableRow row : task.getScanFileRows()) { + if (sb.length() > 0) { + sb.append(","); + } + String fullPath = + io.delta.kernel.internal.InternalScanFileUtils.getAddFileStatus(row) + .getPath(); + String filename = fullPath.substring(fullPath.lastIndexOf('/') + 1); + sb.append(filename); + } + return sb.toString(); + })); + + PAssert.that(taskDescriptions) + .containsInAnyOrder( + "part-00001.parquet,part-00002.parquet", "part-00003.parquet", "part-00004.parquet"); + + writePipeline.run().waitUntilFinish(); + } + + @Test + public void testFullPipelineRead() throws Exception { + File tableDir = tempFolder.newFolder("delta-table-full"); + + // 1. Write a Parquet file using Beam + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row row = Row.withSchema(schema).addValues("test-name").build(); + + org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(schema); + GenericRecord record = AvroUtils.toGenericRecord(row, avroSchema); + + writePipeline + .apply("Create Input", Create.of(record).withCoder(AvroCoder.of(avroSchema))) + .apply( + "Write Parquet", + FileIO.write() + .via(ParquetIO.sink(avroSchema)) + .to(tableDir.getAbsolutePath() + "/") + .withNaming( + (BoundedWindow window, + PaneInfo paneInfo, + int numShards, + int shardIndex, + Compression compression) -> "part-00000.parquet")); + + writePipeline.run().waitUntilFinish(); + + System.out.println("FILES IN TABLE DIR:"); + for (File f : tableDir.listFiles()) { + System.out.println( + " - " + f.getName() + " (size=" + f.length() + ", isDir=" + f.isDirectory() + ")"); + if (f.isDirectory()) { + for (File sub : f.listFiles()) { + System.out.println(" - " + sub.getName() + " (size=" + sub.length() + ")"); + } + } + } + + File parquetFile = new File(tableDir, "part-00000.parquet"); + byte[] fileBytes = Files.readAllBytes(parquetFile.toPath()); + System.out.println("PARQUET FILE LENGTH: " + fileBytes.length); + if (fileBytes.length >= 8) { + System.out.println( + "PARQUET FIRST 4 BYTES: " + + fileBytes[0] + + ", " + + fileBytes[1] + + ", " + + fileBytes[2] + + ", " + + fileBytes[3] + + " ('" + + (char) fileBytes[0] + + (char) fileBytes[1] + + (char) fileBytes[2] + + (char) fileBytes[3] + + "')"); + int len = fileBytes.length; + System.out.println( + "PARQUET LAST 4 BYTES: " + + fileBytes[len - 4] + + ", " + + fileBytes[len - 3] + + ", " + + fileBytes[len - 2] + + ", " + + fileBytes[len - 1] + + " ('" + + (char) fileBytes[len - 4] + + (char) fileBytes[len - 3] + + (char) fileBytes[len - 2] + + (char) fileBytes[len - 1] + + "')"); + } + + // 2. Create the Delta log + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":" + + fileBytes.length + + ",\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + // 3. Read it using DeltaIO + PCollection output = + readPipeline.apply(DeltaIO.readRows().from(tableDir.getAbsolutePath())); + + PAssert.that(output).containsInAnyOrder(row); + + readPipeline.run().waitUntilFinish(); + } + + private byte[] writeParquetFile(File file, Row row) throws Exception { + org.apache.avro.Schema avroSchema = + org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.toAvroSchema(row.getSchema()); + org.apache.avro.generic.GenericRecord record = + org.apache.beam.sdk.extensions.avro.schemas.utils.AvroUtils.toGenericRecord( + row, avroSchema); + org.apache.hadoop.fs.Path path = new org.apache.hadoop.fs.Path(file.getAbsolutePath()); + try (org.apache.parquet.hadoop.ParquetWriter writer = + org.apache.parquet.avro.AvroParquetWriter.builder( + path) + .withSchema(avroSchema) + .withConf(new org.apache.hadoop.conf.Configuration()) + .build()) { + writer.write(record); + } + return java.nio.file.Files.readAllBytes(file.toPath()); + } + + @Test + public void testManagedDeltaRead() throws Exception { + File tableDir = tempFolder.newFolder("managed-delta-table"); + + // 1. Write a Parquet file to simulate a Delta table + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row row = Row.withSchema(schema).addValues("test-name").build(); + writeParquetFile(new File(tableDir, "part-00000.parquet"), row); + + // 2. Create the Delta log + File logDir = new File(tableDir, "_delta_log"); + logDir.mkdirs(); + File commitFile = new File(logDir, "00000000000000000000.json"); + + File parquetFile = new File(tableDir, "part-00000.parquet"); + byte[] fileBytes = Files.readAllBytes(parquetFile.toPath()); + + String commitContent = + "{\"protocol\":{\"minReaderVersion\":1,\"minWriterVersion\":2}}\n" + + "{\"metaData\":{\"id\":\"test-id\",\"format\":{\"provider\":\"parquet\",\"options\":{}},\"schemaString\":\"{\\\"type\\\":\\\"struct\\\",\\\"fields\\\":[{\\\"name\\\":\\\"name\\\",\\\"type\\\":\\\"string\\\",\\\"nullable\\\":true,\\\"metadata\\\":{}}]}\",\"partitionColumns\":[],\"configuration\":{},\"createdAt\":123456789}}\n" + + "{\"add\":{\"path\":\"part-00000.parquet\",\"partitionValues\":{},\"size\":" + + fileBytes.length + + ",\"modificationTime\":123456789,\"dataChange\":true}}"; + + Files.write(commitFile.toPath(), commitContent.getBytes(StandardCharsets.UTF_8)); + + // 3. Read it using Managed + PCollection output = + readPipeline + .apply( + Managed.read(Managed.DELTA_LAKE) + .withConfig(ImmutableMap.of("table", tableDir.getAbsolutePath()))) + .getSinglePCollection(); + + PAssert.that(output).containsInAnyOrder(row); + + readPipeline.run().waitUntilFinish(); + } + + @Test + @org.junit.Ignore("Manual integration test with external local table") + public void testReadingLocalTable() throws Exception { + PCollection output = + readPipeline.apply( + DeltaIO.readRows() + .from("/Users/chamikara/testing/delta_lake/test_repo/test_table_1_gb")); + PCollection counted = output.apply(Count.globally()); + + counted + .apply( + "Convert to String", + org.apache.beam.sdk.transforms.MapElements.into( + org.apache.beam.sdk.values.TypeDescriptors.strings()) + .via(String::valueOf)) + .apply( + "Write to File", + org.apache.beam.sdk.io.TextIO.write() + .to("/Users/chamikara/testing/delta_lake/test_repo_pipeline_output/output") + .withSuffix(".txt") + .withoutSharding()); + + readPipeline.run().waitUntilFinish(); + } + + @Test + public void testConvertToBeamSchema() { + StructType deltaSchema = + new StructType( + java.util.Arrays.asList( + new StructField("string", StringType.STRING, false), + new StructField("integer", IntegerType.INTEGER, false), + new StructField("long", LongType.LONG, false), + new StructField("float", FloatType.FLOAT, false), + new StructField("double", DoubleType.DOUBLE, false), + new StructField("boolean", BooleanType.BOOLEAN, false), + new StructField("binary", BinaryType.BINARY, false), + new StructField("timestamp", TimestampType.TIMESTAMP, false), + new StructField("date", DateType.DATE, false), + new StructField("array", new ArrayType(StringType.STRING, true), false), + new StructField( + "map", new MapType(StringType.STRING, IntegerType.INTEGER, true), false), + new StructField( + "struct", + new StructType( + java.util.Arrays.asList( + new StructField("nested_string", StringType.STRING, false))), + false))); + + Schema nestedSchema = + Schema.builder().addField("nested_string", Schema.FieldType.STRING).build(); + + Schema expectedSchema = + Schema.builder() + .addField("string", Schema.FieldType.STRING) + .addField("integer", Schema.FieldType.INT32) + .addField("long", Schema.FieldType.INT64) + .addField("float", Schema.FieldType.FLOAT) + .addField("double", Schema.FieldType.DOUBLE) + .addField("boolean", Schema.FieldType.BOOLEAN) + .addField("binary", Schema.FieldType.BYTES) + .addField("timestamp", Schema.FieldType.DATETIME) + .addField("date", Schema.FieldType.DATETIME) + .addField("array", Schema.FieldType.iterable(Schema.FieldType.STRING)) + .addField("map", Schema.FieldType.map(Schema.FieldType.STRING, Schema.FieldType.INT32)) + .addField("struct", Schema.FieldType.row(nestedSchema)) + .build(); + + Schema actualSchema = DeltaIO.ReadRows.convertToBeamSchema(deltaSchema); + org.junit.Assert.assertEquals(expectedSchema, actualSchema); + } + + @Test + public void testDeltaReadTaskTracker() { + java.util.List sizes = java.util.Arrays.asList(100L, 200L, 300L); + org.apache.beam.sdk.io.range.OffsetRange range = + new org.apache.beam.sdk.io.range.OffsetRange(0L, 3L); + DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes); + + org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker.Progress progress = + tracker.getProgress(); + org.junit.Assert.assertEquals(0.0, progress.getWorkCompleted(), 0.001); + org.junit.Assert.assertEquals(600.0, progress.getWorkRemaining(), 0.001); + + org.junit.Assert.assertTrue(tracker.tryClaim(0L)); + progress = tracker.getProgress(); + org.junit.Assert.assertEquals(100.0, progress.getWorkCompleted(), 0.001); + org.junit.Assert.assertEquals(500.0, progress.getWorkRemaining(), 0.001); + + org.junit.Assert.assertTrue(tracker.tryClaim(1L)); + progress = tracker.getProgress(); + org.junit.Assert.assertEquals(300.0, progress.getWorkCompleted(), 0.001); + org.junit.Assert.assertEquals(300.0, progress.getWorkRemaining(), 0.001); + + org.junit.Assert.assertTrue(tracker.tryClaim(2L)); + progress = tracker.getProgress(); + org.junit.Assert.assertEquals(600.0, progress.getWorkCompleted(), 0.001); + org.junit.Assert.assertEquals(0.0, progress.getWorkRemaining(), 0.001); + + tracker.checkDone(); + } + + @Test + public void testBeamParquetHandler() { + java.util.List sizes = java.util.Arrays.asList(100L, 200L); + org.apache.beam.sdk.io.range.OffsetRange range = + new org.apache.beam.sdk.io.range.OffsetRange(0L, 2L); + DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes); + + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + io.delta.kernel.engine.ParquetHandler dummyDelegate = + new io.delta.kernel.engine.ParquetHandler() { + @Override + public io.delta.kernel.utils.CloseableIterator + readParquetFiles( + io.delta.kernel.utils.CloseableIterator + fileIter, + io.delta.kernel.types.StructType physicalSchema, + java.util.Optional predicate) + throws java.io.IOException { + return new io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.engine.FileReadResult>() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public io.delta.kernel.engine.FileReadResult next() { + throw new java.util.NoSuchElementException(); + } + + @Override + public void close() {} + }; + } + + @Override + public void writeParquetFileAtomically( + String filePath, + io.delta.kernel.utils.CloseableIterator + data) + throws java.io.IOException {} + + @Override + public io.delta.kernel.utils.CloseableIterator + writeParquetFiles( + String filePath, + io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.data.FilteredColumnarBatch> + data, + java.util.List statsColumns) + throws java.io.IOException { + return new io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.utils.DataFileStatus>() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public io.delta.kernel.utils.DataFileStatus next() { + throw new java.util.NoSuchElementException(); + } + + @Override + public void close() {} + }; + } + }; + + BeamParquetHandler handler = new BeamParquetHandler(conf, dummyDelegate, tracker); + org.junit.Assert.assertNotNull(handler); + + BeamEngine beamEngine = + new BeamEngine(io.delta.kernel.defaults.engine.DefaultEngine.create(conf), handler); + org.junit.Assert.assertEquals(handler, beamEngine.getParquetHandler()); + } + + @Test + public void testBeamParquetHandlerWriteDelegation() throws Exception { + java.util.List sizes = java.util.Arrays.asList(100L); + org.apache.beam.sdk.io.range.OffsetRange range = + new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L); + DeltaReadTaskTracker tracker = new DeltaReadTaskTracker(range, sizes); + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + + boolean[] flags = new boolean[2]; + io.delta.kernel.engine.ParquetHandler delegate = + new io.delta.kernel.engine.ParquetHandler() { + @Override + public io.delta.kernel.utils.CloseableIterator + readParquetFiles( + io.delta.kernel.utils.CloseableIterator + fileIter, + io.delta.kernel.types.StructType physicalSchema, + java.util.Optional predicate) { + return null; + } + + @Override + public void writeParquetFileAtomically( + String filePath, + io.delta.kernel.utils.CloseableIterator + data) { + flags[0] = true; + } + + @Override + public io.delta.kernel.utils.CloseableIterator + writeParquetFiles( + String filePath, + io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.data.FilteredColumnarBatch> + data, + java.util.List statsColumns) { + flags[1] = true; + return new io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.utils.DataFileStatus>() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public io.delta.kernel.utils.DataFileStatus next() { + throw new java.util.NoSuchElementException(); + } + + @Override + public void close() {} + }; + } + }; + + BeamParquetHandler handler = new BeamParquetHandler(conf, delegate, tracker); + handler.writeParquetFileAtomically("path", null); + org.junit.Assert.assertTrue(flags[0]); + + handler.writeParquetFiles("path", null, java.util.Collections.emptyList()); + org.junit.Assert.assertTrue(flags[1]); + } + + @Test + public void testBeamParquetHandlerReadFiltering() throws Exception { + File tableDir = tempFolder.newFolder("parquet-filtering-test"); + + Schema schema = Schema.builder().addField("name", Schema.FieldType.STRING).build(); + Row row = Row.withSchema(schema).addValues("test-name").build(); + org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(schema); + GenericRecord record = AvroUtils.toGenericRecord(row, avroSchema); + + filteringPipeline + .apply("Create Input", Create.of(record).withCoder(AvroCoder.of(avroSchema))) + .apply( + "Write Parquet", + FileIO.write() + .via(ParquetIO.sink(avroSchema)) + .to(tableDir.getAbsolutePath() + "/") + .withNaming((w, p, n, s, c) -> "part-00000.parquet")); + + filteringPipeline.run().waitUntilFinish(); + + File parquetFile = new File(tableDir, "part-00000.parquet"); + io.delta.kernel.utils.FileStatus fileStatus = + io.delta.kernel.utils.FileStatus.of( + parquetFile.getAbsolutePath(), parquetFile.length(), 123456789L); + + org.apache.hadoop.conf.Configuration conf = new org.apache.hadoop.conf.Configuration(); + io.delta.kernel.types.StructType physicalSchema = + new io.delta.kernel.types.StructType( + java.util.Arrays.asList( + new io.delta.kernel.types.StructField( + "name", io.delta.kernel.types.StringType.STRING, true))); + + io.delta.kernel.engine.ParquetHandler dummyDelegate = + new io.delta.kernel.engine.ParquetHandler() { + @Override + public io.delta.kernel.utils.CloseableIterator + readParquetFiles( + io.delta.kernel.utils.CloseableIterator + fileIter, + io.delta.kernel.types.StructType physicalSchema, + java.util.Optional predicate) { + return null; + } + + @Override + public void writeParquetFileAtomically( + String filePath, + io.delta.kernel.utils.CloseableIterator + data) {} + + @Override + public io.delta.kernel.utils.CloseableIterator + writeParquetFiles( + String filePath, + io.delta.kernel.utils.CloseableIterator< + io.delta.kernel.data.FilteredColumnarBatch> + data, + java.util.List statsColumns) { + return null; + } + }; + + // Case A: Out of bounds before (tracker range [10, 20)) + DeltaReadTaskTracker trackerA = + new DeltaReadTaskTracker( + new org.apache.beam.sdk.io.range.OffsetRange(10L, 20L), + java.util.Collections.singletonList(parquetFile.length())); + BeamParquetHandler handlerA = new BeamParquetHandler(conf, dummyDelegate, trackerA); + try (io.delta.kernel.utils.CloseableIterator iter = + handlerA.readParquetFiles( + io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + java.util.Optional.empty())) { + org.junit.Assert.assertFalse(iter.hasNext()); + try { + iter.next(); + org.junit.Assert.fail("Expected NoSuchElementException"); + } catch (java.util.NoSuchElementException e) { + // expected + } + } + + // Case B: Out of bounds after (tracker range [0, 0)) + DeltaReadTaskTracker trackerB = + new DeltaReadTaskTracker( + new org.apache.beam.sdk.io.range.OffsetRange(0L, 0L), + java.util.Collections.singletonList(parquetFile.length())); + BeamParquetHandler handlerB = new BeamParquetHandler(conf, dummyDelegate, trackerB); + try (io.delta.kernel.utils.CloseableIterator iter = + handlerB.readParquetFiles( + io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + java.util.Optional.empty())) { + org.junit.Assert.assertFalse(iter.hasNext()); + } + + // Case C: Claim fails + DeltaReadTaskTracker trackerC = + new DeltaReadTaskTracker( + new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L), + java.util.Collections.singletonList(parquetFile.length())) { + @Override + public boolean tryClaim(Long i) { + return false; // Simulate failure to claim + } + }; + BeamParquetHandler handlerC = new BeamParquetHandler(conf, dummyDelegate, trackerC); + try (io.delta.kernel.utils.CloseableIterator iter = + handlerC.readParquetFiles( + io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + java.util.Optional.empty())) { + org.junit.Assert.assertFalse(iter.hasNext()); + } + + // Case D: Successful claim and read + DeltaReadTaskTracker trackerD = + new DeltaReadTaskTracker( + new org.apache.beam.sdk.io.range.OffsetRange(0L, 1L), + java.util.Collections.singletonList(parquetFile.length())); + BeamParquetHandler handlerD = new BeamParquetHandler(conf, dummyDelegate, trackerD); + try (io.delta.kernel.utils.CloseableIterator iter = + handlerD.readParquetFiles( + io.delta.kernel.internal.util.Utils.singletonCloseableIterator(fileStatus), + physicalSchema, + java.util.Optional.empty())) { + org.junit.Assert.assertTrue(iter.hasNext()); + io.delta.kernel.engine.FileReadResult res = iter.next(); + org.junit.Assert.assertNotNull(res); + org.junit.Assert.assertNotNull(res.getData()); + org.junit.Assert.assertFalse(iter.hasNext()); + try { + iter.next(); + org.junit.Assert.fail("Expected NoSuchElementException"); + } catch (java.util.NoSuchElementException e) { + // expected + } + } + } +} diff --git a/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java new file mode 100644 index 000000000000..69d423032eac --- /dev/null +++ b/sdks/java/io/delta/src/test/java/org/apache/beam/sdk/io/delta/SerializableRowTest.java @@ -0,0 +1,498 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.delta; + +import io.delta.kernel.data.ArrayValue; +import io.delta.kernel.data.ColumnVector; +import io.delta.kernel.data.MapValue; +import io.delta.kernel.data.Row; +import io.delta.kernel.types.ArrayType; +import io.delta.kernel.types.BinaryType; +import io.delta.kernel.types.BooleanType; +import io.delta.kernel.types.ByteType; +import io.delta.kernel.types.DataType; +import io.delta.kernel.types.DateType; +import io.delta.kernel.types.DecimalType; +import io.delta.kernel.types.DoubleType; +import io.delta.kernel.types.FloatType; +import io.delta.kernel.types.IntegerType; +import io.delta.kernel.types.LongType; +import io.delta.kernel.types.MapType; +import io.delta.kernel.types.ShortType; +import io.delta.kernel.types.StringType; +import io.delta.kernel.types.StructField; +import io.delta.kernel.types.StructType; +import io.delta.kernel.types.TimestampType; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SerializableRowTest { + + @Test + public void testAllTypesReadWriteSerialization() throws Exception { + StructType structSchema = + new StructType(Arrays.asList(new StructField("nested_int", IntegerType.INTEGER, false))); + + StructType schema = + new StructType( + Arrays.asList( + new StructField("boolean", BooleanType.BOOLEAN, true), + new StructField("byte", ByteType.BYTE, true), + new StructField("short", ShortType.SHORT, true), + new StructField("integer", IntegerType.INTEGER, true), + new StructField("long", LongType.LONG, true), + new StructField("float", FloatType.FLOAT, true), + new StructField("double", DoubleType.DOUBLE, true), + new StructField("string", StringType.STRING, true), + new StructField("binary", BinaryType.BINARY, true), + new StructField("decimal", new DecimalType(10, 2), true), + new StructField("date", DateType.DATE, true), + new StructField("timestamp", TimestampType.TIMESTAMP, true), + new StructField("struct", structSchema, true), + new StructField("array", new ArrayType(StringType.STRING, true), true), + new StructField( + "map", new MapType(StringType.STRING, IntegerType.INTEGER, true), true))); + + Map values = new LinkedHashMap<>(); + values.put("boolean", true); + values.put("byte", (byte) 1); + values.put("short", (short) 2); + values.put("integer", 3); + values.put("long", 4L); + values.put("float", 5.0f); + values.put("double", 6.0d); + values.put("string", "hello"); + values.put("binary", new byte[] {7, 8}); + values.put("decimal", new BigDecimal("9.20")); + values.put("date", 16543); + values.put("timestamp", 16543000000L); + + Map nestedValues = new LinkedHashMap<>(); + nestedValues.put("nested_int", 42); + values.put("struct", new FakeRow(structSchema, nestedValues)); + + values.put("array", Arrays.asList("a", "b", null)); + + Map mapValues = new LinkedHashMap<>(); + mapValues.put("key1", 100); + mapValues.put("key2", null); + values.put("map", mapValues); + + FakeRow originalRow = new FakeRow(schema, values); + SerializableRow serializableRow = new SerializableRow(originalRow); + + verifyRowContents(serializableRow, schema, values); + + // Test equals and hashCode + SerializableRow serializableRow2 = new SerializableRow(new FakeRow(schema, values)); + Assert.assertEquals(serializableRow, serializableRow2); + Assert.assertEquals(serializableRow.hashCode(), serializableRow2.hashCode()); + Assert.assertNotNull(serializableRow.toString()); + + // Serialize and Deserialize + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(serializableRow); + } + + byte[] bytes = baos.toByteArray(); + SerializableRow deserializedRow; + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + deserializedRow = (SerializableRow) ois.readObject(); + } + + verifyRowContents(deserializedRow, schema, values); + Assert.assertEquals(serializableRow, deserializedRow); + } + + @Test + public void testNullValuesReadWriteSerialization() throws Exception { + StructType structSchema = + new StructType(Arrays.asList(new StructField("nested_int", IntegerType.INTEGER, true))); + + StructType schema = + new StructType( + Arrays.asList( + new StructField("boolean", BooleanType.BOOLEAN, true), + new StructField("byte", ByteType.BYTE, true), + new StructField("short", ShortType.SHORT, true), + new StructField("integer", IntegerType.INTEGER, true), + new StructField("long", LongType.LONG, true), + new StructField("float", FloatType.FLOAT, true), + new StructField("double", DoubleType.DOUBLE, true), + new StructField("string", StringType.STRING, true), + new StructField("binary", BinaryType.BINARY, true), + new StructField("decimal", new DecimalType(10, 2), true), + new StructField("date", DateType.DATE, true), + new StructField("timestamp", TimestampType.TIMESTAMP, true), + new StructField("struct", structSchema, true), + new StructField("array", new ArrayType(StringType.STRING, true), true), + new StructField( + "map", new MapType(StringType.STRING, IntegerType.INTEGER, true), true))); + + Map nullValues = new LinkedHashMap<>(); + for (StructField field : schema.fields()) { + nullValues.put(field.getName(), null); + } + + FakeRow originalRow = new FakeRow(schema, nullValues); + SerializableRow serializableRow = new SerializableRow(originalRow); + + verifyRowContents(serializableRow, schema, nullValues); + + // Serialize and Deserialize + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(serializableRow); + } + + byte[] bytes = baos.toByteArray(); + SerializableRow deserializedRow; + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + deserializedRow = (SerializableRow) ois.readObject(); + } + + verifyRowContents(deserializedRow, schema, nullValues); + Assert.assertEquals(serializableRow, deserializedRow); + } + + private void verifyRowContents(Row row, StructType schema, Map expectedValues) { + Assert.assertEquals(schema.toString(), row.getSchema().toString()); + int i = 0; + for (StructField field : schema.fields()) { + Object expected = expectedValues.get(field.getName()); + Assert.assertEquals(expected == null, row.isNullAt(i)); + if (expected != null) { + DataType type = field.getDataType(); + if (type instanceof BooleanType) { + Assert.assertEquals(expected, row.getBoolean(i)); + } else if (type instanceof ByteType) { + Assert.assertEquals(expected, row.getByte(i)); + } else if (type instanceof ShortType) { + Assert.assertEquals(expected, row.getShort(i)); + } else if (type instanceof IntegerType) { + Assert.assertEquals(expected, row.getInt(i)); + } else if (type instanceof LongType) { + Assert.assertEquals(expected, row.getLong(i)); + } else if (type instanceof FloatType) { + Assert.assertEquals(expected, row.getFloat(i)); + } else if (type instanceof DoubleType) { + Assert.assertEquals(expected, row.getDouble(i)); + } else if (type instanceof StringType) { + Assert.assertEquals(expected, row.getString(i)); + } else if (type instanceof BinaryType) { + Assert.assertArrayEquals((byte[]) expected, row.getBinary(i)); + } else if (type instanceof DecimalType) { + Assert.assertEquals(expected, row.getDecimal(i)); + } else if (type instanceof DateType) { + Assert.assertEquals(expected, row.getInt(i)); + } else if (type instanceof TimestampType) { + Assert.assertEquals(expected, row.getLong(i)); + } else if (type instanceof StructType) { + Row actualStruct = row.getStruct(i); + Assert.assertNotNull(actualStruct); + Row expectedStruct = (Row) expected; + Assert.assertEquals( + expectedStruct.getSchema().toString(), actualStruct.getSchema().toString()); + for (int j = 0; j < expectedStruct.getSchema().fields().size(); j++) { + Assert.assertEquals(expectedStruct.isNullAt(j), actualStruct.isNullAt(j)); + if (!expectedStruct.isNullAt(j)) { + Assert.assertEquals(expectedStruct.getInt(j), actualStruct.getInt(j)); + } + } + } else if (type instanceof ArrayType) { + ArrayValue actualArray = row.getArray(i); + Assert.assertNotNull(actualArray); + List expectedList = (List) expected; + Assert.assertEquals(expectedList.size(), actualArray.getSize()); + ColumnVector vector = actualArray.getElements(); + for (int j = 0; j < expectedList.size(); j++) { + Assert.assertEquals(expectedList.get(j) == null, vector.isNullAt(j)); + if (expectedList.get(j) != null) { + Assert.assertEquals(expectedList.get(j), vector.getString(j)); + } + } + } else if (type instanceof MapType) { + MapValue actualMap = row.getMap(i); + Assert.assertNotNull(actualMap); + Map expectedMap = (Map) expected; + Assert.assertEquals(expectedMap.size(), actualMap.getSize()); + ColumnVector keys = actualMap.getKeys(); + ColumnVector valuesVector = actualMap.getValues(); + int j = 0; + for (Map.Entry entry : expectedMap.entrySet()) { + Assert.assertEquals(entry.getKey(), keys.getString(j)); + Assert.assertEquals(entry.getValue() == null, valuesVector.isNullAt(j)); + if (entry.getValue() != null) { + Assert.assertEquals(entry.getValue(), valuesVector.getInt(j)); + } + j++; + } + } + } + i++; + } + } + + private static class FakeRow implements Row { + private final StructType schema; + private final Map values; + + FakeRow(StructType schema, Map values) { + this.schema = schema; + this.values = values; + } + + @Override + public StructType getSchema() { + return schema; + } + + @Override + public boolean isNullAt(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return values.get(fieldName) == null; + } + + @Override + public boolean getBoolean(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Boolean) values.get(fieldName); + } + + @Override + public byte getByte(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Byte) values.get(fieldName); + } + + @Override + public short getShort(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Short) values.get(fieldName); + } + + @Override + public int getInt(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Integer) values.get(fieldName); + } + + @Override + public long getLong(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Long) values.get(fieldName); + } + + @Override + public float getFloat(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Float) values.get(fieldName); + } + + @Override + public double getDouble(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Double) values.get(fieldName); + } + + @Override + public String getString(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (String) values.get(fieldName); + } + + @Override + public byte[] getBinary(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (byte[]) values.get(fieldName); + } + + @Override + public BigDecimal getDecimal(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (BigDecimal) values.get(fieldName); + } + + @Override + public Row getStruct(int ord) { + String fieldName = schema.fields().get(ord).getName(); + return (Row) values.get(fieldName); + } + + @Override + public ArrayValue getArray(int ord) { + String fieldName = schema.fields().get(ord).getName(); + List list = (List) values.get(fieldName); + if (list == null) { + return null; + } + DataType elementType = ((ArrayType) schema.fields().get(ord).getDataType()).getElementType(); + return new FakeArrayValue(list, elementType); + } + + @Override + public MapValue getMap(int ord) { + String fieldName = schema.fields().get(ord).getName(); + Map map = (Map) values.get(fieldName); + if (map == null) { + return null; + } + MapType mapType = (MapType) schema.fields().get(ord).getDataType(); + return new FakeMapValue(map, mapType.getKeyType(), mapType.getValueType()); + } + } + + private static class FakeArrayValue implements ArrayValue { + private final List list; + private final DataType elementType; + + FakeArrayValue(List list, DataType elementType) { + this.list = list; + this.elementType = elementType; + } + + @Override + public int getSize() { + return list.size(); + } + + @Override + public ColumnVector getElements() { + return new FakeColumnVector(elementType, list); + } + } + + private static class FakeMapValue implements MapValue { + private final Map map; + private final DataType keyType; + private final DataType valueType; + + FakeMapValue(Map map, DataType keyType, DataType valueType) { + this.map = map; + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + public int getSize() { + return map.size(); + } + + @Override + public ColumnVector getKeys() { + return new FakeColumnVector(keyType, new ArrayList<>(map.keySet())); + } + + @Override + public ColumnVector getValues() { + return new FakeColumnVector(valueType, new ArrayList<>(map.values())); + } + } + + private static class FakeColumnVector implements ColumnVector { + private final DataType dataType; + private final List list; + + FakeColumnVector(DataType dataType, List list) { + this.dataType = dataType; + this.list = new ArrayList<>(list); + } + + @Override + public DataType getDataType() { + return dataType; + } + + @Override + public int getSize() { + return list.size(); + } + + @Override + public boolean isNullAt(int rowId) { + return list.get(rowId) == null; + } + + @Override + public boolean getBoolean(int rowId) { + return (Boolean) list.get(rowId); + } + + @Override + public byte getByte(int rowId) { + return (Byte) list.get(rowId); + } + + @Override + public short getShort(int rowId) { + return (Short) list.get(rowId); + } + + @Override + public int getInt(int rowId) { + return (Integer) list.get(rowId); + } + + @Override + public long getLong(int rowId) { + return (Long) list.get(rowId); + } + + @Override + public float getFloat(int rowId) { + return (Float) list.get(rowId); + } + + @Override + public double getDouble(int rowId) { + return (Double) list.get(rowId); + } + + @Override + public String getString(int rowId) { + return (String) list.get(rowId); + } + + @Override + public byte[] getBinary(int rowId) { + return (byte[]) list.get(rowId); + } + + @Override + public BigDecimal getDecimal(int rowId) { + return (BigDecimal) list.get(rowId); + } + + @Override + public void close() {} + } +} diff --git a/sdks/java/io/expansion-service/build.gradle b/sdks/java/io/expansion-service/build.gradle index a6a99c52d712..f7b241a75944 100644 --- a/sdks/java/io/expansion-service/build.gradle +++ b/sdks/java/io/expansion-service/build.gradle @@ -25,8 +25,8 @@ applyJavaNature( exportJavadoc: false, validateShadowJar: false, shadowClosure: {}, - // iceberg requires Java11+ - requireJavaVersion: JavaVersion.VERSION_11 + // iceberg requires Java11+ and delta lake requires Java17+ + requireJavaVersion: JavaVersion.VERSION_17 ) // We don't want to use the latest version for the entire beam sdk since beam Java users can override it themselves. @@ -35,8 +35,6 @@ applyJavaNature( configurations.runtimeClasspath { // Pin kafka-clients version due to <3.4.0 missing auth callback classes. resolutionStrategy.force 'org.apache.kafka:kafka-clients:3.9.0' - // iceberg needs avro:1.12.0 - resolutionStrategy.force 'org.apache.avro:avro:1.12.0' // force parquet-avro:1.15.2 to fix CVE-2025-46762 resolutionStrategy.force 'org.apache.parquet:parquet-avro:1.15.2' @@ -59,6 +57,14 @@ configurations.runtimeClasspath { // Pin nimbus-jose-jwt to 9.37.4 to fix CVE-2025-53864 (transitive via hadoop-auth) resolutionStrategy.force 'com.nimbusds:nimbus-jose-jwt:9.37.4' + + // [iceberg] + // 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' } shadowJar { @@ -66,7 +72,6 @@ shadowJar { attributes(["Multi-Release": true]) } mergeServiceFiles() - outputs.upToDateWhen { false } } description = "Apache Beam :: SDKs :: Java :: IO :: Expansion Service" @@ -85,13 +90,13 @@ dependencies { permitUnusedDeclared project(":sdks:java:extensions:kafka-factories") runtimeOnly project(":sdks:java:io:amazon-web-services2") // FileSystem may be used by Iceberg AddFiles - if (JavaVersion.current().compareTo(JavaVersion.VERSION_11) >= 0 && project.findProperty('testJavaVersion') != '8') { - // iceberg ended support for Java 8 in 1.7.0 - runtimeOnly project(":sdks:java:io:iceberg") - runtimeOnly project(":sdks:java:io:iceberg:hive") - runtimeOnly project(path: ":sdks:java:io:iceberg:bqms", configuration: "shadow") - } + runtimeOnly project(":sdks:java:io:iceberg") + runtimeOnly project(":sdks:java:io:iceberg:hive") + runtimeOnly project(path: ":sdks:java:io:iceberg:bqms", configuration: "shadow") + runtimeOnly library.java.bigdataoss_util_hadoop + runtimeOnly project(":sdks:java:io:datadog") + runtimeOnly project(":sdks:java:io:mongodb") runtimeOnly library.java.kafka_clients runtimeOnly library.java.slf4j_jdk14 diff --git a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/AvroReadSchemaTransformFormatProviderTest.java b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/AvroReadSchemaTransformFormatProviderTest.java index 5725ceff3a12..bfba60fb0c13 100644 --- a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/AvroReadSchemaTransformFormatProviderTest.java +++ b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/AvroReadSchemaTransformFormatProviderTest.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.fileschematransform; import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.ALL_PRIMITIVE_DATA_TYPES_SCHEMA; +import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.ARRAY_PRIMITIVE_DATA_TYPES_SCHEMA; import static org.apache.beam.sdk.io.fileschematransform.FileReadSchemaTransformProvider.FILEPATTERN_ROW_FIELD_NAME; import static org.apache.beam.sdk.io.fileschematransform.FileWriteSchemaTransformFormatProviderTestData.DATA; import static org.apache.beam.sdk.io.fs.ResolveOptions.StandardResolveOptions.RESOLVE_FILE; @@ -215,6 +216,16 @@ public void testReadWithPCollectionOfFilepatterns() { readPipeline.run(); } + // TODO(AVRO-4110): remove this override when Beam upgraded Avro past 1.12.0 + @Override + public void testArrayPrimitiveDataTypes() { + Schema schema = ARRAY_PRIMITIVE_DATA_TYPES_SCHEMA; + List rows = DATA.arrayPrimitiveDataTypesRowsAvro1120; + String filePath = getFilePath(); + + runWriteAndReadTest(schema, rows, filePath, null); + } + private static class TestDynamicDestinations extends DynamicAvroDestinations { final ResourceId baseDir; diff --git a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/FileWriteSchemaTransformFormatProviderTestData.java b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/FileWriteSchemaTransformFormatProviderTestData.java index 4f70dca71e38..d92acaa0ac6c 100644 --- a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/FileWriteSchemaTransformFormatProviderTestData.java +++ b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/FileWriteSchemaTransformFormatProviderTestData.java @@ -35,6 +35,7 @@ import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.timeContainingToRowFn; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -50,6 +51,7 @@ import org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.SinglyNestedDataTypes; import org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.TimeContaining; import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.joda.time.Instant; /** Shared {@link SchemaAwareJavaBeans} data to be used across various tests. */ @@ -60,6 +62,23 @@ class FileWriteSchemaTransformFormatProviderTestData { /* Prevent instantiation outside this class. */ private FileWriteSchemaTransformFormatProviderTestData() {} + private static class ListPatcher { + private ArrayList list; + + ListPatcher(List list) { + this.list = Lists.newArrayList(list); + } + + ArrayList get() { + return list; + } + + ListPatcher patch(int index, T value) { + list.set(index, value); + return this; + } + } + final List allPrimitiveDataTypesList = Arrays.asList( allPrimitiveDataTypes(false, BigDecimal.valueOf(1L), 1.2345, 1.2345f, 1, 1L, "a"), @@ -188,11 +207,53 @@ private FileWriteSchemaTransformFormatProviderTestData() {} Collections.emptyList(), Collections.emptyList())); + // TODO(AVRO-4110): remove this workaround when Beam upgraded Avro past 1.12.0 + final List arrayPrimitiveDataTypesListAvro1120 = + new ListPatcher<>(arrayPrimitiveDataTypesList) + .patch( + 1, + arrayPrimitiveDataTypes( + Collections.emptyList(), + Collections.singletonList((double) Float.MAX_VALUE), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList())) + .patch( + 6, + arrayPrimitiveDataTypes( + Arrays.asList(false, true, false), + Arrays.asList((double) Float.MIN_VALUE, 0.0, (double) Float.MAX_VALUE), + Arrays.asList(Float.MIN_VALUE, 0.0f, Float.MAX_VALUE), + Arrays.asList(Integer.MIN_VALUE, 0, Integer.MAX_VALUE), + Arrays.asList(Long.MIN_VALUE, 0L, Long.MAX_VALUE), + Arrays.asList( + Stream.generate(() -> "🐤").limit(10).collect(Collectors.joining("")), + Stream.generate(() -> "🐥").limit(10).collect(Collectors.joining("")), + Stream.generate(() -> "🐣").limit(10).collect(Collectors.joining(""))))) + .patch( + 7, + arrayPrimitiveDataTypes( + Stream.generate(() -> true).limit(10).collect(Collectors.toList()), + Stream.generate(() -> (double) Float.MIN_VALUE) + .limit(10) + .collect(Collectors.toList()), + Stream.generate(() -> Float.MIN_VALUE).limit(10).collect(Collectors.toList()), + Stream.generate(() -> Integer.MIN_VALUE).limit(10).collect(Collectors.toList()), + Stream.generate(() -> Long.MIN_VALUE).limit(10).collect(Collectors.toList()), + Stream.generate(() -> "🐿").limit(10).collect(Collectors.toList()))) + .get(); + final List arrayPrimitiveDataTypesRows = arrayPrimitiveDataTypesList.stream() .map(arrayPrimitiveDataTypesToRowFn()::apply) .collect(Collectors.toList()); + final List arrayPrimitiveDataTypesRowsAvro1120 = + arrayPrimitiveDataTypesListAvro1120.stream() + .map(arrayPrimitiveDataTypesToRowFn()::apply) + .collect(Collectors.toList()); + final List singlyNestedDataTypesNoRepeat = allPrimitiveDataTypesList.stream() .map(SchemaAwareJavaBeans::singlyNestedDataTypes) diff --git a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/ParquetReadSchemaTransformFormatProviderTest.java b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/ParquetReadSchemaTransformFormatProviderTest.java index b1d6bba06ea9..bbc33698c41f 100644 --- a/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/ParquetReadSchemaTransformFormatProviderTest.java +++ b/sdks/java/io/file-schema-transform/src/test/java/org/apache/beam/sdk/io/fileschematransform/ParquetReadSchemaTransformFormatProviderTest.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.fileschematransform; import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.ALL_PRIMITIVE_DATA_TYPES_SCHEMA; +import static org.apache.beam.sdk.io.common.SchemaAwareJavaBeans.ARRAY_PRIMITIVE_DATA_TYPES_SCHEMA; import static org.apache.beam.sdk.io.fileschematransform.FileReadSchemaTransformProvider.FILEPATTERN_ROW_FIELD_NAME; import static org.apache.beam.sdk.io.fileschematransform.FileWriteSchemaTransformFormatProviderTestData.DATA; import static org.apache.beam.sdk.transforms.Contextful.fn; @@ -218,4 +219,14 @@ public void testReadWithPCollectionOfFilepatterns() { PAssert.that(output.get(FileReadSchemaTransformProvider.OUTPUT_TAG)).containsInAnyOrder(rows); readPipeline.run(); } + + // TODO(AVRO-4110): remove this override when Beam upgraded Avro past 1.12.0 + @Override + public void testArrayPrimitiveDataTypes() { + Schema schema = ARRAY_PRIMITIVE_DATA_TYPES_SCHEMA; + List rows = DATA.arrayPrimitiveDataTypesRowsAvro1120; + String filePath = getFilePath(); + + runWriteAndReadTest(schema, rows, filePath, null); + } } diff --git a/sdks/java/io/google-cloud-platform/expansion-service/build.gradle b/sdks/java/io/google-cloud-platform/expansion-service/build.gradle index d1f65c88bdc5..824a2659b84b 100644 --- a/sdks/java/io/google-cloud-platform/expansion-service/build.gradle +++ b/sdks/java/io/google-cloud-platform/expansion-service/build.gradle @@ -26,12 +26,6 @@ applyJavaNature( shadowClosure: {}, ) -configurations.runtimeClasspath { - // Pin avro to 1.11.4 due to https://github.com/apache/beam/issues/34968 - // cannot upgrade this to the latest version due to https://github.com/apache/beam/issues/34993 - resolutionStrategy.force 'org.apache.avro:avro:1.11.4' -} - description = "Apache Beam :: SDKs :: Java :: IO :: Google Cloud Platform :: Expansion Service" ext.summary = "Expansion service serving GCP Java IOs" diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientCache.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientCache.java new file mode 100644 index 000000000000..b172a4ac3046 --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientCache.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.gcp.bigquery; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.RemovalNotification; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Encapsulates the cache of {@link AppendClientInfo} objects and the synchronization protocol + * required to use them safely. The Guava cache object is thread-safe. However our protocol requires + * that client pin the StreamAppendClient after looking up the cache, and we must ensure that the + * cache is not accessed in between the lookup and the pin (any access of the cache could trigger + * element expiration). + */ +class AppendClientCache { + private static final Logger LOG = LoggerFactory.getLogger(AppendClientCache.class); + private final ExecutorService closeWriterExecutor = Executors.newCachedThreadPool(); + + private final Cache appendCache; + + @SuppressWarnings({"FutureReturnValueIgnored"}) + AppendClientCache(Duration expireAfterAccess) { + this.appendCache = + CacheBuilder.newBuilder() + .expireAfterAccess(expireAfterAccess.getMillis(), TimeUnit.MILLISECONDS) + .removalListener( + (RemovalNotification removal) -> { + LOG.info("Expiring append client for {}", removal.getKey()); + final @Nullable AppendClientInfo appendClientInfo = removal.getValue(); + if (appendClientInfo != null) { + // Remove the pin owned by the cache itself. Since the client has not been + // marked as closed, we + // can call unpin in this thread without worrying about blocking the thread. + appendClientInfo.unpinAppendClient(null); + // Close the client in another thread to avoid blocking the main thread. + closeWriterExecutor.submit(appendClientInfo::close); + } + }) + .build(); + } + + // The cache itself always own one pin on the object. This Callable is always used to ensure that + // the cache + // adds a pin before loading a value. + private static Callable wrapWithPin(Callable loader) { + return () -> { + AppendClientInfo client = loader.call(); + client.pinAppendClient(); + return client; + }; + } + + /** + * Atomically get an append client from the cache and add a pin. This pin is owned by the client, + * which has the responsibility of removing it. If the client is not in the cache, loader will be + * used to load the client; in this case an additional pin will be added owned by the cache, + * removed when the item is evicted. + */ + public AppendClientInfo getAndPin(KeyT key, Callable loader) throws Exception { + synchronized (this) { + AppendClientInfo info = appendCache.get(key, wrapWithPin(loader)); + info.pinAppendClient(); + return info; + } + } + + /** "Refresh" an object by invalidating the old cache entry. */ + public AppendClientInfo refreshObjectAndPin(KeyT key, Callable loader) + throws Exception { + synchronized (this) { + appendCache.invalidate(key); + return getAndPin(key, loader); + } + } + + public void invalidate(KeyT key, AppendClientInfo expectedClient) { + // The default stream is cached across multiple different DoFns. If they all try + // and + // invalidate, then we can get races between threads invalidating and recreating + // streams. For this reason, + // we check to see that the cache still contains the object we created before + // invalidating (in case another + // thread has already invalidated and recreated the stream). + synchronized (this) { + AppendClientInfo cachedAppendClient = appendCache.getIfPresent(key); + if (cachedAppendClient != null + && System.identityHashCode(cachedAppendClient) + == System.identityHashCode(expectedClient)) { + appendCache.invalidate(key); + } + } + } + + public void invalidate(KeyT key) { + synchronized (this) { + appendCache.invalidate(key); + } + } + + public void tickle(KeyT key) { + synchronized (this) { + appendCache.getIfPresent(key); + } + } + + public void clear() { + synchronized (this) { + appendCache.invalidateAll(); + } + } + + public void unpinAsync(AppendClientInfo appendClientInfo) { + appendClientInfo.unpinAppendClient(closeWriterExecutor); + } +} diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientInfo.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientInfo.java index e9adc8097604..55c6007e1986 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientInfo.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/AppendClientInfo.java @@ -27,13 +27,17 @@ import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.google.protobuf.Message; -import java.util.function.Consumer; +import java.util.concurrent.ExecutorService; import java.util.function.Predicate; import java.util.function.Supplier; import javax.annotation.Nullable; +import org.apache.beam.sdk.function.ThrowingConsumer; +import org.apache.beam.sdk.function.ThrowingRunnable; import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Metrics; import org.apache.beam.sdk.util.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Container class used by {@link StorageApiWritesShardedRecords} and {@link @@ -42,6 +46,8 @@ */ @AutoValue abstract class AppendClientInfo { + private static final Logger LOG = LoggerFactory.getLogger(AppendClientInfo.class); + private final Counter activeStreamAppendClients = Metrics.counter(AppendClientInfo.class, "activeStreamAppendClients"); @@ -49,7 +55,7 @@ abstract class AppendClientInfo { abstract TableSchema getTableSchema(); - abstract Consumer getCloseAppendClient(); + abstract ThrowingConsumer getCloseAppendClient(); abstract com.google.api.services.bigquery.model.TableSchema getJsonTableSchema(); @@ -65,7 +71,8 @@ abstract static class Builder { abstract Builder setTableSchema(TableSchema value); - abstract Builder setCloseAppendClient(Consumer value); + abstract Builder setCloseAppendClient( + ThrowingConsumer value); abstract Builder setJsonTableSchema(com.google.api.services.bigquery.model.TableSchema value); @@ -83,7 +90,7 @@ abstract static class Builder { static AppendClientInfo of( TableSchema tableSchema, DescriptorProtos.DescriptorProto descriptor, - Consumer closeAppendClient) + ThrowingConsumer closeAppendClient) throws Exception { return new AutoValue_AppendClientInfo.Builder() .setTableSchema(tableSchema) @@ -97,7 +104,7 @@ static AppendClientInfo of( static AppendClientInfo of( TableSchema tableSchema, - Consumer closeAppendClient, + ThrowingConsumer closeAppendClient, boolean includeCdcColumns) throws Exception { return of( @@ -134,7 +141,12 @@ public AppendClientInfo withAppendClient( public void close() { BigQueryServices.StreamAppendClient client = getStreamAppendClient(); if (client != null) { - getCloseAppendClient().accept(client); + try { + getCloseAppendClient().accept(client); + } catch (Exception e) { + // We ignore errors when closing clients. + LOG.warn("Caught exception whilw trying to close append client. Ignoring", e); + } activeStreamAppendClients.dec(); } } @@ -199,4 +211,32 @@ public TableRow toTableRow(ByteString protoBytes, Predicate includeField throw new RuntimeException(e); } } + + public void pinAppendClient() { + BigQueryServices.StreamAppendClient client = + Preconditions.checkStateNotNull(getStreamAppendClient()); + client.pin(); + } + + public void unpinAppendClient(@Nullable ExecutorService executor) { + BigQueryServices.StreamAppendClient client = + Preconditions.checkStateNotNull(getStreamAppendClient()); + if (executor != null) { + runAsyncIgnoreFailure(executor, client::unpin); + } else { + client.unpin(); + } + } + + @SuppressWarnings({"FutureReturnValueIgnored"}) + private static void runAsyncIgnoreFailure(ExecutorService executor, ThrowingRunnable task) { + executor.submit( + () -> { + try { + task.run(); + } catch (Throwable e) { + LOG.info("Exception happened while executing async task. Ignoring: ", e); + } + }); + } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java index e5dd2b540019..dd59939726bf 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOTranslation.java @@ -73,7 +73,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@SuppressWarnings({"rawtypes", "nullness"}) +@SuppressWarnings({"rawtypes", "nullness", "AutoValueSubclassLeaked"}) public class BigQueryIOTranslation { private static final Logger LOG = LoggerFactory.getLogger(BigQueryIOTranslation.class); @@ -217,7 +217,7 @@ public TypedRead fromConfigRow(Row configRow, PipelineOptions options) { (updateCompatibilityBeamVersion != null) ? updateCompatibilityBeamVersion : "2.53.0"; try { - BigQueryIO.TypedRead.Builder builder = BigQueryIO.TypedRead.builder(); + BigQueryIO.TypedRead.Builder builder = new AutoValue_BigQueryIO_TypedRead.Builder<>(); String jsonTableRef = configRow.getString("json_table_ref"); if (jsonTableRef != null) { @@ -378,9 +378,7 @@ public static class ReadRegistrar implements TransformPayloadTranslatorRegistrar public Map, ? extends TransformPayloadTranslator> getTransformPayloadTranslators() { return ImmutableMap., TransformPayloadTranslator>builder() - .put( - BigQueryIO.read(BigQueryIO.TableRowParser.INSTANCE).getClass(), - new BigQueryIOReadTranslator()) + .put(AutoValue_BigQueryIO_TypedRead.class, new BigQueryIOReadTranslator()) .build(); } } @@ -617,7 +615,7 @@ public Write fromConfigRow(Row configRow, PipelineOptions options) { (updateCompatibilityBeamVersion != null) ? updateCompatibilityBeamVersion : "2.53.0"; try { - BigQueryIO.Write.Builder builder = BigQueryIO.Write.builder(); + BigQueryIO.Write.Builder builder = new AutoValue_BigQueryIO_Write.Builder<>(); String jsonTableRef = configRow.getString("json_table_ref"); if (jsonTableRef != null) { @@ -930,7 +928,7 @@ public static class WriteRegistrar implements TransformPayloadTranslatorRegistra public Map, ? extends TransformPayloadTranslator> getTransformPayloadTranslators() { return ImmutableMap., TransformPayloadTranslator>builder() - .put(BigQueryIO.write().getClass(), new BigQueryIOWriteTranslator()) + .put(AutoValue_BigQueryIO_Write.class, new BigQueryIOWriteTranslator()) .build(); } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java index 2f16a64b0d76..face2ef5841a 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java @@ -153,6 +153,13 @@ public interface BigQueryOptions void setStorageWriteMaxInflightBytes(Long value); + @Description( + "Maximum time in seconds a Storage Write API append request is allowed to wait in the " + + "request callback queue before timing out. Overrides Storage Write API default (5 min)") + Integer getStorageWriteApiMaxRequestCallbackWaitTimeSec(); + + void setStorageWriteApiMaxRequestCallbackWaitTimeSec(Integer value); + @Description( "Enables multiplexing mode, where multiple tables can share the same connection. Only available when writing with STORAGE_API_AT_LEAST_ONCE" + " mode. This is recommended if your write operation is creating 20+ connections. When using multiplexing, consider tuning " diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServices.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServices.java index 78e714a7ccd6..66458a8339f9 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServices.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServices.java @@ -270,7 +270,7 @@ default long getInflightWaitSeconds() { /** * Unpin this object. If the object has been closed, this will release any underlying resources. */ - void unpin() throws Exception; + void unpin(); } /** diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java index 2a9cc7649c21..14765a65ff0b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java @@ -1496,6 +1496,7 @@ public static class WriteStreamServiceImpl implements WriteStreamService { private final BigQueryWriteClient newWriteClient; private final long storageWriteMaxInflightRequests; private final long storageWriteMaxInflightBytes; + private final @Nullable Integer storageWriteApiMaxRequestCallbackWaitTimeSec; private final BigQueryIOMetadata bqIOMetadata; private final PipelineOptions options; @@ -1506,6 +1507,8 @@ public static class WriteStreamServiceImpl implements WriteStreamService { this.options = options; this.storageWriteMaxInflightRequests = bqOptions.getStorageWriteMaxInflightRequests(); this.storageWriteMaxInflightBytes = bqOptions.getStorageWriteMaxInflightBytes(); + this.storageWriteApiMaxRequestCallbackWaitTimeSec = + bqOptions.getStorageWriteApiMaxRequestCallbackWaitTimeSec(); this.bqIOMetadata = BigQueryIOMetadata.create(); } @@ -1514,6 +1517,8 @@ public WriteStreamServiceImpl(BigQueryOptions bqOptions) { this.options = bqOptions; this.storageWriteMaxInflightRequests = bqOptions.getStorageWriteMaxInflightRequests(); this.storageWriteMaxInflightBytes = bqOptions.getStorageWriteMaxInflightBytes(); + this.storageWriteApiMaxRequestCallbackWaitTimeSec = + bqOptions.getStorageWriteApiMaxRequestCallbackWaitTimeSec(); this.bqIOMetadata = BigQueryIOMetadata.create(); } @@ -1578,6 +1583,11 @@ public StreamAppendClient getStreamAppendClient( options.as(BigQueryOptions.class).getMaxConnectionPoolConnections()) .build()); + if (storageWriteApiMaxRequestCallbackWaitTimeSec != null) { + StreamWriter.setMaxRequestCallbackWaitTime( + java.time.Duration.ofSeconds(storageWriteApiMaxRequestCallbackWaitTimeSec)); + } + StreamWriter streamWriter = StreamWriter.newBuilder(streamName, newWriteClient) .setExecutorProvider( @@ -1617,7 +1627,7 @@ public void pin() { } @Override - public void unpin() throws Exception { + public void unpin() { boolean closeWriter; synchronized (this) { Preconditions.checkState(pins > 0, "Tried to unpin when pins==0"); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java index e0713311e2cd..02ef9e9c06a3 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiConvertMessages.java @@ -147,109 +147,117 @@ public PCollectionTuple expand(PCollection> input) { .get(patchTableSchemaTag) .setCoder(KvCoder.of(destinationCoder, ProtoCoder.of(TableSchema.class))); result.get(elementsWaitingForSchemaTag).setCoder(KvCoder.of(destinationCoder, elementCoder)); + if (!hasSchemaUpdateOptions) { + // Don't expand the update graph if it's not needed. + return result; + } else { + final int numShards = + input + .getPipeline() + .getOptions() + .as(BigQueryOptions.class) + .getSchemaUpgradeBufferingShards(); - final int numShards = - input - .getPipeline() - .getOptions() - .as(BigQueryOptions.class) - .getSchemaUpgradeBufferingShards(); + // Throttle the stream to the patch-table function so that only a single update per table per + // two seconds gets processed (to match quotas). The combiner merges incremental schemas, so + // we + // won't miss any updates. + PCollection, ElementT>> tablesPatched = + result + .get(patchTableSchemaTag) + .apply( + "rewindow", + Window.>configure() + .triggering( + Repeatedly.forever( + AfterProcessingTime.pastFirstElementInPane() + .plusDelayOf(Duration.standardSeconds(2)))) + .discardingFiredPanes()) + .apply("merge schemas", Combine.fewKeys(new MergeSchemaCombineFn())) + .setCoder(KvCoder.of(destinationCoder, ProtoCoder.of(TableSchema.class))) + .apply( + "Patch table schema", + ParDo.of( + new PatchTableSchemaDoFn<>(operationName, bqServices, dynamicDestinations))) + .setCoder(KvCoder.of(destinationCoder, NullableCoder.of(elementCoder))) + // We need to make sure that all shards of the buffering transform are notified. + .apply( + "fanout to all shards", + FlatMapElements.via( + new SimpleFunction< + KV, + Iterable, ElementT>>>() { + @Override + public Iterable, ElementT>> apply( + KV elem) { + return IntStream.range(0, numShards) + .mapToObj( + i -> + KV.of( + StorageApiConvertMessages.AssignShardFn.getShardedKey( + elem.getKey(), i, numShards), + elem.getValue())) + .collect(Collectors.toList()); + } + })) + .setCoder( + KvCoder.of(ShardedKey.Coder.of(destinationCoder), NullableCoder.of(elementCoder))) + .apply( + Window., ElementT>>configure() + .triggering(DefaultTrigger.of())); - // Throttle the stream to the patch-table function so that only a single update per table per - // two seconds gets processed (to match quotas). The combiner merges incremental schemas, so we - // won't miss any updates. - PCollection, ElementT>> tablesPatched = - result - .get(patchTableSchemaTag) - .apply( - "rewindow", - Window.>configure() - .triggering( - Repeatedly.forever( - AfterProcessingTime.pastFirstElementInPane() - .plusDelayOf(Duration.standardSeconds(2)))) - .discardingFiredPanes()) - .apply("merge schemas", Combine.fewKeys(new MergeSchemaCombineFn())) - .setCoder(KvCoder.of(destinationCoder, ProtoCoder.of(TableSchema.class))) - .apply( - "Patch table schema", - ParDo.of( - new PatchTableSchemaDoFn<>(operationName, bqServices, dynamicDestinations))) - .setCoder(KvCoder.of(destinationCoder, NullableCoder.of(elementCoder))) - // We need to make sure that all shards of the buffering transform are notified. - .apply( - "fanout to all shards", - FlatMapElements.via( - new SimpleFunction< - KV, - Iterable, ElementT>>>() { - @Override - public Iterable, ElementT>> apply( - KV elem) { - return IntStream.range(0, numShards) - .mapToObj( - i -> - KV.of( - StorageApiConvertMessages.AssignShardFn.getShardedKey( - elem.getKey(), i, numShards), - elem.getValue())) - .collect(Collectors.toList()); - } - })) - .setCoder( - KvCoder.of(ShardedKey.Coder.of(destinationCoder), NullableCoder.of(elementCoder))) - .apply( - Window., ElementT>>configure() - .triggering(DefaultTrigger.of())); + // Any elements that are waiting for a schema update are sent to this stateful DoFn to be + // buffered. + // Note: we currently do not provide the DynamicDestinations object access to the side input + // in + // this path. + // This is because side inputs are not currently available from timer callbacks. Since side + // inputs are generally + // used for getSchema and in this case we read the schema from the table, this is unlikely to + // be + // a problem. + PCollection, ElementT>> shardedWaitingElements = + result + .get(elementsWaitingForSchemaTag) + // TODO: Consider using GroupIntoBatchs.withShardingKey to get auto sharding here + // instead of fixed sharding. + .apply("assignShard", ParDo.of(new AssignShardFn<>(numShards))) + .setCoder( + KvCoder.of( + ShardedKey.Coder.of(destinationCoder), NullableCoder.of(elementCoder))); - // Any elements that are waiting for a schema update are sent to this stateful DoFn to be - // buffered. - // Note: we currently do not provide the DynamicDestinations object access to the side input in - // this path. - // This is because side inputs are not currently available from timer callbacks. Since side - // inputs are generally - // used for getSchema and in this case we read the schema from the table, this is unlikely to be - // a problem. - PCollection, ElementT>> shardedWaitingElements = - result - .get(elementsWaitingForSchemaTag) - // TODO: Consider using GroupIntoBatchs.withShardingKey to get auto sharding here - // instead of fixed sharding. - .apply("assignShard", ParDo.of(new AssignShardFn<>(numShards))) - .setCoder( - KvCoder.of(ShardedKey.Coder.of(destinationCoder), NullableCoder.of(elementCoder))); + PCollectionList, ElementT>> waitingElementsList = + PCollectionList.of(shardedWaitingElements).and(tablesPatched); + PCollectionTuple retryResult = + waitingElementsList + .apply("Buffered flatten", Flatten.pCollections()) + .apply( + "bufferElements", + ParDo.of(new SchemaUpdateHoldingFn<>(elementCoder, convertMessagesDoFn)) + .withOutputTags( + successfulWritesTag, + TupleTagList.of(ImmutableList.of(failedWritesTag, BAD_RECORD_TAG)))); + retryResult.get(successfulWritesTag).setCoder(successCoder); + retryResult.get(failedWritesTag).setCoder(errorCoder); + retryResult.get(BAD_RECORD_TAG).setCoder(BadRecord.getCoder(input.getPipeline())); - PCollectionList, ElementT>> waitingElementsList = - PCollectionList.of(shardedWaitingElements).and(tablesPatched); - PCollectionTuple retryResult = - waitingElementsList - .apply("Buffered flatten", Flatten.pCollections()) - .apply( - "bufferElements", - ParDo.of(new SchemaUpdateHoldingFn<>(elementCoder, convertMessagesDoFn)) - .withOutputTags( - successfulWritesTag, - TupleTagList.of(ImmutableList.of(failedWritesTag, BAD_RECORD_TAG)))); - retryResult.get(successfulWritesTag).setCoder(successCoder); - retryResult.get(failedWritesTag).setCoder(errorCoder); - retryResult.get(BAD_RECORD_TAG).setCoder(BadRecord.getCoder(input.getPipeline())); - - // Flatten successes and failures from both the regular transform and the retry transform. - PCollection> allSuccesses = - PCollectionList.of(result.get(successfulWritesTag)) - .and(retryResult.get(successfulWritesTag)) - .apply("flattenSuccesses", Flatten.pCollections()); - PCollection allFailures = - PCollectionList.of(result.get(failedWritesTag)) - .and(retryResult.get(failedWritesTag)) - .apply("flattenFailures", Flatten.pCollections()); - PCollection allBadRecords = - PCollectionList.of(result.get(BAD_RECORD_TAG)) - .and(retryResult.get(BAD_RECORD_TAG)) - .apply("flattenBadRecords", Flatten.pCollections()); - return PCollectionTuple.of(successfulWritesTag, allSuccesses) - .and(failedWritesTag, allFailures) - .and(BAD_RECORD_TAG, allBadRecords); + // Flatten successes and failures from both the regular transform and the retry transform. + PCollection> allSuccesses = + PCollectionList.of(result.get(successfulWritesTag)) + .and(retryResult.get(successfulWritesTag)) + .apply("flattenSuccesses", Flatten.pCollections()); + PCollection allFailures = + PCollectionList.of(result.get(failedWritesTag)) + .and(retryResult.get(failedWritesTag)) + .apply("flattenFailures", Flatten.pCollections()); + PCollection allBadRecords = + PCollectionList.of(result.get(BAD_RECORD_TAG)) + .and(retryResult.get(BAD_RECORD_TAG)) + .apply("flattenBadRecords", Flatten.pCollections()); + return PCollectionTuple.of(successfulWritesTag, allSuccesses) + .and(failedWritesTag, allFailures) + .and(BAD_RECORD_TAG, allBadRecords); + } } static class AssignShardFn extends DoFn, KV, V>> { diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java index 600f49b0be3e..2dfc8b2f1c00 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java @@ -47,9 +47,6 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; @@ -89,9 +86,6 @@ import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicates; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.RemovalNotification; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; @@ -122,56 +116,17 @@ public class StorageApiWriteUnshardedRecords private final Coder successfulRowsCoder; private final boolean autoUpdateSchema; private final boolean ignoreUnknownValues; - private static final ExecutorService closeWriterExecutor = Executors.newCachedThreadPool(); private final BigQueryIO.Write.CreateDisposition createDisposition; private final @Nullable String kmsKey; private final boolean usesCdc; private final AppendRowsRequest.MissingValueInterpretation defaultMissingValueInterpretation; private final @Nullable Map bigLakeConfiguration; - /** - * The Guava cache object is thread-safe. However our protocol requires that client pin the - * StreamAppendClient after looking up the cache, and we must ensure that the cache is not - * accessed in between the lookup and the pin (any access of the cache could trigger element - * expiration). Therefore most used of APPEND_CLIENTS should synchronize. - */ - private static final Cache APPEND_CLIENTS = - CacheBuilder.newBuilder() - .expireAfterAccess(15, TimeUnit.MINUTES) - .removalListener( - (RemovalNotification removal) -> { - LOG.info("Expiring append client for {}", removal.getKey()); - final @Nullable AppendClientInfo appendClientInfo = removal.getValue(); - if (appendClientInfo != null) { - appendClientInfo.close(); - } - }) - .build(); + private static final AppendClientCache APPEND_CLIENTS = + new AppendClientCache<>(Duration.standardMinutes(15)); static void clearCache() { - APPEND_CLIENTS.invalidateAll(); - } - - // Run a closure asynchronously, ignoring failures. - private interface ThrowingRunnable { - void run() throws Exception; - } - - private static void runAsyncIgnoreFailure(ExecutorService executor, ThrowingRunnable task) { - executor.submit( - () -> { - try { - task.run(); - } catch (Exception e) { - String msg = - e.toString() - + "\n" - + Arrays.stream(e.getStackTrace()) - .map(StackTraceElement::toString) - .collect(Collectors.joining("\n")); - System.err.println("Exception happened while executing async task. Ignoring: " + msg); - } - }); + APPEND_CLIENTS.clear(); } public StorageApiWriteUnshardedRecords( @@ -362,18 +317,9 @@ public TableDestination getTableDestination() { void teardown() { maybeTickleCache(); - if (appendClientInfo != null) { - StreamAppendClient client = appendClientInfo.getStreamAppendClient(); - if (client != null) { - runAsyncIgnoreFailure(closeWriterExecutor, client::unpin); - } - // if this is a PENDING stream, we won't be using it again after cleaning up this - // destination state, so clear it from the cache - if (!useDefaultStream) { - APPEND_CLIENTS.invalidate(streamName); - } - appendClientInfo = null; - } + // if this is a PENDING stream, we won't be using it again after cleaning up this + // destination state, so clear it from the cache + invalidateAppendClient(!useDefaultStream); } String getDefaultStreamName() { @@ -419,18 +365,7 @@ AppendClientInfo generateClient(@Nullable TableSchema updatedSchema) throws Exce AppendClientInfo.of( schemaAndDescriptor.tableSchema, schemaAndDescriptor.descriptor, - // Make sure that the client is always closed in a different thread to avoid - // blocking. - client -> - runAsyncIgnoreFailure( - closeWriterExecutor, - () -> { - synchronized (APPEND_CLIENTS) { - // Remove the pin owned by the cache. - client.unpin(); - client.close(); - } - }))); + AutoCloseable::close)); CreateTableHelpers.createTableWrapper( () -> { @@ -446,9 +381,6 @@ AppendClientInfo generateClient(@Nullable TableSchema updatedSchema) throws Exce return null; }, tryCreateTable); - - // This pin is "owned" by the cache. - Preconditions.checkStateNotNull(appendClientInfo.get().getStreamAppendClient()).pin(); return appendClientInfo.get(); } @@ -515,23 +447,14 @@ AppendClientInfo getAppendClientInfo( try { if (this.appendClientInfo == null) { getOrCreateStreamName(); - final AppendClientInfo newAppendClientInfo; - synchronized (APPEND_CLIENTS) { - if (lookupCache) { - newAppendClientInfo = - APPEND_CLIENTS.get( + this.appendClientInfo = + lookupCache + ? APPEND_CLIENTS.getAndPin( + getStreamAppendClientCacheEntryKey(), () -> generateClient(updatedSchema)) + : APPEND_CLIENTS.refreshObjectAndPin( getStreamAppendClientCacheEntryKey(), () -> generateClient(updatedSchema)); - } else { - newAppendClientInfo = generateClient(updatedSchema); - // override the clients in the cache. - APPEND_CLIENTS.put(getStreamAppendClientCacheEntryKey(), newAppendClientInfo); - } - // This pin is "owned" by the current DoFn. - Preconditions.checkStateNotNull(newAppendClientInfo.getStreamAppendClient()).pin(); - } - nextCacheTickle = Instant.now().plus(java.time.Duration.ofMinutes(1)); - this.appendClientInfo = newAppendClientInfo; } + nextCacheTickle = Instant.now().plus(java.time.Duration.ofMinutes(1)); return Preconditions.checkStateNotNull(appendClientInfo); } catch (Exception e) { throw new RuntimeException(e); @@ -540,37 +463,24 @@ AppendClientInfo getAppendClientInfo( void maybeTickleCache() { if (appendClientInfo != null && Instant.now().isAfter(nextCacheTickle)) { - synchronized (APPEND_CLIENTS) { - APPEND_CLIENTS.getIfPresent(getStreamAppendClientCacheEntryKey()); - } + APPEND_CLIENTS.tickle(getStreamAppendClientCacheEntryKey()); nextCacheTickle = Instant.now().plus(java.time.Duration.ofMinutes(1)); } } - void invalidateWriteStream() { - if (appendClientInfo != null) { - synchronized (APPEND_CLIENTS) { - // Unpin in a different thread, as it may execute a blocking close. - StreamAppendClient client = appendClientInfo.getStreamAppendClient(); - if (client != null) { - runAsyncIgnoreFailure(closeWriterExecutor, client::unpin); - } - // The default stream is cached across multiple different DoFns. If they all try and - // invalidate, then we can get races between threads invalidating and recreating - // streams. For this reason, - // we check to see that the cache still contains the object we created before - // invalidating (in case another - // thread has already invalidated and recreated the stream). - String cacheEntryKey = getStreamAppendClientCacheEntryKey(); - @Nullable - AppendClientInfo cachedAppendClient = APPEND_CLIENTS.getIfPresent(cacheEntryKey); - if (cachedAppendClient != null - && System.identityHashCode(cachedAppendClient) - == System.identityHashCode(appendClientInfo)) { - APPEND_CLIENTS.invalidate(cacheEntryKey); - } + void invalidateAppendClient(boolean invalidateCache) { + if (this.appendClientInfo != null) { + // Unpin in a different thread, as it may execute a blocking close. + StreamAppendClient client = appendClientInfo.getStreamAppendClient(); + if (client != null) { + APPEND_CLIENTS.unpinAsync(Preconditions.checkStateNotNull(this.appendClientInfo)); + } + if (invalidateCache) { + APPEND_CLIENTS.invalidate( + getStreamAppendClientCacheEntryKey(), + Preconditions.checkStateNotNull(this.appendClientInfo)); } - appendClientInfo = null; + this.appendClientInfo = null; } } @@ -677,7 +587,7 @@ long flush( LOG.info("Schema out of date: refreshing table schema for {}.", tableUrn); // Refresh our view of the schema and try again.. this.messageConverter.updateSchemaFromTable(); - invalidateWriteStream(); + invalidateAppendClient(true); this.appendClientInfo = Preconditions.checkStateNotNull( getAppendClientInfo( @@ -874,7 +784,7 @@ long flush( if (!quotaError) { // This forces us to close and reopen all gRPC connections to Storage API on error, // which empirically fixes random stuckness issues. - invalidateWriteStream(); + invalidateAppendClient(true); allowedRetry = 5; } else { allowedRetry = 35; @@ -1031,7 +941,11 @@ void postFlush() { TableSchemaUpdateUtils.getUpdatedSchema( this.messageConverter.getTableSchema(), updatedTableSchemaReturned); if (updatedTableSchema.isPresent()) { - invalidateWriteStream(); + invalidateAppendClient(false); + // TODO: This overwrites whatever is in the cache which can cause races between + // threads. + // A better approach would be to check the cache, and keep whichever schema is + // "larger". appendClientInfo = Preconditions.checkStateNotNull( getAppendClientInfo(false, updatedTableSchema.get())); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java index 5771ea5074b8..b644d7aa752c 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java @@ -37,23 +37,17 @@ import io.grpc.Status.Code; import java.io.IOException; import java.time.Instant; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.stream.Collectors; import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.StringUtf8Coder; @@ -104,9 +98,6 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicates; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.RemovalNotification; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; @@ -149,44 +140,12 @@ public class StorageApiWritesShardedRecords succussfulRowsCoder; private final TupleTag> flushTag = new TupleTag<>("flushTag"); - private static final ExecutorService closeWriterExecutor = Executors.newCachedThreadPool(); - - private static final Cache>, AppendClientInfo> APPEND_CLIENTS = - CacheBuilder.newBuilder() - .expireAfterAccess(5, TimeUnit.MINUTES) - .removalListener( - (RemovalNotification>, AppendClientInfo> removal) -> { - final @Nullable AppendClientInfo appendClientInfo = removal.getValue(); - if (appendClientInfo != null) { - appendClientInfo.close(); - } - }) - .build(); - static void clearCache() { - APPEND_CLIENTS.invalidateAll(); - } + private static final AppendClientCache>> APPEND_CLIENTS = + new AppendClientCache<>(Duration.standardMinutes(5)); - // Run a closure asynchronously, ignoring failures. - private interface ThrowingRunnable { - void run() throws Exception; - } - - private static void runAsyncIgnoreFailure(ExecutorService executor, ThrowingRunnable task) { - executor.submit( - () -> { - try { - task.run(); - } catch (Exception e) { - String msg = - e.toString() - + "\n" - + Arrays.stream(e.getStackTrace()) - .map(StackTraceElement::toString) - .collect(Collectors.joining("\n")); - System.err.println("Exception happened while executing async task. Ignoring: " + msg); - } - }); + static void clearCache() { + APPEND_CLIENTS.clear(); } public StorageApiWritesShardedRecords( @@ -282,7 +241,6 @@ static class AppendRowsContext extends RetryManager.Operation.Context { final ShardedKey key; String streamName = ""; - @Nullable StreamAppendClient client = null; long offset = -1; long numRows = 0; long tryIteration = 0; @@ -487,6 +445,46 @@ public void onTeardown() { } } + // Holder for an AppendClientHolder. Maintains a pin on the client as long as it's active. + private class AppendClientHolder implements AutoCloseable { + private final KV> key; + private final Callable appendClientInfoCallable; + + private AppendClientInfo appendClientInfo; + private boolean valid; + + public AppendClientHolder( + ShardedKey key, Callable appendClientInfoCallable) + throws Exception { + this.key = messageConverters.getAppendClientKey(key); + this.appendClientInfoCallable = appendClientInfoCallable; + this.appendClientInfo = APPEND_CLIENTS.getAndPin(this.key, appendClientInfoCallable); + this.valid = true; + } + + void invalidateAndReset() throws Exception { + APPEND_CLIENTS.unpinAsync(this.appendClientInfo); // Make sure to unpin in another thread. + APPEND_CLIENTS.invalidate(key); + this.appendClientInfo = APPEND_CLIENTS.getAndPin(key, appendClientInfoCallable); + } + + @Override + public void close() { + APPEND_CLIENTS.unpinAsync(this.appendClientInfo); // Make sure to unpin in another thread. + this.valid = false; + } + + AppendClientInfo get() { + org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState( + valid); + return appendClientInfo; + } + + StreamAppendClient getStreamAppendClient() { + return Preconditions.checkStateNotNull(appendClientInfo.getStreamAppendClient()); + } + } + private CreateRetryManagerResult createRetryManager( ShardedKey key, Iterable messages, @@ -554,6 +552,215 @@ private CreateRetryManagerResult createRetryManager( retryManager, failedRows, recordsAppended, histogramUpdates); } + private void handleAppendFailure( + Iterable> failedContexts, + TableReference tableReference, + String shortTableId, + AppendClientInfo appendClientInfo, + Callable tryCreateTable, + Consumer>> initializeContexts, + Runnable resetClient, + ValueState streamName, + ValueState streamOffset, + MultiOutputReceiver o) { + // The first context is always the one that fails. + AppendRowsContext failedContext = + Preconditions.checkStateNotNull(Iterables.getFirst(failedContexts, null)); + BigQuerySinkMetrics.reportFailedRPCMetrics( + failedContext, BigQuerySinkMetrics.RpcMethod.APPEND_ROWS, shortTableId); + String errorCode = BigQuerySinkMetrics.throwableToGRPCCodeString(failedContext.getError()); + + // AppendSerializationError means that BigQuery detected errors on individual rows, e.g. + // a row not conforming to bigQuery invariants. These errors are persistent, so we redirect + // those rows to the + // failedInserts PCollection, and retry with the remaining rows. + if (failedContext.getError() != null + && failedContext.getError() instanceof Exceptions.AppendSerializationError) { + Exceptions.AppendSerializationError error = + Preconditions.checkArgumentNotNull( + (Exceptions.AppendSerializationError) failedContext.getError()); + + Set failedRowIndices = error.getRowIndexToErrorMessage().keySet(); + for (int failedIndex : failedRowIndices) { + // Convert the message to a TableRow and send it to the failedRows collection. + TableRow failedRow = failedContext.failsafeTableRows.get(failedIndex); + if (failedRow == null) { + ByteString protoBytes = failedContext.protoRows.getSerializedRows(failedIndex); + failedRow = appendClientInfo.toTableRow(protoBytes, Predicates.alwaysTrue()); + } + org.joda.time.Instant timestamp = failedContext.timestamps.get(failedIndex); + o.get(failedRowsTag) + .outputWithTimestamp( + new BigQueryStorageApiInsertError( + failedRow, + error.getRowIndexToErrorMessage().get(failedIndex), + tableReference), + timestamp); + } + int failedRows = failedRowIndices.size(); + rowsSentToFailedRowsCollection.inc(failedRows); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.FAILED, errorCode, shortTableId) + .inc(failedRows); + + // Remove the failed row from the payload, so we retry the batch without the failed + // rows. + ProtoRows.Builder retryRows = ProtoRows.newBuilder(); + @Nullable List timestamps = Lists.newArrayList(); + for (int i = 0; i < failedContext.protoRows.getSerializedRowsCount(); ++i) { + if (!failedRowIndices.contains(i)) { + ByteString rowBytes = failedContext.protoRows.getSerializedRows(i); + retryRows.addSerializedRows(rowBytes); + timestamps.add(failedContext.timestamps.get(i)); + } + } + failedContext.protoRows = retryRows.build(); + failedContext.timestamps = timestamps; + int retriedRows = failedContext.protoRows.getSerializedRowsCount(); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.RETRIED, errorCode, shortTableId) + .inc(retriedRows); + + // Since we removed rows, we need to update the insert offsets for all remaining rows. + long offset = failedContext.offset; + for (AppendRowsContext context : failedContexts) { + context.offset = offset; + offset += context.protoRows.getSerializedRowsCount(); + } + streamOffset.write(offset); + return; + } + + Throwable error = Preconditions.checkStateNotNull(failedContext.getError()); + Status.Code statusCode = Status.fromThrowable(error).getCode(); + + // This means that the offset we have stored does not match the current end of + // the stream in the Storage API. Usually this happens because a crash or a bundle + // failure + // happened after an append but before the worker could checkpoint it's + // state. The records that were appended in a failed bundle will be retried, + // meaning that the unflushed tail of the stream must be discarded to prevent + // duplicates. + boolean offsetMismatch = + statusCode.equals(Code.OUT_OF_RANGE) || statusCode.equals(Code.ALREADY_EXISTS); + + boolean quotaError = statusCode.equals(Code.RESOURCE_EXHAUSTED); + if (!offsetMismatch) { + // Don't log errors for expected offset mismatch. These will be logged as warnings + // below. + LOG.error("Got error {} closing {}", failedContext.getError(), failedContext.streamName); + } + + try { + // TODO: Only do this on explicit NOT_FOUND errors once BigQuery reliably produces + // them. + tryCreateTable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + appendFailures.inc(); + int retriedRows = failedContext.protoRows.getSerializedRowsCount(); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.RETRIED, errorCode, shortTableId) + .inc(retriedRows); + + // Schema mismatched exceptions can happen if the table was recently updated. Since + // vortex caches schemas + // we might see the new schema before vortex does. In this case, we simply need to + // retry. + Exceptions.@Nullable StorageException storageException = Exceptions.toStorageException(error); + boolean schemaMismatchError = + (storageException instanceof Exceptions.SchemaMismatchedException); + if (!schemaMismatchError) { + // There's no special error code for missing required fields, and that can also happen + // due to vortex + // being delayed at seeing a new schema. We're forced to parse the description to + // determine that this has happened. + // TODO: Vortex team to introduce a special storage error code for this, so we don't + // have to parse + // descriptions. + Status status = Status.fromThrowable(error); + if (status.getCode() == Code.INVALID_ARGUMENT) { + String description = status.getDescription(); + schemaMismatchError = description != null && description.contains("incompatible fields"); + } + } + if (schemaMismatchError) { + LOG.info( + "Vortex failed stream open due to incompatible fields. This is likely because the BigQuery " + + "schema was recently updated and Vortex hasn't noticed yet, so retrying. error {}", + Preconditions.checkStateNotNull(error).toString()); + } + + boolean explicitStreamFinalized = + failedContext.getError() instanceof StreamFinalizedException; + // This implies that the stream doesn't exist or has already been finalized. In this + // case we have no choice but to create a new stream. + boolean streamDoesNotExist = + explicitStreamFinalized + || statusCode.equals(Code.INVALID_ARGUMENT) + || statusCode.equals(Code.NOT_FOUND) + || statusCode.equals(Code.FAILED_PRECONDITION); + streamDoesNotExist = streamDoesNotExist && !schemaMismatchError; + + if (offsetMismatch || streamDoesNotExist) { + appendOffsetFailures.inc(); + LOG.warn( + "Append to {} failed. Will retry with a new stream", + failedContext, + failedContext.getError()); + // Finalize the stream and clear streamName so a new stream will be created. + o.get(flushTag) + .output(KV.of(failedContext.streamName, new Operation(failedContext.offset - 1, true))); + + // Clear streamName so a new stream will be created in resetClient below. + streamName.write(""); + + // Re-establish the client with the new stream. + resetClient.run(); + + // Reinitialize all contexts with the new stream and new offsets. + initializeContexts.accept(failedContexts); + + // Offset failures imply that all subsequent parallel appends will also fail. + // Retry them all. + } else if (!quotaError) { + resetClient.run(); + } + } + + private void handleAppendSuccess( + AppendRowsContext context, + String shortTableId, + AppendClientInfo appendClientInfo, + MultiOutputReceiver o) { + AppendRowsResponse response = Preconditions.checkStateNotNull(context.getResult()); + o.get(flushTag) + .output( + KV.of( + context.streamName, + new Operation( + context.offset + context.protoRows.getSerializedRowsCount() - 1, false))); + int flushedRows = context.protoRows.getSerializedRowsCount(); + flushesScheduled.inc(flushedRows); + BigQuerySinkMetrics.reportSuccessfulRpcMetrics( + context, BigQuerySinkMetrics.RpcMethod.APPEND_ROWS, shortTableId); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.SUCCESSFUL, BigQuerySinkMetrics.OK, shortTableId) + .inc(flushedRows); + + if (successfulRowsTag != null) { + for (int i = 0; i < context.protoRows.getSerializedRowsCount(); ++i) { + ByteString protoBytes = context.protoRows.getSerializedRows(i); + org.joda.time.Instant timestamp = context.timestamps.get(i); + o.get(successfulRowsTag) + .outputWithTimestamp( + appendClientInfo.toTableRow(protoBytes, successfulRowsPredicate), timestamp); + } + } + } + @ProcessElement public void process( ProcessContext c, @@ -631,10 +838,12 @@ public void process( () -> { @Nullable TableSchema tableSchema; DescriptorProtos.DescriptorProto descriptor; - TableSchema updatedSchemaValue = updatedSchema.read(); + TableSchema updatedSchemaValue = autoUpdateSchema ? updatedSchema.read() : null; + if (autoUpdateSchema && updatedSchemaValue != null) { - // We've seen an updated schema, so we use that instead of querying the - // MessageConverter. + // This means that Vortex has told us in the past that the table schema has been + // updated. We should use + // this updated schema instead of the initial schema from the messageConverter. tableSchema = updatedSchemaValue; descriptor = TableRowToStorageApiProto.descriptorSchemaFromTableSchema( @@ -648,8 +857,8 @@ public void process( if (autoUpdateSchema) { // A StreamWriter ignores table schema updates that happen prior to its creation. // So before creating a StreamWriter below, we fetch the table schema to check if we - // missed an update. - // If so, use the new schema instead of the base schema + // missed an update. If so, use the new schema instead of the base schema. + // TODO: There's still a race here! @Nullable TableSchema streamSchema = MoreObjects.firstNonNull( @@ -671,475 +880,219 @@ public void process( AppendClientInfo.of( Preconditions.checkStateNotNull(tableSchema), descriptor, - // Make sure that the client is always closed in a different thread - // to - // avoid blocking. - client -> - runAsyncIgnoreFailure( - closeWriterExecutor, - () -> { - // Remove the pin that is "owned" by the cache. - client.unpin(); - client.close(); - })) + AutoCloseable::close) .withAppendClient( writeStreamService, getOrCreateStream, false, defaultMissingValueInterpretation); - // This pin is "owned" by the cache. - Preconditions.checkStateNotNull(info.getStreamAppendClient()).pin(); return info; }; - AtomicReference appendClientInfo = - new AtomicReference<>( - APPEND_CLIENTS.get( - messageConverters.getAppendClientKey(element.getKey()), getAppendClientInfo)); - String currentStream = getOrCreateStream.get(); - if (!currentStream.equals(appendClientInfo.get().getStreamName())) { - // Cached append client is inconsistent with persisted state. Throw away cached item and - // force it to be - // recreated. - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - appendClientInfo.set( - APPEND_CLIENTS.get( - messageConverters.getAppendClientKey(element.getKey()), getAppendClientInfo)); - } - - TableSchema updatedSchemaValue = updatedSchema.read(); - if (autoUpdateSchema && updatedSchemaValue != null) { - if (appendClientInfo.get().hasSchemaChanged(updatedSchemaValue)) { - appendClientInfo.set( - AppendClientInfo.of( - updatedSchemaValue, appendClientInfo.get().getCloseAppendClient(), false)); - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - APPEND_CLIENTS.put( - messageConverters.getAppendClientKey(element.getKey()), appendClientInfo.get()); + // The StreamWriter has two pins on it. The static cache holds a pin, as it continues to cache + // values after this + // method exits, so must hold the pin. The local AppendClientHolder also holds a pin, as the + // cache could in + // theory evict the object during execution and we want a pin held throughout the execution of + // this function. + try (AppendClientHolder appendClientHolder = + new AppendClientHolder(element.getKey(), getAppendClientInfo)) { + String currentStream = getOrCreateStream.get(); + if (!currentStream.equals(appendClientHolder.get().getStreamName())) { + // Cached append client is inconsistent with persisted state. Throw away cached item and + // force it to be recreated. + appendClientHolder.invalidateAndReset(); } - } - - // Initialize stream names and offsets for all contexts. This will be called initially, but - // will also be called if we roll over to a new stream on a retry. - BiConsumer>, Boolean> initializeContexts = - (contexts, isFailure) -> { - try { - if (isFailure) { - // Clear the stream name, forcing a new one to be created. - streamName.write(""); - } - appendClientInfo.set( - appendClientInfo - .get() - .withAppendClient( - writeStreamService, - getOrCreateStream, - false, - defaultMissingValueInterpretation)); - StreamAppendClient streamAppendClient = - Preconditions.checkArgumentNotNull( - appendClientInfo.get().getStreamAppendClient()); - String streamNameRead = Preconditions.checkArgumentNotNull(streamName.read()); - long currentOffset = Preconditions.checkArgumentNotNull(streamOffset.read()); - for (AppendRowsContext context : contexts) { - context.streamName = streamNameRead; - streamAppendClient.pin(); - context.client = appendClientInfo.get().getStreamAppendClient(); - context.offset = currentOffset; - ++context.tryIteration; - currentOffset = context.offset + context.protoRows.getSerializedRowsCount(); - } - streamOffset.write(currentOffset); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; - - Consumer>> clearClients = - (contexts) -> { - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - appendClientInfo.set(appendClientInfo.get().withNoAppendClient()); - APPEND_CLIENTS.put( - messageConverters.getAppendClientKey(element.getKey()), appendClientInfo.get()); - for (AppendRowsContext context : contexts) { - if (context.client != null) { - // Unpin in a different thread, as it may execute a blocking close. - runAsyncIgnoreFailure(closeWriterExecutor, context.client::unpin); - context.client = null; - } - } - }; - Function, ApiFuture> runOperation = - context -> { - if (context.protoRows.getSerializedRowsCount() == 0) { - // This might happen if all rows in a batch failed and were sent to the failed-rows - // PCollection. - return ApiFutures.immediateFuture(AppendRowsResponse.newBuilder().build()); - } - try { - appendClientInfo.set( - appendClientInfo - .get() - .withAppendClient( - writeStreamService, - getOrCreateStream, - false, - defaultMissingValueInterpretation)); - return Preconditions.checkStateNotNull(appendClientInfo.get().getStreamAppendClient()) - .appendRows(context.offset, context.protoRows); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; + TableSchema updatedSchemaValue = autoUpdateSchema ? updatedSchema.read() : null; + if ((autoUpdateSchema && updatedSchemaValue != null) + && appendClientHolder.get().hasSchemaChanged(updatedSchemaValue)) { + appendClientHolder.invalidateAndReset(); + } - Function>, RetryType> onError = - failedContexts -> { - // The first context is always the one that fails. - AppendRowsContext failedContext = - Preconditions.checkStateNotNull(Iterables.getFirst(failedContexts, null)); - BigQuerySinkMetrics.reportFailedRPCMetrics( - failedContext, BigQuerySinkMetrics.RpcMethod.APPEND_ROWS, shortTableId); - String errorCode = - BigQuerySinkMetrics.throwableToGRPCCodeString(failedContext.getError()); - - // AppendSerializationError means that BigQuery detected errors on individual rows, e.g. - // a row not conforming - // to bigQuery invariants. These errors are persistent, so we redirect those rows to the - // failedInserts - // PCollection, and retry with the remaining rows. - if (failedContext.getError() != null - && failedContext.getError() instanceof Exceptions.AppendSerializationError) { - Exceptions.AppendSerializationError error = - Preconditions.checkArgumentNotNull( - (Exceptions.AppendSerializationError) failedContext.getError()); - - Set failedRowIndices = error.getRowIndexToErrorMessage().keySet(); - for (int failedIndex : failedRowIndices) { - // Convert the message to a TableRow and send it to the failedRows collection. - TableRow failedRow = failedContext.failsafeTableRows.get(failedIndex); - if (failedRow == null) { - // TODO: MAKE SURE WE USE UPDATED DESCRIPTOR - ByteString protoBytes = failedContext.protoRows.getSerializedRows(failedIndex); - failedRow = - appendClientInfo.get().toTableRow(protoBytes, Predicates.alwaysTrue()); + // Initialize stream names and offsets for all contexts. This will be called initially, but + // will also be called if we roll over to a new stream on a retry. + Consumer>> initializeContexts = + (contexts) -> { + try { + String streamNameRead = Preconditions.checkArgumentNotNull(streamName.read()); + long currentOffset = Preconditions.checkArgumentNotNull(streamOffset.read()); + for (AppendRowsContext context : contexts) { + context.streamName = streamNameRead; + context.offset = currentOffset; + ++context.tryIteration; + currentOffset = context.offset + context.protoRows.getSerializedRowsCount(); } - org.joda.time.Instant timestamp = failedContext.timestamps.get(failedIndex); - o.get(failedRowsTag) - .outputWithTimestamp( - new BigQueryStorageApiInsertError( - failedRow, - error.getRowIndexToErrorMessage().get(failedIndex), - tableReference), - timestamp); + streamOffset.write(currentOffset); + } catch (Exception e) { + throw new RuntimeException(e); } - int failedRows = failedRowIndices.size(); - rowsSentToFailedRowsCollection.inc(failedRows); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.FAILED, errorCode, shortTableId) - .inc(failedRows); - - // Remove the failed row from the payload, so we retry the batch without the failed - // rows. - ProtoRows.Builder retryRows = ProtoRows.newBuilder(); - @Nullable List timestamps = Lists.newArrayList(); - for (int i = 0; i < failedContext.protoRows.getSerializedRowsCount(); ++i) { - if (!failedRowIndices.contains(i)) { - ByteString rowBytes = failedContext.protoRows.getSerializedRows(i); - retryRows.addSerializedRows(rowBytes); - timestamps.add(failedContext.timestamps.get(i)); - } + }; + + Runnable resetClient = + () -> { + try { + appendClientHolder.invalidateAndReset(); + } catch (Exception e) { + throw new RuntimeException(e); } - failedContext.protoRows = retryRows.build(); - failedContext.timestamps = timestamps; - int retriedRows = failedContext.protoRows.getSerializedRowsCount(); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.RETRIED, errorCode, shortTableId) - .inc(retriedRows); - - // Since we removed rows, we need to update the insert offsets for all remaining rows. - long offset = failedContext.offset; - for (AppendRowsContext context : failedContexts) { - context.offset = offset; - offset += context.protoRows.getSerializedRowsCount(); + }; + + Function, ApiFuture> runOperation = + context -> { + if (context.protoRows.getSerializedRowsCount() == 0) { + // This might happen if all rows in a batch failed and were sent to the failed-rows + // PCollection. + return ApiFutures.immediateFuture(AppendRowsResponse.newBuilder().build()); } - streamOffset.write(offset); - return RetryType.RETRY_ALL_OPERATIONS; - } - - Throwable error = Preconditions.checkStateNotNull(failedContext.getError()); - - Status.Code statusCode = Status.fromThrowable(error).getCode(); - - // This means that the offset we have stored does not match the current end of - // the stream in the Storage API. Usually this happens because a crash or a bundle - // failure - // happened after an append but before the worker could checkpoint it's - // state. The records that were appended in a failed bundle will be retried, - // meaning that the unflushed tail of the stream must be discarded to prevent - // duplicates. - boolean offsetMismatch = - statusCode.equals(Code.OUT_OF_RANGE) || statusCode.equals(Code.ALREADY_EXISTS); - - boolean quotaError = statusCode.equals(Code.RESOURCE_EXHAUSTED); - if (!offsetMismatch) { - // Don't log errors for expected offset mismatch. These will be logged as warnings - // below. - LOG.error( - "Got error {} closing {}", failedContext.getError(), failedContext.streamName); - } - - try { - // TODO: Only do this on explicit NOT_FOUND errors once BigQuery reliably produces - // them. - tryCreateTable.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - - if (!quotaError) { - // For known errors (offset mismatch, not found) we must reestablish - // the streams. - // However we've seen that doing this fixes random stuckness issues by reestablishing - // gRPC connections, - // so we close the clients for all non-quota errors. - - clearClients.accept(failedContexts); - } - appendFailures.inc(); - int retriedRows = failedContext.protoRows.getSerializedRowsCount(); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.RETRIED, errorCode, shortTableId) - .inc(retriedRows); - - // Schema mismatched exceptions can happen if the table was recently updated. Since - // vortex caches schemas - // we might see the new schema before vortex does. In this case, we simply need to - // retry. - Exceptions.@Nullable StorageException storageException = - Exceptions.toStorageException(error); - boolean schemaMismatchError = - (storageException instanceof Exceptions.SchemaMismatchedException); - if (!schemaMismatchError) { - // There's no special error code for missing required fields, and that can also happen - // due to vortex - // being delayed at seeing a new schema. We're forced to parse the description to - // determine that this has happened. - // TODO: Vortex team to introduce a special storage error code for this, so we don't - // have to parse - // descriptions. - Status status = Status.fromThrowable(error); - if (status.getCode() == Code.INVALID_ARGUMENT) { - String description = status.getDescription(); - schemaMismatchError = - description != null && description.contains("incompatible fields"); + try { + return appendClientHolder + .getStreamAppendClient() + .appendRows(context.offset, context.protoRows); + } catch (Exception e) { + throw new RuntimeException(e); } - } - if (schemaMismatchError) { - LOG.info( - "Vortex failed stream open due to incompatible fields. This is likely because the BigTable " - + "schema was recently updated and Vortex hasn't noticed yet, so retrying. error {}", - Preconditions.checkStateNotNull(error).toString()); - } - - boolean explicitStreamFinalized = - failedContext.getError() instanceof StreamFinalizedException; - // This implies that the stream doesn't exist or has already been finalized. In this - // case we have no choice but to create a new stream. - boolean streamDoesNotExist = - explicitStreamFinalized - || statusCode.equals(Code.INVALID_ARGUMENT) - || statusCode.equals(Code.NOT_FOUND) - || statusCode.equals(Code.FAILED_PRECONDITION); - streamDoesNotExist = streamDoesNotExist && !schemaMismatchError; - - if (offsetMismatch || streamDoesNotExist) { - appendOffsetFailures.inc(); - LOG.warn( - "Append to {} failed. Will retry with a new stream", - failedContext, - failedContext.getError()); - // Finalize the stream and clear streamName so a new stream will be created. - o.get(flushTag) - .output( - KV.of( - failedContext.streamName, new Operation(failedContext.offset - 1, true))); - // Reinitialize all contexts with the new stream and new offsets. - initializeContexts.accept(failedContexts, true); - - // Offset failures imply that all subsequent parallel appends will also fail. - // Retry them all. + }; + + Function>, RetryType> onError = + failedContexts -> { + handleAppendFailure( + failedContexts, + tableReference, + shortTableId, + appendClientHolder.get(), + tryCreateTable, + initializeContexts, + resetClient, + streamName, + streamOffset, + o); return RetryType.RETRY_ALL_OPERATIONS; - } - - return RetryType.RETRY_ALL_OPERATIONS; - }; + }; + Consumer> onSuccess = + context -> handleAppendSuccess(context, shortTableId, appendClientHolder.get(), o); + + BackOff backoff = + FluentBackoff.DEFAULT + .withInitialBackoff(Duration.standardSeconds(1)) + .withMaxBackoff(Duration.standardMinutes(1)) + .withMaxRetries(500) + .withThrottledTimeCounter( + BigQuerySinkMetrics.throttledTimeCounter( + BigQuerySinkMetrics.RpcMethod.OPEN_WRITE_STREAM)) + .backoff(); + CreateRetryManagerResult createRetryManagerResult; + do { + // Each ProtoRows object contains at most 1MB of rows. + // TODO: Push messageFromTableRow up to top level. That we we cans skip TableRow entirely + // if + // already proto or already schema. + Iterable messages = + new SplittingIterable( + element.getValue(), + splitSize, + // Unknown field merger + (bytes, tableRow) -> + appendClientHolder.get().mergeNewFields(bytes, tableRow, ignoreUnknownValues), + // Convert back to TableRow + bytes -> appendClientHolder.get().toTableRow(bytes, Predicates.alwaysTrue()), + // Failed rows consumer + (failedRow, errorMessage) -> { + o.get(failedRowsTag) + .outputWithTimestamp( + new BigQueryStorageApiInsertError( + failedRow.getValue(), errorMessage, tableReference), + failedRow.getTimestamp()); + rowsSentToFailedRowsCollection.inc(); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.FAILED, + BigQuerySinkMetrics.PAYLOAD_TOO_LARGE, + shortTableId) + .inc(1); + }, + // Get the currently-known TableSchema hash + () -> appendClientHolder.get().getTableSchemaHash(), + () -> + TableRowToStorageApiProto.wrapDescriptorProto( + messageConverter.getDescriptor(false)), + autoUpdateSchema, + elementTs); + + createRetryManagerResult = + createRetryManager( + element.getKey(), + messages, + runOperation, + onError, + onSuccess, + appendClientHolder.get(), + tableReference); + if (createRetryManagerResult.getSchemaMismatchSeen()) { + // TODO: The call to updateSchemaFromTable will throttle the DoFn (both because of the + // RPC + // call and because + // the cache has a delay on refresh). We should update throttling counters here as well. + LOG.info("Schema out of date: refreshing table schema for {}", tableId); + // Force the message converter to get the schema again from the table. + messageConverter.updateSchemaFromTable(); + // Close all RPC clients that were opened with the old descriptor. Clear the cache, + // forcing us to create a new append client with the updated descriptor. + appendClientHolder.invalidateAndReset(); + } + } while (createRetryManagerResult.getSchemaMismatchSeen() + && BackOffUtils.next(Sleeper.DEFAULT, backoff)); + + // Output any rows that failed along they way. + createRetryManagerResult + .getFailedRows() + .forEach( + tv -> o.get(failedRowsTag).outputWithTimestamp(tv.getValue(), tv.getTimestamp())); + rowsSentToFailedRowsCollection.inc(createRetryManagerResult.getFailedRows().size()); + BigQuerySinkMetrics.appendRowsRowStatusCounter( + BigQuerySinkMetrics.RowStatus.FAILED, + BigQuerySinkMetrics.PAYLOAD_TOO_LARGE, + shortTableId) + .inc(createRetryManagerResult.getFailedRows().size()); + + recordsAppended.inc(createRetryManagerResult.getRecordsAppended()); + createRetryManagerResult.getHistogramValues().forEach(appendSizeDistribution::update); + + Instant now = Instant.now(); + + RetryManager> retryManager = + Preconditions.checkStateNotNull(createRetryManagerResult.getRetryManager()); + int numAppends = retryManager.getRemainingOperationCount(); + Iterable> contexts = retryManager.getRemainingContexts(); + + if (numAppends > 0) { + initializeContexts.accept(contexts); + retryManager.run(true); - Consumer> onSuccess = - context -> { - AppendRowsResponse response = Preconditions.checkStateNotNull(context.getResult()); - o.get(flushTag) - .output( - KV.of( - context.streamName, - new Operation( - context.offset + context.protoRows.getSerializedRowsCount() - 1, - false))); - int flushedRows = context.protoRows.getSerializedRowsCount(); - flushesScheduled.inc(flushedRows); - BigQuerySinkMetrics.reportSuccessfulRpcMetrics( - context, BigQuerySinkMetrics.RpcMethod.APPEND_ROWS, shortTableId); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.SUCCESSFUL, BigQuerySinkMetrics.OK, shortTableId) - .inc(flushedRows); - - if (successfulRowsTag != null) { - for (int i = 0; i < context.protoRows.getSerializedRowsCount(); ++i) { - ByteString protoBytes = context.protoRows.getSerializedRows(i); - org.joda.time.Instant timestamp = context.timestamps.get(i); - o.get(successfulRowsTag) - .outputWithTimestamp( - appendClientInfo.get().toTableRow(protoBytes, successfulRowsPredicate), - timestamp); + appendSplitDistribution.update(numAppends); + if (autoUpdateSchema) { + @Nullable + StreamAppendClient streamAppendClient = appendClientHolder.getStreamAppendClient(); + TableSchema originalSchema = appendClientHolder.get().getTableSchema(); + + @Nullable + TableSchema updatedSchemaReturned = + (streamAppendClient != null) ? streamAppendClient.getUpdatedSchema() : null; + // Update the table schema and clear the append client. + if (updatedSchemaReturned != null) { + Optional newSchema = + TableSchemaUpdateUtils.getUpdatedSchema(originalSchema, updatedSchemaReturned); + if (newSchema.isPresent()) { + APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); + LOG.debug( + "Fetched updated schema for table {}:\n\t{}", tableId, updatedSchemaReturned); + updatedSchema.write(newSchema.get()); } } - }; - - BackOff backoff = - FluentBackoff.DEFAULT - .withInitialBackoff(Duration.standardSeconds(1)) - .withMaxBackoff(Duration.standardMinutes(1)) - .withMaxRetries(500) - .withThrottledTimeCounter( - BigQuerySinkMetrics.throttledTimeCounter( - BigQuerySinkMetrics.RpcMethod.OPEN_WRITE_STREAM)) - .backoff(); - CreateRetryManagerResult createRetryManagerResult; - do { - // Each ProtoRows object contains at most 1MB of rows. - // TODO: Push messageFromTableRow up to top level. That we we cans skip TableRow entirely if - // already proto or already schema. - Iterable messages = - new SplittingIterable( - element.getValue(), - splitSize, - // Unknown field merger - (bytes, tableRow) -> - appendClientInfo.get().mergeNewFields(bytes, tableRow, ignoreUnknownValues), - // Convert back to TableRow - bytes -> appendClientInfo.get().toTableRow(bytes, Predicates.alwaysTrue()), - // Failed rows consumer - (failedRow, errorMessage) -> { - o.get(failedRowsTag) - .outputWithTimestamp( - new BigQueryStorageApiInsertError( - failedRow.getValue(), errorMessage, tableReference), - failedRow.getTimestamp()); - rowsSentToFailedRowsCollection.inc(); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.FAILED, - BigQuerySinkMetrics.PAYLOAD_TOO_LARGE, - shortTableId) - .inc(1); - }, - // Get the currently-known TableSchema hash - () -> appendClientInfo.get().getTableSchemaHash(), - () -> - TableRowToStorageApiProto.wrapDescriptorProto( - messageConverter.getDescriptor(false)), - autoUpdateSchema, - elementTs); - - createRetryManagerResult = - createRetryManager( - element.getKey(), - messages, - runOperation, - onError, - onSuccess, - appendClientInfo.get(), - tableReference); - if (createRetryManagerResult.getSchemaMismatchSeen()) { - // TODO: The call to updateSchemaFromTable will throttle the DoFn (both because of the RPC - // call and because - // the cache has a delay on refresh). We should update throttling counters here as well. - LOG.info("Schema out of date: refreshing table schema for {}", tableId); - // Force the message converter to get the schema again from the table. - messageConverter.updateSchemaFromTable(); - // Close all RPC clients that were opened with the old descriptor. Clear the cache, - // forcing us to create a new append client with the updated descriptor. - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - appendClientInfo.set( - APPEND_CLIENTS.get( - messageConverters.getAppendClientKey(element.getKey()), getAppendClientInfo)); - } - } while (createRetryManagerResult.getSchemaMismatchSeen() - && BackOffUtils.next(Sleeper.DEFAULT, backoff)); - - // Output any rows that failed along they way. - createRetryManagerResult - .getFailedRows() - .forEach( - tv -> o.get(failedRowsTag).outputWithTimestamp(tv.getValue(), tv.getTimestamp())); - rowsSentToFailedRowsCollection.inc(createRetryManagerResult.getFailedRows().size()); - BigQuerySinkMetrics.appendRowsRowStatusCounter( - BigQuerySinkMetrics.RowStatus.FAILED, - BigQuerySinkMetrics.PAYLOAD_TOO_LARGE, - shortTableId) - .inc(createRetryManagerResult.getFailedRows().size()); - - recordsAppended.inc(createRetryManagerResult.getRecordsAppended()); - createRetryManagerResult.getHistogramValues().forEach(appendSizeDistribution::update); - - Instant now = Instant.now(); - - RetryManager> retryManager = - Preconditions.checkStateNotNull(createRetryManagerResult.getRetryManager()); - int numAppends = retryManager.getRemainingOperationCount(); - Iterable> contexts = retryManager.getRemainingContexts(); - - if (numAppends > 0) { - initializeContexts.accept(contexts, false); - try { - retryManager.run(true); - } finally { - // Make sure that all pins are removed. - for (AppendRowsContext context : contexts) { - if (context.client != null) { - runAsyncIgnoreFailure(closeWriterExecutor, context.client::unpin); - } - } - } - appendSplitDistribution.update(numAppends); - - if (autoUpdateSchema) { - @Nullable - StreamAppendClient streamAppendClient = appendClientInfo.get().getStreamAppendClient(); - TableSchema originalSchema = appendClientInfo.get().getTableSchema(); - ; - @Nullable - TableSchema updatedSchemaReturned = - (streamAppendClient != null) ? streamAppendClient.getUpdatedSchema() : null; - // Update the table schema and clear the append client. - if (updatedSchemaReturned != null) { - Optional newSchema = - TableSchemaUpdateUtils.getUpdatedSchema(originalSchema, updatedSchemaReturned); - if (newSchema.isPresent()) { - appendClientInfo.set( - AppendClientInfo.of( - newSchema.get(), appendClientInfo.get().getCloseAppendClient(), false)); - APPEND_CLIENTS.invalidate(messageConverters.getAppendClientKey(element.getKey())); - APPEND_CLIENTS.put( - messageConverters.getAppendClientKey(element.getKey()), appendClientInfo.get()); - LOG.debug( - "Fetched updated schema for table {}:\n\t{}", tableId, updatedSchemaReturned); - updatedSchema.write(newSchema.get()); - } } - } - java.time.Duration timeElapsed = java.time.Duration.between(now, Instant.now()); - appendLatencyDistribution.update(timeElapsed.toMillis()); + java.time.Duration timeElapsed = java.time.Duration.between(now, Instant.now()); + appendLatencyDistribution.update(timeElapsed.toMillis()); + } } idleTimer.offset(streamIdleTime).withNoOutputTimestamp().setRelative(); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java index 371c77c5f9c1..ba72bb8682fd 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProto.java @@ -1639,9 +1639,9 @@ public static ByteString mergeNewFields( null, null, collectedExceptions); - if (!collectedExceptions.isEmpty()) { - return null; - } + } + if (!collectedExceptions.isEmpty()) { + return null; } } else if (schemaInformation.getType() == TableFieldSchema.Type.TIMESTAMP && schemaInformation.getTimestampPrecision() == PICOSECOND_PRECISION) { diff --git a/sdks/java/io/google-cloud-platform/src/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/pubsub/ExternalWrite.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/ExternalWrite.java index 658d1fc29e32..23b14b68a08b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/ExternalWrite.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/ExternalWrite.java @@ -53,6 +53,7 @@ public static class Configuration { private String topic; private @Nullable String idAttribute; private @Nullable String timestampAttribute; + private boolean publishWithOrderingKey = false; public void setTopic(String topic) { this.topic = topic; @@ -65,6 +66,10 @@ public void setIdLabel(@Nullable String idAttribute) { public void setTimestampAttribute(@Nullable String timestampAttribute) { this.timestampAttribute = timestampAttribute; } + + public void setPublishWithOrderingKey(Boolean publishWithOrderingKey) { + this.publishWithOrderingKey = publishWithOrderingKey != null && publishWithOrderingKey; + } } public static class WriteBuilder @@ -85,6 +90,7 @@ public PTransform, PDone> buildExternal(Configuration config if (config.timestampAttribute != null) { writeBuilder.setTimestampAttribute(config.timestampAttribute); } + writeBuilder.setPublishWithOrderingKey(config.publishWithOrderingKey); writeBuilder.setDynamicDestinations(false); return writeBuilder.build(); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java index d62d294ed2a7..57005745044b 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubIO.java @@ -1727,7 +1727,10 @@ public void startBundle(StartBundleContext c) throws IOException { this.pubsubClient = getPubsubClientFactory() .newClient( - getTimestampAttribute(), null, c.getPipelineOptions().as(PubsubOptions.class)); + getTimestampAttribute(), + null, + c.getPipelineOptions().as(PubsubOptions.class), + Write.this.getPubsubRootUrl()); } @ProcessElement diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessor.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessor.java index eb1860902c6f..351425db67a3 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessor.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerAccessor.java @@ -247,7 +247,16 @@ static SpannerOptions buildSpannerOptions(SpannerConfig spannerConfig) { } if (plainText != null && Boolean.TRUE.equals(plainText.get())) { builder.setChannelConfigurator(b -> b.usePlaintext()); - builder.setCredentials(NoCredentials.getInstance()); + } + ValueProvider clientCert = spannerConfig.getClientCertPath(); + ValueProvider clientKey = spannerConfig.getClientCertKeyPath(); + if (clientCert != null + && clientKey != null + && clientCert.isAccessible() + && clientKey.isAccessible() + && !Strings.isNullOrEmpty(clientCert.get()) + && !Strings.isNullOrEmpty(clientKey.get())) { + builder.useClientCert(clientCert.get(), clientKey.get()); } } @@ -273,6 +282,8 @@ static SpannerOptions buildSpannerOptions(SpannerConfig spannerConfig) { ValueProvider credentials = spannerConfig.getCredentials(); if (credentials != null && credentials.get() != null) { builder.setCredentials(credentials.get()); + } else if (experimentalHost != null && !Strings.isNullOrEmpty(experimentalHost.get())) { + builder.setCredentials(NoCredentials.getInstance()); } ValueProvider waitForSessionCreationDuration = diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerConfig.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerConfig.java index a17c851f38a0..92eac9108283 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerConfig.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerConfig.java @@ -97,6 +97,10 @@ public String getHostValue() { public abstract @Nullable ValueProvider getPlainText(); + public abstract @Nullable ValueProvider getClientCertPath(); + + public abstract @Nullable ValueProvider getClientCertKeyPath(); + @VisibleForTesting abstract @Nullable ServiceFactory getServiceFactory(); @@ -194,6 +198,10 @@ abstract Builder setExecuteStreamingSqlRetrySettings( abstract Builder setWaitForSessionCreationDuration( ValueProvider waitForSessionCreationDuration); + abstract Builder setClientCertPath(ValueProvider clientCertPath); + + abstract Builder setClientCertKeyPath(ValueProvider clientCertKeyPath); + public abstract SpannerConfig build(); } @@ -414,4 +422,33 @@ public SpannerConfig withWaitForSessionCreationDuration( return withWaitForSessionCreationDuration( ValueProvider.StaticValueProvider.of(waitForSessionCreationDuration)); } + + /** + * Specifies certificate paths to use for mTLS channel. + * + *

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

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

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

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

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

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

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

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

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

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

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

    Note: These parameters are only valid when using Spanner Omni (set via {@code + * withExperimentalHost}). + * + * @param certPath Path to the client certificate file. + * @param keyPath Path to the client certificate key file. + */ + public ReadChangeStream withClientCert(String certPath, String keyPath) { + return withClientCert( + ValueProvider.StaticValueProvider.of(certPath), + ValueProvider.StaticValueProvider.of(keyPath)); + } + /** * Configures low latency experiment for readChangeStream transform. Example usage: * @@ -2177,9 +2334,17 @@ SpannerConfig buildChangeStreamSpannerConfig() { static SpannerConfig buildSpannerConfigWithCredential( SpannerConfig spannerConfig, PipelineOptions pipelineOptions) { if (spannerConfig.getCredentials() == null && pipelineOptions != null) { - final Credentials credentials = pipelineOptions.as(GcpOptions.class).getGcpCredential(); - if (credentials != null) { - spannerConfig = spannerConfig.withCredentials(credentials); + boolean isExperimentalHostEmpty = + spannerConfig.getExperimentalHost() == null + || (spannerConfig.getExperimentalHost().isAccessible() + && Strings.isNullOrEmpty(spannerConfig.getExperimentalHost().get())); + if (isExperimentalHostEmpty) { + final Credentials credentials = pipelineOptions.as(GcpOptions.class).getGcpCredential(); + if (credentials != null) { + spannerConfig = spannerConfig.withCredentials(credentials); + } + } else { + spannerConfig = spannerConfig.withCredentials(NoCredentials.getInstance()); } } return spannerConfig; diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java index 70908f982721..544aa2938d51 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/SpannerTransformRegistrar.java @@ -81,6 +81,8 @@ public abstract static class CrossLanguageConfiguration { @Nullable String emulatorHost; @Nullable String experimentalHost; @Nullable Boolean plainText; + @Nullable String clientCertPath; + @Nullable String clientCertKeyPath; public void setInstanceId(String instanceId) { this.instanceId = instanceId; @@ -110,6 +112,14 @@ public void setPlainText(@Nullable Boolean plainText) { this.plainText = plainText; } + public void setClientCertPath(@Nullable String clientCertPath) { + this.clientCertPath = clientCertPath; + } + + public void setClientCertKeyPath(@Nullable String clientCertKeyPath) { + this.clientCertKeyPath = clientCertKeyPath; + } + void checkMandatoryFields() { if (projectId.isEmpty()) { throw new IllegalArgumentException("projectId can't be empty"); @@ -120,6 +130,10 @@ void checkMandatoryFields() { if (instanceId.isEmpty()) { throw new IllegalArgumentException("instanceId can't be empty"); } + if ((clientCertPath != null) != (clientCertKeyPath != null)) { + throw new IllegalArgumentException( + "Both clientCertPath and clientCertKeyPath must be specified together."); + } } } @@ -249,6 +263,11 @@ public PTransform> buildExternal( if (configuration.plainText != null) { readTransform = readTransform.withUsingPlainTextChannel(configuration.plainText); } + if (configuration.clientCertPath != null && configuration.clientCertKeyPath != null) { + readTransform = + readTransform.withClientCert( + configuration.clientCertPath, configuration.clientCertKeyPath); + } @Nullable TimestampBound timestampBound = configuration.getTimestampBound(); if (timestampBound != null) { readTransform = readTransform.withTimestampBound(timestampBound); @@ -393,6 +412,11 @@ public PTransform, PDone> buildExternal( if (configuration.plainText != null) { writeTransform = writeTransform.withUsingPlainTextChannel(configuration.plainText); } + if (configuration.clientCertPath != null && configuration.clientCertKeyPath != null) { + writeTransform = + writeTransform.withClientCert( + configuration.clientCertPath, configuration.clientCertKeyPath); + } if (configuration.commitDeadline != null) { writeTransform = writeTransform.withCommitDeadline(configuration.commitDeadline); } @@ -504,6 +528,12 @@ public PTransform> buildExternal( readChangeStream = readChangeStream.withMetadataTable(configuration.metadataTable); } + if (configuration.clientCertPath != null && configuration.clientCertKeyPath != null) { + readChangeStream = + readChangeStream.withClientCert( + configuration.clientCertPath, configuration.clientCertKeyPath); + } + if (configuration.rpcPriority != null) { readChangeStream = readChangeStream.withRpcPriority(configuration.rpcPriority); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/MetadataSpannerConfigFactory.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/MetadataSpannerConfigFactory.java index 959582e9c35f..de81814c2110 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/MetadataSpannerConfigFactory.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/MetadataSpannerConfigFactory.java @@ -87,6 +87,12 @@ public static SpannerConfig create( config = config.withUsingPlainTextChannel(plainText.get()); } + ValueProvider clientCert = primaryConfig.getClientCertPath(); + ValueProvider clientKey = primaryConfig.getClientCertKeyPath(); + if (clientCert != null && clientKey != null) { + config = config.withClientCert(clientCert, clientKey); + } + ValueProvider isLocalChannelProvider = primaryConfig.getIsLocalChannelProvider(); if (isLocalChannelProvider != null) { config = diff --git a/sdks/java/io/google-cloud-platform/src/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/testing/FakeDatasetService.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java index 1849df422425..814f4eec421f 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java @@ -764,7 +764,7 @@ public StreamAppendClient getStreamAppendClient( AppendRowsRequest.MissingValueInterpretation missingValueInterpretation) throws Exception { return new StreamAppendClient() { - private Descriptor protoDescriptor; + private Descriptor protoDescriptor = null; private TableSchema currentSchema; private @Nullable com.google.cloud.bigquery.storage.v1.TableSchema updatedSchema; TableRowToStorageApiProto.SchemaInformation schemaInformation; @@ -900,7 +900,7 @@ public void close() throws Exception {} public void pin() {} @Override - public void unpin() throws Exception {} + public void unpin() {} }; } diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/GcpApiSurfaceTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/GcpApiSurfaceTest.java index d9f63b5177ac..c3067ba0934f 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/GcpApiSurfaceTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/GcpApiSurfaceTest.java @@ -122,6 +122,7 @@ public void testGcpApiSurface() throws Exception { classesInPackage("com.fasterxml.jackson.core"), classesInPackage("com.fasterxml.jackson.databind"), classesInPackage("io.grpc"), + classesInPackage("io.opentelemetry.context"), classesInPackage("java"), classesInPackage("javax"), classesInPackage("org.apache.avro"), diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryTableRowEqualityTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryTableRowEqualityTest.java new file mode 100644 index 000000000000..5b8a095a3d09 --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryTableRowEqualityTest.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.gcp.bigquery; + +import static org.apache.beam.sdk.io.gcp.bigquery.TableRowMatchers.isTableRowEqualTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; + +import com.google.api.services.bigquery.model.TableRow; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the {@link TableRowMatchers} class. */ +@RunWith(JUnit4.class) +public class BigQueryTableRowEqualityTest { + + @Test + public void testIdenticalRows() { + TableRow row1 = new TableRow().set("count", 1).set("name", "Alice"); + TableRow row2 = new TableRow().set("count", 1).set("name", "Alice"); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testEmptyRows() { + TableRow row1 = new TableRow(); + TableRow row2 = new TableRow(); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testIntegerVsString() { + TableRow rowWithInteger = new TableRow().set("count", 1); + TableRow rowWithString = new TableRow().set("count", "1"); + assertThat(rowWithInteger, not(isTableRowEqualTo(rowWithString))); + } + + @Test + public void testDoubleVsInteger() { + TableRow rowWithDouble = new TableRow().set("value", 1.0); + TableRow rowWithInteger = new TableRow().set("value", 1); + assertThat(rowWithDouble, not(isTableRowEqualTo(rowWithInteger))); + } + + @Test + public void testBooleanVsString() { + TableRow rowWithBoolean = new TableRow().set("active", true); + TableRow rowWithString = new TableRow().set("active", "true"); + assertThat(rowWithBoolean, not(isTableRowEqualTo(rowWithString))); + } + + @Test + public void testBothFieldsNull() { + TableRow row1 = new TableRow().set("name", null); + TableRow row2 = new TableRow().set("name", null); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testNullVsNonNull() { + TableRow rowWithNull = new TableRow().set("name", null); + TableRow rowWithValue = new TableRow().set("name", "Alice"); + assertThat(rowWithNull, not(isTableRowEqualTo(rowWithValue))); + } + + @Test + public void testEmptyStringVsNull() { + TableRow rowWithEmptyString = new TableRow().set("name", ""); + TableRow rowWithNull = new TableRow().set("name", null); + assertThat(rowWithEmptyString, not(isTableRowEqualTo(rowWithNull))); + } + + @Test + public void testWhitespaceDifference() { + TableRow rowWithoutSpace = new TableRow().set("name", "Alice"); + TableRow rowWithLeadingSpace = new TableRow().set("name", " Alice"); + assertThat(rowWithoutSpace, not(isTableRowEqualTo(rowWithLeadingSpace))); + } + + @Test + public void testDifferentFieldCount() { + TableRow rowWithTwoFields = new TableRow().set("a", 1).set("b", 2); + TableRow rowWithOneField = new TableRow().set("a", 1); + assertThat(rowWithTwoFields, not(isTableRowEqualTo(rowWithOneField))); + } + + @Test + public void testMissingField() { + TableRow rowWithFieldB = new TableRow().set("a", 1).set("b", 2); + TableRow rowWithFieldC = new TableRow().set("a", 1).set("c", 2); + assertThat(rowWithFieldB, not(isTableRowEqualTo(rowWithFieldC))); + } + + @Test + public void testDifferentInsertionOrder() { + TableRow row1 = new TableRow().set("a", 1).set("b", 2); + TableRow row2 = new TableRow().set("b", 2).set("a", 1); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testIdenticalNestedRows() { + TableRow innerRow1 = new TableRow().set("id", 42); + TableRow innerRow2 = new TableRow().set("id", 42); + TableRow outerRow1 = new TableRow().set("nested", innerRow1); + TableRow outerRow2 = new TableRow().set("nested", innerRow2); + assertThat(outerRow1, isTableRowEqualTo(outerRow2)); + } + + @Test + public void testNestedRowsWithTypeMismatch() { + TableRow innerRowWithInteger = new TableRow().set("id", 42); + TableRow innerRowWithString = new TableRow().set("id", "42"); + TableRow outerRow1 = new TableRow().set("nested", innerRowWithInteger); + TableRow outerRow2 = new TableRow().set("nested", innerRowWithString); + assertThat(outerRow1, not(isTableRowEqualTo(outerRow2))); + } + + @Test + public void testDeeplyNestedRowsWithTypeMismatch() { + TableRow level3WithInteger = new TableRow().set("val", 1); + TableRow level3WithString = new TableRow().set("val", "1"); + TableRow level2Row1 = new TableRow().set("l2", level3WithInteger); + TableRow level2Row2 = new TableRow().set("l2", level3WithString); + TableRow level1Row1 = new TableRow().set("l1", level2Row1); + TableRow level1Row2 = new TableRow().set("l1", level2Row2); + assertThat(level1Row1, not(isTableRowEqualTo(level1Row2))); + } + + @Test + public void testIdenticalListFields() { + TableRow row1 = new TableRow().set("tags", Arrays.asList("a", "b")); + TableRow row2 = new TableRow().set("tags", Arrays.asList("a", "b")); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testListFieldsWithDifferentOrder() { + TableRow row1 = new TableRow().set("tags", Arrays.asList("a", "b")); + TableRow row2 = new TableRow().set("tags", Arrays.asList("b", "a")); + assertThat(row1, not(isTableRowEqualTo(row2))); + } + + @Test + public void testZeroIntegerVsZeroDouble() { + TableRow rowWithZeroInteger = new TableRow().set("value", 0); + TableRow rowWithZeroDouble = new TableRow().set("value", 0.0); + assertThat(rowWithZeroInteger, not(isTableRowEqualTo(rowWithZeroDouble))); + } + + @Test + public void testNegativeNumbers() { + TableRow row1 = new TableRow().set("temp", -10); + TableRow row2 = new TableRow().set("temp", -10); + assertThat(row1, isTableRowEqualTo(row2)); + } + + @Test + public void testLongVsInteger() { + TableRow rowWithLong = new TableRow().set("count", 1L); + TableRow rowWithInteger = new TableRow().set("count", 1); + assertThat(rowWithLong, not(isTableRowEqualTo(rowWithInteger))); + } + + @Test + public void testTrueVsFalse() { + TableRow rowWithTrue = new TableRow().set("active", true); + TableRow rowWithFalse = new TableRow().set("active", false); + assertThat(rowWithTrue, not(isTableRowEqualTo(rowWithFalse))); + } + + @Test + public void testLargeIntegerVsLong() { + TableRow rowWithInteger = new TableRow().set("big", Integer.MAX_VALUE); + TableRow rowWithLong = new TableRow().set("big", (long) Integer.MAX_VALUE); + assertThat(rowWithInteger, not(isTableRowEqualTo(rowWithLong))); + } + + @Test + public void testMultipleFieldsWithOneTypeMismatch() { + TableRow row1 = new TableRow().set("id", 1).set("name", "Alice").set("score", 99); + TableRow row2 = new TableRow().set("id", 1).set("name", "Alice").set("score", "99"); + assertThat(row1, not(isTableRowEqualTo(row2))); + } +} diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java index ef390f5a8601..1d35fea44966 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiDataTriggeredSchemaUpdateIT.java @@ -19,6 +19,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects.firstNonNull; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import com.google.api.services.bigquery.model.Table; import com.google.api.services.bigquery.model.TableFieldSchema; @@ -36,6 +37,7 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition; import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.WriteDisposition; import org.apache.beam.sdk.io.gcp.testing.BigqueryClient; +import org.apache.beam.sdk.options.StreamingOptions; import org.apache.beam.sdk.state.StateSpec; import org.apache.beam.sdk.state.StateSpecs; import org.apache.beam.sdk.state.ValueState; @@ -45,6 +47,7 @@ import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.transforms.WithKeys; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; @@ -187,6 +190,7 @@ public void testDataTriggeredSchemaUpgradeAtLeastOnce() throws Exception { private void runTest(Write.Method method) throws Exception { Pipeline p = Pipeline.create(TestPipeline.testingPipelineOptions()); p.getOptions().as(BigQueryOptions.class).setSchemaUpgradeBufferingShards(1); + p.getOptions().as(StreamingOptions.class).setStreaming(true); TableSchema baseSchema = new TableSchema() @@ -262,7 +266,8 @@ private void runTest(Write.Method method) throws Exception { write = write .withTriggeringFrequency(Duration.standardSeconds(1)) - .withNumStorageWriteApiStreams(2); + // One stream — same as other Storage Write ITs here, fewer ordering surprises. + .withNumStorageWriteApiStreams(1); } SequenceRowsDoFn doFn = new SequenceRowsDoFn(5, 20); @@ -279,7 +284,9 @@ private void runTest(Write.Method method) throws Exception { .apply( MapElements.into(TypeDescriptor.of(TableRow.class)) .via(BigQueryStorageApiInsertError::getRow)); - PAssert.that(failedInserts).containsInAnyOrder(doFn.getRow(20)); + // Schema upgrades can race with evolved rows; allow extra DLQ rows but require the + // intentionally malformed row shape to appear. + PAssert.that(failedInserts).satisfies(new VerifyContainsMalformedReqRow()); p.run().waitUntilFinish(); @@ -327,6 +334,23 @@ abstract static class VerificationInfo { abstract int getExpectedCount(); } + private static final class VerifyContainsMalformedReqRow + implements SerializableFunction, Void> { + @Override + public Void apply(Iterable rows) { + boolean sawBadReqShape = false; + for (TableRow row : rows) { + Object reqValue = row.get("req"); + if (reqValue instanceof List && ((List) reqValue).size() == 2) { + sawBadReqShape = true; + break; + } + } + assertTrue("DLQ should include the malformed req row", sawBadReqShape); + return null; + } + } + private void verifyDataWritten(String tableSpec, List verifications) throws IOException, InterruptedException { for (VerificationInfo verification : verifications) { diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowMatchers.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowMatchers.java new file mode 100644 index 000000000000..988b1bd0842c --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowMatchers.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.gcp.bigquery; + +import com.google.api.services.bigquery.model.TableRow; +import java.util.stream.Collectors; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +class TableRowMatchers { + + static Matcher isTableRowEqualTo(TableRow expected) { + return new TypeSafeMatcher() { + + @Override + protected boolean matchesSafely(TableRow actual) { + return rowsMatch(expected, actual); + } + + @Override + public void describeTo(Description description) { + description.appendText("TableRow (strict) "); + description.appendText(formatValue(expected, 0)); + } + + @Override + protected void describeMismatchSafely(TableRow actual, Description mismatch) { + describeRowMismatch(expected, actual, mismatch); + } + }; + } + + private static boolean rowsMatch(TableRow expected, TableRow actual) { + if (expected == null && actual == null) { + return true; + } + + if (expected == null || actual == null) { + return false; + } + + if (actual.size() != expected.size()) { + return false; + } + + for (String key : expected.keySet()) { + if (!actual.containsKey(key)) { + return false; + } + + Object expectedVal = expected.get(key); + Object actualVal = actual.get(key); + + if (expectedVal == null && actualVal == null) { + continue; + } + if (expectedVal == null || actualVal == null) { + return false; + } + + // recursively compare nested TableRows + if (expectedVal instanceof TableRow && actualVal instanceof TableRow) { + if (!rowsMatch((TableRow) expectedVal, (TableRow) actualVal)) { + return false; + } + continue; + } + + if (!expectedVal.getClass().equals(actualVal.getClass())) { + return false; + } + + if (!expectedVal.equals(actualVal)) { + return false; + } + } + return true; + } + + private static void describeRowMismatch( + TableRow expected, TableRow actual, Description mismatch) { + + // size mismatch + if (actual.size() != expected.size()) { + mismatch.appendText( + String.format( + "had %d field(s) %s but expected %d field(s) %s", + actual.size(), actual.keySet(), expected.size(), expected.keySet())); + return; + } + + // missing field + for (String key : expected.keySet()) { + if (!actual.containsKey(key)) { + mismatch.appendText(String.format("missing field '%s'", key)); + return; + } + } + + // value/type mismatch + for (String key : expected.keySet()) { + Object expectedVal = expected.get(key); + Object actualVal = actual.get(key); + + if (expectedVal == null && actualVal == null) { + continue; + } + ; + + if (expectedVal == null) { + mismatch.appendText( + String.format( + "field '%s': expected null but was %s(%s)", + key, actualVal.getClass().getSimpleName(), formatValue(actualVal, 0))); + return; + } + + if (actualVal == null) { + mismatch.appendText( + String.format( + "field '%s': expected %s(%s) but was null", + key, expectedVal.getClass().getSimpleName(), formatValue(expectedVal, 0))); + return; + } + + // recurse into nested TableRows + if (expectedVal instanceof TableRow && actualVal instanceof TableRow) { + if (!rowsMatch((TableRow) expectedVal, (TableRow) actualVal)) { + mismatch.appendText(String.format("field '%s': ", key)); + describeRowMismatch((TableRow) expectedVal, (TableRow) actualVal, mismatch); + return; + } + continue; + } + + // type mismatch + if (!expectedVal.getClass().equals(actualVal.getClass())) { + mismatch.appendText( + String.format( + "field '%s': expected %s(%s) but was %s(%s)", + key, + expectedVal.getClass().getSimpleName(), + formatValue(expectedVal, 0), + actualVal.getClass().getSimpleName(), + formatValue(actualVal, 0))); + return; + } + + // value mismatch + if (!expectedVal.equals(actualVal)) { + mismatch.appendText( + String.format( + "field '%s': expected value (%s) but was (%s)", + key, formatValue(expectedVal, 0), formatValue(actualVal, 0))); + return; + } + } + } + + // recursive formatter + private static String formatValue(Object val, int depth) { + if (val == null) { + return "null"; + } + + // safety net against infinite recursion + if (depth > 10) { + return "..."; + } + if (val instanceof TableRow) { + TableRow row = (TableRow) val; + String fields = + row.keySet().stream() + .map( + k -> { + Object v = row.get(k); + String typeName = + v == null + ? "null" + : v instanceof TableRow ? "TableRow" : v.getClass().getSimpleName(); + return String.format("%s(%s)=%s", k, typeName, formatValue(v, depth + 1)); + }) + .collect(Collectors.joining(", ")); + return "TableRow{" + fields + "}"; + } + return String.valueOf(val); + } +} 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/pubsub/PubsubClientTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClientTest.java index 9d7bc65f5954..e834d4eef5eb 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClientTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/pubsub/PubsubClientTest.java @@ -22,6 +22,7 @@ import com.google.pubsub.v1.Schema; import java.util.Map; +import org.apache.avro.AvroTypeException; import org.apache.avro.SchemaParseException; import org.apache.beam.sdk.io.gcp.pubsub.PubsubClient.ProjectPath; import org.apache.beam.sdk.io.gcp.pubsub.PubsubClient.SchemaPath; @@ -262,7 +263,7 @@ public void fromPubsubSchema() { assertThrows( "'notatype' Avro type should throw an exception", - SchemaParseException.class, + AvroTypeException.class, () -> PubsubClient.fromPubsubSchema( new com.google.api.services.pubsub.model.Schema() diff --git a/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/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/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/storage/GcsMatchIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/storage/GcsMatchIT.java index 332aa067b0eb..4c73e3e2a68f 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/storage/GcsMatchIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/storage/GcsMatchIT.java @@ -134,8 +134,12 @@ public Void apply(Iterable input) { assertEquals(1, countFirst); // file "second" is expected to appear more than once assertEquals(true, countSecond > 1); - // file "second" is expected to appear in growing sizes each time by one byte - assertEquals((maxSecondSize * 2L - countSecond + 1) * countSecond / 2, sumSecondSize); + // Continuous matching sometimes skips a middle size on Dataflow; sum should still fit + // between "all sizes seen" and "every size from 1..maxSecondSize". + long minPossibleSum = (countSecond - 1) * countSecond / 2L + maxSecondSize; + long maxPossibleContiguousSum = (maxSecondSize * 2L - countSecond + 1) * countSecond / 2L; + assertEquals(true, sumSecondSize <= maxPossibleContiguousSum); + assertEquals(true, sumSecondSize >= minPossibleSum); return null; } diff --git a/sdks/java/io/hadoop-file-system/src/main/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystem.java b/sdks/java/io/hadoop-file-system/src/main/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystem.java index 4de1d539e76a..00a77aa84476 100644 --- a/sdks/java/io/hadoop-file-system/src/main/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystem.java +++ b/sdks/java/io/hadoop-file-system/src/main/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystem.java @@ -38,6 +38,7 @@ import org.apache.beam.sdk.io.fs.MatchResult.Metadata; import org.apache.beam.sdk.io.fs.MatchResult.Status; import org.apache.beam.sdk.io.fs.MoveOptions; +import org.apache.beam.sdk.metrics.Lineage; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.hadoop.conf.Configuration; @@ -336,6 +337,22 @@ protected String getScheme() { return scheme; } + @Override + protected void reportLineage(HadoopResourceId resourceId, Lineage lineage, LineageLevel level) { + URI uri = resourceId.toPath().toUri(); + ImmutableList.Builder segments = ImmutableList.builder(); + if (uri.getAuthority() != null && !uri.getAuthority().isEmpty()) { + segments.add(uri.getAuthority()); + } + if (level != LineageLevel.TOP_LEVEL + && uri.getPath() != null + && !uri.getPath().isEmpty() + && !uri.getPath().equals("/")) { + segments.add(uri.getPath()); + } + lineage.add(scheme, segments.build(), "/"); + } + /** An adapter around {@link FSDataInputStream} that implements {@link SeekableByteChannel}. */ private static class HadoopSeekableByteChannel implements SeekableByteChannel { private final FileStatus fileStatus; diff --git a/sdks/java/io/hadoop-file-system/src/test/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystemTest.java b/sdks/java/io/hadoop-file-system/src/test/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystemTest.java index c5918a8c9aa6..e0b299401c76 100644 --- a/sdks/java/io/hadoop-file-system/src/test/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystemTest.java +++ b/sdks/java/io/hadoop-file-system/src/test/java/org/apache/beam/sdk/io/hdfs/HadoopFileSystemTest.java @@ -24,6 +24,9 @@ import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.io.FileNotFoundException; import java.io.InputStream; @@ -37,12 +40,14 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import org.apache.beam.sdk.io.FileSystem; import org.apache.beam.sdk.io.FileSystems; import org.apache.beam.sdk.io.TextIO; import org.apache.beam.sdk.io.fs.CreateOptions.StandardCreateOptions; import org.apache.beam.sdk.io.fs.MatchResult; import org.apache.beam.sdk.io.fs.MatchResult.Metadata; import org.apache.beam.sdk.io.fs.MatchResult.Status; +import org.apache.beam.sdk.metrics.Lineage; import org.apache.beam.sdk.testing.ExpectedLogs; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; @@ -481,6 +486,21 @@ public void testReadPipeline() throws Exception { p.run(); } + @Test + public void testReportLineage() { + verifyLineage( + "hdfs://namenode/path/to/file.txt", ImmutableList.of("namenode", "/path/to/file.txt")); + verifyLineage("hdfs://namenode/", ImmutableList.of("namenode")); + verifyLineage("hdfs://namenode", ImmutableList.of("namenode")); + } + + private void verifyLineage(String uri, List expected) { + HadoopResourceId resourceId = new HadoopResourceId(URI.create(uri)); + Lineage mockLineage = mock(Lineage.class); + fileSystem.reportLineage(resourceId, mockLineage, FileSystem.LineageLevel.FILE); + verify(mockLineage, times(1)).add("hdfs", expected, "/"); + } + private void create(String relativePath, byte[] contents) throws Exception { try (WritableByteChannel channel = fileSystem.create( diff --git a/sdks/java/io/hadoop-format/src/main/java/org/apache/beam/sdk/io/hadoop/format/HadoopFormatIO.java b/sdks/java/io/hadoop-format/src/main/java/org/apache/beam/sdk/io/hadoop/format/HadoopFormatIO.java index 0412e4286bb8..501040928e41 100644 --- a/sdks/java/io/hadoop-format/src/main/java/org/apache/beam/sdk/io/hadoop/format/HadoopFormatIO.java +++ b/sdks/java/io/hadoop-format/src/main/java/org/apache/beam/sdk/io/hadoop/format/HadoopFormatIO.java @@ -34,6 +34,7 @@ import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -54,6 +55,7 @@ import org.apache.beam.sdk.io.BoundedSource; import org.apache.beam.sdk.io.hadoop.SerializableConfiguration; import org.apache.beam.sdk.io.hadoop.WritableCoder; +import org.apache.beam.sdk.metrics.Lineage; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.transforms.Combine; import org.apache.beam.sdk.transforms.Create; @@ -97,6 +99,7 @@ import org.apache.hadoop.mapreduce.TaskAttemptID; import org.apache.hadoop.mapreduce.TaskID; import org.apache.hadoop.mapreduce.lib.db.DBConfiguration; +import org.apache.hadoop.mapreduce.lib.input.FileSplit; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.mapreduce.task.JobContextImpl; import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl; @@ -725,6 +728,7 @@ public List>> split(long desiredBundleSizeBytes, Pipeline return ImmutableList.of(this); } computeSplitsIfNecessary(); + reportSourceLineage(inputSplits); LOG.info( "Generated {} splits. Size of first split is {} ", inputSplits.size(), @@ -744,6 +748,27 @@ public List>> split(long desiredBundleSizeBytes, Pipeline .collect(Collectors.toList()); } + private void reportSourceLineage(final List inputSplits) { + for (SerializableSplit serializableSplit : inputSplits) { + InputSplit split = serializableSplit.getSplit(); + if (split instanceof FileSplit) { + URI uri = ((FileSplit) split).getPath().toUri(); + String scheme = uri.getScheme(); + if (scheme == null) { + continue; + } + ImmutableList.Builder segments = ImmutableList.builder(); + if (uri.getAuthority() != null && !uri.getAuthority().isEmpty()) { + segments.add(uri.getAuthority()); + } + if (uri.getPath() != null && !uri.getPath().isEmpty() && !uri.getPath().equals("/")) { + segments.add(uri.getPath()); + } + Lineage.getSources().add(scheme, segments.build(), "/"); + } + } + } + @Override public long getEstimatedSizeBytes(PipelineOptions po) throws Exception { if (inputSplit == null) { diff --git a/sdks/java/io/iceberg/build.gradle b/sdks/java/io/iceberg/build.gradle index bbd55fee2fc8..8142c5f5b90b 100644 --- a/sdks/java/io/iceberg/build.gradle +++ b/sdks/java/io/iceberg/build.gradle @@ -59,7 +59,7 @@ dependencies { implementation "org.apache.iceberg:iceberg-parquet:$iceberg_version" implementation "org.apache.iceberg:iceberg-orc:$iceberg_version" implementation "org.apache.iceberg:iceberg-data:$iceberg_version" - implementation library.java.hadoop_common + implementation "org.apache.hadoop:hadoop-common:3.3.6" // TODO(https://github.com/apache/beam/issues/21156): Determine how to build without this dependency provided "org.immutables:value:2.8.8" permitUnusedDeclared "org.immutables:value:2.8.8" @@ -70,6 +70,7 @@ dependencies { runtimeOnly "org.apache.iceberg:iceberg-azure:$iceberg_version" runtimeOnly "org.apache.iceberg:iceberg-azure-bundle:$iceberg_version" runtimeOnly library.java.bigdataoss_gcs_connector + runtimeOnly library.java.bigdataoss_util_hadoop runtimeOnly library.java.hadoop_client testImplementation project(":sdks:java:managed") @@ -117,6 +118,12 @@ dependencies { 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 -> 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..9fad990a4391 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java @@ -31,6 +31,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -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( @@ -532,17 +531,23 @@ private Table getOrCreateTable(String filePath, FileFormat format) throws IOExce org.apache.iceberg.Schema schema = getSchema(filePath, format); PartitionSpec spec = PartitionUtils.toPartitionSpec(partitionFields, schema); SortOrder sortOrder = SortOrderUtils.toSortOrder(sortFields, schema); - - Catalog.TableBuilder builder = - catalogConfig - .catalog() - .buildTable(tableId, schema) - .withPartitionSpec(spec) - .withSortOrder(sortOrder); - if (tableProps != null) { - builder.withProperties(tableProps); + Map properties = + tableProps != null ? new HashMap<>(tableProps) : new HashMap<>(); + if (properties.get(TableProperties.DEFAULT_NAME_MAPPING) == null) { + // Forces Name based resolution instead of position based resolution + NameMapping mapping = MappingUtil.create(schema); + String mappingJson = NameMappingParser.toJson(mapping); + properties.put(TableProperties.DEFAULT_NAME_MAPPING, mappingJson); } - return builder.create(); + + return catalogConfig + .catalog() + .buildTable(tableId, schema) + .withPartitionSpec(spec) + .withSortOrder(sortOrder) + .withProperties(properties) + .create(); + } catch (AlreadyExistsException e2) { // if table already exists, just load it return catalogConfig.catalog().loadTable(TableIdentifier.parse(identifier)); } @@ -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,7 +677,7 @@ public CreateManifests(IcebergCatalogConfig catalogConfig, String identifier) { @ProcessElement public void process( - @Element KV, Iterable> batch, + @Element KV> batch, OutputReceiver> output) throws IOException { if (!batch.getValue().iterator().hasNext()) { @@ -682,7 +687,7 @@ public void process( table = catalogConfig.catalog().loadTable(TableIdentifier.parse(identifier)); } - PartitionSpec spec = checkStateNotNull(table.specs().get(batch.getKey().getKey())); + PartitionSpec spec = checkStateNotNull(table.specs().get(batch.getKey())); String manifestPath = String.format( diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java new file mode 100644 index 000000000000..99cd07b23c8e --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AssignDestinationsAndPartitions.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.util.HashMap; +import java.util.Map; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.beam.sdk.transforms.windowing.PaneInfo; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.ValueInSingleWindow; +import org.apache.iceberg.PartitionKey; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.joda.time.Instant; + +/** + * Assigns destination metadata for each input record. + * + *

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

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

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

    Handles special cases: + * + *

      + *
    • UUID: Converts {@code byte[]} to Iceberg's UUID representation. + *
    • DateTime: Converts Beam {@code DateTime} or logical types to microsecond timestamps. + *
    • Nested Rows: Recursively wraps nested structures in a new {@code BeamRowWrapper}. + *
    + */ + private static @Nullable PositionalGetter buildGetter(FieldType beamType, Type icebergType) { + switch (beamType.getTypeName()) { + case BYTE: + return Row::getByte; + case INT16: + return Row::getInt16; + case STRING: + return Row::getString; + case BYTES: + return (row, pos) -> { + byte[] bytes = checkStateNotNull(row.getBytes(pos)); + if (Type.TypeID.UUID == icebergType.typeId()) { + return UUIDUtil.convert(bytes); + } else { + return ByteBuffer.wrap(bytes); + } + }; + case DECIMAL: + return Row::getDecimal; + case DATETIME: + return (row, pos) -> + TimeUnit.MILLISECONDS.toMicros(checkStateNotNull(row.getDateTime(pos)).getMillis()); + case ROW: + Schema beamSchema = checkStateNotNull(beamType.getRowSchema()); + Types.StructType structType = (Types.StructType) icebergType; + + BeamRowWrapper nestedWrapper = new BeamRowWrapper(beamSchema, structType); + return (row, pos) -> nestedWrapper.wrap(row.getRow(pos)); + case LOGICAL_TYPE: + if (beamType.isLogicalType(MicrosInstant.IDENTIFIER)) { + return (row, pos) -> { + Instant instant = checkStateNotNull(row.getLogicalTypeValue(pos, Instant.class)); + return TimeUnit.SECONDS.toMicros(instant.getEpochSecond()) + instant.getNano() / 1000; + }; + } else if (beamType.isLogicalType(DateTime.IDENTIFIER)) { + return (row, pos) -> + DateTimeUtil.microsFromTimestamp( + checkStateNotNull(row.getLogicalTypeValue(pos, LocalDateTime.class))); + } else if (beamType.isLogicalType(Date.IDENTIFIER)) { + return (row, pos) -> + DateTimeUtil.daysFromDate( + checkStateNotNull(row.getLogicalTypeValue(pos, LocalDate.class))); + } else if (beamType.isLogicalType(Time.IDENTIFIER)) { + return (row, pos) -> + DateTimeUtil.microsFromTime( + checkStateNotNull(row.getLogicalTypeValue(pos, LocalTime.class))); + } else if (beamType.isLogicalType(FixedPrecisionNumeric.IDENTIFIER)) { + return (row, pos) -> row.getLogicalTypeValue(pos, BigDecimal.class); + } else { + return null; + } + default: + return null; + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/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/IcebergCatalogConfig.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCatalogConfig.java index e01b174decb0..454fe40269d6 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCatalogConfig.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergCatalogConfig.java @@ -154,7 +154,7 @@ public void createTable( String tableIdentifier, Schema tableSchema, @Nullable List partitionFields, - Map properties) { + @Nullable Map properties) { TableIdentifier icebergIdentifier = TableIdentifier.parse(tableIdentifier); org.apache.iceberg.Schema icebergSchema = IcebergUtils.beamSchemaToIcebergSchema(tableSchema); PartitionSpec icebergSpec = PartitionUtils.toPartitionSpec(partitionFields, tableSchema); @@ -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); @@ -270,7 +276,7 @@ public boolean dropTable(String tableIdentifier) { public Set listTables(String namespace) { return catalog().listTables(Namespace.of(namespace)).stream() - .map(TableIdentifier::name) + .map(TableIdentifier::toString) .collect(Collectors.toSet()); } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java index 1d71ad549094..5c5f934ea205 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergIO.java @@ -31,6 +31,7 @@ import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Predicates; +import org.apache.iceberg.DistributionMode; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Catalog; import org.apache.iceberg.catalog.TableIdentifier; @@ -381,7 +382,11 @@ public class IcebergIO { public static WriteRows writeRows(IcebergCatalogConfig catalog) { - return new AutoValue_IcebergIO_WriteRows.Builder().setCatalogConfig(catalog).build(); + return new AutoValue_IcebergIO_WriteRows.Builder() + .setCatalogConfig(catalog) + .setDistributionMode(DistributionMode.NONE) + .setAutoSharding(false) + .build(); } @AutoValue @@ -397,6 +402,10 @@ public abstract static class WriteRows extends PTransform, Iceb abstract @Nullable Integer getDirectWriteByteLimit(); + abstract DistributionMode getDistributionMode(); + + abstract boolean getAutoSharding(); + abstract Builder toBuilder(); @AutoValue.Builder @@ -411,6 +420,10 @@ abstract static class Builder { abstract Builder setDirectWriteByteLimit(Integer directWriteByteLimit); + abstract Builder setDistributionMode(DistributionMode mode); + + abstract Builder setAutoSharding(boolean autoSharding); + abstract WriteRows build(); } @@ -443,6 +456,24 @@ public WriteRows withDirectWriteByteLimit(Integer directWriteByteLimit) { return toBuilder().setDirectWriteByteLimit(directWriteByteLimit).build(); } + /** + * Defines distribution of write data. Supported distributions: + * + *
      + *
    1. {@link DistributionMode.NONE}: don't shuffle rows (default) + *
    2. {@link DistributionMode.HASH}: shuffle rows by partition key before writing data + *
    + * + * {@link DistributionMode.RANGE} is not supported yet + */ + public WriteRows withDistributionMode(DistributionMode mode) { + return toBuilder().setDistributionMode(mode).build(); + } + + public WriteRows withAutosharding() { + return toBuilder().setAutoSharding(true).build(); + } + @Override public IcebergWriteResult expand(PCollection input) { List allToArgs = Arrays.asList(getTableIdentifier(), getDynamicDestinations()); @@ -464,15 +495,37 @@ public IcebergWriteResult expand(PCollection input) { IcebergUtils.isUnbounded(input), "Must only provide direct write limit for unbounded pipelines."); } - return input - .apply("Assign Table Destinations", new AssignDestinations(destinations)) - .apply( - "Write Rows to Destinations", - new WriteToDestinations( - getCatalogConfig(), - destinations, - getTriggeringFrequency(), - getDirectWriteByteLimit())); + + switch (getDistributionMode()) { + case NONE: + Preconditions.checkArgument( + !getAutoSharding(), + "Autosharding option is only available with " + "'hash' distribution mode."); + return input + .apply("Assign Table Destinations", new AssignDestinations(destinations)) + .apply( + "Write Rows to Destinations", + new WriteToDestinations( + getCatalogConfig(), + destinations, + getTriggeringFrequency(), + getDirectWriteByteLimit())); + case HASH: + return input + .apply( + "AssignDestinationAndPartition", + new AssignDestinationsAndPartitions(destinations, getCatalogConfig())) + .apply( + "Write Rows to Partitions", + new WriteToPartitions( + getCatalogConfig(), + destinations, + getTriggeringFrequency(), + getAutoSharding())); + default: + throw new UnsupportedOperationException( + "Unsupported distribution mode: " + getDistributionMode()); + } } } @@ -602,7 +655,7 @@ public PCollection expand(PBegin input) { TableIdentifier tableId = checkStateNotNull(getTableIdentifier(), "Must set a table to read from."); - Table table = getCatalogConfig().catalog().loadTable(tableId); + Table table = TableCache.get(getCatalogConfig(), tableId); IcebergScanConfig scanConfig = IcebergScanConfig.builder() diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java index 56f21d2bc8b5..cac4a015346c 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java @@ -19,21 +19,26 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.util.Sets.newHashSet; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets.newHashSet; import com.google.auto.value.AutoValue; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.apache.beam.sdk.io.iceberg.IcebergIO.ReadRows.StartingStrategy; +import org.apache.beam.sdk.io.iceberg.cdc.IcebergCdcMetadataColumns; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.iceberg.Table; +import org.apache.iceberg.TableUtil; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.expressions.Evaluator; import org.apache.iceberg.expressions.Expression; @@ -48,7 +53,6 @@ public abstract class IcebergScanConfig implements Serializable { private transient @MonotonicNonNull Table cachedTable; private transient org.apache.iceberg.@MonotonicNonNull Schema cachedProjectedSchema; private transient org.apache.iceberg.@MonotonicNonNull Schema cachedRequiredSchema; - private transient @MonotonicNonNull Evaluator cachedEvaluator; private transient @MonotonicNonNull Expression cachedFilter; public enum ScanType { @@ -68,8 +72,7 @@ public enum ScanType { @Pure public Table getTable() { if (cachedTable == null) { - cachedTable = - getCatalogConfig().catalog().loadTable(TableIdentifier.parse(getTableIdentifier())); + cachedTable = TableCache.get(getCatalogConfig(), TableIdentifier.parse(getTableIdentifier())); } return cachedTable; } @@ -143,15 +146,12 @@ public org.apache.iceberg.Schema getRequiredSchema() { @Pure @Nullable - public Evaluator getEvaluator() { + public Evaluator getEvaluator(org.apache.iceberg.Schema requiredSchema) { @Nullable Expression filter = getFilter(); if (filter == null) { return null; } - if (cachedEvaluator == null) { - cachedEvaluator = new Evaluator(getRequiredSchema().asStruct(), filter); - } - return cachedEvaluator; + return new Evaluator(requiredSchema.asStruct(), filter); } @Pure @@ -226,6 +226,9 @@ public Expression getFilter() { @Pure public abstract @Nullable List getDropFields(); + @Pure + public abstract List getMetadataColumns(); + @Pure public static Builder builder() { return new AutoValue_IcebergScanConfig.Builder() @@ -248,7 +251,8 @@ public static Builder builder() { .setPollInterval(null) .setStartingStrategy(null) .setTag(null) - .setBranch(null); + .setBranch(null) + .setMetadataColumns(ImmutableList.of()); } @AutoValue.Builder @@ -311,6 +315,8 @@ public Builder setTableIdentifier(String... names) { public abstract Builder setDropFields(@Nullable List fields); + public abstract Builder setMetadataColumns(List metadataColumns); + public abstract IcebergScanConfig build(); } @@ -364,6 +370,9 @@ void validate(Table table) { if (getStartingStrategy() != null) { invalidOptions.add("starting_strategy"); } + if (!getMetadataColumns().isEmpty()) { + invalidOptions.add("metadata_columns"); + } if (!invalidOptions.isEmpty()) { throw new IllegalArgumentException( error( @@ -371,6 +380,19 @@ void validate(Table table) { + "reading with Managed.ICEBERG_CDC: " + invalidOptions)); } + } else { + Set primaryKeyIds = new HashSet<>(table.schema().identifierFieldIds()); + checkState( + !primaryKeyIds.isEmpty(), + "Cannot read CDC records as the table schema does not specified any primary key fields."); + Set projectedFieldIds = TypeUtil.getProjectedIds(getProjectedSchema()); + primaryKeyIds.removeAll(projectedFieldIds); + checkArgument( + primaryKeyIds.isEmpty(), + "When reading CDC records, the projected schema must not drop primary key fields. " + + "The specified configuration drops the following PK fields: %s", + primaryKeyIds); + validateMetadataColumns(table); } if (getStartingStrategy() != null) { @@ -393,6 +415,47 @@ void validate(Table table) { } } + private void validateMetadataColumns(Table table) { + List metadataColumns = getMetadataColumns(); + if (metadataColumns.isEmpty()) { + return; + } + + Set uniqueMetadataColumns = new LinkedHashSet<>(metadataColumns); + checkArgument( + uniqueMetadataColumns.size() == metadataColumns.size(), + error("metadata_columns contains duplicate entries: %s"), + metadataColumns); + + List unsupportedMetadataColumns = new ArrayList<>(); + for (String metadataColumn : metadataColumns) { + if (!IcebergCdcMetadataColumns.isSupportedColumn(metadataColumn)) { + unsupportedMetadataColumns.add(metadataColumn); + } + } + checkArgument( + unsupportedMetadataColumns.isEmpty(), + error("unsupported metadata_columns: %s. Supported values are: %s"), + unsupportedMetadataColumns, + IcebergCdcMetadataColumns.SUPPORTED_COLUMNS); + + for (String metadataColumn : metadataColumns) { + checkArgument( + getProjectedSchema().findField(metadataColumn) == null, + error("metadata column '%s' conflicts with a projected data column"), + metadataColumn); + } + + boolean includesRowLineage = + metadataColumns.stream().anyMatch(IcebergCdcMetadataColumns::isRowMetadataColumn); + if (includesRowLineage) { + checkArgument( + TableUtil.formatVersion(table) >= 3, + error("row lineage metadata columns %s are only available for Iceberg format v3+ tables"), + metadataColumns); + } + } + private String error(String message) { return "Invalid source configuration: " + message; } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProvider.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProvider.java index b76ede93a16f..8db4fb77a8e8 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProvider.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProvider.java @@ -42,6 +42,7 @@ import org.apache.beam.sdk.values.PCollectionRowTuple; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.iceberg.DistributionMode; import org.apache.iceberg.FileFormat; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Duration; @@ -144,6 +145,19 @@ public static Builder builder() { + "For more information on sort orders, please visit https://iceberg.apache.org/spec/#sort-orders.") public abstract @Nullable List getSortFields(); + @SchemaFieldDescription( + "Defines distribution of write data. Supported distributions:" + + "\n- none: don't shuffle rows (default)" + + "\n- hash: shuffle rows by partition key before writing data") + public abstract @Nullable String getDistributionMode(); + + @SchemaFieldDescription( + "Enables dynamic sharding to automatically adjust the number of parallel writers " + + "based on data volume. It handles data skew " + + "by further sub-dividing partitions into multiple shards to prevent bottlenecks " + + "during high-throughput writes. Only available with 'hash' distribution mode.") + public abstract @Nullable Boolean getAutosharding(); + @AutoValue.Builder public abstract static class Builder { public abstract Builder setTable(String table); @@ -170,6 +184,10 @@ public abstract static class Builder { public abstract Builder setSortFields(List sortFields); + public abstract Builder setDistributionMode(String mode); + + public abstract Builder setAutosharding(Boolean autosharding); + public abstract Configuration build(); } @@ -251,6 +269,16 @@ public PCollectionRowTuple expand(PCollectionRowTuple input) { writeTransform = writeTransform.withDirectWriteByteLimit(directWriteByteLimit); } + @Nullable String mode = configuration.getDistributionMode(); + if (mode != null) { + writeTransform = writeTransform.withDistributionMode(DistributionMode.fromName(mode)); + } + + @Nullable Boolean autoSharding = configuration.getAutosharding(); + if (autoSharding != null && autoSharding) { + writeTransform = writeTransform.withAutosharding(); + } + // TODO: support dynamic destinations IcebergWriteResult result = rows.apply(writeTransform); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java index 4df3eecb18e5..58cc8f50e0bc 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IncrementalScanSource.java @@ -53,10 +53,8 @@ class IncrementalScanSource extends PTransform> { @Override public PCollection expand(PBegin input) { Table table = - scanConfig - .getCatalogConfig() - .catalog() - .loadTable(TableIdentifier.parse(scanConfig.getTableIdentifier())); + TableCache.get( + scanConfig.getCatalogConfig(), TableIdentifier.parse(scanConfig.getTableIdentifier())); PCollection>> snapshots = MoreObjects.firstNonNull(scanConfig.getStreaming(), false) diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PartitionUtils.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PartitionUtils.java index 805cc0672940..32a25439d850 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PartitionUtils.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/PartitionUtils.java @@ -18,6 +18,7 @@ package org.apache.beam.sdk.io.iceberg; import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.iceberg.data.IdentityPartitionConverters.convertConstant; import java.util.List; import java.util.Map; @@ -25,11 +26,20 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.iceberg.ChangelogScanTask; +import org.apache.iceberg.ContentFile; +import org.apache.iceberg.ContentScanTask; +import org.apache.iceberg.MetadataColumns; +import org.apache.iceberg.PartitionField; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; import org.apache.iceberg.expressions.Expressions; import org.apache.iceberg.expressions.Term; +import org.apache.iceberg.types.Types; import org.checkerframework.checker.nullness.qual.Nullable; class PartitionUtils { @@ -48,23 +58,28 @@ class PartitionUtils { Pattern, BiFunction> TRANSFORMATIONS = ImmutableMap.of( - HOUR, (builder, matcher) -> builder.hour(checkStateNotNull(matcher.group(1))), - DAY, (builder, matcher) -> builder.day(checkStateNotNull(matcher.group(1))), - MONTH, (builder, matcher) -> builder.month(checkStateNotNull(matcher.group(1))), - YEAR, (builder, matcher) -> builder.year(checkStateNotNull(matcher.group(1))), + HOUR, + (builder, matcher) -> builder.hour(checkStateNotNull(matcher.group(1))), + DAY, + (builder, matcher) -> builder.day(checkStateNotNull(matcher.group(1))), + MONTH, + (builder, matcher) -> builder.month(checkStateNotNull(matcher.group(1))), + YEAR, + (builder, matcher) -> builder.year(checkStateNotNull(matcher.group(1))), TRUNCATE, - (builder, matcher) -> - builder.truncate( - checkStateNotNull(matcher.group(1)), - Integer.parseInt(checkStateNotNull(matcher.group(2)))), + (builder, matcher) -> + builder.truncate( + checkStateNotNull(matcher.group(1)), + Integer.parseInt(checkStateNotNull(matcher.group(2)))), BUCKET, - (builder, matcher) -> - builder.bucket( - checkStateNotNull(matcher.group(1)), - Integer.parseInt(checkStateNotNull(matcher.group(2)))), - VOID, (builder, matcher) -> builder.alwaysNull(checkStateNotNull(matcher.group(1))), + (builder, matcher) -> + builder.bucket( + checkStateNotNull(matcher.group(1)), + Integer.parseInt(checkStateNotNull(matcher.group(2)))), + VOID, + (builder, matcher) -> builder.alwaysNull(checkStateNotNull(matcher.group(1))), IDENTITY, - (builder, matcher) -> builder.identity(checkStateNotNull(matcher.group(1)))); + (builder, matcher) -> builder.identity(checkStateNotNull(matcher.group(1)))); static PartitionSpec toPartitionSpec( @Nullable List fields, org.apache.beam.sdk.schemas.Schema beamSchema) { @@ -130,4 +145,61 @@ static Term toIcebergTerm(String field) { throw new IllegalArgumentException("Could not find a partition term for '" + field + "'."); } + + /** + * Copied over from Apache Iceberg's PartitionUtil. + * + *

    Needed to accommodate CDC reads, where scans produce {@link ChangelogScanTask}s instead of + * {@link ContentScanTask}s. + */ + public static Map constantsMap( + PartitionSpec spec, ContentFile file, @Nullable Long fileSequenceNumber) { + Preconditions.checkState( + spec.specId() == file.specId(), + "File spec ID (%s) does not match PartitionSpec ID (%s)", + file.specId(), + spec.specId()); + StructLike partitionData = file.partition(); + + // use java.util.HashMap because partition data may contain null values + Map idToConstant = Maps.newHashMap(); + + // add first_row_id as _row_id + if (file.firstRowId() != null) { + idToConstant.put( + MetadataColumns.ROW_ID.fieldId(), + convertConstant(Types.LongType.get(), file.firstRowId())); + } + + // When reconstructing a DataFile, we lose the ability to attach its fileSequenceNumber, + // so we pipe it along the util methods to include it here. + fileSequenceNumber = + fileSequenceNumber != null ? fileSequenceNumber : file.fileSequenceNumber(); + idToConstant.put( + MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.fieldId(), + convertConstant(Types.LongType.get(), fileSequenceNumber)); + + // add _file + idToConstant.put( + MetadataColumns.FILE_PATH.fieldId(), + convertConstant(Types.StringType.get(), file.location())); + + // add _spec_id + idToConstant.put( + MetadataColumns.SPEC_ID.fieldId(), convertConstant(Types.IntegerType.get(), file.specId())); + + List partitionFields = spec.partitionType().fields(); + List fields = spec.fields(); + for (int pos = 0; pos < fields.size(); pos += 1) { + PartitionField field = fields.get(pos); + if (field.transform().isIdentity()) { + Object converted = + convertConstant(partitionFields.get(pos).type(), partitionData.get(pos, Object.class)); + idToConstant.put(field.sourceId(), converted); + } + } + + return idToConstant; + } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java index 528b89c203bf..71114437731c 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFromTasks.java @@ -51,11 +51,6 @@ class ReadFromTasks extends DoFn, Row> { this.scanConfig = scanConfig; } - @Setup - public void setup() { - TableCache.setup(scanConfig); - } - @ProcessElement public void process( @Element KV element, @@ -63,7 +58,7 @@ public void process( OutputReceiver out) throws IOException, ExecutionException, InterruptedException { ReadTask readTask = element.getValue(); - Table table = TableCache.get(scanConfig.getTableIdentifier()); + Table table = TableCache.get(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); List fileScanTasks = readTask.getFileScanTasks(); @@ -75,9 +70,7 @@ public void process( } FileScanTask task = fileScanTasks.get((int) l); Schema beamSchema = IcebergUtils.icebergSchemaToBeamSchema(scanConfig.getProjectedSchema()); - try (CloseableIterable fullIterable = - ReadUtils.createReader(task, table, scanConfig.getRequiredSchema())) { - CloseableIterable reader = ReadUtils.maybeApplyFilter(fullIterable, scanConfig); + try (CloseableIterable reader = ReadUtils.createReader(task, table, scanConfig)) { for (Record record : reader) { Row row = IcebergUtils.icebergRecordToBeamRow(beamSchema, record); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadUtils.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadUtils.java index e7f50882f433..ea2dc7589c4e 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadUtils.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadUtils.java @@ -20,25 +20,22 @@ import static org.apache.iceberg.util.SnapshotUtil.ancestorsOf; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.function.BiFunction; import java.util.stream.Collectors; import org.apache.beam.sdk.io.iceberg.IcebergIO.ReadRows.StartingStrategy; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.MoreObjects; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; import org.apache.hadoop.conf.Configuration; -import org.apache.iceberg.FileScanTask; +import org.apache.iceberg.ContentFile; +import org.apache.iceberg.ContentScanTask; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; import org.apache.iceberg.Snapshot; import org.apache.iceberg.Table; import org.apache.iceberg.TableProperties; -import org.apache.iceberg.data.IdentityPartitionConverters; import org.apache.iceberg.data.InternalRecordWrapper; import org.apache.iceberg.data.Record; import org.apache.iceberg.data.parquet.GenericParquetReaders; @@ -46,17 +43,12 @@ import org.apache.iceberg.encryption.EncryptedInputFile; import org.apache.iceberg.expressions.Evaluator; import org.apache.iceberg.expressions.Expression; -import org.apache.iceberg.expressions.Expressions; import org.apache.iceberg.hadoop.HadoopInputFile; import org.apache.iceberg.io.CloseableIterable; import org.apache.iceberg.io.InputFile; -import org.apache.iceberg.mapping.MappingUtil; import org.apache.iceberg.mapping.NameMapping; import org.apache.iceberg.mapping.NameMappingParser; import org.apache.iceberg.parquet.ParquetReader; -import org.apache.iceberg.types.Type; -import org.apache.iceberg.types.TypeUtil; -import org.apache.iceberg.util.PartitionUtil; import org.apache.iceberg.util.SnapshotUtil; import org.apache.parquet.HadoopReadOptions; import org.apache.parquet.ParquetReadOptions; @@ -73,13 +65,34 @@ public class ReadUtils { "parquet.read.support.class", "parquet.crypto.factory.class"); - static ParquetReader createReader(FileScanTask task, Table table, Schema schema) { - String filePath = task.file().path().toString(); + public static CloseableIterable createReader( + ContentScanTask task, Table table, IcebergScanConfig scanConfig) { + return createReader( + table, + scanConfig, + scanConfig.getRequiredSchema(), + task.spec(), + task.file(), + null, + task.start(), + task.length(), + task.residual()); + } + + public static CloseableIterable createReader( + Table table, + IcebergScanConfig scanConfig, + Schema requiredSchema, + PartitionSpec spec, + ContentFile file, + @Nullable Long fileSequenceNumber, + long start, + long length, + Expression residual) { EncryptedInputFile encryptedInput = - EncryptedFiles.encryptedInput(table.io().newInputFile(filePath), task.file().keyMetadata()); + EncryptedFiles.encryptedInput(table.io().newInputFile(file.location()), file.keyMetadata()); InputFile inputFile = table.encryption().decrypt(encryptedInput); - Map idToConstants = - ReadUtils.constantsMap(task, IdentityPartitionConverters::convertConstant, table.schema()); + Map idToConstants = PartitionUtils.constantsMap(spec, file, fileSequenceNumber); ParquetReadOptions.Builder optionsBuilder; if (inputFile instanceof HadoopInputFile) { @@ -94,71 +107,30 @@ static ParquetReader createReader(FileScanTask task, Table table, Schema } optionsBuilder = optionsBuilder - .withRange(task.start(), task.start() + task.length()) + .withRange(start, start + length) .withMaxAllocationInBytes(MAX_FILE_BUFFER_SIZE); @Nullable String nameMapping = table.properties().get(TableProperties.DEFAULT_NAME_MAPPING); NameMapping mapping = nameMapping != null ? NameMappingParser.fromJson(nameMapping) : NameMapping.empty(); - return new ParquetReader<>( - inputFile, - schema, - optionsBuilder.build(), - // TODO(ahmedabu98): Implement a Parquet-to-Beam Row reader, bypassing conversion to Iceberg - // Record - fileSchema -> GenericParquetReaders.buildReader(schema, fileSchema, idToConstants), - mapping, - task.residual(), - false, - true); + ParquetReader records = + new ParquetReader<>( + inputFile, + requiredSchema, + optionsBuilder.build(), + // TODO(ahmedabu98): Implement a Parquet-to-Beam Row reader, bypassing conversion to + // Iceberg Record + fileSchema -> + GenericParquetReaders.buildReader(requiredSchema, fileSchema, idToConstants), + mapping, + residual, + false, + true); + return maybeApplyFilter(records, scanConfig, requiredSchema); } - static ParquetReader createReader(InputFile inputFile, Schema schema) { - ParquetReadOptions.Builder optionsBuilder; - if (inputFile instanceof HadoopInputFile) { - // remove read properties already set that may conflict with this read - Configuration conf = new Configuration(((HadoopInputFile) inputFile).getConf()); - for (String property : READ_PROPERTIES_TO_REMOVE) { - conf.unset(property); - } - optionsBuilder = HadoopReadOptions.builder(conf); - } else { - optionsBuilder = ParquetReadOptions.builder(); - } - optionsBuilder = - optionsBuilder - .withRange(0, inputFile.getLength()) - .withMaxAllocationInBytes(MAX_FILE_BUFFER_SIZE); - - return new ParquetReader<>( - inputFile, - schema, - optionsBuilder.build(), - fileSchema -> GenericParquetReaders.buildReader(schema, fileSchema), - MappingUtil.create(schema), - Expressions.alwaysTrue(), - false, - true); - } - - static Map constantsMap( - FileScanTask task, - BiFunction converter, - org.apache.iceberg.Schema schema) { - PartitionSpec spec = task.spec(); - Set idColumns = spec.identitySourceIds(); - org.apache.iceberg.Schema partitionSchema = TypeUtil.select(schema, idColumns); - boolean projectsIdentityPartitionColumns = !partitionSchema.columns().isEmpty(); - - if (projectsIdentityPartitionColumns) { - return PartitionUtil.constantsMap(task, converter); - } else { - return Collections.emptyMap(); - } - } - - static @Nullable Long getFromSnapshotExclusive(Table table, IcebergScanConfig scanConfig) { + public static @Nullable Long getFromSnapshotInclusive(Table table, IcebergScanConfig scanConfig) { @Nullable StartingStrategy startingStrategy = scanConfig.getStartingStrategy(); boolean isStreaming = MoreObjects.firstNonNull(scanConfig.getStreaming(), false); if (startingStrategy == null) { @@ -179,6 +151,13 @@ static ParquetReader createReader(InputFile inputFile, Schema schema) { fromSnapshot = currentSnapshot.snapshotId(); } } + + return fromSnapshot; + } + + public static @Nullable Long getFromSnapshotExclusive(Table table, IcebergScanConfig scanConfig) { + @Nullable Long fromSnapshot = getFromSnapshotInclusive(table, scanConfig); + // incremental append scan can only be configured with an *exclusive* starting snapshot, // so we need to provide this snapshot's parent id. if (fromSnapshot != null) { @@ -189,7 +168,7 @@ static ParquetReader createReader(InputFile inputFile, Schema schema) { return fromSnapshot; } - static @Nullable Long getToSnapshot(Table table, IcebergScanConfig scanConfig) { + public static @Nullable Long getToSnapshot(Table table, IcebergScanConfig scanConfig) { // 1. fetch from to_snapshot @Nullable Long toSnapshot = scanConfig.getToSnapshot(); // 2. fetch from to_timestamp @@ -205,7 +184,7 @@ static ParquetReader createReader(InputFile inputFile, Schema schema) { * Returns a list of snapshots in the range (fromSnapshotId, toSnapshotId], ordered * chronologically. */ - static List snapshotsBetween( + public static List snapshotsBetween( Table table, String tableIdentifier, @Nullable Long fromSnapshotId, long toSnapshotId) { long from = MoreObjects.firstNonNull(fromSnapshotId, -1L); @SuppressWarnings("return") @@ -225,10 +204,14 @@ static List snapshotsBetween( public static CloseableIterable maybeApplyFilter( CloseableIterable iterable, IcebergScanConfig scanConfig) { - InternalRecordWrapper wrapper = - new InternalRecordWrapper(scanConfig.getRequiredSchema().asStruct()); + return maybeApplyFilter(iterable, scanConfig, scanConfig.getRequiredSchema()); + } + + public static CloseableIterable maybeApplyFilter( + CloseableIterable iterable, IcebergScanConfig scanConfig, Schema requiredSchema) { + InternalRecordWrapper wrapper = new InternalRecordWrapper(requiredSchema.asStruct()); Expression filter = scanConfig.getFilter(); - Evaluator evaluator = scanConfig.getEvaluator(); + Evaluator evaluator = scanConfig.getEvaluator(requiredSchema); if (filter != null && evaluator != null && filter.op() != Expression.Operation.TRUE) { return CloseableIterable.filter(iterable, record -> evaluator.eval(wrapper.wrap(record))); } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java index 82251c00e72e..fd3d5d63327c 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriter.java @@ -23,6 +23,7 @@ import org.apache.iceberg.DataFile; import org.apache.iceberg.FileFormat; import org.apache.iceberg.PartitionKey; +import org.apache.iceberg.StructLike; import org.apache.iceberg.Table; import org.apache.iceberg.avro.Avro; import org.apache.iceberg.catalog.Catalog; @@ -56,7 +57,7 @@ class RecordWriter { partitionKey); } - RecordWriter(Table table, FileFormat fileFormat, String filename, PartitionKey partitionKey) + RecordWriter(Table table, FileFormat fileFormat, String filename, StructLike partitionKey) throws IOException { this.table = table; this.fileFormat = fileFormat; diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java index 2f532a08754c..1e25e6b9c234 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/RecordWriterManager.java @@ -21,8 +21,6 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; import java.io.IOException; -import java.time.Duration; -import java.time.Instant; import java.time.LocalDateTime; import java.time.YearMonth; import java.time.ZoneOffset; @@ -248,7 +246,7 @@ static String getPartitionDataPath( DateTimeFormatter.ofPattern("yyyy-MM-dd-HH"); private static final LocalDateTime EPOCH = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC); - private final Catalog catalog; + private final IcebergCatalogConfig catalogConfig; private final String filePrefix; private final long maxFileSize; private final int maxNumWriters; @@ -260,46 +258,11 @@ static String getPartitionDataPath( private final Map, List> totalSerializableDataFiles = Maps.newHashMap(); - static final class LastRefreshedTable { - final Table table; - volatile Instant lastRefreshTime; - static final Duration STALENESS_THRESHOLD = Duration.ofMinutes(2); - - LastRefreshedTable(Table table, Instant lastRefreshTime) { - this.table = table; - this.lastRefreshTime = lastRefreshTime; - } - - /** - * Refreshes the table metadata if it is considered stale (older than 2 minutes). - * - *

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

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

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

    Note that this is a best-effort operation that depends on the {@link Catalog} * implementation. Although it is expected, some implementations may not support creating a table @@ -319,13 +282,13 @@ void refreshIfStale() { @VisibleForTesting Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) { TableIdentifier identifier = destination.getTableIdentifier(); - @Nullable - LastRefreshedTable lastRefreshedTable = LAST_REFRESHED_TABLE_CACHE.getIfPresent(identifier); - if (lastRefreshedTable != null && lastRefreshedTable.table != null) { - lastRefreshedTable.refreshIfStale(); - return lastRefreshedTable.table; - } + return TableCache.getAndRefreshIfStale( + catalogConfig, identifier, () -> loadOrCreateTable(destination, dataSchema)); + } + private Table loadOrCreateTable(IcebergDestination destination, Schema dataSchema) { + Catalog catalog = catalogConfig.catalog(); + TableIdentifier identifier = destination.getTableIdentifier(); Namespace namespace = identifier.namespace(); @Nullable IcebergTableCreateConfig createConfig = destination.getTableCreateConfig(); PartitionSpec partitionSpec = @@ -336,53 +299,48 @@ Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) { ? createConfig.getTableProperties() : Maps.newHashMap(); - @Nullable Table table = null; - synchronized (LAST_REFRESHED_TABLE_CACHE) { - // Create namespace if it does not exist yet - if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) { - SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog; - if (!supportsNamespaces.namespaceExists(namespace)) { - try { - supportsNamespaces.createNamespace(namespace); - LOG.info("Created new namespace '{}'.", namespace); - } catch (AlreadyExistsException ignored) { - // race condition: another worker already created this namespace - } + // Create namespace if it does not exist yet + if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) { + SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog; + if (!supportsNamespaces.namespaceExists(namespace)) { + try { + supportsNamespaces.createNamespace(namespace); + LOG.info("Created new namespace '{}'.", namespace); + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this namespace } } + } - // If table exists, just load it - // Note: the implementation of catalog.tableExists() will load the table to check its - // existence. We don't use it here to avoid double loadTable() calls. + // If table exists, just load it + // Note: the implementation of catalog.tableExists() will load the table to check its + // existence. We don't use it here to avoid double loadTable() calls. + try { + return catalog.loadTable(identifier); + } catch (NoSuchTableException e) { // Otherwise, create the table + org.apache.iceberg.Schema tableSchema = IcebergUtils.beamSchemaToIcebergSchema(dataSchema); try { - table = catalog.loadTable(identifier); - } catch (NoSuchTableException e) { // Otherwise, create the table - org.apache.iceberg.Schema tableSchema = IcebergUtils.beamSchemaToIcebergSchema(dataSchema); - try { - table = - catalog - .buildTable(identifier, tableSchema) - .withPartitionSpec(partitionSpec) - .withSortOrder(sortOrder) - .withProperties(tableProperties) - .create(); - LOG.info( - "Created Iceberg table '{}' with schema: {}\n" - + ", partition spec: {}, sort order: {}, table properties: {}", - identifier, - tableSchema, - partitionSpec, - sortOrder, - tableProperties); - } catch (AlreadyExistsException ignored) { - // race condition: another worker already created this table - table = catalog.loadTable(identifier); - } + Table table = + catalog + .buildTable(identifier, tableSchema) + .withPartitionSpec(partitionSpec) + .withSortOrder(sortOrder) + .withProperties(tableProperties) + .create(); + LOG.info( + "Created Iceberg table '{}' with schema: {}\n" + + ", partition spec: {}, sort order: {}, table properties: {}", + identifier, + tableSchema, + partitionSpec, + sortOrder, + tableProperties); + return table; + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this table + return catalog.loadTable(identifier); } } - lastRefreshedTable = new LastRefreshedTable(table, Instant.now()); - LAST_REFRESHED_TABLE_CACHE.put(identifier, lastRefreshedTable); - return table; } /** diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java index 19218b85b63c..c407ef8d3e2d 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanSource.java @@ -47,7 +47,8 @@ public ScanSource(IcebergScanConfig scanConfig) { } private TableScan getTableScan() { - Table table = scanConfig.getTable(); + Table table = + TableCache.getRefreshed(scanConfig.getCatalogConfig(), scanConfig.getTableIdentifier()); TableScan tableScan = table.newScan().project(scanConfig.getProjectedSchema()); if (scanConfig.getFilter() != null) { diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanTaskReader.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanTaskReader.java index b3485a7bcc4f..c9ad372a0751 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanTaskReader.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ScanTaskReader.java @@ -36,7 +36,6 @@ import org.apache.iceberg.TableProperties; import org.apache.iceberg.avro.Avro; import org.apache.iceberg.data.GenericDeleteFilter; -import org.apache.iceberg.data.IdentityPartitionConverters; import org.apache.iceberg.data.Record; import org.apache.iceberg.data.avro.DataReader; import org.apache.iceberg.data.orc.GenericOrcReader; @@ -121,8 +120,7 @@ public boolean advance() throws IOException { DataFile file = fileTask.file(); InputFile input = decryptor.getInputFile(fileTask); Map idToConstants = - ReadUtils.constantsMap( - fileTask, IdentityPartitionConverters::convertConstant, requiredSchema); + PartitionUtils.constantsMap(fileTask.spec(), fileTask.file(), null); CloseableIterable iterable; switch (file.format()) { diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java index 1f717b82c21e..e1291601d149 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDataFile.java @@ -54,13 +54,13 @@ */ @DefaultSchema(AutoValueSchema.class) @AutoValue -abstract class SerializableDataFile { +public abstract class SerializableDataFile { public static Builder builder() { return new AutoValue_SerializableDataFile.Builder(); } @SchemaFieldNumber("0") - abstract String getPath(); + public abstract String getPath(); @SchemaFieldNumber("1") abstract String getFileFormat(); @@ -69,10 +69,10 @@ public static Builder builder() { abstract long getRecordCount(); @SchemaFieldNumber("3") - abstract long getFileSizeInBytes(); + public abstract long getFileSizeInBytes(); @SchemaFieldNumber("4") - abstract String getPartitionPath(); + public abstract String getPartitionPath(); @SchemaFieldNumber("5") abstract int getPartitionSpecId(); @@ -96,13 +96,22 @@ public static Builder builder() { abstract @Nullable Map getNanValueCounts(); @SchemaFieldNumber("12") - abstract @Nullable Map getLowerBounds(); + public abstract @Nullable Map getLowerBounds(); @SchemaFieldNumber("13") - abstract @Nullable Map getUpperBounds(); + public abstract @Nullable Map getUpperBounds(); + + @SchemaFieldNumber("14") + public abstract @Nullable Long getDataSequenceNumber(); + + @SchemaFieldNumber("15") + public abstract @Nullable Long getFileSequenceNumber(); + + @SchemaFieldNumber("16") + public abstract @Nullable Long getFirstRowId(); @AutoValue.Builder - abstract static class Builder { + public abstract static class Builder { abstract Builder setPath(String path); abstract Builder setFileFormat(String fileFormat); @@ -131,31 +140,49 @@ abstract static class Builder { abstract Builder setUpperBounds(@Nullable Map upperBounds); + abstract Builder setDataSequenceNumber(@Nullable Long number); + + abstract Builder setFileSequenceNumber(@Nullable Long number); + + abstract Builder setFirstRowId(@Nullable Long id); + abstract SerializableDataFile build(); } + public static SerializableDataFile from(DataFile f, String partitionPath) { + return from(f, partitionPath, true); + } + /** * Create a {@link SerializableDataFile} from a {@link DataFile} and its associated {@link * PartitionKey}. */ - static SerializableDataFile from(DataFile f, String partitionPath) { - - return SerializableDataFile.builder() - .setPath(f.location().toString()) - .setFileFormat(f.format().toString()) - .setRecordCount(f.recordCount()) - .setFileSizeInBytes(f.fileSizeInBytes()) - .setPartitionPath(partitionPath) - .setPartitionSpecId(f.specId()) - .setKeyMetadata(f.keyMetadata()) - .setSplitOffsets(f.splitOffsets()) - .setColumnSizes(f.columnSizes()) - .setValueCounts(f.valueCounts()) - .setNullValueCounts(f.nullValueCounts()) - .setNanValueCounts(f.nanValueCounts()) - .setLowerBounds(toByteArrayMap(f.lowerBounds())) - .setUpperBounds(toByteArrayMap(f.upperBounds())) - .build(); + public static SerializableDataFile from( + DataFile f, String partitionPath, boolean includeMetrics) { + SerializableDataFile.Builder builder = + SerializableDataFile.builder() + .setPath(f.location()) + .setFileFormat(f.format().toString()) + .setRecordCount(f.recordCount()) + .setFileSizeInBytes(f.fileSizeInBytes()) + .setPartitionPath(partitionPath) + .setPartitionSpecId(f.specId()) + .setKeyMetadata(f.keyMetadata()) + .setSplitOffsets(f.splitOffsets()) + .setColumnSizes(f.columnSizes()) + .setValueCounts(f.valueCounts()) + .setNullValueCounts(f.nullValueCounts()) + .setNanValueCounts(f.nanValueCounts()) + .setDataSequenceNumber(f.dataSequenceNumber()) + .setFileSequenceNumber(f.fileSequenceNumber()) + .setFirstRowId(f.firstRowId()); + if (includeMetrics) { + builder = + builder + .setLowerBounds(toByteArrayMap(f.lowerBounds())) + .setUpperBounds(toByteArrayMap(f.upperBounds())); + } + return builder.build(); } /** @@ -165,7 +192,7 @@ static SerializableDataFile from(DataFile f, String partitionPath) { * it from Beam-compatible types. */ @SuppressWarnings("nullness") - DataFile createDataFile(Map partitionSpecs) { + public DataFile createDataFile(Map partitionSpecs) { PartitionSpec partitionSpec = checkStateNotNull( partitionSpecs.get(getPartitionSpecId()), @@ -192,26 +219,37 @@ DataFile createDataFile(Map partitionSpecs) { .withFileSizeInBytes(getFileSizeInBytes()) .withMetrics(dataFileMetrics) .withSplitOffsets(getSplitOffsets()) + .withFirstRowId(getFirstRowId()) .build(); } // ByteBuddyUtils has trouble converting Map value type ByteBuffer // to byte[] and back to ByteBuffer, so we perform these conversions manually // TODO(https://github.com/apache/beam/issues/32701) - private static @Nullable Map toByteArrayMap( - @Nullable Map input) { + static @Nullable Map toByteArrayMap(@Nullable Map input) { if (input == null) { return null; } 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; } @@ -244,10 +282,13 @@ && getPartitionSpecId() == that.getPartitionSpecId() && Objects.equals(getNullValueCounts(), that.getNullValueCounts()) && Objects.equals(getNanValueCounts(), that.getNanValueCounts()) && mapEquals(getLowerBounds(), that.getLowerBounds()) - && mapEquals(getUpperBounds(), that.getUpperBounds()); + && mapEquals(getUpperBounds(), that.getUpperBounds()) + && Objects.equals(getDataSequenceNumber(), that.getDataSequenceNumber()) + && Objects.equals(getFileSequenceNumber(), that.getFileSequenceNumber()) + && Objects.equals(getFirstRowId(), that.getFirstRowId()); } - private static boolean mapEquals( + static boolean mapEquals( @Nullable Map map1, @Nullable Map map2) { if (map1 == null && map2 == null) { return true; @@ -285,13 +326,16 @@ public final int hashCode() { getColumnSizes(), getValueCounts(), getNullValueCounts(), - getNanValueCounts()); + getNanValueCounts(), + getDataSequenceNumber(), + getFileSequenceNumber(), + getFirstRowId()); hashCode = 31 * hashCode + computeMapByteHashCode(getLowerBounds()); hashCode = 31 * hashCode + computeMapByteHashCode(getUpperBounds()); return hashCode; } - private static int computeMapByteHashCode(@Nullable Map map) { + static int computeMapByteHashCode(@Nullable Map map) { if (map == null) { return 0; } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFile.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFile.java new file mode 100644 index 000000000000..ceb96d50f8aa --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableDeleteFile.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.apache.beam.sdk.io.iceberg.SerializableDataFile.computeMapByteHashCode; +import static org.apache.beam.sdk.io.iceberg.SerializableDataFile.mapEquals; +import static org.apache.beam.sdk.io.iceberg.SerializableDataFile.toByteArrayMap; +import static org.apache.beam.sdk.io.iceberg.SerializableDataFile.toByteBufferMap; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import com.google.auto.value.AutoValue; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileContent; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.FileMetadata; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.SortOrder; +import org.checkerframework.checker.nullness.qual.Nullable; + +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class SerializableDeleteFile { + public static SerializableDeleteFile.Builder builder() { + return new AutoValue_SerializableDeleteFile.Builder(); + } + + @SchemaFieldNumber("0") + public abstract FileContent getContentType(); + + @SchemaFieldNumber("1") + public abstract String getLocation(); + + @SchemaFieldNumber("2") + public abstract String getFileFormat(); + + @SchemaFieldNumber("3") + public abstract long getRecordCount(); + + @SchemaFieldNumber("4") + public abstract long getFileSizeInBytes(); + + @SchemaFieldNumber("5") + public abstract String getPartitionPath(); + + @SchemaFieldNumber("6") + public abstract int getPartitionSpecId(); + + @SchemaFieldNumber("7") + public abstract @Nullable Integer getSortOrderId(); + + @SchemaFieldNumber("8") + public abstract @Nullable List getEqualityFieldIds(); + + @SchemaFieldNumber("9") + public abstract @Nullable ByteBuffer getKeyMetadata(); + + @SchemaFieldNumber("10") + public abstract @Nullable List getSplitOffsets(); + + @SchemaFieldNumber("11") + public abstract @Nullable Map getColumnSizes(); + + @SchemaFieldNumber("12") + public abstract @Nullable Map getValueCounts(); + + @SchemaFieldNumber("13") + public abstract @Nullable Map getNullValueCounts(); + + @SchemaFieldNumber("14") + public abstract @Nullable Map getNanValueCounts(); + + @SchemaFieldNumber("15") + public abstract @Nullable Map getLowerBounds(); + + @SchemaFieldNumber("16") + public abstract @Nullable Map getUpperBounds(); + + @SchemaFieldNumber("17") + public abstract @Nullable Long getContentOffset(); + + @SchemaFieldNumber("18") + public abstract @Nullable Long getContentSizeInBytes(); + + @SchemaFieldNumber("19") + public abstract @Nullable String getReferencedDataFile(); + + @SchemaFieldNumber("20") + public abstract @Nullable Long getDataSequenceNumber(); + + @SchemaFieldNumber("21") + public abstract @Nullable Long getFileSequenceNumber(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setContentType(FileContent content); + + abstract Builder setLocation(String path); + + abstract Builder setFileFormat(String fileFormat); + + abstract Builder setRecordCount(long recordCount); + + abstract Builder setFileSizeInBytes(long fileSizeInBytes); + + abstract Builder setPartitionPath(String partitionPath); + + abstract Builder setPartitionSpecId(int partitionSpec); + + abstract Builder setSortOrderId(@Nullable Integer sortOrderId); + + abstract Builder setEqualityFieldIds(List equalityFieldIds); + + abstract Builder setKeyMetadata(ByteBuffer keyMetadata); + + abstract Builder setSplitOffsets(List splitOffsets); + + abstract Builder setColumnSizes(Map columnSizes); + + abstract Builder setValueCounts(Map valueCounts); + + abstract Builder setNullValueCounts(Map nullValueCounts); + + abstract Builder setNanValueCounts(Map nanValueCounts); + + abstract Builder setLowerBounds(@Nullable Map lowerBounds); + + abstract Builder setUpperBounds(@Nullable Map upperBounds); + + abstract Builder setContentOffset(@Nullable Long offset); + + abstract Builder setContentSizeInBytes(@Nullable Long sizeInBytes); + + abstract Builder setReferencedDataFile(@Nullable String dataFile); + + abstract Builder setDataSequenceNumber(@Nullable Long number); + + abstract Builder setFileSequenceNumber(@Nullable Long number); + + abstract SerializableDeleteFile build(); + } + + public static SerializableDeleteFile from( + DeleteFile deleteFile, String partitionPath, boolean includeMetrics) { + + SerializableDeleteFile.Builder builder = + SerializableDeleteFile.builder() + .setLocation(deleteFile.location()) + .setFileFormat(deleteFile.format().name()) + .setFileSizeInBytes(deleteFile.fileSizeInBytes()) + .setPartitionPath(partitionPath) + .setPartitionSpecId(deleteFile.specId()) + .setRecordCount(deleteFile.recordCount()) + .setColumnSizes(deleteFile.columnSizes()) + .setValueCounts(deleteFile.valueCounts()) + .setNullValueCounts(deleteFile.nullValueCounts()) + .setNanValueCounts(deleteFile.nanValueCounts()) + .setSplitOffsets(deleteFile.splitOffsets()) + .setKeyMetadata(deleteFile.keyMetadata()) + .setEqualityFieldIds(deleteFile.equalityFieldIds()) + .setSortOrderId(deleteFile.sortOrderId()) + .setContentOffset(deleteFile.contentOffset()) + .setContentSizeInBytes(deleteFile.contentSizeInBytes()) + .setReferencedDataFile(deleteFile.referencedDataFile()) + .setContentType(deleteFile.content()) + .setDataSequenceNumber(deleteFile.dataSequenceNumber()) + .setFileSequenceNumber(deleteFile.fileSequenceNumber()); + + if (includeMetrics) { + builder = + builder + .setLowerBounds(toByteArrayMap(deleteFile.lowerBounds())) + .setUpperBounds(toByteArrayMap(deleteFile.upperBounds())); + } + + return builder.build(); + } + + @SuppressWarnings("nullness") + public DeleteFile createDeleteFile( + Map partitionSpecs, @Nullable Map sortOrders) { + PartitionSpec partitionSpec = + checkStateNotNull( + partitionSpecs.get(getPartitionSpecId()), + "This DeleteFile was originally created with spec id '%s', " + + "but table only has spec ids: %s.", + getPartitionSpecId(), + partitionSpecs.keySet()); + + Metrics metrics = + new Metrics( + getRecordCount(), + getColumnSizes(), + getValueCounts(), + getNullValueCounts(), + getNanValueCounts(), + toByteBufferMap(getLowerBounds()), + toByteBufferMap(getUpperBounds())); + + FileMetadata.Builder deleteFileBuilder = + FileMetadata.deleteFileBuilder(partitionSpec) + .withPath(getLocation()) + .withFormat(getFileFormat()) + .withFileSizeInBytes(getFileSizeInBytes()) + .withRecordCount(getRecordCount()) + .withMetrics(metrics) + .withSplitOffsets(getSplitOffsets()) + .withEncryptionKeyMetadata(getKeyMetadata()) + .withPartitionPath(getPartitionPath()); + + switch (getContentType()) { + case POSITION_DELETES: + deleteFileBuilder = deleteFileBuilder.ofPositionDeletes(); + break; + case EQUALITY_DELETES: + List fieldIds = getEqualityFieldIds(); + int[] equalityFieldIds = new int[fieldIds != null ? fieldIds.size() : 0]; + if (fieldIds != null) { + for (int i = 0; i < fieldIds.size(); i++) { + equalityFieldIds[i] = fieldIds.get(i); + } + } + SortOrder sortOrder = SortOrder.unsorted(); + if (sortOrders != null) { + sortOrder = + checkStateNotNull( + sortOrders.get(getSortOrderId()), + "This DeleteFile was originally created with sort order id '%s', " + + "but table only has sort order ids: %s.", + getSortOrderId(), + sortOrders.keySet()); + } + deleteFileBuilder = + deleteFileBuilder.ofEqualityDeletes(equalityFieldIds).withSortOrder(sortOrder); + break; + default: + throw new IllegalStateException( + "Unexpected content type for DeleteFile: " + getContentType()); + } + + // needed for puffin files + if (getFileFormat().equalsIgnoreCase(FileFormat.PUFFIN.name())) { + deleteFileBuilder = + deleteFileBuilder + .withContentOffset(checkStateNotNull(getContentOffset())) + .withContentSizeInBytes(checkStateNotNull(getContentSizeInBytes())) + .withReferencedDataFile(checkStateNotNull(getReferencedDataFile())); + } + return deleteFileBuilder.build(); + } + + @Override + public final boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SerializableDeleteFile)) { + return false; + } + SerializableDeleteFile that = (SerializableDeleteFile) o; + return getContentType().equals(that.getContentType()) + && getLocation().equals(that.getLocation()) + && getFileFormat().equals(that.getFileFormat()) + && getRecordCount() == that.getRecordCount() + && getFileSizeInBytes() == that.getFileSizeInBytes() + && getPartitionPath().equals(that.getPartitionPath()) + && getPartitionSpecId() == that.getPartitionSpecId() + && Objects.equals(getSortOrderId(), that.getSortOrderId()) + && Objects.equals(getEqualityFieldIds(), that.getEqualityFieldIds()) + && Objects.equals(getKeyMetadata(), that.getKeyMetadata()) + && Objects.equals(getSplitOffsets(), that.getSplitOffsets()) + && Objects.equals(getColumnSizes(), that.getColumnSizes()) + && Objects.equals(getValueCounts(), that.getValueCounts()) + && Objects.equals(getNullValueCounts(), that.getNullValueCounts()) + && Objects.equals(getNanValueCounts(), that.getNanValueCounts()) + && mapEquals(getLowerBounds(), that.getLowerBounds()) + && mapEquals(getUpperBounds(), that.getUpperBounds()) + && Objects.equals(getContentOffset(), that.getContentOffset()) + && Objects.equals(getContentSizeInBytes(), that.getContentSizeInBytes()) + && Objects.equals(getReferencedDataFile(), that.getReferencedDataFile()) + && Objects.equals(getDataSequenceNumber(), that.getDataSequenceNumber()) + && Objects.equals(getFileSequenceNumber(), that.getFileSequenceNumber()); + } + + @Override + public final int hashCode() { + int hashCode = + Objects.hash( + getContentType(), + getLocation(), + getFileFormat(), + getRecordCount(), + getFileSizeInBytes(), + getPartitionPath(), + getPartitionSpecId(), + getSortOrderId(), + getEqualityFieldIds(), + getKeyMetadata(), + getSplitOffsets(), + getColumnSizes(), + getValueCounts(), + getNullValueCounts(), + getNanValueCounts(), + getContentOffset(), + getContentSizeInBytes(), + getReferencedDataFile(), + getDataSequenceNumber(), + getFileSequenceNumber()); + hashCode = 31 * hashCode + computeMapByteHashCode(getLowerBounds()); + hashCode = 31 * hashCode + computeMapByteHashCode(getUpperBounds()); + return hashCode; + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/TableCache.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/TableCache.java index cb00d90f7fb3..e95a6a5f66fd 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/TableCache.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/TableCache.java @@ -17,70 +17,170 @@ */ package org.apache.beam.sdk.io.iceberg; -import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; -import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.Cache; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheBuilder; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.CacheLoader; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.cache.LoadingCache; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.Futures; -import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.ListenableFuture; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.util.concurrent.UncheckedExecutionException; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; +import org.checkerframework.checker.nullness.qual.Nullable; -/** Utility to fetch and cache Iceberg {@link Table}s. */ +/** + * Process-wide cache for Iceberg {@link Table}s. + * + *

    Entries are keyed by catalog configuration and table identifier, so one machine can share + * table metadata across source and sink threads without colliding when different catalogs contain + * the same identifier. The underlying catalog is only resolved from {@link IcebergCatalogConfig} + * when the table has to be loaded. Refreshes are synchronized per table entry: if another thread + * refreshed after a caller started its request, the caller reuses that refresh instead of making + * another catalog call. + */ class TableCache { - private static final Map CATALOG_CACHE = new ConcurrentHashMap<>(); - private static final LoadingCache INTERNAL_CACHE = - CacheBuilder.newBuilder() - .expireAfterAccess(1, TimeUnit.HOURS) - .refreshAfterWrite(3, TimeUnit.MINUTES) - .build( - new CacheLoader() { - @Override - public Table load(String identifier) { - return checkStateNotNull(CATALOG_CACHE.get(identifier)) - .catalog() - .loadTable(TableIdentifier.parse(identifier)); - } - - @Override - public ListenableFuture reload(String unusedIdentifier, Table table) { - table.refresh(); - return Futures.immediateFuture(table); - } - });; - - static Table get(String identifier) { + static final Duration DEFAULT_REFRESH_INTERVAL = Duration.ofMinutes(2); + + private static final Cache TABLES = + CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.HOURS).build(); + + /** Returns the cached table, loading it from the catalog on a cache miss. */ + static Table get(IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + return get(catalogConfig, identifier, () -> catalogConfig.catalog().loadTable(identifier)); + } + + /** Returns the cached table for a string identifier, loading it on a cache miss. */ + static Table get(IcebergCatalogConfig catalogConfig, String identifier) { + return get(catalogConfig, TableIdentifier.parse(identifier)); + } + + /** Returns the cached table, using the given loader only on a cache miss. */ + static Table get( + IcebergCatalogConfig catalogConfig, TableIdentifier identifier, Callable
    loader) { + return getEntry(catalogConfig, identifier, loader).table; + } + + /** Returns the cached table after forcing a refresh of any pre-existing cache entry. */ + static Table getRefreshed(IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + Instant refreshRequestTime = Instant.now(); + CachedTable cachedTable = + getEntry(catalogConfig, identifier, () -> catalogConfig.catalog().loadTable(identifier)); + cachedTable.refreshIfOlderThan(refreshRequestTime); + return cachedTable.table; + } + + /** Returns the cached table for a string identifier after refreshing any pre-existing entry. */ + static Table getRefreshed(IcebergCatalogConfig catalogConfig, String identifier) { + return getRefreshed(catalogConfig, TableIdentifier.parse(identifier)); + } + + /** + * Returns the cached table, refreshing it only if it is older than {@link + * #DEFAULT_REFRESH_INTERVAL}. + */ + static Table getAndRefreshIfStale( + IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + return getAndRefreshIfStale( + catalogConfig, identifier, () -> catalogConfig.catalog().loadTable(identifier)); + } + + /** Returns the cached table, using the loader on a miss and refreshing stale entries. */ + static Table getAndRefreshIfStale( + IcebergCatalogConfig catalogConfig, TableIdentifier identifier, Callable
    loader) { + CachedTable cachedTable = getEntry(catalogConfig, identifier, loader); + cachedTable.refreshIfOlderThan(Instant.now().minus(DEFAULT_REFRESH_INTERVAL)); + return cachedTable.table; + } + + private static CachedTable getEntry( + IcebergCatalogConfig catalogConfig, TableIdentifier identifier, Callable
    loader) { + CacheKey key = new CacheKey(catalogConfig, identifier); try { - return INTERNAL_CACHE.get(identifier); - } catch (ExecutionException e) { + return TABLES.get(key, () -> new CachedTable(loader.call(), Instant.now())); + } catch (ExecutionException | UncheckedExecutionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } throw new RuntimeException( "Encountered a problem fetching table " + identifier + " from cache.", e); } } - /** Forces a table refresh and returns. */ - static Table getRefreshed(String identifier) { - INTERNAL_CACHE.refresh(identifier); - return get(identifier); + @VisibleForTesting + static long size() { + return TABLES.size(); } - static void setup(IcebergScanConfig scanConfig) { - String tableIdentifier = scanConfig.getTableIdentifier(); - IcebergCatalogConfig catalogConfig = scanConfig.getCatalogConfig(); - if (CATALOG_CACHE.containsKey(tableIdentifier)) { - checkState( - catalogConfig.equals(CATALOG_CACHE.get(tableIdentifier)), - "TableCache is already set up with a different catalog. " + "Existing: %s, new: %s.", - CATALOG_CACHE.get(tableIdentifier), - catalogConfig); - } else { - CATALOG_CACHE.put(scanConfig.getTableIdentifier(), scanConfig.getCatalogConfig()); + @VisibleForTesting + static void invalidateAll() { + TABLES.invalidateAll(); + } + + @VisibleForTesting + static void put( + IcebergCatalogConfig catalogConfig, + TableIdentifier identifier, + Table table, + Instant lastRefreshTime) { + TABLES.put(new CacheKey(catalogConfig, identifier), new CachedTable(table, lastRefreshTime)); + } + + @VisibleForTesting + static void markStale(IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + CachedTable cachedTable = TABLES.getIfPresent(new CacheKey(catalogConfig, identifier)); + if (cachedTable != null) { + cachedTable.lastRefreshTime = Instant.EPOCH; + } + } + + private static class CachedTable { + private final Table table; + private volatile Instant lastRefreshTime; + + private CachedTable(Table table, Instant lastRefreshTime) { + this.table = table; + this.lastRefreshTime = lastRefreshTime; + } + + private void refreshIfOlderThan(Instant refreshRequestTime) { + if (lastRefreshTime.isAfter(refreshRequestTime)) { + return; + } + synchronized (this) { + if (lastRefreshTime.isBefore(refreshRequestTime)) { + table.refresh(); + lastRefreshTime = Instant.now(); + } + } + } + } + + private static class CacheKey { + private final IcebergCatalogConfig catalogConfig; + private final TableIdentifier identifier; + + private CacheKey(IcebergCatalogConfig catalogConfig, TableIdentifier identifier) { + this.catalogConfig = catalogConfig; + this.identifier = identifier; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CacheKey)) { + return false; + } + CacheKey other = (CacheKey) obj; + return catalogConfig.equals(other.catalogConfig) && identifier.equals(other.identifier); + } + + @Override + public int hashCode() { + return 31 * catalogConfig.hashCode() + identifier.hashCode(); } } } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java index 1af5588c2519..8bd436c55700 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WatchForSnapshots.java @@ -87,7 +87,6 @@ public PCollection>> expand(PBegin input) { private static class SnapshotPollFn extends Watch.Growth.PollFn> { private final IcebergScanConfig scanConfig; private @Nullable Long fromSnapshotId; - boolean isCacheSetup = false; SnapshotPollFn(IcebergScanConfig scanConfig) { this.scanConfig = scanConfig; @@ -95,11 +94,7 @@ private static class SnapshotPollFn extends Watch.Growth.PollFn> apply(String tableIdentifier, Context c) { - if (!isCacheSetup) { - TableCache.setup(scanConfig); - isCacheSetup = true; - } - Table table = TableCache.getRefreshed(tableIdentifier); + Table table = TableCache.getRefreshed(scanConfig.getCatalogConfig(), tableIdentifier); @Nullable Long userSpecifiedToSnapshot = ReadUtils.getToSnapshot(table, scanConfig); boolean isComplete = userSpecifiedToSnapshot != null; diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java index fbd6c15095e9..5cf095dc3c30 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteDirectRowsToFiles.java @@ -31,8 +31,6 @@ import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.iceberg.catalog.Catalog; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; class WriteDirectRowsToFiles @@ -66,7 +64,6 @@ private static class WriteDirectRowsToFilesDoFn extends DoFn, Fi private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; - private transient @MonotonicNonNull Catalog catalog; private final String filePrefix; private final long maxFileSize; private transient @Nullable RecordWriterManager recordWriterManager; @@ -83,17 +80,10 @@ private static class WriteDirectRowsToFilesDoFn extends DoFn, Fi this.recordWriterManager = null; } - private org.apache.iceberg.catalog.Catalog getCatalog() { - if (catalog == null) { - this.catalog = catalogConfig.catalog(); - } - return catalog; - } - @StartBundle public void startBundle() { recordWriterManager = - new RecordWriterManager(getCatalog(), filePrefix, maxFileSize, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, filePrefix, maxFileSize, Integer.MAX_VALUE); } @ProcessElement diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java index 12d9570d4a38..b16496240d18 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteGroupedRowsToFiles.java @@ -30,8 +30,6 @@ import org.apache.beam.sdk.values.WindowedValue; import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; -import org.apache.iceberg.catalog.Catalog; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; class WriteGroupedRowsToFiles extends PTransform< @@ -67,7 +65,6 @@ private static class WriteGroupedRowsToFilesDoFn private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; - private transient @MonotonicNonNull Catalog catalog; private final String filePrefix; private final long maxFileSize; @@ -82,13 +79,6 @@ private static class WriteGroupedRowsToFilesDoFn this.maxFileSize = maxFileSize; } - private org.apache.iceberg.catalog.Catalog getCatalog() { - if (catalog == null) { - this.catalog = catalogConfig.catalog(); - } - return catalog; - } - @ProcessElement public void processElement( ProcessContext c, @@ -103,7 +93,7 @@ public void processElement( WindowedValues.of(destination, window.maxTimestamp(), window, paneInfo); RecordWriterManager writer; try (RecordWriterManager openWriter = - new RecordWriterManager(getCatalog(), filePrefix, maxFileSize, Integer.MAX_VALUE)) { + new RecordWriterManager(catalogConfig, filePrefix, maxFileSize, Integer.MAX_VALUE)) { writer = openWriter; for (Row e : element.getValue()) { writer.write(windowedDestination, e); diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.java new file mode 100644 index 000000000000..8b4ae0863f72 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WritePartitionedRowsToFiles.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.iceberg; + +import static org.apache.beam.sdk.io.iceberg.AssignDestinationsAndPartitions.DESTINATION; +import static org.apache.beam.sdk.io.iceberg.AssignDestinationsAndPartitions.PARTITION; +import static org.apache.beam.sdk.io.iceberg.RecordWriterManager.getPartitionDataPath; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; + +import java.util.Map; +import java.util.UUID; +import org.apache.beam.sdk.coders.IterableCoder; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.PartitionField; +import org.apache.iceberg.PartitionKey; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SupportsNamespaces; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.exceptions.AlreadyExistsException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class WritePartitionedRowsToFiles + extends PTransform>>, PCollection> { + private static final Logger LOG = LoggerFactory.getLogger(WritePartitionedRowsToFiles.class); + private final DynamicDestinations dynamicDestinations; + private final IcebergCatalogConfig catalogConfig; + private final String filePrefix; + + WritePartitionedRowsToFiles( + IcebergCatalogConfig catalogConfig, + DynamicDestinations dynamicDestinations, + String filePrefix) { + this.catalogConfig = catalogConfig; + this.dynamicDestinations = dynamicDestinations; + this.filePrefix = filePrefix; + } + + @Override + public PCollection expand(PCollection>> input) { + Schema dataSchema = + ((RowCoder) + ((IterableCoder) + ((KvCoder>) input.getCoder()).getValueCoder()) + .getElemCoder()) + .getSchema(); + return input.apply( + ParDo.of(new WriteDoFn(catalogConfig, dynamicDestinations, filePrefix, dataSchema))); + } + + private static class WriteDoFn extends DoFn>, FileWriteResult> { + + private final DynamicDestinations dynamicDestinations; + private final IcebergCatalogConfig catalogConfig; + private final String filePrefix; + private final Schema dataSchema; + private transient @MonotonicNonNull Map specIds; + private transient @MonotonicNonNull Map> + partitionFieldMaps; + + WriteDoFn( + IcebergCatalogConfig catalogConfig, + DynamicDestinations dynamicDestinations, + String filePrefix, + Schema dataSchema) { + this.catalogConfig = catalogConfig; + this.dynamicDestinations = dynamicDestinations; + this.filePrefix = filePrefix; + this.dataSchema = dataSchema; + } + + @Setup + public void setup() { + partitionFieldMaps = Maps.newHashMap(); + specIds = Maps.newHashMap(); + } + + @ProcessElement + public void processElement( + @Element KV> element, OutputReceiver out) + throws Exception { + String tableIdentifier = checkStateNotNull(element.getKey().getString(DESTINATION)); + String partitionPath = checkStateNotNull(element.getKey().getString(PARTITION)); + + IcebergDestination destination = dynamicDestinations.instantiateDestination(tableIdentifier); + Table table = getOrCreateTable(destination, dataSchema); + partitionPath = + getPartitionDataPath( + partitionPath, getPartitionFieldMap(destination.getTableIdentifier(), table)); + + StructLike partitionData = + table.spec().isPartitioned() + ? DataFiles.data(table.spec(), partitionPath) + : new PartitionKey(table.spec(), table.schema()); + + String fileName = + destination + .getFileFormat() + .addExtension(String.format("%s-%s", filePrefix, UUID.randomUUID())); + + RecordWriter writer = + new RecordWriter(table, destination.getFileFormat(), fileName, partitionData); + try { + for (Row row : element.getValue()) { + Record record = IcebergUtils.beamRowToIcebergRecord(table.schema(), row); + writer.write(record); + } + } finally { + writer.close(); + } + + SerializableDataFile sdf = SerializableDataFile.from(writer.getDataFile(), partitionPath); + out.output( + FileWriteResult.builder() + .setTableIdentifier(destination.getTableIdentifier()) + .setSerializableDataFile(sdf) + .build()); + } + + 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)); + } + 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; + } + + Table getOrCreateTable(IcebergDestination destination, Schema dataSchema) { + TableIdentifier identifier = destination.getTableIdentifier(); + 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(); + Map tableProperties = + createConfig != null && createConfig.getTableProperties() != null + ? createConfig.getTableProperties() + : Maps.newHashMap(); + + // Create namespace if it does not exist yet + if (!namespace.isEmpty() && catalog instanceof SupportsNamespaces) { + SupportsNamespaces supportsNamespaces = (SupportsNamespaces) catalog; + if (!supportsNamespaces.namespaceExists(namespace)) { + try { + supportsNamespaces.createNamespace(namespace); + LOG.info("Created new namespace '{}'.", namespace); + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this namespace + LOG.info("Namespace `{}` already exists.", namespace); + } + } + } + + // If table exists, just load it + // Note: the implementation of catalog.tableExists() will load the table to check its + // existence. We don't use it here to avoid double loadTable() calls. + try { + return catalog.loadTable(identifier); + } catch (NoSuchTableException e) { // Otherwise, create the table + org.apache.iceberg.Schema tableSchema = IcebergUtils.beamSchemaToIcebergSchema(dataSchema); + try { + Table table = + catalog.createTable(identifier, tableSchema, partitionSpec, tableProperties); + LOG.info( + "Created Iceberg table '{}' with schema: {}\n" + + ", partition spec: {}, table properties: {}", + identifier, + tableSchema, + partitionSpec, + tableProperties); + return table; + } catch (AlreadyExistsException ignored) { + // race condition: another worker already created this table + return catalog.loadTable(identifier); + } + } + } + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java new file mode 100644 index 000000000000..310fa1bede41 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteToPartitions.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; +import static org.apache.beam.sdk.values.TypeDescriptors.iterables; +import static org.apache.beam.sdk.values.TypeDescriptors.kvs; +import static org.apache.beam.sdk.values.TypeDescriptors.rows; + +import java.util.UUID; +import org.apache.beam.sdk.coders.IterableCoder; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.RowCoder; +import org.apache.beam.sdk.transforms.GroupIntoBatches; +import org.apache.beam.sdk.transforms.MapElements; +import org.apache.beam.sdk.transforms.PTransform; +import org.apache.beam.sdk.transforms.windowing.AfterProcessingTime; +import org.apache.beam.sdk.transforms.windowing.GlobalWindows; +import org.apache.beam.sdk.transforms.windowing.Repeatedly; +import org.apache.beam.sdk.transforms.windowing.Window; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.Row; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; + +class WriteToPartitions extends PTransform>, IcebergWriteResult> { + private static final long DEFAULT_BYTES_PER_FILE = (1L << 29); // 512mb + private final IcebergCatalogConfig catalogConfig; + private final DynamicDestinations dynamicDestinations; + private final @Nullable Duration triggeringFrequency; + private final String filePrefix; + private final boolean autoSharding; + + WriteToPartitions( + IcebergCatalogConfig catalogConfig, + DynamicDestinations dynamicDestinations, + @Nullable Duration triggeringFrequency, + boolean autoSharding) { + this.dynamicDestinations = dynamicDestinations; + this.catalogConfig = catalogConfig; + this.triggeringFrequency = triggeringFrequency; + // single unique prefix per write transform + this.filePrefix = UUID.randomUUID().toString(); + this.autoSharding = autoSharding; + } + + private PCollection>> groupByPartition(PCollection> input) { + RowCoder destinationCoder = RowCoder.of(AssignDestinationsAndPartitions.OUTPUT_SCHEMA); + RowCoder dataCoder = RowCoder.of(dynamicDestinations.getDataSchema()); + + GroupIntoBatches groupIntoPartitions = + GroupIntoBatches.ofByteSize(DEFAULT_BYTES_PER_FILE); + if (IcebergUtils.isUnbounded(input) && triggeringFrequency != null) { + groupIntoPartitions = groupIntoPartitions.withMaxBufferingDuration(triggeringFrequency); + } + + if (autoSharding) { + return input + .apply(groupIntoPartitions.withShardedKey()) + .setCoder( + KvCoder.of( + org.apache.beam.sdk.util.ShardedKey.Coder.of(destinationCoder), + IterableCoder.of(dataCoder))) + .apply( + "DropShardId", + MapElements.into(kvs(rows(), iterables(rows()))) + .via(kv -> KV.of(kv.getKey().getKey(), kv.getValue()))) + .setCoder(KvCoder.of(destinationCoder, IterableCoder.of(dataCoder))); + } else { + return input + .apply(groupIntoPartitions) + .setCoder(KvCoder.of(destinationCoder, IterableCoder.of(dataCoder))); + } + } + + @Override + public IcebergWriteResult expand(PCollection> input) { + PCollection>> groupedRows = groupByPartition(input); + + PCollection writtenFiles = + groupedRows.apply( + new WritePartitionedRowsToFiles(catalogConfig, dynamicDestinations, filePrefix)); + + if (IcebergUtils.isUnbounded(input) && triggeringFrequency != null) { + writtenFiles = + writtenFiles.apply( + "ApplyUserTrigger", + Window.into(new GlobalWindows()) + .triggering( + Repeatedly.forever( + AfterProcessingTime.pastFirstElementInPane() + .plusDelayOf(checkArgumentNotNull(triggeringFrequency)))) + .discardingFiredPanes()); + } + + // Commit files to tables + PCollection> snapshots = + writtenFiles.apply(new AppendFilesToTables(catalogConfig, filePrefix)); + + return new IcebergWriteResult(input.getPipeline(), snapshots); + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java index 1db6ede30165..ff9a98c62005 100644 --- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/WriteUngroupedRowsToFiles.java @@ -47,8 +47,6 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; -import org.apache.iceberg.catalog.Catalog; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -193,7 +191,6 @@ private static class WriteUngroupedRowsToFilesDoFn private final long maxFileSize; private final DynamicDestinations dynamicDestinations; private final IcebergCatalogConfig catalogConfig; - private transient @MonotonicNonNull Catalog catalog; private transient @Nullable RecordWriterManager recordWriterManager; private int spilledShardNumber; @@ -210,17 +207,10 @@ public WriteUngroupedRowsToFilesDoFn( this.maxFileSize = maxFileSize; } - private org.apache.iceberg.catalog.Catalog getCatalog() { - if (catalog == null) { - this.catalog = catalogConfig.catalog(); - } - return catalog; - } - @StartBundle public void startBundle() { recordWriterManager = - new RecordWriterManager(getCatalog(), filename, maxFileSize, maxWritersPerBundle); + new RecordWriterManager(catalogConfig, filename, maxFileSize, maxWritersPerBundle); this.spilledShardNumber = ThreadLocalRandom.current().nextInt(SPILLED_RECORD_SHARDING_FACTOR); } diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java new file mode 100644 index 000000000000..147e2adda1a7 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/CdcOutputUtils.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import java.util.ArrayList; +import java.util.List; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.ValueKind; +import org.apache.iceberg.ChangelogOperation; +import org.apache.iceberg.types.Types; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Helpers for CDC schemas and output row construction. + * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      + *
    • Iceberg row metadata: {@code _row_id} and {@code _last_updated_sequence_number}. These are + * requested from the physical Iceberg reader and are only available for row-lineage tables + * (v3+). + *
    • Changelog context metadata: {@code _change_type}, {@code _commit_snapshot_id}, and {@code + * _commit_snapshot_sequence_number}. These are known from the changelog snapshot/task context + * and are appended when Beam output rows are built. + *
    + */ +@Internal +public final class IcebergCdcMetadataColumns { + public static final String CHANGE_TYPE = MetadataColumns.CHANGE_TYPE.name(); + public static final String COMMIT_SNAPSHOT_SEQUENCE_NUMBER = "_commit_snapshot_sequence_number"; + public static final String COMMIT_SNAPSHOT_ID = MetadataColumns.COMMIT_SNAPSHOT_ID.name(); + public static final String ROW_ID = MetadataColumns.ROW_ID.name(); + public static final String LAST_UPDATED_SEQUENCE_NUMBER = + MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.name(); + + public static final ImmutableList SUPPORTED_COLUMNS = + ImmutableList.of( + CHANGE_TYPE, + COMMIT_SNAPSHOT_ID, + COMMIT_SNAPSHOT_SEQUENCE_NUMBER, + ROW_ID, + LAST_UPDATED_SEQUENCE_NUMBER); + + private static final ImmutableSet ROW_METADATA_COLUMNS = + ImmutableSet.of(ROW_ID, LAST_UPDATED_SEQUENCE_NUMBER); + + public static boolean isSupportedColumn(String name) { + return SUPPORTED_COLUMNS.contains(name); + } + + public static boolean isRowMetadataColumn(String name) { + return ROW_METADATA_COLUMNS.contains(name); + } + + public static Schema.Field beamField(String name) { + if (CHANGE_TYPE.equals(name)) { + return Schema.Field.of(name, Schema.FieldType.STRING); + } + if (COMMIT_SNAPSHOT_ID.equals(name) || COMMIT_SNAPSHOT_SEQUENCE_NUMBER.equals(name)) { + return Schema.Field.of(name, Schema.FieldType.INT64); + } + if (ROW_ID.equals(name) || LAST_UPDATED_SEQUENCE_NUMBER.equals(name)) { + return Schema.Field.nullable(name, Schema.FieldType.INT64); + } + throw new IllegalArgumentException("Unsupported CDC metadata column: " + name); + } + + /** Returns the Iceberg reader field for row-sourced metadata, or null for commit metadata. */ + public static Types.@Nullable NestedField icebergRowMetadataField(String name) { + if (ROW_ID.equals(name)) { + return MetadataColumns.ROW_ID; + } + if (LAST_UPDATED_SEQUENCE_NUMBER.equals(name)) { + return MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER; + } + return null; + } +} diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTask.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTask.java new file mode 100644 index 000000000000..9b6955d9e4a5 --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTask.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; + +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.beam.sdk.io.iceberg.SerializableDataFile; +import org.apache.beam.sdk.io.iceberg.SerializableDeleteFile; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.NoSuchSchemaException; +import org.apache.beam.sdk.schemas.SchemaCoder; +import org.apache.beam.sdk.schemas.SchemaRegistry; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber; +import org.apache.beam.sdk.schemas.annotations.SchemaIgnore; +import org.apache.iceberg.AddedRowsScanTask; +import org.apache.iceberg.ChangelogOperation; +import org.apache.iceberg.ChangelogScanTask; +import org.apache.iceberg.ContentScanTask; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.DeletedDataFileScanTask; +import org.apache.iceberg.DeletedRowsScanTask; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.ExpressionParser; + +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class SerializableChangelogTask { + public enum Type { + ADDED_ROWS, + DELETED_ROWS, + DELETED_FILE + } + + public static SchemaCoder coder() { + try { + return SchemaRegistry.createDefault().getSchemaCoder(SerializableChangelogTask.class); + } catch (NoSuchSchemaException e) { + throw new RuntimeException(e); + } + } + + public static SerializableChangelogTask.Builder builder() { + return new AutoValue_SerializableChangelogTask.Builder() + .setExistingDeletes(Collections.emptyList()) + .setAddedDeletes(Collections.emptyList()); + } + + @SchemaFieldNumber("0") + public abstract Type getType(); + + @SchemaFieldNumber("1") + public abstract SerializableDataFile getDataFile(); + + @SchemaFieldNumber("2") + public abstract List getExistingDeletes(); + + @SchemaFieldNumber("3") + public abstract List getAddedDeletes(); + + @SchemaFieldNumber("4") + public abstract int getSpecId(); + + @SchemaFieldNumber("5") + public abstract ChangelogOperation getOperation(); + + @SchemaFieldNumber("6") + public abstract int getOrdinal(); + + @SchemaFieldNumber("7") + public abstract long getCommitSnapshotId(); + + @SchemaFieldNumber("8") + public abstract long getStart(); + + @SchemaFieldNumber("9") + public abstract long getLength(); + + @SchemaFieldNumber("10") + public abstract String getJsonExpression(); + + @SchemaIgnore + public Expression getExpression(Schema schema) { + return ExpressionParser.fromJson(getJsonExpression(), schema); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + public abstract static class Builder { + abstract Builder setType(Type type); + + abstract Builder setDataFile(SerializableDataFile dataFile); + + @SchemaIgnore + public Builder setDataFile(DataFile df, String partitionPath, boolean includeMetrics) { + return setDataFile(SerializableDataFile.from(df, partitionPath, includeMetrics)); + } + + abstract Builder setExistingDeletes(List existingDeletes); + + abstract Builder setAddedDeletes(List addedDeletes); + + abstract Builder setSpecId(int specId); + + abstract Builder setOperation(ChangelogOperation operation); + + abstract Builder setOrdinal(int ordinal); + + abstract Builder setCommitSnapshotId(long commitSnapshotId); + + abstract Builder setStart(long start); + + abstract Builder setLength(long length); + + abstract Builder setJsonExpression(String expression); + + abstract SerializableChangelogTask build(); + } + + public static SerializableChangelogTask from( + ChangelogScanTask task, Map specs) { + return from(task, specs, false); + } + + public static SerializableChangelogTask from( + ChangelogScanTask task, Map specs, boolean includeMetrics) { + checkState( + task instanceof ContentScanTask, "Expected ChangelogScanTask to also be a ContentScanTask"); + ContentScanTask contentScanTask = (ContentScanTask) task; + PartitionSpec spec = contentScanTask.spec(); + SerializableChangelogTask.Builder builder = + SerializableChangelogTask.builder() + .setOperation(task.operation()) + .setOrdinal(task.changeOrdinal()) + .setCommitSnapshotId(task.commitSnapshotId()) + .setDataFile( + contentScanTask.file(), + spec.partitionToPath(contentScanTask.partition()), + includeMetrics) + .setSpecId(spec.specId()) + .setStart(contentScanTask.start()) + .setLength(contentScanTask.length()) + .setJsonExpression(ExpressionParser.toJson(contentScanTask.residual())); + + if (task instanceof AddedRowsScanTask) { + AddedRowsScanTask addedRowsTask = (AddedRowsScanTask) task; + builder = + builder + .setType(Type.ADDED_ROWS) + .setAddedDeletes( + toSerializableDeletes(addedRowsTask.deletes(), specs, includeMetrics)); + } else if (task instanceof DeletedRowsScanTask) { + DeletedRowsScanTask deletedRowsTask = (DeletedRowsScanTask) task; + builder = + builder + .setType(Type.DELETED_ROWS) + .setAddedDeletes( + toSerializableDeletes(deletedRowsTask.addedDeletes(), specs, includeMetrics)) + .setExistingDeletes( + toSerializableDeletes(deletedRowsTask.existingDeletes(), specs, includeMetrics)); + } else if (task instanceof DeletedDataFileScanTask) { + DeletedDataFileScanTask deletedFileTask = (DeletedDataFileScanTask) task; + builder = + builder + .setType(Type.DELETED_FILE) + .setExistingDeletes( + toSerializableDeletes(deletedFileTask.existingDeletes(), specs, includeMetrics)); + } else { + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + return builder.build(); + } + + static Type getType(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return Type.ADDED_ROWS; + } else if (task instanceof DeletedRowsScanTask) { + return Type.DELETED_ROWS; + } else if (task instanceof DeletedDataFileScanTask) { + return Type.DELETED_FILE; + } else { + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + } + + static long getTotalLength(List tasks) { + return tasks.stream().mapToLong(SerializableChangelogTask::getLength).sum(); + } + + static long getLength(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).length(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).length(); + } else if (task instanceof DeletedDataFileScanTask) { + return ((DeletedDataFileScanTask) task).length(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + static StructLike getPartition(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).partition(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).partition(); + } else if (task instanceof DeletedDataFileScanTask) { + return ((DeletedDataFileScanTask) task).partition(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + static PartitionSpec getSpec(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).spec(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).spec(); + } else if (task instanceof DeletedDataFileScanTask) { + return ((DeletedDataFileScanTask) task).spec(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + static DataFile getDataFile(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).file(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).file(); + } else if (task instanceof DeletedDataFileScanTask) { + return ((DeletedDataFileScanTask) task).file(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + static List getAddedDeleteFiles(ChangelogScanTask task) { + if (task instanceof AddedRowsScanTask) { + return ((AddedRowsScanTask) task).deletes(); + } else if (task instanceof DeletedRowsScanTask) { + return ((DeletedRowsScanTask) task).addedDeletes(); + } else if (task instanceof DeletedDataFileScanTask) { + return Collections.emptyList(); + } + throw new IllegalStateException("Unknown ChangelogScanTask type: " + task.getClass()); + } + + private static List toSerializableDeletes( + List dfs, Map specs, boolean includeMetrics) { + return dfs.stream() + .map( + df -> + SerializableDeleteFile.from( + df, + checkStateNotNull(specs.get(df.specId())).partitionToPath(df.partition()), + includeMetrics)) + .collect(Collectors.toList()); + } +} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/package-info.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java similarity index 88% rename from runners/samza/src/main/java/org/apache/beam/runners/samza/package-info.java rename to sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/package-info.java index 549a4d81c2de..8285d91689be 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/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. */ -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza; +/** Iceberg CDC connectors. */ +package org.apache.beam.sdk.io.iceberg.cdc; diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BaseIncrementalChangelogScan.java b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BaseIncrementalChangelogScan.java new file mode 100644 index 000000000000..6b12e16690ba --- /dev/null +++ b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/BaseIncrementalChangelogScan.java @@ -0,0 +1,1014 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.iceberg; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Comparator; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.FluentIterable; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; +import org.apache.iceberg.ManifestGroup.CreateTasksFunction; +import org.apache.iceberg.ManifestGroup.TaskContext; +import org.apache.iceberg.expressions.Evaluator; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.expressions.ManifestEvaluator; +import org.apache.iceberg.expressions.Projections; +import org.apache.iceberg.expressions.ResidualEvaluator; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.util.ContentFileUtil; +import org.apache.iceberg.util.Pair; +import org.apache.iceberg.util.PartitionMap; +import org.apache.iceberg.util.PartitionSet; +import org.apache.iceberg.util.SnapshotUtil; +import org.apache.iceberg.util.SortedMerge; +import org.apache.iceberg.util.TableScanUtil; +import org.apache.iceberg.util.Tasks; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Copied over from Iceberg PR #14264. + */ +@SuppressWarnings("nullness") +public class BaseIncrementalChangelogScan + extends BaseIncrementalScan< + IncrementalChangelogScan, ChangelogScanTask, ScanTaskGroup> + implements IncrementalChangelogScan { + private static final DeleteFileIndex EMPTY = createEmptyInstance(); + + private static DeleteFileIndex createEmptyInstance() { + try { + var constructor = + DeleteFileIndex.class.getDeclaredConstructor( + DeleteFileIndex.EqualityDeletes.class, + PartitionMap.class, + PartitionMap.class, + Map.class, + Map.class); + constructor.setAccessible(true); + return constructor.newInstance(null, null, null, null, null); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize EMPTY DeleteFileIndex", e); + } + } + + private static final Logger LOG = LoggerFactory.getLogger(BaseIncrementalChangelogScan.class); + + public BaseIncrementalChangelogScan(Table table) { + this(table, table.schema(), TableScanContext.empty()); + } + + private BaseIncrementalChangelogScan(Table table, Schema schema, TableScanContext context) { + super(table, schema, context); + } + + @Override + protected IncrementalChangelogScan newRefinedScan( + Table newTable, Schema newSchema, TableScanContext newContext) { + return new BaseIncrementalChangelogScan(newTable, newSchema, newContext); + } + + // Private fields to track build call count and cache (accessed via package-private methods for + // testing) + private int existingDeleteIndexBuildCallCount = 0; + // Cache for the built index (null if not built yet) + private DeleteFileIndex cachedExistingDeleteIndex = null; + + @Override + protected CloseableIterable doPlanFiles( + Long fromSnapshotIdExclusive, long toSnapshotIdInclusive) { + + Deque changelogSnapshots = + orderedChangelogSnapshots(fromSnapshotIdExclusive, toSnapshotIdInclusive); + + if (changelogSnapshots.isEmpty()) { + return CloseableIterable.empty(); + } + + Set changelogSnapshotIds = toSnapshotIds(changelogSnapshots); + + Set newDataManifests = + FluentIterable.from(changelogSnapshots) + .transformAndConcat(snapshot -> snapshot.dataManifests(table().io())) + .filter(manifest -> changelogSnapshotIds.contains(manifest.snapshotId())) + .toSet(); + + // Build per-snapshot delete file indexes for added deletes + Map addedDeletesBySnapshot = buildAddedDeleteIndexes(changelogSnapshots); + + // Check if existing delete index is needed for equality deletes + boolean hasEqualityDeletes = + addedDeletesBySnapshot.values().stream() + .anyMatch(index -> !index.isEmpty() && index.hasEqualityDeletes()); + + // Build existing index early if needed for equality deletes, otherwise use lazy initialization + DeleteFileIndex existingDeleteIndex = + hasEqualityDeletes ? buildExistingDeleteIndexTracked(fromSnapshotIdExclusive) : EMPTY; + + ManifestGroup manifestGroup = + new ManifestGroup(table().io(), newDataManifests, ImmutableList.of()) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .select(scanColumns()) + .filterData(filter()) + .filterManifestEntries(entry -> changelogSnapshotIds.contains(entry.snapshotId())) + .ignoreExisting() + .columnsToKeepStats(columnsToKeepStats()); + + if (shouldIgnoreResiduals()) { + manifestGroup = manifestGroup.ignoreResiduals(); + } + + if (newDataManifests.size() > 1 && shouldPlanWithExecutor()) { + manifestGroup = manifestGroup.planWith(planExecutor()); + } + + // Create a supplier that reuses already-built index or builds lazily when first DELETED entry + // is encountered + Supplier existingDeleteIndexSupplier = + () -> { + if (cachedExistingDeleteIndex != null) { + return cachedExistingDeleteIndex; + } + return buildExistingDeleteIndexTracked(fromSnapshotIdExclusive); + }; + + // Plan data file tasks (ADDED and DELETED) + Map> cumulativeDeletesMap = + buildCumulativeDeletesBySnapshot(changelogSnapshots, addedDeletesBySnapshot); + + CloseableIterable dataFileTasks = + manifestGroup.plan( + new CreateDataFileChangeTasks( + changelogSnapshots, + existingDeleteIndexSupplier, + addedDeletesBySnapshot, + cumulativeDeletesMap, + table().specs(), + isCaseSensitive())); + + // Find EXISTING data files affected by newly added delete files and create tasks for them + CloseableIterable deletedRowsTasks = + planDeletedRowsTasks( + changelogSnapshots, existingDeleteIndex, addedDeletesBySnapshot, changelogSnapshotIds); + + // Merge tasks from both iterables in order by changeOrdinal + Comparator byOrdinal = + Comparator.comparing(ChangelogScanTask::changeOrdinal) + .thenComparing(ChangelogScanTask::commitSnapshotId); + + return new SortedMerge<>(byOrdinal, ImmutableList.of(dataFileTasks, deletedRowsTasks)); + } + + @Override + public CloseableIterable> planTasks() { + return TableScanUtil.planTaskGroups( + planFiles(), targetSplitSize(), splitLookback(), splitOpenFileCost()); + } + + // builds a collection of changelog snapshots (oldest to newest) + // the order of the snapshots is important as it is used to determine change ordinals + private Deque orderedChangelogSnapshots(Long fromIdExcl, long toIdIncl) { + Deque changelogSnapshots = new ArrayDeque<>(); + + for (Snapshot snapshot : SnapshotUtil.ancestorsBetween(table(), toIdIncl, fromIdExcl)) { + if (!snapshot.operation().equals(DataOperations.REPLACE)) { + changelogSnapshots.addFirst(snapshot); + } + } + + return changelogSnapshots; + } + + private Set toSnapshotIds(Collection snapshots) { + return snapshots.stream().map(Snapshot::snapshotId).collect(Collectors.toSet()); + } + + private static Map computeSnapshotOrdinals(Deque snapshots) { + Map snapshotOrdinals = Maps.newHashMap(); + + int ordinal = 0; + + for (Snapshot snapshot : snapshots) { + snapshotOrdinals.put(snapshot.snapshotId(), ordinal++); + } + + return snapshotOrdinals; + } + + /** + * Builds a delete file index for existing deletes that were present before the start snapshot. + * These deletes should be applied to data files but should not generate DELETE changelog rows. + * Uses manifest pruning and caching to optimize performance. + */ + private DeleteFileIndex buildExistingDeleteIndex(Long fromSnapshotIdExclusive) { + if (fromSnapshotIdExclusive == null) { + return EMPTY; + } + Snapshot fromSnapshot = table().snapshot(fromSnapshotIdExclusive); + Preconditions.checkState( + fromSnapshot != null, "Cannot find starting snapshot: %s", fromSnapshotIdExclusive); + + List existingDeleteManifests = fromSnapshot.deleteManifests(table().io()); + if (existingDeleteManifests.isEmpty()) { + return EMPTY; + } + + // Prune manifests based on partition filter to avoid processing irrelevant manifests + List prunedManifests = pruneManifestsByPartition(existingDeleteManifests); + if (prunedManifests.isEmpty()) { + return EMPTY; + } + + // Load delete files from manifests + Iterable deleteFiles = loadDeleteFiles(prunedManifests, null); + + return DeleteFileIndex.builderFor(deleteFiles) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .build(); + } + + /** + * Wrapper method that tracks build calls and caches the result for reuse. This ensures we only + * build the index once even if called from multiple places. + */ + private DeleteFileIndex buildExistingDeleteIndexTracked(Long fromSnapshotIdExclusive) { + if (cachedExistingDeleteIndex != null) { + return cachedExistingDeleteIndex; + } + existingDeleteIndexBuildCallCount++; + cachedExistingDeleteIndex = buildExistingDeleteIndex(fromSnapshotIdExclusive); + return cachedExistingDeleteIndex; + } + + // Visible for testing + int getExistingDeleteIndexBuildCallCount() { + return existingDeleteIndexBuildCallCount; + } + + // Visible for testing + boolean wasExistingDeleteIndexBuilt() { + return existingDeleteIndexBuildCallCount > 0; + } + + /** + * Builds per-snapshot delete file indexes for newly added delete files in each changelog + * snapshot. These deletes should generate DELETE changelog rows. Uses caching to avoid re-parsing + * manifests. + */ + private Map buildAddedDeleteIndexes(Deque changelogSnapshots) { + Map addedDeletesBySnapshot = Maps.newConcurrentMap(); + Tasks.foreach(changelogSnapshots) + .retry(3) + .stopOnFailure() + .throwFailureWhenFinished() + .executeWith(planExecutor()) + .onFailure( + (snapshot, exc) -> + LOG.warn( + "Failed to build delete index for snapshot {}", snapshot.snapshotId(), exc)) + .run( + snapshot -> { + List snapshotDeleteManifests = snapshot.deleteManifests(table().io()); + if (snapshotDeleteManifests.isEmpty()) { + addedDeletesBySnapshot.put(snapshot.snapshotId(), EMPTY); + return; + } + + // Filter to only include delete files added in this snapshot + List addedDeleteManifests = + snapshotDeleteManifests.stream() + .filter(manifest -> manifest.snapshotId().equals(snapshot.snapshotId())) + .collect(Collectors.toUnmodifiableList()); + + if (addedDeleteManifests.isEmpty()) { + addedDeletesBySnapshot.put(snapshot.snapshotId(), EMPTY); + } else { + // Load delete files from manifests + Iterable deleteFiles = + loadDeleteFiles(addedDeleteManifests, snapshot.snapshotId()); + + DeleteFileIndex index = + DeleteFileIndex.builderFor(deleteFiles) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .build(); + addedDeletesBySnapshot.put(snapshot.snapshotId(), index); + } + }); + return addedDeletesBySnapshot; + } + + /** + * Plans tasks for EXISTING data files that are affected by newly added delete files. These files + * were not added or deleted in the changelog snapshot range, but have new delete files applied to + * them. + */ + private CloseableIterable planDeletedRowsTasks( + Deque changelogSnapshots, + DeleteFileIndex existingDeleteIndex, + Map addedDeletesBySnapshot, + Set changelogSnapshotIds) { + + Map snapshotOrdinals = computeSnapshotOrdinals(changelogSnapshots); + List tasks = Lists.newArrayList(); + + // Build a map of file statuses and collect affected partitions for each snapshot + Pair>, PartitionSet> fileStatusAndPartitions = + buildFileStatusBySnapshot(changelogSnapshots, changelogSnapshotIds); + Map> fileStatusBySnapshot = + fileStatusAndPartitions.first(); + PartitionSet affectedPartitions = fileStatusAndPartitions.second(); + + // Accumulate actual DeleteFile entries chronologically + List accumulatedDeletes = Lists.newArrayList(); + + // Start with deletes from before the changelog range + if (!existingDeleteIndex.isEmpty()) { + for (DeleteFile df : existingDeleteIndex.referencedDeleteFiles()) { + accumulatedDeletes.add(df); + } + } + + for (Snapshot snapshot : changelogSnapshots) { + DeleteFileIndex addedDeleteIndex = addedDeletesBySnapshot.get(snapshot.snapshotId()); + if (addedDeleteIndex.isEmpty()) { + continue; + } + + // Collect partitions of newly added delete files for pruning (important for the current + // snapshot) + for (DeleteFile df : addedDeleteIndex.referencedDeleteFiles()) { + affectedPartitions.add(df.specId(), df.partition()); + } + + DeleteFileIndex cumulativeDeleteIndex = + buildDeleteIndex(accumulatedDeletes, affectedPartitions); + + // Process data files for this snapshot + // Use a local set per snapshot to track processed files + Set alreadyProcessedPaths = Sets.newHashSet(); + processSnapshotForDeletedRowsTasks( + snapshot, + addedDeleteIndex, + cumulativeDeleteIndex, + fileStatusBySnapshot.get(snapshot.snapshotId()), + alreadyProcessedPaths, + snapshotOrdinals, + affectedPartitions, + tasks); + + // Accumulate this snapshot's added deletes for subsequent snapshots + for (DeleteFile df : addedDeleteIndex.referencedDeleteFiles()) { + accumulatedDeletes.add(df); + } + } + + return CloseableIterable.withNoopClose(tasks); + } + + /** + * Builds a map of file statuses for each snapshot, tracking which files were added or deleted in + * each snapshot. + */ + private Pair>, PartitionSet> + buildFileStatusBySnapshot( + Deque changelogSnapshots, Set changelogSnapshotIds) { + + Map> fileStatusBySnapshot = Maps.newConcurrentMap(); + java.util.Queue localPartitionsQueue = + new java.util.concurrent.ConcurrentLinkedQueue<>(); + + Tasks.foreach(changelogSnapshots) + .stopOnFailure() + .throwFailureWhenFinished() + .executeWith(planExecutor()) + .run( + snapshot -> { + Map fileStatuses = Maps.newHashMap(); + PartitionSet localAffected = PartitionSet.create(table().specs()); + + List changedDataManifests = + FluentIterable.from(snapshot.dataManifests(table().io())) + .filter(manifest -> manifest.snapshotId().equals(snapshot.snapshotId())) + .toList(); + + if (!changedDataManifests.isEmpty()) { + ManifestGroup changedGroup = + new ManifestGroup(table().io(), changedDataManifests, ImmutableList.of()) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .select(scanColumns()) + .filterData(filter()) + .ignoreExisting() + .columnsToKeepStats(columnsToKeepStats()); + + try (CloseableIterable> entries = changedGroup.entries()) { + for (ManifestEntry entry : entries) { + if (changelogSnapshotIds.contains(entry.snapshotId())) { + fileStatuses.put(entry.file().location(), entry.status()); + localAffected.add(entry.file().specId(), entry.file().partition()); + } + } + } catch (Exception e) { + throw new RuntimeException( + "Failed to collect file statuses for snapshot " + snapshot.snapshotId(), e); + } + } + + fileStatusBySnapshot.put(snapshot.snapshotId(), fileStatuses); + localPartitionsQueue.add(localAffected); + }); + + PartitionSet globalAffected = PartitionSet.create(table().specs()); + for (PartitionSet local : localPartitionsQueue) { + globalAffected.addAll(local); + } + + return Pair.of(fileStatusBySnapshot, globalAffected); + } + + private List pruneManifestsByAffectedPartitions( + List manifests, PartitionSet affectedPartitions) { + if (affectedPartitions.isEmpty()) { + return manifests; + } + + Expression affectedExpr = buildAffectedPartitionExpression(affectedPartitions); + if (affectedExpr == Expressions.alwaysFalse()) { + return manifests; + } + + List pruned = Lists.newArrayList(); + for (ManifestFile manifest : manifests) { + PartitionSpec spec = table().specs().get(manifest.partitionSpecId()); + if (spec == null || spec.isUnpartitioned()) { + pruned.add(manifest); + } else if (manifestOverlapsFilter(manifest, spec, affectedExpr)) { + pruned.add(manifest); + } + } + return pruned; + } + + private Expression buildAffectedPartitionExpression(PartitionSet affectedPartitions) { + Expression combined = null; + + for (Pair pair : affectedPartitions) { + int specId = pair.first(); + StructLike partition = pair.second(); + PartitionSpec spec = table().specs().get(specId); + if (spec == null) { + continue; + } else if (spec.isUnpartitioned()) { + return Expressions.alwaysTrue(); // FALLBACK: Global delete exists, include ALL manifests! + } + + Expression specExpr = null; + for (int i = 0; i < spec.fields().size(); i++) { + org.apache.iceberg.PartitionField field = spec.fields().get(i); + Object value = partition.get(i, Object.class); + if (value != null) { + String columnName = table().schema().findColumnName(field.sourceId()); + if (columnName != null) { + Expression equalExpr = Expressions.equal(columnName, value); + specExpr = (specExpr == null) ? equalExpr : Expressions.and(specExpr, equalExpr); + } + } + } + + if (specExpr != null) { + combined = (combined == null) ? specExpr : Expressions.or(combined, specExpr); + } + } + + return combined != null ? combined : Expressions.alwaysFalse(); + } + + /** + * Builds a map of snapshot ID -> all delete files that were added in the scan range up to that + * snapshot, PRUNING files that were removed in the middle. + */ + private Map> buildCumulativeDeletesBySnapshot( + Deque snapshots, Map addedDeletesBySnapshot) { + Map> result = Maps.newHashMap(); + List accumulatedDeletes = Lists.newArrayList(); + + for (Snapshot snapshot : snapshots) { + // Save state first, so that this snapshot's tasks can use any deletes active up to this point + result.put(snapshot.snapshotId(), Lists.newArrayList(accumulatedDeletes)); + + // Check for removed deletes and prune from accumulatedDeletes for FUTURE snapshots + List changedDeletes = + FluentIterable.from(snapshot.deleteManifests(table().io())) + .filter(manifest -> manifest.snapshotId().equals(snapshot.snapshotId())) + .toList(); + + if (!changedDeletes.isEmpty()) { + Iterable removedDeletes = + loadRemovedDeleteFiles(changedDeletes, snapshot.snapshotId()); + Set removedPaths = Sets.newHashSet(); + for (DeleteFile rdf : removedDeletes) { + removedPaths.add(rdf.location()); + } + accumulatedDeletes.removeIf(df -> removedPaths.contains(df.location())); + } + + // Add new deletes for FUTURE snapshots + DeleteFileIndex addedDeleteIndex = addedDeletesBySnapshot.get(snapshot.snapshotId()); + if (addedDeleteIndex != null && !addedDeleteIndex.isEmpty()) { + for (DeleteFile df : addedDeleteIndex.referencedDeleteFiles()) { + accumulatedDeletes.add(df); + } + } + } + + return result; + } + + /** + * Builds a delete index from the accumulated list of delete files, pruning by affected + * partitions. + */ + private DeleteFileIndex buildDeleteIndex( + List accumulatedDeletes, PartitionSet affectedPartitions) { + if (accumulatedDeletes.isEmpty()) { + return EMPTY; + } + + List filteredDeletes = accumulatedDeletes; + if (!affectedPartitions.isEmpty()) { + filteredDeletes = Lists.newArrayList(); + for (DeleteFile df : accumulatedDeletes) { + PartitionSpec spec = table().specs().get(df.specId()); + if (spec == null || spec.isUnpartitioned()) { + filteredDeletes.add(df); // Always include unpartitioned deletes + } else if (affectedPartitions.contains(df.specId(), df.partition())) { + filteredDeletes.add(df); + } + } + } + + return DeleteFileIndex.builderFor(filteredDeletes) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .build(); + } + + /** + * Processes data files for a snapshot to create DeletedRowsScanTask for existing files affected + * by new delete files. + */ + private void processSnapshotForDeletedRowsTasks( + Snapshot snapshot, + DeleteFileIndex addedDeleteIndex, + DeleteFileIndex cumulativeDeleteIndex, + Map currentSnapshotFiles, + Set alreadyProcessedPaths, + Map snapshotOrdinals, + PartitionSet affectedPartitions, + List tasks) { + + // Get all data files that exist in this snapshot, pruned by affected partitions + List allDataManifests = snapshot.dataManifests(table().io()); + List prunedManifests = + pruneManifestsByAffectedPartitions(allDataManifests, affectedPartitions); + + ManifestGroup allDataGroup = + new ManifestGroup(table().io(), prunedManifests, ImmutableList.of()) + .specsById(table().specs()) + .caseSensitive(isCaseSensitive()) + .select(scanColumns()) + .filterData(filter()) + .ignoreDeleted() + .columnsToKeepStats(columnsToKeepStats()); + + if (shouldIgnoreResiduals()) { + allDataGroup = allDataGroup.ignoreResiduals(); + } + + String schemaString = SchemaParser.toJson(schema()); + + // Cache per specId - same for all files with same specId + Map specStringCache = Maps.newHashMap(); + Map residualCache = Maps.newHashMap(); + Expression residualFilter = shouldIgnoreResiduals() ? Expressions.alwaysTrue() : filter(); + + try (CloseableIterable> entries = allDataGroup.entries()) { + for (ManifestEntry entry : entries) { + DataFile dataFile = entry.file(); + String filePath = dataFile.location(); + + // Skip if this file was ADDED or DELETED in this snapshot + // (those are handled by CreateDataFileChangeTasks) + if (currentSnapshotFiles.containsKey(filePath)) { + continue; + } + + // Skip if we already created a task for this file in this snapshot + // Note: alreadyProcessedPaths is local to this snapshot's processing + if (alreadyProcessedPaths.contains(filePath)) { + continue; + } + + // Check if this data file is affected by newly added delete files + DeleteFile[] addedDeletes = addedDeleteIndex.forEntry(entry); + if (addedDeletes.length == 0) { + continue; + } + + // This data file was EXISTING but has new delete files applied + // Get existing deletes from before this snapshot (cumulative) + DeleteFile[] existingDeletes = + cumulativeDeleteIndex.isEmpty() + ? new DeleteFile[0] + : cumulativeDeleteIndex.forEntry(entry); + + // Create a DeletedRowsScanTask + int changeOrdinal = snapshotOrdinals.get(snapshot.snapshotId()); + + // Use cached values (calculate once per specId) + int specId = dataFile.specId(); + String specString = + specStringCache.computeIfAbsent( + specId, id -> PartitionSpecParser.toJson(table().specs().get(id))); + ResidualEvaluator residuals = + residualCache.computeIfAbsent( + specId, + id -> { + PartitionSpec spec = table().specs().get(id); + return ResidualEvaluator.of(spec, residualFilter, isCaseSensitive()); + }); + + tasks.add( + new BaseDeletedRowsScanTask( + changeOrdinal, + snapshot.snapshotId(), + dataFile.copy(shouldKeepStats()), + addedDeletes, + existingDeletes, + schemaString, + specString, + residuals)); + + // Mark this file as processed for this snapshot + alreadyProcessedPaths.add(filePath); + } + } catch (Exception e) { + throw new RuntimeException("Failed to plan deleted rows tasks", e); + } + } + + private boolean shouldKeepStats() { + Set columns = columnsToKeepStats(); + return columns != null && !columns.isEmpty(); + } + + /** + * Loads delete files from manifests by parsing each manifest. + * + * @param manifests the delete manifests to load + * @return list of delete files + */ + private Iterable loadDeleteFiles( + List manifests, Long targetSnapshotId) { + Queue allDeleteFiles = new ConcurrentLinkedQueue<>(); + + Tasks.foreach(manifests) + .stopOnFailure() + .throwFailureWhenFinished() + .executeWith(planExecutor()) + .run( + manifest -> { + List deleteFiles = + loadDeleteFilesFromManifest(manifest, targetSnapshotId); + allDeleteFiles.addAll(deleteFiles); + }); + + return allDeleteFiles; + } + + private Iterable loadRemovedDeleteFiles( + List manifests, Long targetSnapshotId) { + Queue allDeleteFiles = new ConcurrentLinkedQueue<>(); + + Tasks.foreach(manifests) + .stopOnFailure() + .throwFailureWhenFinished() + .executeWith(planExecutor()) + .run( + manifest -> { + List deleteFiles = + loadRemovedDeleteFilesFromManifest(manifest, targetSnapshotId); + allDeleteFiles.addAll(deleteFiles); + }); + + return allDeleteFiles; + } + + private List loadRemovedDeleteFilesFromManifest( + ManifestFile manifest, Long targetSnapshotId) { + List deleteFiles = Lists.newArrayList(); + + try (ManifestReader reader = + ManifestFiles.readDeleteManifest(manifest, table().io(), table().specs())) { + for (ManifestEntry entry : reader.entries()) { + if (entry.status() == ManifestEntry.Status.DELETED + && entry.snapshotId().equals(targetSnapshotId)) { + DeleteFile file = entry.file(); + + if (!partitionMatchesFilter(file)) { + continue; + } + + Set columns = + file.content() == FileContent.POSITION_DELETES + ? Set.of(MetadataColumns.DELETE_FILE_PATH.fieldId()) + : Set.copyOf(file.equalityFieldIds()); + deleteFiles.add(ContentFileUtil.copy(file, true, columns)); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to read delete manifest: " + manifest.path(), e); + } + + return deleteFiles; + } + + /** + * Prunes delete manifests based on partition filter to avoid processing irrelevant manifests. + * This significantly improves performance when only a subset of partitions are relevant to the + * scan. + * + * @param manifests all delete manifests to consider + * @return list of manifests that might contain relevant delete files + */ + private List pruneManifestsByPartition(List manifests) { + Expression currentFilter = filter(); + + // If there's no filter, return all manifests + if (currentFilter == null || currentFilter.equals(Expressions.alwaysTrue())) { + return manifests; + } + + List prunedManifests = Lists.newArrayList(); + + for (ManifestFile manifest : manifests) { + PartitionSpec spec = table().specs().get(manifest.partitionSpecId()); + if (spec == null || spec.isUnpartitioned()) { + // Include unpartitioned manifests + prunedManifests.add(manifest); + } else if (manifestOverlapsFilter(manifest, spec, currentFilter)) { + // Check if manifest partition range overlaps with filter + prunedManifests.add(manifest); + } + } + + return prunedManifests; + } + + /** + * Checks if a manifest's partition range overlaps with the given filter. + * + * @param manifest the manifest to check + * @param spec the partition spec for the manifest + * @param filter the scan filter + * @return true if the manifest might contain matching partitions, false otherwise + */ + private boolean manifestOverlapsFilter( + ManifestFile manifest, PartitionSpec spec, Expression filter) { + try { + // Use inclusive projection to transform row filter to partition filter + Expression partitionFilter = Projections.inclusive(spec, isCaseSensitive()).project(filter); + + // Create evaluator for the partition filter + ManifestEvaluator evaluator = + ManifestEvaluator.forPartitionFilter(partitionFilter, spec, isCaseSensitive()); + + // Check if manifest could contain matching partitions + return evaluator.eval(manifest); + } catch (Exception e) { + // If evaluation fails, be conservative and include the manifest + return true; + } + } + + /** + * Checks if a delete file's partition overlaps with the current scan filter. This enables + * partition pruning to reduce memory footprint and planning overhead by skipping delete files + * that cannot possibly match any rows in the scan. + * + * @param file the delete file to check + * @return true if the delete file's partition might contain matching rows, false otherwise + */ + private boolean partitionMatchesFilter(DeleteFile file) { + // If there's no filter, all partitions match + Expression currentFilter = filter(); + if (currentFilter == null || currentFilter.equals(Expressions.alwaysTrue())) { + return true; + } + + // Get the partition spec for this delete file + PartitionSpec spec = table().specs().get(file.specId()); + if (spec == null || spec.isUnpartitioned()) { + // If spec not found or table is unpartitioned, be conservative and include the file + return true; + } + + try { + // Project the row filter to partition space using inclusive projection + // This transforms expressions on source columns to expressions on partition columns + Expression partitionFilter = + Projections.inclusive(spec, isCaseSensitive()).project(currentFilter); + + // Evaluate the projected filter against the delete file's partition + Evaluator evaluator = new Evaluator(spec.partitionType(), partitionFilter, isCaseSensitive()); + return evaluator.eval(file.partition()); + } catch (Exception e) { + // If evaluation fails, be conservative and include the file + return true; + } + } + + /** + * Loads delete files from a single manifest, parsing the manifest entries. + * + * @param manifest the delete manifest to load + * @return list of delete files from this manifest + */ + private List loadDeleteFilesFromManifest( + ManifestFile manifest, Long targetSnapshotId) { + List deleteFiles = Lists.newArrayList(); + + try (ManifestReader reader = + ManifestFiles.readDeleteManifest(manifest, table().io(), table().specs())) { + for (ManifestEntry entry : reader.entries()) { + if (entry.status() != ManifestEntry.Status.DELETED + && (targetSnapshotId == null || entry.snapshotId().equals(targetSnapshotId))) { + // Only include live delete files, copy with minimal stats to save memory + DeleteFile file = entry.file(); + + // Apply partition pruning - skip delete files that cannot match the scan filter + if (!partitionMatchesFilter(file)) { + continue; + } + + Set columns = + file.content() == FileContent.POSITION_DELETES + ? Set.of(MetadataColumns.DELETE_FILE_PATH.fieldId()) + : Set.copyOf(file.equalityFieldIds()); + deleteFiles.add(ContentFileUtil.copy(file, true, columns)); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to read delete manifest: " + manifest.path(), e); + } + + return deleteFiles; + } + + private static class CreateDataFileChangeTasks implements CreateTasksFunction { + private static final DeleteFile[] NO_DELETES = new DeleteFile[0]; + + private final Map snapshotOrdinals; + private final Supplier existingDeleteIndexSupplier; + private final Map addedDeletesBySnapshot; + private final Map> cumulativeDeletesMap; + private final Map specsById; + private final boolean caseSensitive; + + CreateDataFileChangeTasks( + Deque snapshots, + Supplier existingDeleteIndexSupplier, + Map addedDeletesBySnapshot, + Map> cumulativeDeletesMap, + Map specsById, + boolean caseSensitive) { + this.snapshotOrdinals = computeSnapshotOrdinals(snapshots); + this.existingDeleteIndexSupplier = existingDeleteIndexSupplier; + this.addedDeletesBySnapshot = addedDeletesBySnapshot; + this.cumulativeDeletesMap = cumulativeDeletesMap; + this.specsById = specsById; + this.caseSensitive = caseSensitive; + } + + @Override + public CloseableIterable apply( + CloseableIterable> entries, TaskContext context) { + + return CloseableIterable.transform( + entries, + entry -> { + long commitSnapshotId = entry.snapshotId(); + int changeOrdinal = snapshotOrdinals.get(commitSnapshotId); + DataFile dataFile = entry.file().copy(context.shouldKeepStats()); + + switch (entry.status()) { + case ADDED: + // For ADDED data files, attach delete files added in this snapshot + DeleteFile[] addedFileDeletes = getDeletesForAddedFile(entry, commitSnapshotId); + return new BaseAddedRowsScanTask( + changeOrdinal, + commitSnapshotId, + dataFile, + addedFileDeletes, + context.schemaAsString(), + context.specAsString(), + context.residuals()); + + case DELETED: + // For DELETED data files, attach ALL deletes that were present up to deletion + // This includes existing deletes AND deletes added in the scan range + DeleteFile[] deletedFileDeletes = getDeletesForDeletedFile(entry, commitSnapshotId); + return new BaseDeletedDataFileScanTask( + changeOrdinal, + commitSnapshotId, + dataFile, + deletedFileDeletes, + context.schemaAsString(), + context.specAsString(), + context.residuals()); + + default: + throw new IllegalArgumentException("Unexpected entry status: " + entry.status()); + } + }); + } + + /** + * Gets delete files that apply to an ADDED data file. Only includes deletes added in the same + * snapshot as the file. + */ + private DeleteFile[] getDeletesForAddedFile( + ManifestEntry entry, long commitSnapshotId) { + DeleteFileIndex addedDeleteIndex = addedDeletesBySnapshot.get(commitSnapshotId); + return addedDeleteIndex == null || addedDeleteIndex.isEmpty() + ? NO_DELETES + : addedDeleteIndex.forEntry(entry); + } + + /** + * Gets all delete files that were applied to a DELETED data file up to the point it was + * deleted. This includes existing deletes and all deletes added in the scan range up to (but + * not including) the deletion snapshot. + */ + private DeleteFile[] getDeletesForDeletedFile( + ManifestEntry entry, long deletionSnapshotId) { + + List allDeletes = Lists.newArrayList(); + + // Build existing delete index lazily when first DELETED entry is encountered + DeleteFileIndex existingDeleteIndex = existingDeleteIndexSupplier.get(); + DeleteFile[] existingDeletes = + existingDeleteIndex.isEmpty() ? NO_DELETES : existingDeleteIndex.forEntry(entry); + for (DeleteFile df : existingDeletes) { + allDeletes.add(df); + } + + // Add all deletes from snapshots in the scan range BEFORE the deletion + List cumulativeDeletes = cumulativeDeletesMap.get(deletionSnapshotId); + if (cumulativeDeletes != null && !cumulativeDeletes.isEmpty()) { + DeleteFileIndex tempIndex = + DeleteFileIndex.builderFor(cumulativeDeletes) + .specsById(specsById) + .caseSensitive(caseSensitive) + .build(); + DeleteFile[] applicable = tempIndex.forEntry(entry); + for (DeleteFile deleteFile : applicable) { + allDeletes.add(deleteFile); + } + } + + return allDeletes.isEmpty() ? NO_DELETES : allDeletes.toArray(new DeleteFile[0]); + } + } +} diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/package-info.java b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/package-info.java similarity index 87% rename from runners/samza/src/main/java/org/apache/beam/runners/samza/util/package-info.java rename to sdks/java/io/iceberg/src/main/java/org/apache/iceberg/package-info.java index 23290fd4ffbb..a32b307b45d3 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/util/package-info.java +++ b/sdks/java/io/iceberg/src/main/java/org/apache/iceberg/package-info.java @@ -16,5 +16,5 @@ * limitations under the License. */ -/** Internal implementation of the Beam runner for Apache Samza. */ -package org.apache.beam.runners.samza.util; +/** 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/BeamRowWrapperTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapperTest.java new file mode 100644 index 000000000000..bd8cead72987 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/BeamRowWrapperTest.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.logicaltypes.Date; +import org.apache.beam.sdk.schemas.logicaltypes.DateTime; +import org.apache.beam.sdk.schemas.logicaltypes.FixedPrecisionNumeric; +import org.apache.beam.sdk.schemas.logicaltypes.MicrosInstant; +import org.apache.beam.sdk.schemas.logicaltypes.Time; +import org.apache.beam.sdk.values.Row; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.DateTimeUtil; +import org.apache.iceberg.util.UUIDUtil; +import org.junit.Test; + +public class BeamRowWrapperTest { + private static final Schema NESTED_BEAM_SCHEMA = + Schema.builder().addInt32Field("nested_int").build(); + private static final Schema BEAM_SCHEMA = + Schema.builder() + .addByteField("byte_field") + .addInt16Field("int16_field") + .addStringField("string_field") + .addByteArrayField("bytes_field") + .addByteArrayField("uuid_field") + .addDecimalField("decimal_field") + .addDateTimeField("datetime_field") + .addLogicalTypeField("micros_instant_field", new MicrosInstant()) + .addLogicalTypeField("date_time_field", new DateTime()) + .addLogicalTypeField("date_field", new Date()) + .addLogicalTypeField("time_field", new Time()) + .addLogicalTypeField("fixed_numeric_field", FixedPrecisionNumeric.of(10, 2)) + .addRowField("row_field", NESTED_BEAM_SCHEMA) + .addInt32Field("pass_through_field") + .build(); + private static final Types.StructType ICEBERG_STRUCT = + Types.StructType.of( + Types.NestedField.required(1, "byte_field", Types.IntegerType.get()), + Types.NestedField.required(2, "int16_field", Types.IntegerType.get()), + Types.NestedField.required(3, "string_field", Types.StringType.get()), + Types.NestedField.required(4, "bytes_field", Types.BinaryType.get()), + Types.NestedField.required(5, "uuid_field", Types.UUIDType.get()), + Types.NestedField.required(6, "decimal_field", Types.DecimalType.of(10, 2)), + Types.NestedField.required(7, "datetime_field", Types.TimestampType.withZone()), + Types.NestedField.required(8, "micros_instant_field", Types.TimestampType.withZone()), + Types.NestedField.required(9, "date_time_field", Types.TimestampType.withoutZone()), + Types.NestedField.required(10, "date_field", Types.DateType.get()), + Types.NestedField.required(11, "time_field", Types.TimeType.get()), + Types.NestedField.required(12, "fixed_numeric_field", Types.DecimalType.of(10, 2)), + Types.NestedField.required( + 13, + "row_field", + Types.StructType.of( + Types.NestedField.required(1, "nested_int", Types.IntegerType.get()))), + Types.NestedField.required(14, "pass_through_field", Types.IntegerType.get())); + private static final UUID TEST_UUID = UUID.randomUUID(); + private static final Row NESTED_ROW = Row.withSchema(NESTED_BEAM_SCHEMA).addValue(999).build(); + private static final Row ROW = + Row.withSchema(BEAM_SCHEMA) + .addValues( + (byte) 42, + (short) 123, + "testString", + new byte[] {0x01, 0x02, 0x03}, + ByteBuffer.allocate(16) + .putLong(TEST_UUID.getMostSignificantBits()) + .putLong(TEST_UUID.getLeastSignificantBits()) + .array(), + new BigDecimal("123.45"), + org.joda.time.Instant.now(), + Instant.now(), + LocalDateTime.now(ZoneId.systemDefault()), + LocalDate.now(ZoneId.systemDefault()), + LocalTime.now(ZoneId.systemDefault()), + new BigDecimal("567.89"), + NESTED_ROW, + 888) + .build(); + private static final BeamRowWrapper WRAPPER = + new BeamRowWrapper(BEAM_SCHEMA, ICEBERG_STRUCT).wrap(ROW); + + @Test + public void testSize() { + assertEquals("Size should match the schema field count", 14, WRAPPER.size()); + } + + @Test + public void testUnsupportedSetThrowsException() { + assertThrows(UnsupportedOperationException.class, () -> WRAPPER.set(0, "test")); + } + + @Test + public void testNullRowHandling() { + BeamRowWrapper emptyWrapper = new BeamRowWrapper(BEAM_SCHEMA, ICEBERG_STRUCT); + assertNull( + "Should return null if the underlying row is null", emptyWrapper.get(0, Object.class)); + } + + @Test + public void testNullFieldHandling() { + Schema nullableSchema = Schema.builder().addNullableStringField("nullable_str").build(); + Types.StructType nullableIcebergType = + Types.StructType.of(Types.NestedField.optional(1, "nullable_str", Types.StringType.get())); + + Row nullRow = Row.withSchema(nullableSchema).addValue(null).build(); + BeamRowWrapper nullableWrapper = + new BeamRowWrapper(nullableSchema, nullableIcebergType).wrap(nullRow); + + assertNull("Should return null for a null field value", nullableWrapper.get(0, String.class)); + } + + // --- Type Conversion Tests --- + + @Test + public void testByteConversion() { + assertEquals(ROW.getByte(0), WRAPPER.get(0, Byte.class)); + } + + @Test + public void testInt16Conversion() { + assertEquals(ROW.getInt16(1), WRAPPER.get(1, Short.class)); + } + + @Test + public void testStringConversion() { + assertEquals(ROW.getString(2), WRAPPER.get(2, String.class)); + } + + @Test + public void testBytesToByteBufferConversion() { + assertEquals(ByteBuffer.wrap(ROW.getBytes(3)), WRAPPER.get(3, ByteBuffer.class)); + } + + @Test + public void testBytesToUUIDConversion() { + assertEquals(UUIDUtil.convert(ROW.getBytes(4)), WRAPPER.get(4, UUID.class)); + } + + @Test + public void testDecimalConversion() { + assertEquals(ROW.getDecimal(5), WRAPPER.get(5, BigDecimal.class)); + } + + @Test + public void testDateTimeConversion() { + long expectedJodaMicros = TimeUnit.MILLISECONDS.toMicros(ROW.getDateTime(6).getMillis()); + assertEquals(expectedJodaMicros, (long) WRAPPER.get(6, Long.class)); + } + + @Test + public void testMicrosInstantLogicalTypeConversion() { + Instant javaInstant = ROW.getLogicalTypeValue(7, Instant.class); + long expectedMicrosInstant = + TimeUnit.SECONDS.toMicros(javaInstant.getEpochSecond()) + javaInstant.getNano() / 1000; + assertEquals(expectedMicrosInstant, (long) WRAPPER.get(7, Long.class)); + } + + @Test + public void testDateTimeLogicalTypeConversion() { + long expectedDateTime = + DateTimeUtil.microsFromTimestamp(ROW.getLogicalTypeValue(8, LocalDateTime.class)); + assertEquals(expectedDateTime, (long) WRAPPER.get(8, Long.class)); + } + + @Test + public void testDateLogicalTypeConversion() { + int expectedDate = DateTimeUtil.daysFromDate(ROW.getLogicalTypeValue(9, LocalDate.class)); + assertEquals(expectedDate, (int) WRAPPER.get(9, Integer.class)); + } + + @Test + public void testTimeLogicalTypeConversion() { + long expectedTime = DateTimeUtil.microsFromTime(ROW.getLogicalTypeValue(10, LocalTime.class)); + assertEquals(expectedTime, (long) WRAPPER.get(10, Long.class)); + } + + @Test + public void testFixedPrecisionNumericLogicalTypeConversion() { + assertEquals(ROW.getLogicalTypeValue(11, BigDecimal.class), WRAPPER.get(11, BigDecimal.class)); + } + + @Test + public void testNestedRowConversion() { + StructLike nestedWrapperResult = WRAPPER.get(12, StructLike.class); + assertTrue( + "Should return a nested BeamRowWrapper", nestedWrapperResult instanceof BeamRowWrapper); + assertEquals(999, (int) nestedWrapperResult.get(0, Integer.class)); + } + + @Test + public void testPassThroughFallbackConversion() { + // Tests the 'default' case in the switch statement + assertEquals(ROW.getInt32(13), WRAPPER.get(13, Integer.class)); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java index 9b08e1ff86e1..5849cbd00774 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java @@ -35,6 +35,7 @@ import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.PCollectionRowTuple; import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.iceberg.CatalogUtil; import org.apache.iceberg.Snapshot; @@ -52,6 +53,9 @@ public class IcebergCdcReadSchemaTransformProviderTest { @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); + private static final org.apache.iceberg.Schema CDC_SCHEMA = + new org.apache.iceberg.Schema(TestFixtures.SCHEMA.columns(), ImmutableSet.of(1)); + @Rule public TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); @Rule public TestPipeline testPipeline = TestPipeline.create(); @@ -83,8 +87,8 @@ public void testSimpleScan() throws Exception { String identifier = "default.table_" + Long.toString(UUID.randomUUID().hashCode(), 16); TableIdentifier tableId = TableIdentifier.parse(identifier); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - final Schema schema = IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, CDC_SCHEMA); + final Schema schema = IcebergUtils.icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); @@ -122,8 +126,8 @@ public void testStreamingReadUsingManagedTransform() throws Exception { String identifier = "default.table_" + Long.toString(UUID.randomUUID().hashCode(), 16); TableIdentifier tableId = TableIdentifier.parse(identifier); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - final Schema schema = IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, CDC_SCHEMA); + final Schema schema = IcebergUtils.icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable).subList(3, 9); List snapshots = Lists.newArrayList(simpleTable.snapshots()); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java index f79991cee571..d7c97efa19f3 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java @@ -54,6 +54,7 @@ import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; @@ -119,6 +120,14 @@ public static Iterable data() { @Parameter(0) public boolean useIncrementalScan; + private org.apache.iceberg.Schema schemaForMode( + org.apache.iceberg.Schema schema, Integer... identifierFieldIds) { + if (!useIncrementalScan) { + return schema; + } + return new org.apache.iceberg.Schema(schema.columns(), ImmutableSet.copyOf(identifierFieldIds)); + } + static class PrintRow extends PTransform, PCollection> { @Override @@ -143,7 +152,7 @@ public void process(@Element Row row, OutputReceiver output) { public void testFailWhenBothStartingSnapshotAndTimestampAreSet() { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - warehouse.createTable(tableId, TestFixtures.SCHEMA); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()) .from(tableId) @@ -161,7 +170,7 @@ public void testFailWhenBothStartingSnapshotAndTimestampAreSet() { public void testFailWhenBothEndingSnapshotAndTimestampAreSet() { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - warehouse.createTable(tableId, TestFixtures.SCHEMA); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()) .withCdc() @@ -179,7 +188,7 @@ public void testFailWhenBothEndingSnapshotAndTimestampAreSet() { public void testFailWhenStartingPointAndStartingStrategyAreSet() { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - warehouse.createTable(tableId, TestFixtures.SCHEMA); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()) .withCdc() @@ -197,7 +206,7 @@ public void testFailWhenStartingPointAndStartingStrategyAreSet() { public void testFailWhenPollIntervalIsSetOnBatchRead() { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - warehouse.createTable(tableId, TestFixtures.SCHEMA); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()) .withCdc() @@ -210,6 +219,32 @@ public void testFailWhenPollIntervalIsSetOnBatchRead() { read.expand(PBegin.in(testPipeline)); } + @Test + public void testCdcFailsWhenTableHasNoIdentifierFields() { + assumeTrue(useIncrementalScan); + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + warehouse.createTable(tableId, TestFixtures.SCHEMA); + IcebergIO.ReadRows read = IcebergIO.readRows(catalogConfig()).from(tableId).withCdc(); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Cannot read CDC records"); + thrown.expectMessage("primary key fields"); + read.expand(PBegin.in(testPipeline)); + } + + @Test + public void testCdcFailsWhenProjectionDropsIdentifierFields() { + assumeTrue(useIncrementalScan); + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + IcebergIO.ReadRows read = + IcebergIO.readRows(catalogConfig()).from(tableId).withCdc().dropping(singletonList("id")); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("projected schema must not drop primary key fields"); + read.expand(PBegin.in(testPipeline)); + } + @Test public void testFailWhenDropAndKeepAreSet() { TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); @@ -307,8 +342,8 @@ public void testProjectedSchemaWithNestedFields() { public void testSimpleScan() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - final Schema schema = icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + final Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); @@ -339,8 +374,8 @@ public void testSimpleScan() throws Exception { public void testScanSelectedFields() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - final Schema schema = icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + final Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); @@ -368,6 +403,11 @@ public void testScanSelectedFields() throws Exception { return null; }); + if (useIncrementalScan) { + testPipeline.run(); + return; + } + // test drop fields read = read.keeping(null).dropping(singletonList("id")); PCollection outputDrop = @@ -387,7 +427,7 @@ public void testScanSelectedFields() throws Exception { public void testScanWithFilter() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); List> expectedRecords = warehouse.commitData(simpleTable); @@ -440,6 +480,7 @@ public void testReadSchemaWithRandomlyOrderedIds() throws IOException { required(1, "a", Types.IntegerType.get()), required(2, "b", StructType.of(nestedSchema.columns())), required(5, "c", StringType.get())); + schema = schemaForMode(schema, 1); // hadoop catalog will re-order by breadth-first ordering Table simpleTable = warehouse.createTable(tableId, schema); @@ -484,7 +525,8 @@ public void testReadSchemaWithRandomlyOrderedIds() throws IOException { public void testIdentityColumnScan() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); + org.apache.iceberg.Schema baseSchema = schemaForMode(TestFixtures.SCHEMA, 1); + Table simpleTable = warehouse.createTable(tableId, baseSchema); String identityColumnName = "identity"; String identityColumnValue = "some-value"; @@ -499,11 +541,7 @@ public void testIdentityColumnScan() throws Exception { .newFastAppend() .appendFile( warehouse.writeRecords( - "file1s1.parquet", - TestFixtures.SCHEMA, - spec, - partitionKey, - TestFixtures.FILE1SNAPSHOT1)) + "file1s1.parquet", baseSchema, spec, partitionKey, TestFixtures.FILE1SNAPSHOT1)) .commit(); final Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); @@ -609,9 +647,10 @@ public void testNameMappingScan() throws Exception { TableIdentifier tableId = TableIdentifier.of("default", "table" + Long.toString(UUID.randomUUID().hashCode(), 16)); + org.apache.iceberg.Schema tableSchema = schemaForMode(TestFixtures.NESTED_SCHEMA, 1); Table simpleTable = warehouse - .buildTable(tableId, TestFixtures.NESTED_SCHEMA) + .buildTable(tableId, tableSchema) .withProperties(tableProperties) .withPartitionSpec(PartitionSpec.unpartitioned()) .create(); @@ -625,7 +664,7 @@ public void testNameMappingScan() throws Exception { .withMetrics(metrics) .build(); - final Schema beamSchema = icebergSchemaToBeamSchema(TestFixtures.NESTED_SCHEMA); + final Schema beamSchema = icebergSchemaToBeamSchema(simpleTable.schema()); simpleTable.newFastAppend().appendFile(dataFile).commit(); @@ -637,7 +676,7 @@ public void testNameMappingScan() throws Exception { final Row[] expectedRows = recordData.stream() - .map(data -> icebergGenericRecord(TestFixtures.NESTED_SCHEMA.asStruct(), data)) + .map(data -> icebergGenericRecord(simpleTable.schema().asStruct(), data)) .map(record -> IcebergUtils.icebergRecordToBeamRow(beamSchema, record)) .toArray(Row[]::new); @@ -695,8 +734,8 @@ public void runWithStartingStrategy(@Nullable StartingStrategy strategy, boolean throws IOException { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - Schema schema = icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); List> expectedRecords = warehouse.commitData(simpleTable); if ((strategy == StartingStrategy.LATEST) || (streaming && strategy == null)) { @@ -731,8 +770,8 @@ public void runReadWithBoundary(boolean useSnapshotBoundary, boolean streaming) throws IOException { assumeTrue(useIncrementalScan); TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); - Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); - Schema schema = icebergSchemaToBeamSchema(TestFixtures.SCHEMA); + Table simpleTable = warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1)); + Schema schema = icebergSchemaToBeamSchema(simpleTable.schema()); // only read data committed in the second and third snapshots List> expectedRecords = warehouse.commitData(simpleTable).subList(3, 9); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java index a7349bffdfa0..52d92911f4e4 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOWriteTest.java @@ -17,24 +17,54 @@ */ package org.apache.beam.sdk.io.iceberg; +import static java.util.Arrays.asList; import static org.apache.beam.sdk.io.iceberg.IcebergUtils.beamRowToIcebergRecord; +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import static org.apache.beam.sdk.values.TypeDescriptors.integers; +import static org.apache.beam.sdk.values.TypeDescriptors.kvs; +import static org.apache.beam.sdk.values.TypeDescriptors.strings; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import java.io.Serializable; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.LongStream; +import org.apache.beam.sdk.PipelineResult; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.VarLongCoder; +import org.apache.beam.sdk.io.GenerateSequence; +import org.apache.beam.sdk.metrics.Counter; +import org.apache.beam.sdk.metrics.MetricNameFilter; +import org.apache.beam.sdk.metrics.Metrics; +import org.apache.beam.sdk.metrics.MetricsFilter; import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaCoder; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.testing.TestStream; import org.apache.beam.sdk.transforms.Count; import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.Flatten; +import org.apache.beam.sdk.transforms.GroupByKey; +import org.apache.beam.sdk.transforms.MapElements; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.transforms.Redistribute; +import org.apache.beam.sdk.transforms.Values; +import org.apache.beam.sdk.transforms.WithKeys; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionList; import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptors; import org.apache.beam.sdk.values.ValueInSingleWindow; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; @@ -44,7 +74,9 @@ import org.apache.iceberg.AppendFiles; import org.apache.iceberg.CatalogUtil; import org.apache.iceberg.DataFile; +import org.apache.iceberg.DistributionMode; import org.apache.iceberg.FileFormat; +import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.SupportsNamespaces; @@ -64,14 +96,26 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class IcebergIOWriteTest implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(IcebergIOWriteTest.class); + private static final String NONE = "none"; + private static final String HASH = "hash"; + private static final String HASH_WITH_AUTOSHARDING = "hashWithAutoSharding"; + + @Parameterized.Parameters + public static Iterable data() { + return asList(new Object[][] {{NONE}, {HASH}, {HASH_WITH_AUTOSHARDING}}); + } + + @Parameterized.Parameter(0) + public String distributionMode; + @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); @Rule @@ -79,6 +123,28 @@ public class IcebergIOWriteTest implements Serializable { @Rule public transient TestPipeline testPipeline = TestPipeline.create(); + private IcebergIO.WriteRows writeTransform( + IcebergCatalogConfig catalog, TableIdentifier tableId) { + IcebergIO.WriteRows write = IcebergIO.writeRows(catalog).to(tableId); + return applyDistribution(write); + } + + private IcebergIO.WriteRows writeTransform( + IcebergCatalogConfig catalog, DynamicDestinations dynamicDestinations) { + IcebergIO.WriteRows write = IcebergIO.writeRows(catalog).to(dynamicDestinations); + return applyDistribution(write); + } + + private IcebergIO.WriteRows applyDistribution(IcebergIO.WriteRows write) { + if (distributionMode.contains(HASH)) { + write = write.withDistributionMode(DistributionMode.HASH); + } + if (distributionMode.equals(HASH_WITH_AUTOSHARDING)) { + write = write.withAutosharding(); + } + return write; + } + @Test public void testSimpleAppend() throws Exception { TableIdentifier tableId = @@ -99,7 +165,7 @@ public void testSimpleAppend() throws Exception { testPipeline .apply("Records To Add", Create.of(TestFixtures.asRows(TestFixtures.FILE1SNAPSHOT1))) .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA)) - .apply("Append To Table", IcebergIO.writeRows(catalog).to(tableId)); + .apply("Append To Table", writeTransform(catalog, tableId)); LOG.info("Executing pipeline"); testPipeline.run().waitUntilFinish(); @@ -129,7 +195,7 @@ public void testCreateNamespaceAndTable() { testPipeline .apply("Records To Add", Create.of(TestFixtures.asRows(TestFixtures.FILE1SNAPSHOT1))) .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA)) - .apply("Append To Table", IcebergIO.writeRows(catalog).to(tableId)); + .apply("Append To Table", writeTransform(catalog, tableId)); assertFalse(((SupportsNamespaces) catalog.catalog()).namespaceExists(newNamespace)); LOG.info("Executing pipeline"); @@ -200,7 +266,7 @@ public IcebergDestination instantiateDestination(String dest) { TestFixtures.FILE1SNAPSHOT2, TestFixtures.FILE1SNAPSHOT3)))) .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA)) - .apply("Append To Table", IcebergIO.writeRows(catalog).to(dynamicDestinations)); + .apply("Append To Table", writeTransform(catalog, dynamicDestinations)); LOG.info("Executing pipeline"); testPipeline.run().waitUntilFinish(); @@ -293,7 +359,7 @@ public IcebergDestination instantiateDestination(String dest) { testPipeline .apply("Records To Add", Create.of(TestFixtures.asRows(elements))) .setRowSchema(IcebergUtils.icebergSchemaToBeamSchema(TestFixtures.SCHEMA)) - .apply("Append To Table", IcebergIO.writeRows(catalog).to(dynamicDestinations)); + .apply("Append To Table", writeTransform(catalog, dynamicDestinations)); LOG.info("Executing pipeline"); testPipeline.run().waitUntilFinish(); @@ -386,8 +452,7 @@ public void testStreamingWrite() { .apply("Stream Records", stream) .apply( "Append To Table", - IcebergIO.writeRows(catalog) - .to(tableId) + writeTransform(catalog, tableId) .withTriggeringFrequency(Duration.standardSeconds(3))) .getSnapshots(); // verify that 2 snapshots are created (one per triggering interval) @@ -400,4 +465,313 @@ public void testStreamingWrite() { List writtenRecords = ImmutableList.copyOf(IcebergGenerics.read(table).build()); assertThat(writtenRecords, Matchers.containsInAnyOrder(TestFixtures.FILE1SNAPSHOT1.toArray())); } + + @Test + public void testHashDistribution() { + assumeTrue(distributionMode.equals(HASH_WITH_AUTOSHARDING)); + Schema schema = Schema.builder().addInt64Field("id").addStringField("name").build(); + + TableIdentifier tableId = + TableIdentifier.of("default", "hash_" + Long.toString(UUID.randomUUID().hashCode(), 16)); + Map catalogProps = + ImmutableMap.builder() + .put("type", CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP) + .put("warehouse", warehouse.location) + .build(); + IcebergCatalogConfig catalog = + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties(catalogProps) + .build(); + + // create table with two partitions + catalog + .catalog() + .createTable( + tableId, + IcebergUtils.beamSchemaToIcebergSchema(schema), + PartitionSpec.builderFor(IcebergUtils.beamSchemaToIcebergSchema(schema)) + .bucket("id", 2) + .build()); + + // Prepare 100 rows and split them up into separate keys. + // The "none" distribution will process each key in a separate writer DoFn, + // essentially creating one file per parallel thread. This means one file per + // record since each record is in its own key. + // The "hash" distribution will group records by partition key first, resulting + // in a much smaller number of files created. + PCollection rows = + testPipeline + .apply(GenerateSequence.from(0).to(100)) + .apply( + "Make rows", + MapElements.into(TypeDescriptors.rows()) + .via(i -> Row.withSchema(schema).addValues(i, "name_" + i).build())) + .setRowSchema(schema) + .apply(WithKeys.of(1L)) + .setCoder(KvCoder.of(VarLongCoder.of(), SchemaCoder.of(schema))) + .apply(Redistribute.byKey()) + .apply(Values.create()); + + Function, KV>> getAddedFilesFunc = + (distribution) -> + MapElements.into(kvs(strings(), integers())) + .via( + snapshot -> + KV.of( + distribution, + Integer.parseInt( + checkStateNotNull(snapshot.getValue().getSummary()) + .get("added-data-files")))); + + // 1. Write files without any additional config + PCollection> noneDistributionAddedFiles = + rows.apply( + "none distribution write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withDistributionMode(DistributionMode.NONE)) + .getSnapshots() + .apply("Get none files", getAddedFilesFunc.apply(NONE)); + // 2. Write files with hash distribution + PCollection> hashDistributionAddedFiles = + rows.apply( + "hash distribution write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withDistributionMode(DistributionMode.HASH)) + .getSnapshots() + .apply("Get hash files", getAddedFilesFunc.apply(HASH)); + // 3. Write files with hash distribution AND auto-sharding + PCollection> hashAutoShardingDistributionAddedFiles = + rows.apply( + "hash distribution + autosharding write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withDistributionMode(DistributionMode.HASH) + .withAutosharding()) + .getSnapshots() + .apply("Get hash autosharded files", getAddedFilesFunc.apply(HASH_WITH_AUTOSHARDING)); + + PCollectionList.of( + Arrays.asList( + hashDistributionAddedFiles, + noneDistributionAddedFiles, + hashAutoShardingDistributionAddedFiles)) + .apply(Flatten.pCollections()) + .apply("add dummy key", WithKeys.of(1)) + .apply("group together", GroupByKey.create()) + .apply("unwrap values", Values.create()) + .apply( + "validate num files", + ParDo.of( + new DoFn>, Void>() { + @ProcessElement + public void process(@Element Iterable> sums) { + List> sumList = Lists.newArrayList(sums.iterator()); + assertEquals(3, sumList.size()); + + int numFilesAddedNoneDist = + Iterables.getOnlyElement( + sumList.stream() + .filter(kv -> kv.getKey().equals(NONE)) + .map(KV::getValue) + .collect(Collectors.toList())); + + int numFilesAddedHashDist = + Iterables.getOnlyElement( + sumList.stream() + .filter(kv -> kv.getKey().equals(HASH)) + .map(KV::getValue) + .collect(Collectors.toList())); + + int numFilesAddedHashAutoShardingDist = + Iterables.getOnlyElement( + sumList.stream() + .filter(kv -> kv.getKey().equals(HASH_WITH_AUTOSHARDING)) + .map(KV::getValue) + .collect(Collectors.toList())); + + System.out.println("none: " + numFilesAddedNoneDist); + System.out.println("hash: " + numFilesAddedHashDist); + System.out.println( + "hash with autosharding: " + numFilesAddedHashAutoShardingDist); + // plain hash distribution should have exactly the same number of partitions + assertEquals(2, numFilesAddedHashDist); + // hash with autosharding may create sub-shards and lead to more than just 2 + // files. + // should still be less than 'none' distribution though + assertTrue(numFilesAddedHashDist < numFilesAddedNoneDist); + } + })); + + testPipeline.run().waitUntilFinish(); + } + + @Test + public void testHashDistributionStreaming() { + assumeTrue(distributionMode.equals(HASH_WITH_AUTOSHARDING)); + Schema schema = Schema.builder().addInt64Field("id").addStringField("name").build(); + + TableIdentifier tableId = + TableIdentifier.of( + "default", "hash_streaming" + Long.toString(UUID.randomUUID().hashCode(), 16)); + Map catalogProps = + ImmutableMap.builder() + .put("type", CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP) + .put("warehouse", warehouse.location) + .build(); + IcebergCatalogConfig catalog = + IcebergCatalogConfig.builder() + .setCatalogName("name") + .setCatalogProperties(catalogProps) + .build(); + + // create table with two partitions + catalog + .catalog() + .createTable( + tableId, + IcebergUtils.beamSchemaToIcebergSchema(schema), + PartitionSpec.builderFor(IcebergUtils.beamSchemaToIcebergSchema(schema)) + .bucket("id", 2) + .build()); + + // Prepare 100 rows and split them up into separate keys. + // The "none" distribution will process each key in a separate writer DoFn, + // essentially creating one file per parallel thread. This means one file per + // record since each record is in its own key. + // The "hash" distribution will group records by partition key first, resulting + // in a much smaller number of files created. + PCollection rows = + testPipeline + .apply( + TestStream.create(VarLongCoder.of()) + .addElements(0L, LongStream.range(1, 10).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .addElements(10L, LongStream.range(11, 20).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .addElements(20L, LongStream.range(21, 30).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .addElements(30L, LongStream.range(31, 40).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .addElements(40L, LongStream.range(41, 50).boxed().toArray(Long[]::new)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .advanceProcessingTime(Duration.standardSeconds(10)) + .advanceWatermarkToInfinity()) + .apply( + "Make rows", + MapElements.into(TypeDescriptors.rows()) + .via(i -> Row.withSchema(schema).addValues(i, "name_" + i).build())) + .setRowSchema(schema) + .apply(WithKeys.of(r -> r.getInt64("id"))) + .setCoder(KvCoder.of(VarLongCoder.of(), SchemaCoder.of(schema))) + .apply(Redistribute.byKey()) + .apply(Values.create()); + + Function, KV>> getAddedFilesFunc = + (distribution) -> + MapElements.into(kvs(strings(), integers())) + .via( + snapshot -> + KV.of( + distribution, + Integer.parseInt( + checkStateNotNull(snapshot.getValue().getSummary()) + .get("added-data-files")))); + + // 1. Write files without any additional config + PCollection> noneDistributionAddedFiles = + rows.apply( + "none distribution write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withTriggeringFrequency(Duration.standardSeconds(5)) + .withDistributionMode(DistributionMode.NONE)) + .getSnapshots() + .apply("Get none files", getAddedFilesFunc.apply(NONE)); + // 2. Write files with hash distribution + PCollection> hashDistributionAddedFiles = + rows.apply( + "hash distribution write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withTriggeringFrequency(Duration.standardSeconds(5)) + .withDistributionMode(DistributionMode.HASH)) + .getSnapshots() + .apply("Get hash files", getAddedFilesFunc.apply(HASH)); + // 3. Write files with hash distribution AND auto-sharding + PCollection> hashAutoShardingDistributionAddedFiles = + rows.apply( + "hash distribution + autosharding write", + IcebergIO.writeRows(catalog) + .to(tableId) + .withTriggeringFrequency(Duration.standardSeconds(5)) + .withDistributionMode(DistributionMode.HASH) + .withAutosharding()) + .getSnapshots() + .apply("Get hash autosharded files", getAddedFilesFunc.apply(HASH_WITH_AUTOSHARDING)); + + PCollectionList.of( + Arrays.asList( + hashDistributionAddedFiles, + noneDistributionAddedFiles, + hashAutoShardingDistributionAddedFiles)) + .apply(Flatten.pCollections()) + .apply("add dummy key", WithKeys.of(1)) + .apply("group together", GroupByKey.create()) + .apply( + "validate num files", + ParDo.of( + new DoFn>>, Void>() { + private final Counter numWaves = + Metrics.counter(IcebergIOWriteTest.class, "numWaves"); + + @ProcessElement + public void process(@Element KV>> sums) { + List> sumList = + Lists.newArrayList(sums.getValue().iterator()); + // each wave should have one snapshot per write branch + System.out.println("list: " + sumList); + assertEquals(3, sumList.size()); + + // get the number of files written by each branch + int numFilesAddedHashDist = + Iterables.getOnlyElement( + sumList.stream() + .filter(kv -> kv.getKey().equals(HASH)) + .map(KV::getValue) + .collect(Collectors.toList())); + + // plain hash distribution should have exactly the same number of partitions + assertEquals(2, numFilesAddedHashDist); + // hash with autosharding may create sub-shards and lead to more than just 2 + // files. + // In a production runner like Dataflow, hash + autosharding would still + // make less files than 'none' distribution. + // We're testing with DirectRunner though, which doesn't have a smart + // autosharding implementation, so it may sometimes make more files + // than even 'none' distribution. + // assertTrue(numFilesAddedHashAutoShardingDist < numFilesAddedNoneDist); + numWaves.inc(); + } + })); + + PipelineResult result = testPipeline.run(); + result.waitUntilFinish(); + + // verify total number of snapshot commit waves + long numWaves = + result + .metrics() + .queryMetrics( + MetricsFilter.builder() + .addNameFilter(MetricNameFilter.named(IcebergIOWriteTest.class, "numWaves")) + .build()) + .getCounters() + .iterator() + .next() + .getCommitted(); + assertEquals(5L, numWaves); + } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java index a3217503564c..1319efa7229a 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergSchemaTransformTranslationTest.java @@ -46,9 +46,14 @@ import org.apache.beam.sdk.values.PCollectionRowTuple; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.InvalidProtocolBufferException; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; import org.apache.iceberg.CatalogUtil; +import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.types.Types; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -77,6 +82,13 @@ public class IcebergSchemaTransformTranslationTest { .build(); private static final Map CONFIG_PROPERTIES = ImmutableMap.builder().put("key", "value").put("key2", "value2").build(); + private static final org.apache.iceberg.Schema CDC_TRANSLATION_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.required(1, "id", Types.LongType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get()), + Types.NestedField.required(3, "event_micros", Types.LongType.get())), + ImmutableSet.of(1)); private static final Row WRITE_CONFIG_ROW = Row.withSchema(WRITE_PROVIDER.configurationSchema()) .withFieldValue("table", "test_table_identifier") @@ -104,6 +116,8 @@ public class IcebergSchemaTransformTranslationTest { .withFieldValue("to_timestamp", 456L) .withFieldValue("poll_interval_seconds", 123) .withFieldValue("streaming", true) + .withFieldValue("keep", ImmutableList.of("id", "event_micros")) + .withFieldValue("filter", "\"data\" = 'keep'") .build(); @Test @@ -269,7 +283,15 @@ public void testCdcReadTransformProtoTranslation() // First build a pipeline Pipeline p = Pipeline.create(); String identifier = "default.table_" + Long.toString(UUID.randomUUID().hashCode(), 16); - warehouse.createTable(TableIdentifier.parse(identifier), TestFixtures.SCHEMA); + Table table = warehouse.createTable(TableIdentifier.parse(identifier), CDC_TRANSLATION_SCHEMA); + table + .newFastAppend() + .appendFile( + warehouse.writeRecords( + "cdc-translation.parquet", + table.schema(), + Collections.singletonList(record(1L, "keep", 123L)))) + .commit(); Map properties = new HashMap<>(CATALOG_PROPERTIES); properties.put("warehouse", warehouse.location); @@ -278,6 +300,9 @@ public void testCdcReadTransformProtoTranslation() Row.fromRow(READ_CDC_CONFIG_ROW) .withFieldValue("table", identifier) .withFieldValue("catalog_properties", properties) + .withFieldValue("from_snapshot", table.currentSnapshot().snapshotId()) + .withFieldValue("to_snapshot", table.currentSnapshot().snapshotId()) + .withFieldValue("to_timestamp", null) .build(); IcebergCdcReadSchemaTransform readTransform = @@ -320,4 +345,10 @@ public void testCdcReadTransformProtoTranslation() assertEquals(transformConfigRow, readTransformFromSpec.getConfigurationRow()); } + + private static Record record(long id, String data, long eventMicros) { + return TestFixtures.createRecord( + CDC_TRANSLATION_SCHEMA, + ImmutableMap.of("id", id, "data", data, "event_micros", eventMicros)); + } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProviderTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProviderTest.java index 7028a394d2fd..c5fc5a6b6fe7 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProviderTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergWriteSchemaTransformProviderTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.io.iceberg; +import static java.util.Arrays.asList; import static org.apache.beam.sdk.io.iceberg.IcebergWriteSchemaTransformProvider.Configuration; import static org.apache.beam.sdk.io.iceberg.IcebergWriteSchemaTransformProvider.INPUT_TAG; import static org.apache.beam.sdk.io.iceberg.IcebergWriteSchemaTransformProvider.SNAPSHOTS_TAG; @@ -24,6 +25,7 @@ import static org.apache.iceberg.util.DateTimeUtil.timestampFromMicros; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; import java.time.LocalDate; import java.time.LocalDateTime; @@ -56,6 +58,7 @@ import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.iceberg.CatalogUtil; +import org.apache.iceberg.DistributionMode; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.TableIdentifier; @@ -72,12 +75,19 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; import org.yaml.snakeyaml.Yaml; /** Tests for {@link IcebergWriteSchemaTransformProvider}. */ -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class IcebergWriteSchemaTransformProviderTest { + @Parameterized.Parameters + public static Iterable data() { + return asList(new Object[][] {{DistributionMode.NONE}, {DistributionMode.HASH}}); + } + + @Parameterized.Parameter(0) + public DistributionMode distributionMode; @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); @@ -115,6 +125,7 @@ public void testSimpleAppend() { .setTable(identifier) .setCatalogName("name") .setCatalogProperties(properties) + .setDistributionMode(distributionMode.name()) .build(); PCollectionRowTuple input = @@ -151,10 +162,14 @@ public void testWriteUsingManagedTransform() { String.format( "table: %s\n" + "catalog_name: test-name\n" + + "distribution_mode: %s\n" + "catalog_properties: \n" + " type: %s\n" + " warehouse: %s", - identifier, CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP, warehouse.location); + identifier, + distributionMode.name(), + CatalogUtil.ICEBERG_CATALOG_TYPE_HADOOP, + warehouse.location); Map configMap = new Yaml().load(yamlConfig); PCollection inputRows = @@ -204,6 +219,7 @@ private void writeToDynamicDestinationsAndFilter(@Nullable String operation, boo ImmutableMap.builder() .put("table", destinationTemplate) .put("catalog_name", "test-name") + .put("distribution_mode", distributionMode.name()) .put( "catalog_properties", ImmutableMap.builder() @@ -372,8 +388,18 @@ public Void apply(Iterable input) { } } + @Test + public void testWritePartitionedDataWithAutosharding() { + assumeTrue(distributionMode.equals(DistributionMode.HASH)); + writePartitionedData(true); + } + @Test public void testWritePartitionedData() { + writePartitionedData(false); + } + + public void writePartitionedData(boolean autosharding) { Schema schema = Schema.builder() .addStringField("str") @@ -415,7 +441,11 @@ public void testWritePartitionedData() { "table", identifier, "catalog_properties", - ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)); + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location), + "distribution_mode", + distributionMode.name(), + "autosharding", + autosharding); List rows = new ArrayList<>(); for (int i = 0; i < 30; i++) { @@ -491,7 +521,9 @@ public void testWriteCreateTableWithPartitionSpec() { "year(y_datetime)", "month(m_date)", "day(d_date)", - "hour(h_datetimetz)")); + "hour(h_datetimetz)"), + "distribution_mode", + distributionMode.name()); List rows = new ArrayList<>(); for (int i = 0; i < 30; i++) { @@ -563,7 +595,9 @@ public void testWriteCreateTableWithTablePropertiesSpec() { "catalog_properties", ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location), "table_properties", - tableProperties); + tableProperties, + "distribution_mode", + distributionMode.name()); List rows = new ArrayList<>(); for (int i = 0; i < 10; i++) { @@ -622,7 +656,9 @@ public void testWriteCreateTableWithTableProperties() { "table", identifier, "catalog_properties", - ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)); + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location), + "distribution_mode", + distributionMode.name()); PCollection result = testPipeline diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/PartitionUtilsTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/PartitionUtilsTest.java index d80ec4f95310..740ede55811b 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/PartitionUtilsTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/PartitionUtilsTest.java @@ -23,17 +23,25 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Objects; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.MetadataColumns; import org.apache.iceberg.PartitionField; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.transforms.Days; import org.apache.iceberg.transforms.Hours; import org.apache.iceberg.transforms.Months; import org.apache.iceberg.transforms.Transform; +import org.apache.iceberg.types.Types; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Test; @@ -166,6 +174,66 @@ public void testAll() { assertEquals(expectedSpec, spec); } + @Test + public void testConstantsMapIncludesCdcMetadataAndIdentityConstants() throws Exception { + org.apache.iceberg.Schema icebergSchema = + new org.apache.iceberg.Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "category", Types.StringType.get())); + PartitionSpec spec = PartitionSpec.builderFor(icebergSchema).identity("category").build(); + DataFile file = + DataFiles.builder(spec) + .withFormat(FileFormat.PARQUET) + .withPath("file:///tmp/table/category=A/data.parquet") + .withPartitionPath("category=A") + .withFileSizeInBytes(100L) + .withRecordCount(2L) + .withFirstRowId(99L) + .build(); + setFileSequenceNumber(file, 42L); + + Map constants = PartitionUtils.constantsMap(spec, file, null); + + assertEquals(99L, constants.get(MetadataColumns.ROW_ID.fieldId())); + assertEquals(42L, constants.get(MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.fieldId())); + assertEquals(file.location(), constants.get(MetadataColumns.FILE_PATH.fieldId())); + assertEquals(file.specId(), constants.get(MetadataColumns.SPEC_ID.fieldId())); + assertEquals("A", constants.get(2)); + } + + @Test + public void testConstantsMapUsesExplicitSequenceNumberWhenFileSequenceIsUnavailable() { + org.apache.iceberg.Schema icebergSchema = + new org.apache.iceberg.Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "category", Types.StringType.get())); + PartitionSpec spec = PartitionSpec.builderFor(icebergSchema).identity("category").build(); + DataFile file = + DataFiles.builder(spec) + .withFormat(FileFormat.PARQUET) + .withPath("file:///tmp/table/category=B/data.parquet") + .withPartitionPath("category=B") + .withFileSizeInBytes(100L) + .withRecordCount(2L) + .build(); + + Map constants = PartitionUtils.constantsMap(spec, file, 123L); + + assertEquals(123L, constants.get(MetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER.fieldId())); + assertEquals("B", constants.get(2)); + } + + private static void setFileSequenceNumber(DataFile dataFile, long fileSequenceNumber) + throws Exception { + Method method = dataFile.getClass().getMethod("setFileSequenceNumber", Long.class); + method.setAccessible(true); + try { + method.invoke(dataFile, fileSequenceNumber); + } catch (InvocationTargetException e) { + throw (Exception) e.getCause(); + } + } + static class TestCase { private final String field; private @Nullable String name; diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadUtilsTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadUtilsTest.java index 73a0fd19e893..df9c44b7b4f3 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadUtilsTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadUtilsTest.java @@ -30,6 +30,7 @@ import java.util.Objects; import java.util.stream.Collectors; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Splitter; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Lists; @@ -40,7 +41,6 @@ import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.data.Record; import org.apache.iceberg.io.CloseableIterable; -import org.apache.iceberg.parquet.ParquetReader; import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.ClassRule; import org.junit.Rule; @@ -75,14 +75,25 @@ public void testCreateReader() throws IOException { .commit(); } + IcebergScanConfig scanConfig = + IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(simpleTable.schema())) + .build(); + int numFiles = 0; try (CloseableIterable iterable = simpleTable.newScan().planTasks()) { for (CombinedScanTask combinedScanTask : iterable) { for (FileScanTask fileScanTask : combinedScanTask.tasks()) { String fileName = Iterables.getLast(Splitter.on("/").split(fileScanTask.file().path())); List recordsRead = new ArrayList<>(); - try (ParquetReader reader = - ReadUtils.createReader(fileScanTask, simpleTable, simpleTable.schema())) { + try (CloseableIterable reader = + ReadUtils.createReader(fileScanTask, simpleTable, scanConfig)) { reader.forEach(recordsRead::add); } @@ -94,6 +105,36 @@ public void testCreateReader() throws IOException { assertEquals(data.size(), numFiles); } + @Test + public void testMaybeApplyFilterUsesRequiredSchemaWithFilterOnlyField() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table simpleTable = warehouse.createTable(tableId, TestFixtures.SCHEMA); + + IcebergScanConfig scanConfig = + IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(simpleTable.schema())) + .setKeepFields(ImmutableList.of("data")) + .setFilterString("id = 2") + .build(); + + assertEquals(ImmutableList.of("data"), fieldNames(scanConfig.getProjectedSchema())); + assertEquals(ImmutableList.of("id", "data"), fieldNames(scanConfig.getRequiredSchema())); + + CloseableIterable filtered = + ReadUtils.maybeApplyFilter( + CloseableIterable.withNoopClose(TestFixtures.FILE1SNAPSHOT1), + scanConfig, + scanConfig.getRequiredSchema()); + + assertEquals(ImmutableList.of("falafel"), dataOf(filtered)); + } + @Test public void testSnapshotsBetween() throws IOException { TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); @@ -239,4 +280,16 @@ static TestCase of( return new TestCase(scanConfig, expectedSnapshotId, description); } } + + private static List fieldNames(org.apache.iceberg.Schema schema) { + return schema.columns().stream() + .map(org.apache.iceberg.types.Types.NestedField::name) + .collect(Collectors.toList()); + } + + private static List dataOf(CloseableIterable records) { + return ImmutableList.copyOf(records).stream() + .map(record -> (String) record.getField("data")) + .collect(Collectors.toList()); + } } diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java index f27a86cc72a2..390e8d87af28 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/RecordWriterManagerTest.java @@ -53,7 +53,6 @@ import org.apache.beam.sdk.values.WindowedValues; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; import org.apache.commons.lang3.RandomStringUtils; -import org.apache.hadoop.conf.Configuration; import org.apache.iceberg.AppendFiles; import org.apache.iceberg.DataFile; import org.apache.iceberg.FileFormat; @@ -69,7 +68,6 @@ import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.data.Record; -import org.apache.iceberg.hadoop.HadoopCatalog; import org.apache.iceberg.io.FileIO; import org.apache.iceberg.io.InputFile; import org.apache.iceberg.io.OutputFile; @@ -109,16 +107,26 @@ public class RecordWriterManagerTest { IcebergUtils.beamSchemaToIcebergSchema(BEAM_SCHEMA); private static final PartitionSpec PARTITION_SPEC = PartitionSpec.builderFor(ICEBERG_SCHEMA).truncate("name", 3).identity("bool").build(); + private IcebergCatalogConfig catalogConfig; private WindowedValue windowedDestination; - private HadoopCatalog catalog; @Before public void setUp() { windowedDestination = getWindowedDestination("table_" + testName.getMethodName(), PARTITION_SPEC); - catalog = new HadoopCatalog(new Configuration(), warehouse.location); - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.invalidateAll(); + catalogConfig = + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build(); + TableCache.invalidateAll(); + } + + private static IcebergCatalogConfig mockCatalogConfigFor(Catalog mockCatalog) { + IcebergCatalogConfig catalogConfig = mock(IcebergCatalogConfig.class); + Mockito.doReturn(mockCatalog).when(catalogConfig).catalog(); + return catalogConfig; } private WindowedValue getWindowedDestination( @@ -145,7 +153,8 @@ private WindowedValue getWindowedDestination( @Test public void testCreateNamespaceAndTable() { - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); Namespace newNamespace = Namespace.of("new_namespace"); TableIdentifier identifier = TableIdentifier.of(newNamespace, testName.getMethodName()); WindowedValue dest = @@ -157,15 +166,16 @@ public void testCreateNamespaceAndTable() { Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); - assertFalse(catalog.namespaceExists(newNamespace)); + assertFalse(catalogConfig.namespaceExists(newNamespace.toString())); boolean writeSuccess = writerManager.write(dest, row); assertTrue(writeSuccess); - assertTrue(catalog.namespaceExists(newNamespace)); + assertTrue(catalogConfig.namespaceExists(newNamespace.toString())); } @Test public void testCreateTableWithSortOrder() throws IOException { - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); TableIdentifier identifier = TableIdentifier.of("default", testName.getMethodName()); WindowedValue dest = WindowedValues.valueInGlobalWindow( @@ -184,7 +194,7 @@ public void testCreateTableWithSortOrder() throws IOException { assertTrue(writerManager.write(dest, row)); writerManager.close(); - Table created = catalog.loadTable(identifier); + Table created = catalogConfig.catalog().loadTable(identifier); SortOrder order = created.sortOrder(); assertEquals(2, order.fields().size()); assertEquals(SortDirection.DESC, order.fields().get(0).direction()); @@ -196,7 +206,8 @@ public void testCreateTableWithSortOrder() throws IOException { @Test public void testCreateNewWriterForEachDestination() throws IOException { // Writer manager with a maximum limit of 3 writers - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); assertEquals(0, writerManager.openWriters); boolean writeSuccess; @@ -257,7 +268,8 @@ public void testCreateNewWriterForEachDestination() throws IOException { @Test public void testCreateNewWriterForEachPartition() throws IOException { // Writer manager with a maximum limit of 3 writers - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); assertEquals(0, writerManager.openWriters); boolean writeSuccess; @@ -318,7 +330,8 @@ public void testCreateNewWriterForEachPartition() throws IOException { @Test public void testRespectMaxFileSize() throws IOException { // Writer manager with a maximum file size of 100 bytes - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 100, 2); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 100, 2); assertEquals(0, writerManager.openWriters); boolean writeSuccess; @@ -364,7 +377,8 @@ public void testRespectMaxFileSize() throws IOException { @Test public void testRequireClosingBeforeFetchingDataFiles() { - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 100, 2); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 100, 2); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); writerManager.write(windowedDestination, row); assertEquals(1, writerManager.openWriters); @@ -401,7 +415,11 @@ public void testSerializableDataFileRoundTripEquality() throws IOException { partitionKey.partition(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row)); RecordWriter writer = - new RecordWriter(catalog, windowedDestination.getValue(), "test_file_name", partitionKey); + new RecordWriter( + catalogConfig.catalog(), + windowedDestination.getValue(), + "test_file_name", + partitionKey); writer.write(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row)); writer.write(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row2)); @@ -443,7 +461,11 @@ public void testRecreateSerializableDataAfterUpdatingPartitionSpec() throws IOEx // write some rows RecordWriter writer = - new RecordWriter(catalog, windowedDestination.getValue(), "test_file_name", partitionKey); + new RecordWriter( + catalogConfig.catalog(), + windowedDestination.getValue(), + "test_file_name", + partitionKey); writer.write(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row)); writer.write(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row2)); writer.close(); @@ -462,7 +484,8 @@ public void testRecreateSerializableDataAfterUpdatingPartitionSpec() throws IOEx assertEquals(serializableDataFile.getPartitionSpecId(), datafile.specId()); // update spec - Table table = catalog.loadTable(windowedDestination.getValue().getTableIdentifier()); + Table table = + catalogConfig.catalog().loadTable(windowedDestination.getValue().getTableIdentifier()); table.updateSpec().addField("id").removeField("bool").commit(); Map updatedSpecs = table.specs(); @@ -473,13 +496,14 @@ public void testRecreateSerializableDataAfterUpdatingPartitionSpec() throws IOEx @Test public void testWriterKeepsUpWithUpdatingPartitionSpec() throws IOException { - Table table = catalog.loadTable(windowedDestination.getValue().getTableIdentifier()); + Table table = + catalogConfig.catalog().loadTable(windowedDestination.getValue().getTableIdentifier()); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "abcdef", true).build(); Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "abcxyz", true).build(); // write some rows RecordWriterManager writer = - new RecordWriterManager(catalog, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); writer.write(windowedDestination, row); writer.write(windowedDestination, row2); writer.close(); @@ -497,20 +521,17 @@ public void testWriterKeepsUpWithUpdatingPartitionSpec() throws IOException { assertThat(dataFile.path().toString(), containsString("bool=true")); // table is cached - assertEquals(1, RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.size()); + assertEquals(1, TableCache.size()); // update spec table.updateSpec().addField("id").removeField("bool").commit(); - // Make the cached table stale to force reloading its metadata. - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.getIfPresent( - windowedDestination.getValue().getTableIdentifier()) - .lastRefreshTime = - Instant.EPOCH; + // Make the cached table stale to force refreshing its metadata. + TableCache.markStale(catalogConfig, windowedDestination.getValue().getTableIdentifier()); // write a second data file // should refresh the table and use the new partition spec RecordWriterManager writer2 = - new RecordWriterManager(catalog, "test_prefix_2", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix_2", Long.MAX_VALUE, Integer.MAX_VALUE); writer2.write(windowedDestination, row); writer2.write(windowedDestination, row2); writer2.close(); @@ -578,7 +599,7 @@ public void testIdentityPartitioning() throws IOException { getWindowedDestination("identity_partitioning", icebergSchema, spec); RecordWriterManager writer = - new RecordWriterManager(catalog, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); writer.write(dest, row); writer.close(); List files = writer.getSerializableDataFiles().get(dest); @@ -664,7 +685,7 @@ public void testBucketPartitioning() throws IOException { getWindowedDestination("bucket_partitioning", icebergSchema, spec); RecordWriterManager writer = - new RecordWriterManager(catalog, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); writer.write(dest, row); writer.close(); List files = writer.getSerializableDataFiles().get(dest); @@ -730,7 +751,7 @@ public void testTimePartitioning() throws IOException { // write some rows RecordWriterManager writer = - new RecordWriterManager(catalog, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); + new RecordWriterManager(catalogConfig, "test_prefix", Long.MAX_VALUE, Integer.MAX_VALUE); writer.write(dest, row); writer.close(); List files = writer.getSerializableDataFiles().get(dest); @@ -763,7 +784,7 @@ public void testTimePartitioning() throws IOException { String expectedPartition = String.join("/", expectedPartitions); DataFile dataFile = serializableDataFile.createDataFile( - catalog.loadTable(dest.getValue().getTableIdentifier()).specs()); + catalogConfig.catalog().loadTable(dest.getValue().getTableIdentifier()).specs()); assertThat(dataFile.path().toString(), containsString(expectedPartition)); } @@ -771,7 +792,8 @@ public void testTimePartitioning() throws IOException { @Test public void testWriterExceptionGetsCaught() throws IOException { - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 100, 2); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 100, 2); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "abcdef", true).build(); PartitionKey partitionKey = new PartitionKey(PARTITION_SPEC, ICEBERG_SCHEMA); partitionKey.partition(IcebergUtils.beamRowToIcebergRecord(ICEBERG_SCHEMA, row)); @@ -783,7 +805,10 @@ public void testWriterExceptionGetsCaught() throws IOException { // replace with a failing record writer FailingRecordWriter failingWriter = new FailingRecordWriter( - catalog, windowedDestination.getValue(), "test_failing_writer", partitionKey); + catalogConfig.catalog(), + windowedDestination.getValue(), + "test_failing_writer", + partitionKey); state.writers.put(partitionKey, failingWriter); writerManager.write(windowedDestination, row); @@ -843,7 +868,8 @@ public void testColumnSpecificMetricsCollection() throws IOException { WindowedValue singleDestination = WindowedValues.valueInGlobalWindow(destination); - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bbb", false).build(); Row row3 = Row.withSchema(BEAM_SCHEMA).addValues(3, "ccc", true).build(); @@ -905,7 +931,8 @@ public void testDefaultMetrics() throws IOException { WindowedValue singleDestination = WindowedValues.valueInGlobalWindow(destination); - RecordWriterManager writerManager = new RecordWriterManager(catalog, "test_file_name", 1000, 3); + RecordWriterManager writerManager = + new RecordWriterManager(catalogConfig, "test_file_name", 1000, 3); Row row1 = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); Row row2 = Row.withSchema(BEAM_SCHEMA).addValues(2, "bbb", false).build(); Row row3 = Row.withSchema(BEAM_SCHEMA).addValues(3, "ccc", true).build(); @@ -1090,15 +1117,16 @@ public void testRecordWriterManagerDoesNotCloseSharedFileIO() throws IOException Mockito.doReturn(sharedTrackingIO).when(spyTable1).io(); Mockito.doReturn(sharedTrackingIO).when(spyTable2).io(); - Catalog spyCatalog = Mockito.spy(catalog); + Catalog spyCatalog = Mockito.spy(catalogConfig.catalog()); Mockito.doReturn(spyTable1).when(spyCatalog).loadTable(tableId1); Mockito.doReturn(spyTable2).when(spyCatalog).loadTable(tableId2); WindowedValue dest1 = getWindowedDestination(tableName1, null); WindowedValue dest2 = getWindowedDestination(tableName2, null); + IcebergCatalogConfig spyCatalogConfig = mockCatalogConfigFor(spyCatalog); RecordWriterManager writerManager = - new RecordWriterManager(spyCatalog, "test_file_name", 1000, 3); + new RecordWriterManager(spyCatalogConfig, "test_file_name", 1000, 3); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); assertTrue(writerManager.write(dest1, row)); @@ -1205,17 +1233,15 @@ public void testGetOrCreateTable_refreshLogic() { // test. Schema beamSchema = null; - // Instantiate a RecordWriterManager with a dummy catalog. - RecordWriterManager writer = new RecordWriterManager(null, "p", 1L, 1); + IcebergCatalogConfig mockCatalogConfig = mock(IcebergCatalogConfig.class); + RecordWriterManager writer = new RecordWriterManager(mockCatalogConfig, "p", 1L, 1); // Clean up cache before test - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.invalidateAll(); + TableCache.invalidateAll(); // --- 1. Test the fast path (entry is not stale) --- Instant freshTimestamp = Instant.now().minus(Duration.ofMinutes(1)); - RecordWriterManager.LastRefreshedTable freshEntry = - new RecordWriterManager.LastRefreshedTable(mockTable, freshTimestamp); - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.put(identifier, freshEntry); + TableCache.put(mockCatalogConfig, identifier, mockTable, freshTimestamp); // Access the table writer.getOrCreateTable(destination, beamSchema); @@ -1225,9 +1251,7 @@ public void testGetOrCreateTable_refreshLogic() { // --- 2. Test the stale path (entry is stale) --- Instant staleTimestamp = Instant.now().minus(Duration.ofMinutes(5)); - RecordWriterManager.LastRefreshedTable staleEntry = - new RecordWriterManager.LastRefreshedTable(mockTable, staleTimestamp); - RecordWriterManager.LAST_REFRESHED_TABLE_CACHE.put(identifier, staleEntry); + TableCache.put(mockCatalogConfig, identifier, mockTable, staleTimestamp); // Access the table again writer.getOrCreateTable(destination, beamSchema); @@ -1253,14 +1277,15 @@ public void testFileIOSurvivesAcrossBundles() throws IOException { Table spyTable = Mockito.spy(realTable); Mockito.doReturn(sharedIO).when(spyTable).io(); - Catalog spyCatalog = Mockito.spy(catalog); + Catalog spyCatalog = Mockito.spy(catalogConfig.catalog()); Mockito.doReturn(spyTable).when(spyCatalog).loadTable(tableId); WindowedValue dest = getWindowedDestination(tableName, null); Row row = Row.withSchema(BEAM_SCHEMA).addValues(1, "aaa", true).build(); + IcebergCatalogConfig spyCatalogConfig = mockCatalogConfigFor(spyCatalog); // Bundle 1: write and close - RecordWriterManager bundle1 = new RecordWriterManager(spyCatalog, "file_b1", 1000, 3); + RecordWriterManager bundle1 = new RecordWriterManager(spyCatalogConfig, "file_b1", 1000, 3); assertTrue(bundle1.write(dest, row)); bundle1.close(); assertFalse("FileIO must survive after bundle 1 close", sharedIO.closed); @@ -1268,7 +1293,7 @@ public void testFileIOSurvivesAcrossBundles() throws IOException { "Bundle 1 should produce data files", bundle1.getSerializableDataFiles().containsKey(dest)); // Bundle 2: write and close using the same catalog (simulates DoFn reuse) - RecordWriterManager bundle2 = new RecordWriterManager(spyCatalog, "file_b2", 1000, 3); + RecordWriterManager bundle2 = new RecordWriterManager(spyCatalogConfig, "file_b2", 1000, 3); assertTrue(bundle2.write(dest, row)); bundle2.close(); assertFalse("FileIO must survive after bundle 2 close", sharedIO.closed); diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java index 983f021fd7ce..5126822c06f6 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/SerializableDataFileTest.java @@ -17,13 +17,27 @@ */ package org.apache.beam.sdk.io.iceberg; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.types.Conversions; +import org.apache.iceberg.types.Types; import org.junit.Test; /** @@ -47,6 +61,9 @@ public class SerializableDataFileTest { .add("nanValueCounts") .add("lowerBounds") .add("upperBounds") + .add("dataSequenceNumber") + .add("fileSequenceNumber") + .add("firstRowId") .build(); @Test @@ -73,4 +90,55 @@ public void testFieldsInEqualsMethodInSyncWithGetterFields() { + "to this test class's FIELDS_SET."); } } + + /** + * Bounds with {@code capacity > limit} must be copied by {@code [position, limit)}, not by {@link + * ByteBuffer#array()}. Otherwise trailing 0x00 bytes leak into the manifest bounds and break + * equality predicate pushdown in some query engines. + */ + @Test + public void testBoundByteBufferIsCopiedByLimitNotBackingArrayLength() { + // Encode bounds the same way iceberg-parquet does in the wild — via + // Conversions.toByteBuffer(STRING, value). For UTF-8 strings of 10+ + // characters the underlying JDK CharsetEncoder over-allocates by ~10% + // and flips, producing a ByteBuffer with capacity > limit. + int columnId = 3; + String lowerValue = "lower_bound_str"; + String upperValue = "upper_bound_str"; + byte[] expectedLower = lowerValue.getBytes(StandardCharsets.UTF_8); + byte[] expectedUpper = upperValue.getBytes(StandardCharsets.UTF_8); + + ByteBuffer lower = Conversions.toByteBuffer(Types.StringType.get(), lowerValue); + ByteBuffer upper = Conversions.toByteBuffer(Types.StringType.get(), upperValue); + + Map lowerBounds = new HashMap<>(); + lowerBounds.put(columnId, lower); + Map upperBounds = new HashMap<>(); + upperBounds.put(columnId, upper); + + Metrics metrics = new Metrics(1L, null, null, null, null, lowerBounds, upperBounds); + + DataFile dataFile = + DataFiles.builder(PartitionSpec.unpartitioned()) + .withFormat(FileFormat.PARQUET) + .withPath("gs://test-bucket/data/test-file.parquet") + .withFileSizeInBytes(1024L) + .withMetrics(metrics) + .build(); + + SerializableDataFile serialized = SerializableDataFile.from(dataFile, ""); + + byte[] serializedLower = serialized.getLowerBounds().get(columnId); + byte[] serializedUpper = serialized.getUpperBounds().get(columnId); + assertEquals( + "lower bound length must match content, not backing array", + expectedLower.length, + serializedLower.length); + assertEquals( + "upper bound length must match content, not backing array", + expectedUpper.length, + serializedUpper.length); + assertArrayEquals(expectedLower, serializedLower); + assertArrayEquals(expectedUpper, serializedUpper); + } } 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/IcebergCatalogBaseIT.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java index 606fb492e4ba..f0c7ae925df7 100644 --- a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/catalog/IcebergCatalogBaseIT.java @@ -318,7 +318,8 @@ public Row apply(Long num) { }; protected static final org.apache.iceberg.Schema ICEBERG_SCHEMA = - beamSchemaToIcebergSchema(BEAM_SCHEMA); + new org.apache.iceberg.Schema( + beamSchemaToIcebergSchema(BEAM_SCHEMA).columns(), Collections.singleton(1)); protected static final SimpleFunction RECORD_FUNC = new SimpleFunction() { @Override @@ -450,7 +451,7 @@ public void testReadWithColumnPruning_keep() throws Exception { List expectedRows = populateTable(table); - List fieldsToKeep = Arrays.asList("row", "str", "modulo_5", "nullable_long"); + List fieldsToKeep = Arrays.asList("row", "modulo_5", "nullable_long"); RowFilter rowFilter = new RowFilter(BEAM_SCHEMA).keep(fieldsToKeep); Map config = new HashMap<>(managedIcebergConfig(tableId())); @@ -543,7 +544,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())); @@ -712,6 +713,32 @@ public void testWriteWithSortOrder() throws IOException { returnedRecords, containsInAnyOrder(inputRows.stream().map(RECORD_FUNC::apply).toArray())); } + @Test + public void testWriteToPartitionedTableWithHashDistribution() throws IOException { + Map config = new HashMap<>(managedIcebergConfig(tableId())); + int truncLength = "value_x".length(); + List partitionFields = + Arrays.asList("bool_field", "hour(datetime)", "truncate(str, " + truncLength + ")"); + config.put("partition_fields", partitionFields); + config.put("distribution_mode", "hash"); + PCollection input = pipeline.apply(Create.of(inputRows)).setRowSchema(BEAM_SCHEMA); + input.apply(Managed.write(ICEBERG).withConfig(config)); + pipeline.run().waitUntilFinish(); + + // Read back and check records are correct + Table table = catalog.loadTable(TableIdentifier.parse(tableId())); + List returnedRecords = readRecords(table); + PartitionSpec expectedSpec = + PartitionSpec.builderFor(table.schema()) + .identity("bool_field") + .hour("datetime") + .truncate("str", truncLength) + .build(); + assertEquals(expectedSpec, table.spec()); + assertThat( + returnedRecords, containsInAnyOrder(inputRows.stream().map(RECORD_FUNC::apply).toArray())); + } + private PeriodicImpulse getStreamingSource() { return PeriodicImpulse.create() .stopAfter(Duration.millis(numRecords() - 1)) @@ -824,6 +851,8 @@ private void writeToDynamicDestinations( if (partitioning) { Preconditions.checkState(filterOp == null || !filterOp.equals("only")); writeConfig.put("partition_fields", Arrays.asList("bool_field", "modulo_5")); + writeConfig.put("distribution_mode", "hash"); + writeConfig.put("autosharding", true); } // Write with Beam diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtilsTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtilsTest.java new file mode 100644 index 000000000000..546386073ff6 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/CdcReadUtilsTest.java @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.beam.sdk.io.iceberg.IcebergCatalogConfig; +import org.apache.beam.sdk.io.iceberg.IcebergScanConfig; +import org.apache.beam.sdk.io.iceberg.IcebergUtils; +import org.apache.beam.sdk.io.iceberg.SerializableDeleteFile; +import org.apache.beam.sdk.io.iceberg.TestDataWarehouse; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet; +import org.apache.iceberg.ChangelogOperation; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.data.GenericAppenderFactory; +import org.apache.iceberg.data.GenericRecord; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.deletes.EqualityDeleteWriter; +import org.apache.iceberg.deletes.PositionDelete; +import org.apache.iceberg.deletes.PositionDeleteWriter; +import org.apache.iceberg.encryption.EncryptedFiles; +import org.apache.iceberg.expressions.ExpressionParser; +import org.apache.iceberg.expressions.Expressions; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.types.Types; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CdcReadUtils}. */ +@RunWith(JUnit4.class) +public class CdcReadUtilsTest { + private static final org.apache.iceberg.Schema CDC_SCHEMA = + new org.apache.iceberg.Schema( + ImmutableList.of( + Types.NestedField.required(1, "id", Types.LongType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get())), + ImmutableSet.of(1)); + + @ClassRule public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder(); + + @Rule public TestDataWarehouse warehouse = new TestDataWarehouse(TEMPORARY_FOLDER, "default"); + @Rule public TestName testName = new TestName(); + + @Test + public void addedRowsFiltersPositionAndEqualityDeletesWithUnprojectedEqualityColumn() + throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-added.parquet", + table.schema(), + ImmutableList.of( + record(0L, "keep-0"), + record(1L, "drop-by-pos"), + record(2L, "drop-by-data"), + record(3L, "keep-3"))); + + DeleteFile positionDelete = + writePositionDelete(table, dataFile, "cdc-added-pos-delete.parquet", 1L); + DeleteFile equalityDelete = + writeEqualityDelete(table, dataFile, "cdc-added-eq-delete.parquet", "drop-by-data"); + + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.ADDED_ROWS, + dataFile, + ImmutableList.of(positionDelete, equalityDelete), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), true); + + assertEquals(ImmutableList.of(0L, 3L), idsOf(records)); + } + + @Test + public void addedRowsUsesFullTableSchemaWhenProjectionDisabled() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-added-full-schema.parquet", + table.schema(), + ImmutableList.of(record(0L, "non-projected-value"))); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.ADDED_ROWS, + dataFile, + ImmutableList.of(), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), false); + + // with projection disabled, all columns should be returned + assertEquals(ImmutableList.of("non-projected-value"), dataValuesOf(records)); + } + + @Test + public void deletedRowsExcludesRowsAlreadyHiddenByExistingDeletes() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-deleted-rows.parquet", + table.schema(), + ImmutableList.of( + record(0L, "already-hidden-by-position"), + record(1L, "new-position-delete"), + record(2L, "already-hidden-by-equality"), + record(3L, "new-equality-delete"), + record(4L, "still-live"))); + + DeleteFile existingPositionDelete = + writePositionDelete(table, dataFile, "cdc-existing-pos-delete.parquet", 0L); + DeleteFile existingEqualityDelete = + writeEqualityDelete( + table, dataFile, "cdc-existing-eq-delete.parquet", "already-hidden-by-equality"); + DeleteFile addedPositionDelete = + writePositionDelete(table, dataFile, "cdc-added-pos-delete.parquet", 1L); + DeleteFile addedEqualityDelete = + writeEqualityDelete(table, dataFile, "cdc-added-eq-delete.parquet", "new-equality-delete"); + + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_ROWS, + dataFile, + ImmutableList.of(addedPositionDelete, addedEqualityDelete), + ImmutableList.of(existingPositionDelete, existingEqualityDelete), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), true); + + assertEquals(ImmutableList.of(1L, 3L), idsOf(records)); + } + + @Test + public void deletedRowsUsesFullTableSchemaWhenProjectionDisabled() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-deleted-rows-full-schema.parquet", + table.schema(), + ImmutableList.of(record(0L, "position-deleted-value"), record(1L, "still-live-value"))); + DeleteFile positionDelete = + writePositionDelete(table, dataFile, "cdc-deleted-rows-full-schema-pos-delete.parquet", 0L); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_ROWS, + dataFile, + ImmutableList.of(positionDelete), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), false); + + // with projection disabled, all columns should be returned + assertEquals(ImmutableList.of("position-deleted-value"), dataValuesOf(records)); + } + + @Test + public void deletedFileEmitsOnlyRowsNotAlreadyHiddenByExistingDeletes() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-deleted-file.parquet", + table.schema(), + ImmutableList.of( + record(0L, "already-hidden-by-position"), + record(1L, "still-live-one"), + record(2L, "already-hidden-by-equality"), + record(3L, "still-live-two"))); + + DeleteFile existingPositionDelete = + writePositionDelete(table, dataFile, "cdc-deleted-file-pos-delete.parquet", 0L); + DeleteFile existingEqualityDelete = + writeEqualityDelete( + table, dataFile, "cdc-deleted-file-eq-delete.parquet", "already-hidden-by-equality"); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_FILE, + dataFile, + ImmutableList.of(), + ImmutableList.of(existingPositionDelete, existingEqualityDelete), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), true); + + assertEquals(ImmutableList.of(1L, 3L), idsOf(records)); + } + + @Test + public void deletedFileUsesFullTableSchemaWhenProjectionDisabled() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-deleted-file-full-schema.parquet", + table.schema(), + ImmutableList.of(record(0L, "deleted-file-value"))); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_FILE, + dataFile, + ImmutableList.of(), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), false); + + // with projection disabled, all columns should be returned + assertEquals(ImmutableList.of("deleted-file-value"), dataValuesOf(records)); + } + + @Test + public void deletedRowsHandlesNullEqualityDeletesWithoutPushdown() throws IOException { + TableIdentifier tableId = TableIdentifier.of("default", testName.getMethodName()); + Table table = warehouse.createTable(tableId, CDC_SCHEMA); + DataFile dataFile = + warehouse.writeRecords( + "cdc-null-equality-delete.parquet", + table.schema(), + ImmutableList.of(record(0L, "live"), record(1L, null), record(2L, "also-live"))); + DeleteFile nullEqualityDelete = + writeEqualityDelete(table, dataFile, "cdc-null-eq-delete.parquet", null); + SerializableChangelogTask task = + task( + SerializableChangelogTask.Type.DELETED_ROWS, + dataFile, + ImmutableList.of(nullEqualityDelete), + ImmutableList.of(), + table); + + CloseableIterable records = + CdcReadUtils.changelogRecordsForTask(task, table, scanConfig(table, tableId), true); + + assertEquals(ImmutableList.of(1L), idsOf(records)); + } + + private static Record record(long id, String data) { + GenericRecord record = GenericRecord.create(CDC_SCHEMA); + record.setField("id", id); + record.setField("data", data); + return record; + } + + private static DeleteFile writePositionDelete( + Table table, DataFile dataFile, String filename, long... positions) throws IOException { + GenericAppenderFactory appenderFactory = + new GenericAppenderFactory(table.schema(), table.spec()); + PositionDeleteWriter writer = + appenderFactory.newPosDeleteWriter( + EncryptedFiles.plainAsEncryptedOutput( + table.io().newOutputFile(dataFile.location() + "." + filename)), + FileFormat.PARQUET, + null); + try (writer) { + for (long position : positions) { + writer.write(PositionDelete.create().set(dataFile.location(), position)); + } + } + return writer.toDeleteFile(); + } + + private static DeleteFile writeEqualityDelete( + Table table, DataFile dataFile, String filename, @Nullable String data) throws IOException { + org.apache.iceberg.Schema deleteSchema = table.schema().select("data"); + GenericAppenderFactory appenderFactory = + new GenericAppenderFactory(table.schema(), table.spec(), new int[] {2}, deleteSchema, null); + EqualityDeleteWriter writer = + appenderFactory.newEqDeleteWriter( + EncryptedFiles.plainAsEncryptedOutput( + table.io().newOutputFile(dataFile.location() + "." + filename)), + FileFormat.PARQUET, + null); + try (writer) { + GenericRecord deleteRecord = GenericRecord.create(deleteSchema); + deleteRecord.setField("data", data); + writer.write(deleteRecord); + } + return writer.toDeleteFile(); + } + + private IcebergScanConfig scanConfig(Table table, TableIdentifier tableId) { + return IcebergScanConfig.builder() + .setCatalogConfig( + IcebergCatalogConfig.builder() + .setCatalogProperties( + ImmutableMap.of("type", "hadoop", "warehouse", warehouse.location)) + .build()) + .setTableIdentifier(tableId) + .setSchema(IcebergUtils.icebergSchemaToBeamSchema(table.schema())) + .setKeepFields(ImmutableList.of("id")) + .build(); + } + + private static SerializableChangelogTask task( + SerializableChangelogTask.Type type, + DataFile dataFile, + List addedDeletes, + List existingDeletes, + Table table) { + return SerializableChangelogTask.builder() + .setType(type) + .setDataFile(dataFile, table.spec().partitionToPath(dataFile.partition()), true) + .setAddedDeletes(serializableDeletes(addedDeletes, table)) + .setExistingDeletes(serializableDeletes(existingDeletes, table)) + .setSpecId(table.spec().specId()) + .setOperation( + type == SerializableChangelogTask.Type.ADDED_ROWS + ? ChangelogOperation.INSERT + : ChangelogOperation.DELETE) + .setOrdinal(0) + .setCommitSnapshotId(1L) + .setStart(0L) + .setLength(dataFile.fileSizeInBytes()) + .setJsonExpression(ExpressionParser.toJson(Expressions.alwaysTrue())) + .build(); + } + + private static List serializableDeletes( + List deletes, Table table) { + return deletes.stream() + .map( + delete -> + SerializableDeleteFile.from( + delete, table.spec().partitionToPath(delete.partition()), true)) + .collect(Collectors.toList()); + } + + private static List idsOf(CloseableIterable records) { + return ImmutableList.copyOf(records).stream() + .map(record -> (Long) record.getField("id")) + .collect(Collectors.toList()); + } + + private static List dataValuesOf(CloseableIterable records) { + return ImmutableList.copyOf(records).stream() + .map(record -> (String) record.getField("data")) + .collect(Collectors.toList()); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReaderTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReaderTest.java new file mode 100644 index 000000000000..bf3aaf414579 --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/DeleteReaderTest.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.FileMetadata; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.data.DeleteLoader; +import org.apache.iceberg.data.GenericRecord; +import org.apache.iceberg.data.Record; +import org.apache.iceberg.deletes.PositionDeleteIndex; +import org.apache.iceberg.io.CloseableIterable; +import org.apache.iceberg.io.InputFile; +import org.apache.iceberg.types.TypeUtil; +import org.apache.iceberg.types.Types; +import org.apache.iceberg.util.StructLikeSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Verifies that {@link DeleteReader#read} returns the union of records matched by position + * and equality deletes. + * + *

    The tests stub the {@link DeleteLoader} so we exercise the predicate-composition logic + * directly without writing real delete files. End-to-end is covered by other tests. + */ +@RunWith(JUnit4.class) +public class DeleteReaderTest { + private static final Schema TABLE_SCHEMA = + new Schema( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.required(2, "name", Types.StringType.get())); + + private static final DeleteFile POS_FILE = + FileMetadata.deleteFileBuilder(PartitionSpec.unpartitioned()) + .ofPositionDeletes() + .withPath("/test/pos.parquet") + .withFileSizeInBytes(100) + .withRecordCount(3) + .build(); + + private static final DeleteFile EQ_FILE_ID = + FileMetadata.deleteFileBuilder(PartitionSpec.unpartitioned()) + .ofEqualityDeletes(1) + .withPath("/test/eq.parquet") + .withFileSizeInBytes(100) + .withRecordCount(2) + .build(); + + private static final DeleteFile EQ_FILE_NAME = + FileMetadata.deleteFileBuilder(PartitionSpec.unpartitioned()) + .ofEqualityDeletes(2) + .withPath("/test/eq-name.parquet") + .withFileSizeInBytes(100) + .withRecordCount(2) + .build(); + + /** {@link DeleteReader} that returns a stubbed {@link DeleteLoader} for tests. */ + private static class StubDeleteReader extends DeleteReader { + private final DeleteLoader stub; + + StubDeleteReader(List deletes, DeleteLoader stub) { + super( + "/test/data.parquet", + deletes, + TABLE_SCHEMA, + TABLE_SCHEMA, + true, + PreloadedDeletes.empty()); + this.stub = stub; + } + + StubDeleteReader( + List deletes, + DeleteLoader stub, + DeleteReader.PreloadedDeletes preloadedDeletes) { + super("/test/data.parquet", deletes, TABLE_SCHEMA, TABLE_SCHEMA, true, preloadedDeletes); + this.stub = stub; + } + + StubDeleteReader( + List deletes, + DeleteLoader stub, + Schema requestedSchema, + boolean needRowPosCol) { + super( + "/test/data.parquet", + deletes, + TABLE_SCHEMA, + requestedSchema, + needRowPosCol, + PreloadedDeletes.empty()); + this.stub = stub; + } + + @Override + protected StructLike asStructLike(Record record) { + return record; + } + + @Override + protected InputFile getInputFile(String location) { + throw new UnsupportedOperationException("not used with a stubbed DeleteLoader"); + } + + @Override + protected DeleteLoader newDeleteLoader() { + return stub; + } + } + + /** {@link DeleteLoader} that returns pre-built indexes. */ + private static class StubLoader implements DeleteLoader { + private final PositionDeleteIndex posIndex; + private final Map, StructLikeSet> eqSets; + private int posLoadCount = 0; + private int eqLoadCount = 0; + + StubLoader(PositionDeleteIndex posIndex, StructLikeSet eqSet) { + this(posIndex, Collections.singletonMap(Collections.singleton(1), eqSet)); + } + + StubLoader(PositionDeleteIndex posIndex, Map, StructLikeSet> eqSets) { + this.posIndex = posIndex; + this.eqSets = eqSets; + } + + @Override + public PositionDeleteIndex loadPositionDeletes(Iterable files, CharSequence path) { + posLoadCount++; + return posIndex; + } + + @Override + public StructLikeSet loadEqualityDeletes(Iterable files, Schema schema) { + eqLoadCount++; + return eqSets.getOrDefault( + Sets.newHashSet(TypeUtil.getProjectedIds(new Schema(schema.asStruct().fields()))), + StructLikeSet.create(schema.asStruct())); + } + } + + /** A minimal HashSet-backed {@link PositionDeleteIndex} for tests. */ + private static PositionDeleteIndex posIndexOf(long... positions) { + Set backing = new HashSet<>(); + for (long p : positions) { + backing.add(p); + } + return new PositionDeleteIndex() { + @Override + public void delete(long pos) { + backing.add(pos); + } + + @Override + public void delete(long from, long to) { + for (long p = from; p < to; p++) { + backing.add(p); + } + } + + @Override + public boolean isDeleted(long pos) { + return backing.contains(pos); + } + + @Override + public boolean isEmpty() { + return backing.isEmpty(); + } + + @Override + public long cardinality() { + return backing.size(); + } + }; + } + + private static StructLikeSet eqSetOfIds(int... ids) { + Schema idSchema = TABLE_SCHEMA.select("id"); + StructLikeSet set = StructLikeSet.create(idSchema.asStruct()); + for (int id : ids) { + GenericRecord r = GenericRecord.create(idSchema); + r.setField("id", id); + set.add(r); + } + return set; + } + + private static StructLikeSet eqSetOfNames(String... names) { + Schema nameSchema = TABLE_SCHEMA.select("name"); + StructLikeSet set = StructLikeSet.create(nameSchema.asStruct()); + for (String name : names) { + GenericRecord r = GenericRecord.create(nameSchema); + r.setField("name", name); + set.add(r); + } + return set; + } + + /** Builds N records (id=0..N-1, name="v0".."vN-1") matching {@code readSchema}. */ + private static List records(Schema readSchema, int n) { + boolean hasPos = readSchema.findField("_pos") != null; + List recs = new ArrayList<>(n); + for (long i = 0; i < n; i++) { + GenericRecord r = GenericRecord.create(readSchema); + r.setField("id", (int) i); + r.setField("name", "v" + i); + if (hasPos) { + r.setField("_pos", i); + } + recs.add(r); + } + return recs; + } + + /** Sorted list of "id" values from the output, for stable assertions. */ + private static List idsOf(CloseableIterable records) { + return ImmutableList.copyOf(records).stream() + .map(r -> (Integer) r.getField("id")) + .sorted() + .collect(Collectors.toList()); + } + + /** With no delete files at all, {@code read()} emits nothing. */ + @Test + public void noDeletesEmitsNothing() { + DeleteLoader loader = new StubLoader(posIndexOf(), eqSetOfIds()); + DeleteReader reader = new StubDeleteReader(Collections.emptyList(), loader); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(Collections.emptyList(), idsOf(output)); + } + + /** Pos-only emits only the pos-deleted records. */ + @Test + public void posOnlyEmitsPosDeletedRecords() { + DeleteLoader loader = new StubLoader(posIndexOf(1L, 3L), eqSetOfIds()); + DeleteReader reader = new StubDeleteReader(ImmutableList.of(POS_FILE), loader); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(1, 3), idsOf(output)); + } + + /** Only equality deletes, emits records matching the eq set. */ + @Test + public void eqOnlyEmitsEqDeletedRecords() { + DeleteLoader loader = new StubLoader(posIndexOf(), eqSetOfIds(2, 4)); + DeleteReader reader = new StubDeleteReader(ImmutableList.of(EQ_FILE_ID), loader); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(2, 4), idsOf(output)); + } + + /** Pos-deletes plus equality deletes, emit the union without duplication. */ + @Test + public void posAndEqEmitUnion() { + DeleteLoader loader = new StubLoader(posIndexOf(0L, 4L), eqSetOfIds(2, 4)); + DeleteReader reader = + new StubDeleteReader(ImmutableList.of(POS_FILE, EQ_FILE_ID), loader); + List input = records(reader.requiredSchema(), 6); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + // id 4 is in both sides; it must appear exactly once. + assertEquals(ImmutableList.of(0, 2, 4), idsOf(output)); + } + + /** Preloaded position deletes are reused instead of loading the same delete files again. */ + @Test + public void preloadedPositionDeletesAvoidSecondLoad() { + StubLoader loader = new StubLoader(posIndexOf(), eqSetOfIds()); + PositionDeleteIndex preloadedPosIndex = posIndexOf(1L, 3L); + DeleteReader reader = + new StubDeleteReader( + ImmutableList.of(POS_FILE), + loader, + DeleteReader.PreloadedDeletes.of(preloadedPosIndex, Collections.emptyMap())); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(1, 3), idsOf(output)); + assertEquals(0, loader.posLoadCount); + } + + /** Preloaded equality delete sets are reused instead of loading the same delete files again. */ + @Test + public void preloadedEqualityDeletesAvoidSecondLoad() { + StubLoader loader = new StubLoader(posIndexOf(), eqSetOfIds()); + Map, StructLikeSet> preloadedEqSets = new HashMap<>(); + preloadedEqSets.put(Collections.singleton(1), eqSetOfIds(2, 4)); + DeleteReader reader = + new StubDeleteReader( + ImmutableList.of(EQ_FILE_ID), + loader, + DeleteReader.PreloadedDeletes.of(null, preloadedEqSets)); + List input = records(reader.requiredSchema(), 5); + + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(2, 4), idsOf(output)); + assertEquals(0, loader.eqLoadCount); + } + + @Test + public void requiredSchemaAddsUnprojectedEqualityDeleteField() { + Schema requestedSchema = TABLE_SCHEMA.select("id"); + DeleteLoader loader = + new StubLoader( + posIndexOf(), Collections.singletonMap(Collections.singleton(2), eqSetOfNames("v2"))); + DeleteReader reader = + new StubDeleteReader(ImmutableList.of(EQ_FILE_NAME), loader, requestedSchema, true); + + assertEquals( + ImmutableList.of("id", "name"), + reader.requiredSchema().columns().stream() + .map(Types.NestedField::name) + .collect(Collectors.toList())); + + List input = records(reader.requiredSchema(), 4); + CloseableIterable output = reader.read(CloseableIterable.withNoopClose(input)); + + assertEquals(ImmutableList.of(2), idsOf(output)); + } + + @Test + public void rowPositionColumnIsOnlyAddedWhenRequiredForPositionDeletes() { + DeleteLoader loader = new StubLoader(posIndexOf(1L), eqSetOfIds()); + + DeleteReader posReaderNeedsPos = + new StubDeleteReader(ImmutableList.of(POS_FILE), loader, TABLE_SCHEMA, true); + DeleteReader posReaderDoesNotNeedPos = + new StubDeleteReader(ImmutableList.of(POS_FILE), loader, TABLE_SCHEMA, false); + DeleteReader eqReader = + new StubDeleteReader(ImmutableList.of(EQ_FILE_ID), loader, TABLE_SCHEMA, true); + + assertNotNull(posReaderNeedsPos.requiredSchema().findField("_pos")); + assertNull(posReaderDoesNotNeedPos.requiredSchema().findField("_pos")); + assertNull(eqReader.requiredSchema().findField("_pos")); + } + + @Test + public void multipleEqualityDeleteGroupsAreOrCombined() { + Map, StructLikeSet> eqSets = new HashMap<>(); + eqSets.put(Collections.singleton(1), eqSetOfIds(1)); + eqSets.put(Collections.singleton(2), eqSetOfNames("v3")); + StubLoader loader = new StubLoader(posIndexOf(), eqSets); + DeleteReader reader = + new StubDeleteReader(ImmutableList.of(EQ_FILE_ID, EQ_FILE_NAME), loader); + + CloseableIterable output = + reader.read(CloseableIterable.withNoopClose(records(reader.requiredSchema(), 5))); + + assertEquals(ImmutableList.of(1, 3), idsOf(output)); + assertEquals(2, loader.eqLoadCount); + } + + @Test + public void preloadedEqualityDeleteKeysAreDefensivelyCopied() { + StructLikeSet idDeletes = eqSetOfIds(2); + Set mutableKey = new HashSet<>(Collections.singleton(1)); + Map, StructLikeSet> preloadedEqSets = new HashMap<>(); + preloadedEqSets.put(mutableKey, idDeletes); + + DeleteReader.PreloadedDeletes preloadedDeletes = + DeleteReader.PreloadedDeletes.of(null, preloadedEqSets); + mutableKey.add(2); + + assertEquals(idDeletes, preloadedDeletes.equalityDeleteSet(Collections.singleton(1))); + + StubLoader loader = new StubLoader(posIndexOf(), eqSetOfIds()); + DeleteReader reader = + new StubDeleteReader(ImmutableList.of(EQ_FILE_ID), loader, preloadedDeletes); + CloseableIterable output = + reader.read(CloseableIterable.withNoopClose(records(reader.requiredSchema(), 4))); + + assertEquals(ImmutableList.of(2), idsOf(output)); + assertEquals(0, loader.eqLoadCount); + } +} diff --git a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTaskTest.java b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTaskTest.java new file mode 100644 index 000000000000..aa77d37af7af --- /dev/null +++ b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/SerializableChangelogTaskTest.java @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.iceberg.cdc; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; + +import java.util.Collections; +import java.util.List; +import org.apache.beam.sdk.io.iceberg.SerializableDataFile; +import org.apache.beam.sdk.util.CoderUtils; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList; +import org.apache.iceberg.AddedRowsScanTask; +import org.apache.iceberg.ChangelogOperation; +import org.apache.iceberg.ChangelogScanTask; +import org.apache.iceberg.ContentScanTask; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.DeleteFile; +import org.apache.iceberg.DeletedDataFileScanTask; +import org.apache.iceberg.DeletedRowsScanTask; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.FileMetadata; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.StructLike; +import org.apache.iceberg.expressions.Expression; +import org.apache.iceberg.expressions.ExpressionParser; +import org.apache.iceberg.expressions.Expressions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link SerializableChangelogTask}. */ +@RunWith(JUnit4.class) +public class SerializableChangelogTaskTest { + private static final PartitionSpec SPEC = PartitionSpec.unpartitioned(); + private static final DataFile DATA_FILE = + DataFiles.builder(SPEC) + .withFormat(FileFormat.PARQUET) + .withPath("gs://bucket/data/file.parquet") + .withFileSizeInBytes(512L) + .withMetrics(new Metrics(3L, null, null, null, null, null, null)) + .build(); + private static final DeleteFile ADDED_DELETE = + FileMetadata.deleteFileBuilder(SPEC) + .ofPositionDeletes() + .withPath("gs://bucket/delete/added.parquet") + .withFileSizeInBytes(32L) + .withRecordCount(1L) + .build(); + private static final DeleteFile EXISTING_DELETE = + FileMetadata.deleteFileBuilder(SPEC) + .ofPositionDeletes() + .withPath("gs://bucket/delete/existing.parquet") + .withFileSizeInBytes(64L) + .withRecordCount(2L) + .build(); + + @Test + public void coderRoundTripPreservesTaskBasics() throws Exception { + SerializableChangelogTask task = + SerializableChangelogTask.builder() + .setType(SerializableChangelogTask.Type.ADDED_ROWS) + .setDataFile(SerializableDataFile.from(DATA_FILE, "", false)) + .setSpecId(SPEC.specId()) + .setOperation(ChangelogOperation.INSERT) + .setOrdinal(7) + .setCommitSnapshotId(123L) + .setStart(5L) + .setLength(99L) + .setJsonExpression(ExpressionParser.toJson(Expressions.alwaysTrue())) + .build(); + + SerializableChangelogTask decoded = CoderUtils.clone(SerializableChangelogTask.coder(), task); + + assertEquals(SerializableChangelogTask.Type.ADDED_ROWS, decoded.getType()); + assertEquals(ChangelogOperation.INSERT, decoded.getOperation()); + assertEquals(7, decoded.getOrdinal()); + assertEquals(123L, decoded.getCommitSnapshotId()); + assertEquals(5L, decoded.getStart()); + assertEquals(99L, decoded.getLength()); + assertEquals(DATA_FILE.location(), decoded.getDataFile().getPath()); + assertEquals(DATA_FILE.fileSizeInBytes(), decoded.getDataFile().getFileSizeInBytes()); + assertEquals(Collections.emptyList(), decoded.getExistingDeletes()); + assertEquals(Collections.emptyList(), decoded.getAddedDeletes()); + assertEquals(Expressions.alwaysTrue().toString(), decoded.getExpression(null).toString()); + } + + @Test + public void helperMethodsReadSupportedTaskTypes() { + FakeAddedRowsTask added = new FakeAddedRowsTask(ImmutableList.of(ADDED_DELETE)); + FakeDeletedRowsTask deletedRows = + new FakeDeletedRowsTask(ImmutableList.of(ADDED_DELETE), ImmutableList.of(EXISTING_DELETE)); + FakeDeletedDataFileTask deletedFile = + new FakeDeletedDataFileTask(ImmutableList.of(EXISTING_DELETE)); + + assertEquals( + SerializableChangelogTask.Type.ADDED_ROWS, SerializableChangelogTask.getType(added)); + assertEquals( + SerializableChangelogTask.Type.DELETED_ROWS, + SerializableChangelogTask.getType(deletedRows)); + assertEquals( + SerializableChangelogTask.Type.DELETED_FILE, + SerializableChangelogTask.getType(deletedFile)); + + assertEquals(22L, SerializableChangelogTask.getLength(added)); + assertEquals( + 44L, SerializableChangelogTask.getTotalLength(ImmutableList.of(added, deletedRows))); + assertSame(DATA_FILE, SerializableChangelogTask.getDataFile(deletedRows)); + assertSame(SPEC, SerializableChangelogTask.getSpec(deletedFile)); + assertSame(DATA_FILE.partition(), SerializableChangelogTask.getPartition(added)); + assertThat(SerializableChangelogTask.getAddedDeleteFiles(added), contains(ADDED_DELETE)); + assertThat(SerializableChangelogTask.getAddedDeleteFiles(deletedRows), contains(ADDED_DELETE)); + assertEquals( + Collections.emptyList(), SerializableChangelogTask.getAddedDeleteFiles(deletedFile)); + } + + @Test + public void unsupportedTaskTypeFailsClearly() { + ChangelogScanTask unsupported = + new ChangelogScanTask() { + @Override + public ChangelogOperation operation() { + return ChangelogOperation.INSERT; + } + + @Override + public int changeOrdinal() { + return 0; + } + + @Override + public long commitSnapshotId() { + return 0L; + } + }; + + IllegalStateException thrown = + assertThrows( + IllegalStateException.class, () -> SerializableChangelogTask.getLength(unsupported)); + + assertThat( + thrown.getMessage(), + containsString("Unknown ChangelogScanTask type: " + unsupported.getClass())); + } + + private abstract static class FakeContentTask + implements ChangelogScanTask, ContentScanTask { + @Override + public DataFile file() { + return DATA_FILE; + } + + @Override + public PartitionSpec spec() { + return SPEC; + } + + @Override + public StructLike partition() { + return DATA_FILE.partition(); + } + + @Override + public long start() { + return 11L; + } + + @Override + public long length() { + return 22L; + } + + @Override + public Expression residual() { + return Expressions.alwaysTrue(); + } + + @Override + public int changeOrdinal() { + return 2; + } + + @Override + public long commitSnapshotId() { + return 101L; + } + } + + private static class FakeAddedRowsTask extends FakeContentTask implements AddedRowsScanTask { + private final List deletes; + + FakeAddedRowsTask(List deletes) { + this.deletes = deletes; + } + + @Override + public List deletes() { + return deletes; + } + } + + private static class FakeDeletedRowsTask extends FakeContentTask implements DeletedRowsScanTask { + private final List addedDeletes; + private final List existingDeletes; + + FakeDeletedRowsTask(List addedDeletes, List existingDeletes) { + this.addedDeletes = addedDeletes; + this.existingDeletes = existingDeletes; + } + + @Override + public List addedDeletes() { + return addedDeletes; + } + + @Override + public List existingDeletes() { + return existingDeletes; + } + } + + private static class FakeDeletedDataFileTask extends FakeContentTask + implements DeletedDataFileScanTask { + private final List existingDeletes; + + FakeDeletedDataFileTask(List existingDeletes) { + this.existingDeletes = existingDeletes; + } + + @Override + public List existingDeletes() { + return existingDeletes; + } + } +} diff --git a/sdks/java/io/messaging-expansion-service/build.gradle b/sdks/java/io/messaging-expansion-service/build.gradle new file mode 100644 index 000000000000..6cdf67b86d6e --- /dev/null +++ b/sdks/java/io/messaging-expansion-service/build.gradle @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'org.apache.beam.module' +apply plugin: 'application' +mainClassName = "org.apache.beam.sdk.expansion.service.ExpansionService" + +applyJavaNature( + automaticModuleName: 'org.apache.beam.sdk.io.messaging.expansion.service', + exportJavadoc: false, + validateShadowJar: false, + shadowClosure: {}, +) + +shadowJar { + manifest { + attributes(["Multi-Release": true]) + } + mergeServiceFiles() +} + +description = "Apache Beam :: SDKs :: Java :: IO :: Messaging Expansion Service" +ext.summary = "Expansion service serving messaging IOs (e.g. MQTT)" + +dependencies { + implementation project(":sdks:java:expansion-service") + permitUnusedDeclared project(":sdks:java:expansion-service") // BEAM-11761 + implementation project(":sdks:java:io:mqtt") + permitUnusedDeclared project(":sdks:java:io:mqtt") // BEAM-11761 + runtimeOnly library.java.slf4j_jdk14 +} + +task runExpansionService (type: JavaExec) { + mainClass = "org.apache.beam.sdk.expansion.service.ExpansionService" + classpath = sourceSets.test.runtimeClasspath + args = [project.findProperty("constructionService.port") ?: "8097"] +} diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbUtils.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbUtils.java new file mode 100644 index 000000000000..a5acfb1d19fe --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbUtils.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.schemas.Schema.Field; +import org.apache.beam.sdk.values.Row; +import org.bson.BsonNull; +import org.bson.Document; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Utility methods for MongoDB IO. */ +public class MongoDbUtils { + + /** Converts a Beam {@link Row} to a BSON {@link Document}. */ + public static Document toDocument(Row row) { + Object converted = convertToBsonValue(row); + if (converted instanceof Document) { + return (Document) converted; + } + throw new IllegalArgumentException( + "Expected Document but got " + + (converted != null ? converted.getClass().getName() : "null")); + } + + private static @Nullable Object convertToBsonValue(@Nullable Object value) { + if (value == null) { + return new BsonNull(); + } + if (value instanceof Row) { + Row row = (Row) value; + Document doc = new Document(); + for (Field field : row.getSchema().getFields()) { + Object fieldValue = row.getValue(field.getName()); + Object converted = convertToBsonValue(fieldValue); + doc.append(field.getName(), converted != null ? converted : new BsonNull()); + } + return doc; + } else if (value instanceof Iterable) { + List bsonList = new ArrayList<>(); + for (Object item : (Iterable) value) { + Object converted = convertToBsonValue(item); + bsonList.add(converted != null ? converted : new BsonNull()); + } + return bsonList; + } else if (value instanceof Map) { + Map map = (Map) value; + Document doc = new Document(); + for (Map.Entry entry : map.entrySet()) { + Object converted = convertToBsonValue(entry.getValue()); + doc.append(String.valueOf(entry.getKey()), converted != null ? converted : new BsonNull()); + } + return doc; + } + return value; + } +} diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformConfiguration.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformConfiguration.java new file mode 100644 index 000000000000..275cfe880014 --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformConfiguration.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; +import java.io.Serializable; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Configuration class for the MongoDB Write transform. */ +@DefaultSchema(AutoValueSchema.class) +@AutoValue +public abstract class MongoDbWriteSchemaTransformConfiguration implements Serializable { + + @SchemaFieldDescription("The connection URI for the MongoDB server.") + public abstract String getUri(); + + @SchemaFieldDescription("The MongoDB database to write to.") + public abstract String getDatabase(); + + @SchemaFieldDescription("The MongoDB collection to write to.") + public abstract String getCollection(); + + @SchemaFieldDescription("The number of documents to include in each batch write.") + @Nullable + public abstract Long getBatchSize(); + + @SchemaFieldDescription( + "This option specifies whether and where to output unwritable rows. Note: Error handling is currently limited to data conversion failures before sending to the MongoDB driver, as the underlying MongoDbIO does not yet support dead-letter queues for write failures.") + @Nullable + public abstract ErrorHandling getErrorHandling(); + + public void validate() { + checkArgument(getUri() != null && !getUri().isEmpty(), "MongoDB URI must be specified."); + checkArgument( + getDatabase() != null && !getDatabase().isEmpty(), "MongoDB database must be specified."); + checkArgument( + getCollection() != null && !getCollection().isEmpty(), + "MongoDB collection must be specified."); + + Long batchSize = getBatchSize(); + if (batchSize != null) { + checkArgument(batchSize > 0, "Batch size must be positive."); + } + } + + public static Builder builder() { + return new AutoValue_MongoDbWriteSchemaTransformConfiguration.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setUri(String uri); + + public abstract Builder setDatabase(String database); + + public abstract Builder setCollection(String collection); + + public abstract Builder setBatchSize(Long batchSize); + + public abstract Builder setErrorHandling(ErrorHandling errorHandling); + + public abstract MongoDbWriteSchemaTransformConfiguration build(); + } +} diff --git a/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProvider.java b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProvider.java new file mode 100644 index 000000000000..dde265ce2b81 --- /dev/null +++ b/sdks/java/io/mongodb/src/main/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProvider.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import com.google.auto.service.AutoService; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import org.apache.beam.sdk.coders.AtomicCoder; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; +import org.bson.Document; + +/** An implementation of {@link TypedSchemaTransformProvider} for writing to MongoDB. */ +@AutoService(SchemaTransformProvider.class) +public class MongoDbWriteSchemaTransformProvider + extends TypedSchemaTransformProvider { + + public static class DocumentCoder extends AtomicCoder implements Serializable { + private static final DocumentCoder INSTANCE = new DocumentCoder(); + + private DocumentCoder() {} + + public static DocumentCoder of() { + return INSTANCE; + } + + @Override + public void encode(Document value, OutputStream outStream) throws java.io.IOException { + StringUtf8Coder.of().encode(value.toJson(), outStream); + } + + @Override + public Document decode(InputStream inStream) throws java.io.IOException { + String json = StringUtf8Coder.of().decode(inStream); + return Document.parse(json); + } + } + + private static final String INPUT_TAG = "input"; + public static final TupleTag OUTPUT_TAG = new TupleTag() {}; + public static final TupleTag ERROR_TAG = new TupleTag() {}; + + private static final org.apache.beam.sdk.metrics.Counter errorCounter = + org.apache.beam.sdk.metrics.Metrics.counter( + MongoDbWriteSchemaTransformProvider.class, "MongoDB-write-error-counter"); + + @Override + protected SchemaTransform from(MongoDbWriteSchemaTransformConfiguration configuration) { + return new MongoDbWriteSchemaTransform(configuration); + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:mongodb_write:v1"; + } + + @Override + public List inputCollectionNames() { + return Collections.singletonList(INPUT_TAG); + } + + /** The {@link SchemaTransform} that performs the write operation. */ + private static class MongoDbWriteSchemaTransform extends SchemaTransform { + private final MongoDbWriteSchemaTransformConfiguration configuration; + + MongoDbWriteSchemaTransform(MongoDbWriteSchemaTransformConfiguration configuration) { + configuration.validate(); + this.configuration = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + // Retrieve the input PCollection of Rows and its schema. + PCollection rows = input.get(INPUT_TAG); + org.apache.beam.sdk.schemas.Schema inputSchema = rows.getSchema(); + + // Determine if error handling is enabled and set up the error schema. + boolean handleErrors = ErrorHandling.hasOutput(configuration.getErrorHandling()); + org.apache.beam.sdk.schemas.Schema errorSchema = ErrorHandling.errorSchema(inputSchema); + + // Convert Beam Rows to BSON Documents, emitting errors to a separate tag if enabled. + PCollectionTuple outputTuple = + rows.apply( + "ConvertToDocument", + ParDo.of(new RowToBsonDocumentFn(handleErrors, errorSchema)) + .withOutputTags(OUTPUT_TAG, TupleTagList.of(ERROR_TAG))); + + PCollection documents = outputTuple.get(OUTPUT_TAG).setCoder(DocumentCoder.of()); + + // Configure the MongoDB write operation. + MongoDbIO.Write write = + MongoDbIO.write() + .withUri(configuration.getUri()) + .withDatabase(configuration.getDatabase()) + .withCollection(configuration.getCollection()); + + Long batchSize = configuration.getBatchSize(); + if (batchSize != null) { + write = write.withBatchSize(batchSize); + } + + // Apply the MongoDB write transform. + documents.apply("WriteToMongo", write); + + // Extract and format the error collection. + PCollection errorOutput = outputTuple.get(ERROR_TAG).setRowSchema(errorSchema); + + // Return the error collection as specified by the configuration. + ErrorHandling errorHandling = configuration.getErrorHandling(); + return PCollectionRowTuple.of( + (handleErrors && errorHandling != null) ? errorHandling.getOutput() : "errors", + errorOutput); + } + } + + /** Converts a Beam {@link Row} to a BSON {@link Document}. */ + static class RowToBsonDocumentFn extends DoFn { + private final boolean handleErrors; + private final org.apache.beam.sdk.schemas.Schema errorSchema; + + RowToBsonDocumentFn(boolean handleErrors, org.apache.beam.sdk.schemas.Schema errorSchema) { + this.handleErrors = handleErrors; + this.errorSchema = errorSchema; + } + + @ProcessElement + public void processElement(@Element Row row, MultiOutputReceiver receiver) { + try { + receiver.get(OUTPUT_TAG).output(MongoDbUtils.toDocument(row)); + } catch (Exception e) { + if (!handleErrors) { + throw new RuntimeException(e); + } + errorCounter.inc(); + receiver.get(ERROR_TAG).output(ErrorHandling.errorRecord(errorSchema, row, e)); + } + } + } +} diff --git a/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbUtilsTest.java b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbUtilsTest.java new file mode 100644 index 000000000000..ec11bb865913 --- /dev/null +++ b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbUtilsTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.values.Row; +import org.bson.BsonNull; +import org.bson.Document; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link MongoDbUtils}. */ +@RunWith(JUnit4.class) +public class MongoDbUtilsTest { + + @Test + public void testToDocumentWithSimplePrimitives() { + Schema schema = + Schema.builder() + .addStringField("stringField") + .addInt32Field("intField") + .addBooleanField("booleanField") + .addDoubleField("doubleField") + .build(); + + Row row = Row.withSchema(schema).addValues("hello", 42, true, 3.14).build(); + + Document doc = MongoDbUtils.toDocument(row); + + assertNotNull(doc); + assertEquals("hello", doc.get("stringField")); + assertEquals(42, doc.get("intField")); + assertEquals(true, doc.get("booleanField")); + assertEquals(3.14, doc.get("doubleField")); + } + + @Test + public void testToDocumentWithNestedRow() { + Schema nestedSchema = + Schema.builder().addStringField("nestedString").addInt32Field("nestedInt").build(); + + Schema parentSchema = + Schema.builder() + .addStringField("parentString") + .addRowField("nestedRow", nestedSchema) + .build(); + + Row nestedRow = Row.withSchema(nestedSchema).addValues("nestedValue", 100).build(); + Row parentRow = Row.withSchema(parentSchema).addValues("parentValue", nestedRow).build(); + + Document doc = MongoDbUtils.toDocument(parentRow); + + assertNotNull(doc); + assertEquals("parentValue", doc.get("parentString")); + + Object nestedObj = doc.get("nestedRow"); + assertTrue(nestedObj instanceof Document); + Document nestedDoc = (Document) nestedObj; + assertEquals("nestedValue", nestedDoc.get("nestedString")); + assertEquals(100, nestedDoc.get("nestedInt")); + } + + @Test + public void testToDocumentWithIterable() { + Schema schema = Schema.builder().addArrayField("listField", FieldType.STRING).build(); + + Row row = Row.withSchema(schema).addValue(Arrays.asList("a", "b", "c")).build(); + + Document doc = MongoDbUtils.toDocument(row); + + assertNotNull(doc); + Object listObj = doc.get("listField"); + assertTrue(listObj instanceof List); + List list = (List) listObj; + assertEquals(3, list.size()); + assertEquals("a", list.get(0)); + assertEquals("b", list.get(1)); + assertEquals("c", list.get(2)); + } + + @Test + public void testToDocumentWithMap() { + Schema schema = + Schema.builder().addMapField("mapField", FieldType.STRING, FieldType.INT32).build(); + + Map map = Collections.singletonMap("key", 42); + Row row = Row.withSchema(schema).addValue(map).build(); + + Document doc = MongoDbUtils.toDocument(row); + + assertNotNull(doc); + Object mapObj = doc.get("mapField"); + assertTrue(mapObj instanceof Document); + Document mapDoc = (Document) mapObj; + assertEquals(42, mapDoc.get("key")); + } + + @Test + public void testToDocumentWithNullValues() { + Schema schema = Schema.builder().addNullableField("nullableString", FieldType.STRING).build(); + + Row row = Row.withSchema(schema).addValue(null).build(); + + Document doc = MongoDbUtils.toDocument(row); + + assertNotNull(doc); + Object val = doc.get("nullableString"); + assertTrue(val instanceof BsonNull); + } +} diff --git a/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProviderTest.java b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProviderTest.java new file mode 100644 index 000000000000..864c516617e5 --- /dev/null +++ b/sdks/java/io/mongodb/src/test/java/org/apache/beam/sdk/io/mongodb/MongoDbWriteSchemaTransformProviderTest.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mongodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.util.Collections; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.SchemaRegistry; +import org.apache.beam.sdk.schemas.transforms.providers.ErrorHandling; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TupleTagList; +import org.bson.Document; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link MongoDbWriteSchemaTransformProvider}. */ +@RunWith(JUnit4.class) +public class MongoDbWriteSchemaTransformProviderTest { + + @Rule public transient TestPipeline p = TestPipeline.create(); + + @Test + public void testInvalidConfigMissingUri() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbWriteSchemaTransformConfiguration.builder() + .setDatabase("db") + .setCollection("col") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigMissingDatabase() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbWriteSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setCollection("col") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigMissingCollection() { + assertThrows( + IllegalStateException.class, + () -> { + MongoDbWriteSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setDatabase("db") + .build() + .validate(); + }); + } + + @Test + public void testInvalidConfigNegativeBatchSize() { + assertThrows( + IllegalArgumentException.class, + () -> { + MongoDbWriteSchemaTransformConfiguration.builder() + .setUri("mongodb://localhost:27017") + .setDatabase("db") + .setCollection("col") + .setBatchSize(-1L) + .build() + .validate(); + }); + } + + @Test + public void testConfigurationSchema() throws Exception { + Schema schema = + SchemaRegistry.createDefault().getSchema(MongoDbWriteSchemaTransformConfiguration.class); + + // We expect 5 fields now (uri, database, collection, batchSize, errorHandling) + assertEquals(5, schema.getFieldCount()); + assertNotNull(schema.getField("uri")); + assertNotNull(schema.getField("database")); + assertNotNull(schema.getField("collection")); + assertNotNull(schema.getField("batchSize")); + assertNotNull(schema.getField("errorHandling")); + } + + @Test + public void testRowToBsonDocumentFn() { + Schema beamSchema = + Schema.builder() + .addStringField("name") + .addInt32Field("age") + .addNullableStringField("country") + .build(); + + Row row = + Row.withSchema(beamSchema) + .withFieldValue("name", "John") + .withFieldValue("age", 30) + .withFieldValue("country", null) + .build(); + + PCollection inputRows = + p.apply(Create.of(Collections.singletonList(row))).setRowSchema(beamSchema); + + Schema errorSchema = ErrorHandling.errorSchema(beamSchema); + PCollectionTuple outputTuple = + inputRows.apply( + "ConvertToDocument", + ParDo.of( + new MongoDbWriteSchemaTransformProvider.RowToBsonDocumentFn(false, errorSchema)) + .withOutputTags( + MongoDbWriteSchemaTransformProvider.OUTPUT_TAG, + TupleTagList.of(MongoDbWriteSchemaTransformProvider.ERROR_TAG))); + + PCollection bsonDocuments = + outputTuple + .get(MongoDbWriteSchemaTransformProvider.OUTPUT_TAG) + .setCoder(MongoDbWriteSchemaTransformProvider.DocumentCoder.of()); + + outputTuple.get(MongoDbWriteSchemaTransformProvider.ERROR_TAG).setRowSchema(errorSchema); + + PAssert.that(bsonDocuments) + .satisfies( + documents -> { + Document doc = documents.iterator().next(); + assertEquals("John", doc.get("name")); + assertEquals(30, doc.get("age")); + // The RowToBsonDocumentFn retains nulls explicitly in the BSON document + assertEquals(true, doc.containsKey("country")); + assertEquals(null, doc.get("country")); + return null; + }); + + p.run().waitUntilFinish(); + } +} diff --git a/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java index 78876eb6534d..72449c0697ae 100644 --- a/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java +++ b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java @@ -37,7 +37,10 @@ import org.apache.beam.sdk.coders.SerializableCoder; import org.apache.beam.sdk.io.UnboundedSource; import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.schemas.AutoValueSchema; import org.apache.beam.sdk.schemas.NoSuchSchemaException; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -205,13 +208,17 @@ public static Write dynamicWrite() { private MqttIO() {} /** A POJO describing a MQTT connection. */ + @DefaultSchema(AutoValueSchema.class) @AutoValue public abstract static class ConnectionConfiguration implements Serializable { + @SchemaFieldDescription("The MQTT broker URI.") abstract String getServerUri(); + @SchemaFieldDescription("The MQTT topic pattern.") abstract @Nullable String getTopic(); + @SchemaFieldDescription("The client ID prefix, which is used to construct a unique client ID.") abstract @Nullable String getClientId(); abstract @Nullable String getUsername(); diff --git a/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttReadSchemaTransformProvider.java b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttReadSchemaTransformProvider.java new file mode 100644 index 000000000000..b83d9bba9f48 --- /dev/null +++ b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttReadSchemaTransformProvider.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mqtt; + +import static org.apache.beam.sdk.io.mqtt.MqttIO.ConnectionConfiguration; +import static org.apache.beam.sdk.io.mqtt.MqttReadSchemaTransformProvider.ReadConfiguration; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import java.io.Serializable; +import javax.annotation.Nullable; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; +import org.joda.time.Duration; + +@AutoService(SchemaTransformProvider.class) +public class MqttReadSchemaTransformProvider + extends TypedSchemaTransformProvider { + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class ReadConfiguration implements Serializable { + public static Builder builder() { + return new AutoValue_MqttReadSchemaTransformProvider_ReadConfiguration.Builder(); + } + + @SchemaFieldDescription("Configuration options to set up the MQTT connection.") + public abstract ConnectionConfiguration getConnectionConfiguration(); + + @SchemaFieldDescription( + "The max number of records to receive. Setting this will result in a bounded PCollection.") + @Nullable + public abstract Long getMaxNumRecords(); + + @SchemaFieldDescription( + "The maximum time for this source to read messages. Setting this will result in a bounded PCollection.") + @Nullable + public abstract Long getMaxReadTimeSeconds(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConnectionConfiguration( + ConnectionConfiguration connectionConfiguration); + + public abstract Builder setMaxNumRecords(Long maxNumRecords); + + public abstract Builder setMaxReadTimeSeconds(Long maxReadTimeSeconds); + + public abstract ReadConfiguration build(); + } + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:mqtt_read:v1"; + } + + @Override + public String description() { + return "Reads messages from an MQTT broker and outputs each payload as a single `bytes` " + + "field.\n" + + "\n" + + "By default the read is unbounded (streaming): it keeps consuming messages from the " + + "subscribed topic until the pipeline is stopped. Setting `maxNumRecords` and/or " + + "`maxReadTimeSeconds` bounds the read, producing a bounded (batch) PCollection.\n" + + "\n" + + "Note: streaming reads require a runner that supports portable streaming (e.g. Prism, " + + "Flink, or Dataflow). The legacy local Python DirectRunner does not execute portable " + + "streaming cross-language reads."; + } + + @Override + protected SchemaTransform from(ReadConfiguration configuration) { + return new MqttReadSchemaTransform(configuration); + } + + private static class MqttReadSchemaTransform extends SchemaTransform { + private final ReadConfiguration config; + + MqttReadSchemaTransform(ReadConfiguration configuration) { + this.config = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + Preconditions.checkState( + input.getAll().isEmpty(), + "Expected zero input PCollections for this source, but found: %s", + input.getAll().keySet()); + + MqttIO.Read readTransform = + MqttIO.read().withConnectionConfiguration(config.getConnectionConfiguration()); + + Long maxRecords = config.getMaxNumRecords(); + Long maxReadTime = config.getMaxReadTimeSeconds(); + if (maxRecords != null) { + readTransform = readTransform.withMaxNumRecords(maxRecords); + } + if (maxReadTime != null) { + readTransform = readTransform.withMaxReadTime(Duration.standardSeconds(maxReadTime)); + } + + Schema outputSchema = Schema.builder().addByteArrayField("bytes").build(); + + PCollection outputRows = + input + .getPipeline() + .apply(readTransform) + .apply( + "Wrap in Beam Rows", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element byte[] data, OutputReceiver outputReceiver) { + outputReceiver.output( + Row.withSchema(outputSchema).addValue(data).build()); + } + })) + .setRowSchema(outputSchema); + + return PCollectionRowTuple.of("output", outputRows); + } + } +} diff --git a/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttWriteSchemaTransformProvider.java b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttWriteSchemaTransformProvider.java new file mode 100644 index 000000000000..95ee00c8c3a2 --- /dev/null +++ b/sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttWriteSchemaTransformProvider.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mqtt; + +import static org.apache.beam.sdk.io.mqtt.MqttIO.ConnectionConfiguration; +import static org.apache.beam.sdk.io.mqtt.MqttWriteSchemaTransformProvider.WriteConfiguration; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import java.io.Serializable; +import javax.annotation.Nullable; +import org.apache.beam.sdk.schemas.AutoValueSchema; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.annotations.DefaultSchema; +import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription; +import org.apache.beam.sdk.schemas.transforms.SchemaTransform; +import org.apache.beam.sdk.schemas.transforms.SchemaTransformProvider; +import org.apache.beam.sdk.schemas.transforms.TypedSchemaTransformProvider; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions; + +@AutoService(SchemaTransformProvider.class) +public class MqttWriteSchemaTransformProvider + extends TypedSchemaTransformProvider { + @DefaultSchema(AutoValueSchema.class) + @AutoValue + public abstract static class WriteConfiguration implements Serializable { + public static Builder builder() { + return new AutoValue_MqttWriteSchemaTransformProvider_WriteConfiguration.Builder(); + } + + @SchemaFieldDescription("Configuration options to set up the MQTT connection.") + public abstract ConnectionConfiguration getConnectionConfiguration(); + + @SchemaFieldDescription( + "Whether or not the publish message should be retained by the messaging engine. " + + "When a subscriber connects, it gets the latest retained message. " + + "Defaults to `False`, which will clear the retained message from the server.") + @Nullable + public abstract Boolean getRetained(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setConnectionConfiguration( + ConnectionConfiguration connectionConfiguration); + + public abstract Builder setRetained(Boolean retained); + + public abstract WriteConfiguration build(); + } + } + + @Override + public String identifier() { + return "beam:schematransform:org.apache.beam:mqtt_write:v1"; + } + + @Override + public String description() { + return "Publishes messages to an MQTT broker. Expects an input PCollection of rows with a " + + "single `bytes` field, each of which is published as one MQTT message.\n" + + "\n" + + "Works with both bounded (batch) and unbounded (streaming) input PCollections."; + } + + @Override + protected SchemaTransform from(WriteConfiguration configuration) { + return new MqttWriteSchemaTransform(configuration); + } + + private static class MqttWriteSchemaTransform extends SchemaTransform { + private final WriteConfiguration config; + + MqttWriteSchemaTransform(WriteConfiguration configuration) { + this.config = configuration; + } + + @Override + public PCollectionRowTuple expand(PCollectionRowTuple input) { + PCollection inputRows = input.getSinglePCollection(); + + Preconditions.checkState( + inputRows.getSchema().getFieldCount() == 1 + && inputRows.getSchema().getField(0).getType().equals(Schema.FieldType.BYTES), + "Expected only one Schema field containing bytes, but instead received: %s", + inputRows.getSchema()); + + MqttIO.Write writeTransform = + MqttIO.write().withConnectionConfiguration(config.getConnectionConfiguration()); + Boolean retained = config.getRetained(); + if (retained != null) { + writeTransform = writeTransform.withRetained(retained); + } + + inputRows + .apply( + "Extract bytes", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement( + @Element Row row, OutputReceiver outputReceiver) { + outputReceiver.output( + org.apache.beam.sdk.util.Preconditions.checkStateNotNull( + row.getBytes(0))); + } + })) + .apply(writeTransform); + + return PCollectionRowTuple.empty(inputRows.getPipeline()); + } + } +} diff --git a/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttSchemaTransformProviderTest.java b/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttSchemaTransformProviderTest.java new file mode 100644 index 000000000000..60bdd1104dbe --- /dev/null +++ b/sdks/java/io/mqtt/src/test/java/org/apache/beam/sdk/io/mqtt/MqttSchemaTransformProviderTest.java @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.mqtt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListSet; +import org.apache.activemq.broker.BrokerService; +import org.apache.beam.sdk.Pipeline; +import org.apache.beam.sdk.PipelineResult; +import org.apache.beam.sdk.io.common.NetworkTestHelper; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.MapElements; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionRowTuple; +import org.apache.beam.sdk.values.Row; +import org.apache.beam.sdk.values.TypeDescriptors; +import org.fusesource.hawtbuf.Buffer; +import org.fusesource.mqtt.client.BlockingConnection; +import org.fusesource.mqtt.client.MQTT; +import org.fusesource.mqtt.client.Message; +import org.fusesource.mqtt.client.QoS; +import org.fusesource.mqtt.client.Topic; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MqttSchemaTransformProviderTest { + private static final Logger LOG = LoggerFactory.getLogger(MqttSchemaTransformProviderTest.class); + + private BrokerService brokerService; + + private int port; + + @Rule public final transient TestPipeline pipeline = TestPipeline.create(); + + @Before + public void startBroker() throws Exception { + port = NetworkTestHelper.getAvailableLocalPort(); + LOG.info("Starting ActiveMQ brokerService on {}", port); + brokerService = new BrokerService(); + brokerService.setDeleteAllMessagesOnStartup(true); + // use memory persistence for the test: it's faster and don't pollute test folder with KahaDB + brokerService.setPersistent(false); + brokerService.addConnector("mqtt://localhost:" + port); + brokerService.start(); + brokerService.waitUntilStarted(); + } + + @Test(timeout = 30 * 1000) + public void testReceiveWithTimeoutAndNoData() { + MqttReadSchemaTransformProvider.ReadConfiguration readConfiguration = + MqttReadSchemaTransformProvider.ReadConfiguration.builder() + .setConnectionConfiguration( + MqttIO.ConnectionConfiguration.create("tcp://localhost:" + port, "READ_TOPIC") + .withClientId("READ_PIPELINE")) + .setMaxReadTimeSeconds(2L) + .build(); + + PCollectionRowTuple.empty(pipeline) + .apply(new MqttReadSchemaTransformProvider().from(readConfiguration)); + + // should stop before the test timeout + pipeline.run().waitUntilFinish(); + } + + /** Collects the bytes field of every output row into a shared queue (DirectRunner is in-JVM). */ + private static final ConcurrentLinkedQueue STREAMING_RECEIVED = + new ConcurrentLinkedQueue<>(); + + private static class CollectBytesFn extends DoFn { + @ProcessElement + public void processElement(@Element Row row) { + byte[] bytes = row.getBytes("bytes"); + if (bytes != null) { + STREAMING_RECEIVED.add(new String(bytes, StandardCharsets.UTF_8)); + } + } + } + + /** + * Reads in streaming mode: when neither {@code maxNumRecords} nor {@code maxReadTimeSeconds} is + * set the SchemaTransform produces an unbounded PCollection. Verifies that messages published + * after the reader subscribes flow through continuously, then cancels the running pipeline. + */ + @Test(timeout = 90 * 1000) + public void testReadUnboundedStreaming() throws Exception { + STREAMING_RECEIVED.clear(); + final String topicName = "STREAM_READ_TOPIC"; + + // No bound -> unbounded (streaming) read. + MqttReadSchemaTransformProvider.ReadConfiguration readConfiguration = + MqttReadSchemaTransformProvider.ReadConfiguration.builder() + .setConnectionConfiguration( + MqttIO.ConnectionConfiguration.create("tcp://localhost:" + port, topicName) + .withClientId("STREAM_READ_PIPELINE")) + .build(); + + // Use a local pipeline so run() does not block (the read never terminates on its own). + PipelineOptions options = + PipelineOptionsFactory.fromArgs("--blockOnRun=false").withoutStrictParsing().create(); + Pipeline p = Pipeline.create(options); + PCollectionRowTuple.empty(p) + .apply(new MqttReadSchemaTransformProvider().from(readConfiguration)) + .get("output") + .apply(ParDo.of(new CollectBytesFn())); + + // Publish a steady stream of messages until the reader has consumed enough. + final boolean[] keepPublishing = {true}; + MQTT client = new MQTT(); + client.setHost("tcp://localhost:" + port); + final BlockingConnection publishConnection = client.blockingConnection(); + publishConnection.connect(); + Thread publisher = + new Thread( + () -> { + int i = 0; + try { + while (keepPublishing[0]) { + publishConnection.publish( + topicName, + ("stream-" + i).getBytes(StandardCharsets.UTF_8), + QoS.AT_LEAST_ONCE, + false); + i++; + Thread.sleep(200); + } + } catch (Exception e) { + // ignore: connection closed on teardown + } + }); + + PipelineResult result = p.run(); + publisher.start(); + + // Wait until the unbounded read delivers a meaningful number of records. + int expected = 10; + long deadline = System.currentTimeMillis() + 60 * 1000; + while (STREAMING_RECEIVED.size() < expected && System.currentTimeMillis() < deadline) { + Thread.sleep(500); + } + + keepPublishing[0] = false; + publisher.join(); + publishConnection.disconnect(); + result.cancel(); + + assertTrue( + "expected at least " + expected + " streamed records, got " + STREAMING_RECEIVED.size(), + STREAMING_RECEIVED.size() >= expected); + for (String received : STREAMING_RECEIVED) { + assertTrue("unexpected payload: " + received, received.startsWith("stream-")); + } + } + + @Test + public void testWrite() throws Exception { + final int numberOfTestMessages = 200; + MQTT client = new MQTT(); + client.setHost("tcp://localhost:" + port); + final BlockingConnection connection = client.blockingConnection(); + connection.connect(); + connection.subscribe(new Topic[] {new Topic(Buffer.utf8("WRITE_TOPIC"), QoS.EXACTLY_ONCE)}); + + final Set messages = new ConcurrentSkipListSet<>(); + + Thread subscriber = + new Thread( + () -> { + try { + for (int i = 0; i < numberOfTestMessages; i++) { + Message message = connection.receive(); + messages.add(new String(message.getPayload(), StandardCharsets.UTF_8)); + message.ack(); + LOG.info("message: {}", new String(message.getPayload(), StandardCharsets.UTF_8)); + } + } catch (Exception e) { + LOG.error("Can't receive message", e); + } + }); + subscriber.start(); + + ArrayList data = new ArrayList<>(); + for (int i = 0; i < numberOfTestMessages; i++) { + data.add(("Test " + i).getBytes(StandardCharsets.UTF_8)); + } + + MqttWriteSchemaTransformProvider.WriteConfiguration writeConfiguration = + MqttWriteSchemaTransformProvider.WriteConfiguration.builder() + .setConnectionConfiguration( + MqttIO.ConnectionConfiguration.create("tcp://localhost:" + port, "WRITE_TOPIC")) + .build(); + Schema dataSchema = Schema.builder().addByteArrayField("bytes").build(); + + PCollection inputRows = + pipeline + .apply(Create.of(data)) + .apply( + MapElements.into(TypeDescriptors.rows()) + .via(d -> Row.withSchema(dataSchema).addValue(d).build())) + .setRowSchema(dataSchema); + PCollectionRowTuple.of("input", inputRows) + .apply(new MqttWriteSchemaTransformProvider().from(writeConfiguration)); + pipeline.run(); + subscriber.join(); + + connection.disconnect(); + + assertEquals(numberOfTestMessages, messages.size()); + for (int i = 0; i < numberOfTestMessages; i++) { + assertTrue(messages.contains("Test " + i)); + } + } + + @After + public void stopBroker() throws Exception { + if (brokerService != null) { + brokerService.stop(); + brokerService.waitUntilStopped(); + brokerService = null; + } + } +} diff --git a/sdks/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 b515957459be..616a178d1c39 100644 --- a/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java +++ b/sdks/java/io/rrio/src/main/java/org/apache/beam/io/requestresponse/Call.java @@ -270,34 +270,35 @@ public void teardown() throws UserCodeExecutionException { Sleeper sleeper = configuration.getSleeperSupplier().get(); backoffIfNeeded(backOff, sleeper); - - if (!configuration.getShouldRepeat()) { - incIfPresent(teardownCounter); - setupTeardown.teardown(); - return; - } - - Repeater repeater = - Repeater.builder() - .setBackOff(backOff) - .setSleeper(sleeper) - .setThrowableFunction( - ignored -> { - incIfPresent(teardownCounter); - setupTeardown.teardown(); - return null; - }) - .build() - .withBackoffCounter(backoffCounter) - .withSleeperCounter(sleeperCounter); - - repeater.apply(null); - - checkStateNotNull(executor).shutdown(); try { - boolean ignored = executor.awaitTermination(3L, TimeUnit.SECONDS); - } catch (InterruptedException ignored) { - // Ignore the interrupt during teardown. + if (!configuration.getShouldRepeat()) { + incIfPresent(teardownCounter); + setupTeardown.teardown(); + return; + } + + Repeater repeater = + Repeater.builder() + .setBackOff(backOff) + .setSleeper(sleeper) + .setThrowableFunction( + ignored -> { + incIfPresent(teardownCounter); + setupTeardown.teardown(); + return null; + }) + .build() + .withBackoffCounter(backoffCounter) + .withSleeperCounter(sleeperCounter); + + repeater.apply(null); + } finally { + checkStateNotNull(executor).shutdown(); + try { + boolean ignored = executor.awaitTermination(3L, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + // Ignore the interrupt during teardown. + } } } @@ -534,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); diff --git a/sdks/java/io/rrio/src/test/java/org/apache/beam/io/requestresponse/CallTest.java b/sdks/java/io/rrio/src/test/java/org/apache/beam/io/requestresponse/CallTest.java index 23ced30ada55..0764ab8db405 100644 --- a/sdks/java/io/rrio/src/test/java/org/apache/beam/io/requestresponse/CallTest.java +++ b/sdks/java/io/rrio/src/test/java/org/apache/beam/io/requestresponse/CallTest.java @@ -123,7 +123,7 @@ public void givenCallerThrowsQuotaException_emitsIntoFailurePCollection() { @Test public void givenCallerTimeout_emitsFailurePCollection() { - Duration timeout = Duration.standardMinutes(1L); + Duration timeout = Duration.standardSeconds(1L); Result result = pipeline .apply(Create.of(new Request("a"))) @@ -182,8 +182,7 @@ public void givenSetupThrowsQuotaException_throwsError() { @Test public void givenSetupTimeout_throwsError() { - Duration timeout = Duration.standardMinutes(1L); - + Duration timeout = Duration.standardSeconds(1L); pipeline .apply(Create.of(new Request(""))) .apply( @@ -231,7 +230,7 @@ public void givenTeardownThrowsQuotaException_throwsError() { @Test public void givenTeardownTimeout_throwsError() { - Duration timeout = Duration.standardMinutes(1L); + Duration timeout = Duration.standardSeconds(1L); pipeline .apply(Create.of(new Request(""))) .apply( @@ -271,7 +270,7 @@ public void givenValidCaller_emitValidResponse() { private static class ValidCaller implements Caller { @Override - public Response call(Request request) throws UserCodeExecutionException { + public Response call(Request request) { return new Response(request.id); } } @@ -282,7 +281,7 @@ private static class UnSerializableCaller implements Caller { private final UnSerializable nestedThing = new UnSerializable(); @Override - public Response call(Request request) throws UserCodeExecutionException { + public Response call(Request request) { return new Response(request.id); } } @@ -291,10 +290,10 @@ private static class UnSerializableCallerWithSetupTeardown extends UnSerializabl implements SetupTeardown { @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class UnSerializable {} @@ -358,11 +357,11 @@ private static class CallerExceedsTimeout implements Caller { private final Duration timeout; CallerExceedsTimeout(Duration timeout) { - this.timeout = timeout.plus(Duration.standardSeconds(1L)); + this.timeout = timeout.plus(Duration.standardSeconds(10L)); } @Override - public Response call(Request request) throws UserCodeExecutionException { + public Response call(Request request) { sleep(timeout); return new Response(request.id); } @@ -397,16 +396,16 @@ private static class SetupExceedsTimeout implements SetupTeardown { private final Duration timeout; private SetupExceedsTimeout(Duration timeout) { - this.timeout = timeout.plus(Duration.standardSeconds(1L)); + this.timeout = timeout.plus(Duration.standardSeconds(10L)); } @Override - public void setup() throws UserCodeExecutionException { + public void setup() { sleep(timeout); } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class SetupThrowsUserCodeExecutionException implements SetupTeardown { @@ -416,7 +415,7 @@ public void setup() throws UserCodeExecutionException { } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class SetupThrowsUserCodeQuotaException implements SetupTeardown { @@ -426,7 +425,7 @@ public void setup() throws UserCodeExecutionException { } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class SetupThrowsUserCodeTimeoutException implements SetupTeardown { @@ -436,28 +435,28 @@ public void setup() throws UserCodeExecutionException { } @Override - public void teardown() throws UserCodeExecutionException {} + public void teardown() {} } private static class TeardownExceedsTimeout implements SetupTeardown { private final Duration timeout; private TeardownExceedsTimeout(Duration timeout) { - this.timeout = timeout.plus(Duration.standardSeconds(1L)); + this.timeout = timeout.plus(Duration.standardSeconds(10L)); } @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override - public void teardown() throws UserCodeExecutionException { + public void teardown() { sleep(timeout); } } private static class TeardownThrowsUserCodeExecutionException implements SetupTeardown { @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override public void teardown() throws UserCodeExecutionException { @@ -467,7 +466,7 @@ public void teardown() throws UserCodeExecutionException { private static class TeardownThrowsUserCodeQuotaException implements SetupTeardown { @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override public void teardown() throws UserCodeExecutionException { @@ -477,7 +476,7 @@ public void teardown() throws UserCodeExecutionException { private static class TeardownThrowsUserCodeTimeoutException implements SetupTeardown { @Override - public void setup() throws UserCodeExecutionException {} + public void setup() {} @Override public void teardown() throws UserCodeExecutionException { @@ -519,14 +518,12 @@ private static class DeterministicRequestCoder extends CustomCoder<@NonNull Requ private static final Coder ID_CODER = StringUtf8Coder.of(); @Override - public void encode(Request value, @NotNull OutputStream outStream) - throws CoderException, IOException { + public void encode(Request value, @NotNull OutputStream outStream) throws IOException { ID_CODER.encode(checkStateNotNull(value).id, outStream); } @Override - public @NonNull Request decode(@NotNull InputStream inStream) - throws CoderException, IOException { + public @NonNull Request decode(@NotNull InputStream inStream) throws IOException { String id = ID_CODER.decode(inStream); return new Request(id); } @@ -542,7 +539,7 @@ private static class DeterministicResponseCoder extends CustomCoder { @Override public void encode(@Nullable Response value, @NotNull OutputStream outStream) - throws CoderException, IOException { + throws IOException { if (value == null) { ID_CODER.encode(null, outStream); return; @@ -551,7 +548,7 @@ public void encode(@Nullable Response value, @NotNull OutputStream outStream) } @Override - public Response decode(@NotNull InputStream inStream) throws CoderException, IOException { + public Response decode(@NotNull InputStream inStream) throws IOException { try { String id = ID_CODER.decode(inStream); return new Response(id); diff --git a/sdks/java/io/synthetic/src/main/java/org/apache/beam/sdk/io/synthetic/SyntheticStep.java b/sdks/java/io/synthetic/src/main/java/org/apache/beam/sdk/io/synthetic/SyntheticStep.java index 78e0d4ff5f3a..1e0de64bc091 100644 --- a/sdks/java/io/synthetic/src/main/java/org/apache/beam/sdk/io/synthetic/SyntheticStep.java +++ b/sdks/java/io/synthetic/src/main/java/org/apache/beam/sdk/io/synthetic/SyntheticStep.java @@ -21,6 +21,7 @@ import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; import com.fasterxml.jackson.annotation.JsonProperty; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.Random; import java.util.concurrent.TimeUnit; import org.apache.beam.sdk.io.synthetic.delay.SyntheticDelay; @@ -83,6 +84,7 @@ public SyntheticStep(Options options) { } @ProcessElement + @SuppressFBWarnings("DMI_RANDOM_USED_ONLY_ONCE") // intended public void processElement(ProcessContext c) throws Exception { byte[] key = c.element().getKey(); byte[] val = c.element().getValue(); diff --git a/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java b/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java index a5e7d879b441..9589992e079a 100644 --- a/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java +++ b/sdks/java/managed/src/main/java/org/apache/beam/sdk/managed/Managed.java @@ -93,6 +93,7 @@ public class Managed { // TODO: Dynamically generate a list of supported transforms public static final String ICEBERG = "iceberg"; + public static final String DELTA_LAKE = "delta"; public static final String ICEBERG_CDC = "iceberg_cdc"; public static final String KAFKA = "kafka"; public static final String BIGQUERY = "bigquery"; @@ -104,6 +105,7 @@ public class Managed { public static final Map READ_TRANSFORMS = ImmutableMap.builder() .put(ICEBERG, getUrn(ExternalTransforms.ManagedTransforms.Urns.ICEBERG_READ)) + .put(DELTA_LAKE, getUrn(ExternalTransforms.ManagedTransforms.Urns.DELTA_LAKE_READ)) .put(ICEBERG_CDC, getUrn(ExternalTransforms.ManagedTransforms.Urns.ICEBERG_CDC_READ)) .put(KAFKA, getUrn(ExternalTransforms.ManagedTransforms.Urns.KAFKA_READ)) .put(BIGQUERY, getUrn(ExternalTransforms.ManagedTransforms.Urns.BIGQUERY_READ)) @@ -128,6 +130,8 @@ public class Managed { *

    - - - - - - + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -338,12 +358,12 @@ To find out which version of Flink is compatible with Beam please see the table - + - + diff --git a/website/www/site/content/en/documentation/runners/samza.md b/website/www/site/content/en/documentation/runners/samza.md index c36d06fee861..9dbbcffb49d4 100644 --- a/website/www/site/content/en/documentation/runners/samza.md +++ b/website/www/site/content/en/documentation/runners/samza.md @@ -19,7 +19,7 @@ limitations under the License. # Using the Apache Samza Runner -**Note** Samza runner is deprecated and the support is planned to be removed in Beam 3.0 ([Issue](https://github.com/apache/beam/issues/35448)). +**Note** Samza runner is no longer supported since Beam 2.74.0 ([Issue](https://github.com/apache/beam/issues/35448)). The Apache Samza Runner can be used to execute Beam pipelines using [Apache Samza](https://samza.apache.org/). The Samza Runner executes Beam pipeline in a Samza application and can run locally. The application can further be built into a .tgz file, and deployed to a YARN cluster or Samza standalone cluster with Zookeeper. @@ -44,7 +44,7 @@ The Samza Runner is built on Samza version greater than 1.0. org.apache.beam beam-runners-samza - {{< param release_latest >}} + 2.72.0 runtime diff --git a/website/www/site/content/en/documentation/sdks/java.md b/website/www/site/content/en/documentation/sdks/java.md index 971d9951197f..659e1078ed89 100644 --- a/website/www/site/content/en/documentation/sdks/java.md +++ b/website/www/site/content/en/documentation/sdks/java.md @@ -78,6 +78,6 @@ Apache Beam lets you combine transforms written in any supported SDK language an - +
    Read Configuration Write Configuration
    ICEBERG - table (str)
    - catalog_name (str)
    - catalog_properties (map[str, str])
    - config_properties (map[str, str])
    - drop (list[str])
    - filter (str)
    - keep (list[str])
    -
    - table (str)
    - catalog_name (str)
    - catalog_properties (map[str, str])
    - config_properties (map[str, str])
    - direct_write_byte_limit (int32)
    - drop (list[str])
    - keep (list[str])
    - only (str)
    - partition_fields (list[str])
    - table_properties (map[str, str])
    - triggering_frequency_seconds (int32)
    -
    ICEBERG_CDC @@ -134,10 +109,39 @@ and Beam SQL is invoked via the Managed API under the hood.
    POSTGRESICEBERG + table (str)
    + catalog_name (str)
    + catalog_properties (map[str, str])
    + config_properties (map[str, str])
    + drop (list[str])
    + filter (str)
    + keep (list[str])
    +
    + table (str)
    + 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)
    +
    SQLSERVER jdbc_url (str)
    connection_properties (str)
    + disable_auto_commit (boolean)
    fetch_size (int32)
    location (str)
    num_partitions (int32)
    @@ -159,29 +163,10 @@ and Beam SQL is invoked via the Managed API under the hood.
    BIGQUERY - kms_key (str)
    - query (str)
    - row_restriction (str)
    - fields (list[str])
    - table (str)
    -
    - table (str)
    - drop (list[str])
    - keep (list[str])
    - kms_key (str)
    - only (str)
    - triggering_frequency_seconds (int64)
    -
    SQLSERVERPOSTGRES jdbc_url (str)
    connection_properties (str)
    - disable_auto_commit (boolean)
    fetch_size (int32)
    location (str)
    num_partitions (int32)
    @@ -202,6 +187,24 @@ and Beam SQL is invoked via the Managed API under the hood. write_statement (str)
    BIGQUERY + kms_key (str)
    + query (str)
    + row_restriction (str)
    + fields (list[str])
    + table (str)
    +
    + table (str)
    + drop (list[str])
    + keep (list[str])
    + kms_key (str)
    + only (str)
    + triggering_frequency_seconds (int64)
    +
    MYSQL @@ -235,7 +238,7 @@ and Beam SQL is invoked via the Managed API under the hood. ## Configuration Details -### `ICEBERG` Read +### `ICEBERG_CDC` Read
    @@ -312,330 +315,351 @@ and Beam SQL is invoked via the Managed API under the hood. -
    - keep + from_snapshot - list[str] + int64 - A subset of column names to read exclusively. If null or empty, all columns will be read. + Starts reading from this snapshot ID (inclusive).
    -
    - -### `ICEBERG` Write - -
    - - - - - - +
    ConfigurationTypeDescription
    - table + from_timestamp - str + int64 - 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`. + Starts reading from the first snapshot (inclusive) that was created after this timestamp (in milliseconds).
    - catalog_name + keep - str + list[str] - Name of the catalog containing the table. + A subset of column names to read exclusively. If null or empty, all columns will be read.
    - catalog_properties + poll_interval_seconds - map[str, str] + int32 - Properties used to set up the Iceberg catalog. + The interval at which to poll for new snapshots. Defaults to 60 seconds.
    - config_properties + starting_strategy - map[str, str] + str - Properties passed to the Hadoop Configuration. + 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.
    - direct_write_byte_limit + streaming - int32 + boolean - For a streaming pipeline, sets the limit for lifting bundles into the direct write path. + Enables streaming reads, where source continuously polls for snapshots forever.
    - drop + to_snapshot - list[str] + int64 - A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'. + Reads up to this snapshot ID (inclusive).
    - keep + to_timestamp - 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'. + Reads up to the latest snapshot (inclusive) created before this timestamp (in milliseconds).
    +
    + +### `KAFKA` Read + +
    + - - - + + + -
    - only - - str - - The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'. - ConfigurationTypeDescription
    - partition_fields + bootstrap_servers - list[str] + str - Fields used to create a partition spec that is applied when tables are created. For a field 'foo', the available partition transforms are: - -- `foo` -- `truncate(foo, N)` -- `bucket(foo, N)` -- `hour(foo)` -- `day(foo)` -- `month(foo)` -- `year(foo)` -- `void(foo)` - -For more information on partition transforms, please visit https://iceberg.apache.org/spec/#partition-transforms. + A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. This list should be in the form `host1:port1,host2:port2,...`
    - table_properties + topic - map[str, str] + str - Iceberg table properties to be set on the table when it is created. -For more information on table properties, please visit https://iceberg.apache.org/docs/latest/configuration/#table-properties. + n/a
    - triggering_frequency_seconds + allow_duplicates - int32 + boolean - For a streaming pipeline, sets the frequency at which snapshots are produced. + If the Kafka read allows duplicates.
    -
    - -### `ICEBERG_CDC` Read - -
    - - - - - - +
    ConfigurationTypeDescription
    - table + confluent_schema_registry_subject str - Identifier of the Iceberg table. + n/a
    - catalog_name + confluent_schema_registry_url str - Name of the catalog containing the table. + n/a
    - catalog_properties + consumer_config_updates map[str, str] - Properties used to set up the Iceberg catalog. + A list of key-value pairs that act as configuration parameters for Kafka consumers. Most of these configurations will not be needed, but if you need to customize your Kafka consumer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html
    - 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 + format - list[str] + str - A subset of column names to exclude from reading. If null or empty, all columns will be read. + The encoding format for the data stored in Kafka. Valid options are: RAW,STRING,AVRO,JSON,PROTO
    - filter + message_name str - SQL-like predicate to filter data at scan time. Example: "id > 5 AND status = 'ACTIVE'". Uses Apache Calcite syntax: https://calcite.apache.org/docs/reference.html + The name of the Protocol Buffer message to be used for schema extraction and data conversion.
    - from_snapshot + offset_deduplication - int64 + boolean - Starts reading from this snapshot ID (inclusive). + If the redistribute is using offset deduplication mode.
    - from_timestamp + redistribute_by_record_key - int64 + boolean - Starts reading from the first snapshot (inclusive) that was created after this timestamp (in milliseconds). + If the redistribute keys by the Kafka record key.
    - keep + redistribute_num_keys - list[str] + int32 - A subset of column names to read exclusively. If null or empty, all columns will be read. + The number of keys for redistributing Kafka inputs.
    - poll_interval_seconds + redistributed - int32 + boolean - The interval at which to poll for new snapshots. Defaults to 60 seconds. + If the Kafka read should be redistributed.
    - starting_strategy + schema 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. + The schema in which the data is encoded in the Kafka topic. For AVRO data, this is a schema defined with AVRO schema syntax (https://avro.apache.org/docs/1.10.2/spec.html#schemas). For JSON data, this is a schema defined with JSON-schema syntax (https://json-schema.org/). If a URL to Confluent Schema Registry is provided, then this field is ignored, and the schema is fetched from Confluent Schema Registry.
    +
    + +### `KAFKA` Write + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ConfigurationTypeDescription
    - streaming + bootstrap_servers - boolean + str - Enables streaming reads, where source continuously polls for snapshots forever. + 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,...
    - to_snapshot + format - int64 + str - Reads up to this snapshot ID (inclusive). + The encoding format for the data stored in Kafka. Valid options are: RAW,JSON,AVRO,PROTO
    - to_timestamp + topic - int64 + str - Reads up to the latest snapshot (inclusive) created before this timestamp (in milliseconds). + n/a +
    + file_descriptor_path + + str + + The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization. +
    + message_name + + str + + The name of the Protocol Buffer message to be used for schema extraction and data conversion. +
    + producer_config_updates + + map[str, str] + + A list of key-value pairs that act as configuration parameters for Kafka producers. Most of these configurations will not be needed, but if you need to customize your Kafka producer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html +
    + schema + + str + + n/a
    -### `KAFKA` Write +### `ICEBERG` Read
    @@ -646,85 +670,85 @@ For more information on table properties, please visit https://iceberg.apache.or
    - bootstrap_servers + table str - A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. | Format: host1:port1,host2:port2,... + Identifier of the Iceberg table.
    - format + catalog_name str - The encoding format for the data stored in Kafka. Valid options are: RAW,JSON,AVRO,PROTO + Name of the catalog containing the table.
    - topic + catalog_properties - str + map[str, str] - n/a + Properties used to set up the Iceberg catalog.
    - file_descriptor_path + config_properties - str + map[str, str] - The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization. + Properties passed to the Hadoop Configuration.
    - message_name + drop - str + list[str] - The name of the Protocol Buffer message to be used for schema extraction and data conversion. + A subset of column names to exclude from reading. If null or empty, all columns will be read.
    - producer_config_updates + filter - map[str, 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 + 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
    - schema + keep - str + list[str] - n/a + A subset of column names to read exclusively. If null or empty, all columns will be read.
    -### `KAFKA` Read +### `ICEBERG` Write
    @@ -735,162 +759,310 @@ For more information on table properties, please visit https://iceberg.apache.or + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - bootstrap_servers + table str - A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. The client will make use of all servers irrespective of which servers are specified here for bootstrapping—this list only impacts the initial hosts used to discover the full set of servers. This list should be in the form `host1:port1,host2:port2,...` + A fully-qualified table identifier. You may also provide a template to write to multiple dynamic destinations, for example: `dataset.my_{col1}_{col2.nested}_table`.
    - topic + autosharding + + boolean + + Enables dynamic sharding to automatically adjust the number of parallel writers based on data volume. It handles data skew by further sub-dividing partitions into multiple shards to prevent bottlenecks during high-throughput writes. Only available with 'hash' distribution mode. +
    + catalog_name str - n/a + Name of the catalog containing the table.
    - allow_duplicates + catalog_properties - boolean + map[str, str] - If the Kafka read allows duplicates. + Properties used to set up the Iceberg catalog.
    - confluent_schema_registry_subject + config_properties + + map[str, str] + + Properties passed to the Hadoop Configuration. +
    + direct_write_byte_limit + + int32 + + For a streaming pipeline, sets the limit for lifting bundles into the direct write path. +
    + distribution_mode str - n/a + Defines distribution of write data. Supported distributions: +- none: don't shuffle rows (default) +- hash: shuffle rows by partition key before writing data
    - confluent_schema_registry_url + drop + + list[str] + + A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'. +
    + keep + + list[str] + + A list of field names to keep in the input record. All other fields are dropped before writing. Is mutually exclusive with 'drop' and 'only'. +
    + only str - n/a + The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'.
    - consumer_config_updates + partition_fields + + list[str] + + Fields used to create a partition spec that is applied when tables are created. For a field 'foo', the available partition transforms are: + +- `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. +
    + sort_fields + + list[str] + + Fields used to set the table's sort order, applied when the table is created. Each entry has the form ` [asc|desc] [nulls first|nulls last]`, where `` is a field name or one of the partition transforms (e.g. `bucket(col, 4)`, `day(ts)`). Direction defaults to ascending; null order defaults to nulls-first for ascending and nulls-last for descending. Note: this sets the table's declared sort order as metadata; it does not cause Beam to physically sort records before writing. +For more information on sort orders, please visit https://iceberg.apache.org/spec/#sort-orders. +
    + table_properties map[str, str] - A list of key-value pairs that act as configuration parameters for Kafka consumers. Most of these configurations will not be needed, but if you need to customize your Kafka consumer, you may use this. See a detailed list: https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html + 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.
    - file_descriptor_path + triggering_frequency_seconds + + int32 + + For a streaming pipeline, sets the frequency at which snapshots are produced. +
    +
    + +### `SQLSERVER` Read + +
    + + + + + + + + + + + + + + + + + + + + + + +
    ConfigurationTypeDescription
    + jdbc_url str - The path to the Protocol Buffer File Descriptor Set file. This file is used for schema definition and message serialization. + Connection URL for the JDBC source.
    - format + connection_properties str - The encoding format for the data stored in Kafka. Valid options are: RAW,STRING,AVRO,JSON,PROTO + 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;".
    - message_name + disable_auto_commit + + boolean + + Whether to disable auto commit on read. Defaults to true if not provided. The need for this config varies depending on the database platform. Informix requires this to be set to false while Postgres requires this to be set to true. +
    + fetch_size + + int32 + + This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors. +
    + location str - The name of the Protocol Buffer message to be used for schema extraction and data conversion. + Name of the table to read from.
    - offset_deduplication + num_partitions + + int32 + + The number of partitions +
    + output_parallelization boolean - If the redistribute is using offset deduplication mode. + Whether to reshuffle the resulting PCollection so results are distributed to all workers.
    - redistribute_by_record_key + partition_column - boolean + str - If the redistribute keys by the Kafka record key. + Name of a column of numeric type that will be used for partitioning.
    - redistribute_num_keys + password - int32 + str - The number of keys for redistributing Kafka inputs. + Password for the JDBC source.
    - redistributed + read_query - boolean + str - If the Kafka read should be redistributed. + SQL query used to query the JDBC source.
    - schema + username str - The schema in which the data is encoded in the Kafka topic. For AVRO data, this is a schema defined with AVRO schema syntax (https://avro.apache.org/docs/1.10.2/spec.html#schemas). For JSON data, this is a schema defined with JSON-schema syntax (https://json-schema.org/). If a URL to Confluent Schema Registry is provided, then this field is ignored, and the schema is fetched from Confluent Schema Registry. + Username for the JDBC source.
    -### `POSTGRES` Write +### `SQLSERVER` Write
    @@ -1112,7 +1284,7 @@ For more information on table properties, please visit https://iceberg.apache.or
    -### `BIGQUERY` Read +### `POSTGRES` Write
    @@ -1123,141 +1295,96 @@ For more information on table properties, please visit https://iceberg.apache.or - - - - - -
    - kms_key - - str - - Use this Cloud KMS key to encrypt your data -
    - query + jdbc_url str - The SQL query to be executed to read from the BigQuery table. + Connection URL for the JDBC sink.
    - row_restriction + autosharding - str + boolean - Read only rows that match this filter, which must be compatible with Google standard SQL. This is not supported when reading via query. + If true, enables using a dynamically determined number of shards to write.
    - fields + batch_size - list[str] + int64 - 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" + n/a
    - table + connection_properties str - The fully-qualified name of the BigQuery table to read from. Format: [${PROJECT}:]${DATASET}.${TABLE} + 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;".
    -
    - -### `BIGQUERY` Write - -
    - - - - - - - - - - - - - - - -
    ConfigurationTypeDescription
    - table + location str - The bigquery table to write to. Format: [${PROJECT}:]${DATASET}.${TABLE} -
    - drop - - list[str] - - A list of field names to drop from the input record before writing. Is mutually exclusive with 'keep' and 'only'. -
    - keep - - list[str] - - A list of field names to keep in the input record. All other fields are dropped before writing. Is mutually exclusive with 'drop' and 'only'. + Name of the table to write to.
    - kms_key + password str - Use this Cloud KMS key to encrypt your data + Password for the JDBC source.
    - only + username str - The name of a single record field that should be written. Is mutually exclusive with 'keep' and 'drop'. + Username for the JDBC source.
    - triggering_frequency_seconds + write_statement - int64 + str - Determines how often to 'commit' progress into BigQuery. Default is every 5 seconds. + SQL query used to insert records into the JDBC sink.
    -### `SQLSERVER` Read +### `BIGQUERY` Write
    @@ -1268,129 +1395,74 @@ For more information on table properties, please visit https://iceberg.apache.or - - - - - - - - - - - - - - - - - - - - - - - - -
    - jdbc_url - - str - - Connection URL for the JDBC source. -
    - connection_properties - - str - - Used to set connection properties passed to the JDBC driver not already defined as standalone parameter (e.g. username and password can be set using parameters above accordingly). Format of the string must be "key1=value1;key2=value2;". -
    - disable_auto_commit - - boolean - - Whether to disable auto commit on read. Defaults to true if not provided. The need for this config varies depending on the database platform. Informix requires this to be set to false while Postgres requires this to be set to true. -
    - fetch_size - - int32 - - This method is used to override the size of the data that is going to be fetched and loaded in memory per every database call. It should ONLY be used if the default value throws memory errors. -
    - location + table str - Name of the table to read from. -
    - num_partitions - - int32 - - 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 +### `BIGQUERY` Read
    @@ -1401,90 +1473,57 @@ For more information on table properties, please visit https://iceberg.apache.or - - - - - - - - - - - - - - -
    - jdbc_url - - str - - Connection URL for the JDBC sink. -
    - autosharding - - boolean - - If true, enables using a dynamically determined number of shards to write. -
    - batch_size - - int64 - - n/a -
    - connection_properties + kms_key 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;". + Use this Cloud KMS key to encrypt your data
    - location + query str - Name of the table to write to. + The SQL query to be executed to read from the BigQuery table.
    - password + row_restriction str - Password for the JDBC source. + Read only rows that match this filter, which must be compatible with Google standard SQL. This is not supported when reading via query.
    - username + fields - str + list[str] - Username for the JDBC source. + Read only the specified fields (columns) from a BigQuery table. Fields may not be returned in the order specified. If no value is specified, then all fields are returned. Example: "col1, col2, col3"
    - write_statement + table str - SQL query used to insert records into the JDBC sink. + The fully-qualified name of the BigQuery table to read from. Format: [${PROJECT}:]${DATASET}.${TABLE}
    diff --git a/website/www/site/content/en/documentation/programming-guide.md b/website/www/site/content/en/documentation/programming-guide.md index 343fb128b3ef..3f37f45bace6 100644 --- a/website/www/site/content/en/documentation/programming-guide.md +++ b/website/www/site/content/en/documentation/programming-guide.md @@ -7501,6 +7501,89 @@ class BufferDoFn(DoFn): {{< code_sample "sdks/go/examples/snippets/04transforms.go" batching_dofn_example >}} {{< /highlight >}} +#### 11.5.3. Looping timers {#looping-timers} + +Looping timers are a pattern where a timer sets another timer for a future time, creating a loop. This is useful for producing periodic outputs or heartbeats in the absence of data for a specific key. + +When draining a pipeline, it is important to terminate these loops to allow the pipeline to finish. In the Java SDK, you can use the `CausedByDrain` parameter in the `@OnTimer` method to check if the timer firing was induced by a drain operation. **Note:** `CausedByDrain` will be set only in certain runners. Check the [capability matrix](/documentation/runners/capability-matrix/) for more details. + +{{< highlight java >}} +public static class LoopingStatefulTimer extends DoFn, KV> { + @TimerId("loopingTimer") private final TimerSpec loopingTimer = TimerSpecs.timer(TimeDomain.EVENT_TIME); + + @ProcessElement + public void process( + @Element KV element, + @TimerId("loopingTimer") Timer timer, + OutputReceiver> output) { + + // Set initial timer + timer.offset(Duration.standardMinutes(1)).setRelative(); + output.output(element); + } + + @OnTimer("loopingTimer") + public void onTimer( + @Key String key, + @TimerId("loopingTimer") Timer timer, + OutputReceiver> output, + CausedByDrain drain) { + + output.output(KV.of(key, 0)); + + // Cancel looping timer if drain is in progress + if (drain == CausedByDrain.CAUSED_BY_DRAIN) { + return; + } + + // Set next timer + timer.offset(Duration.standardMinutes(1)).setRelative(); + } +} +{{< /highlight >}} + +{{< highlight py >}} +# Python does not currently support detecting drain in OnTimer. +# The following example demonstrates a looping timer without drain support, +# using event time. + +class LoopingTimerDoFn(DoFn): + TIMER = TimerSpec('timer', TimeDomain.WATERMARK) + + def process(self, element, ts=DoFn.TimestampParam, timer=DoFn.TimerParam(TIMER)): + timer.set(ts + Duration(seconds=60)) + yield element + + @on_timer(TIMER) + def on_timer(self, key=DoFn.KeyParam, timestamp=DoFn.TimestampParam, timer=DoFn.TimerParam(TIMER)): + yield (key, 0) + # Loops forever, cannot handle drain safely if it never stops. + timer.set(timestamp + Duration(seconds=60)) +{{< /highlight >}} + +{{< highlight go >}} +// Go does not currently support detecting drain in OnTimer. +// The following example demonstrates a looping timer without drain support, +// using event time. + +type LoopingTimerFn struct { + Timer timers.EventTime +} + +func (fn *LoopingTimerFn) ProcessElement(et beam.EventTime, sp state.Provider, tp timers.Provider, key string, value int, emit func(string, int)) { + nextTime := et.ToTime().Add(60 * time.Second) + fn.Timer.Set(tp, nextTime) + emit(key, value) +} + +func (fn *LoopingTimerFn) OnTimer(et beam.EventTime, sp state.Provider, tp timers.Provider, key string, timer timers.Context, emit func(string, int)) { + emit(key, 0) + // Loops forever, cannot handle drain safely if it never stops. + nextTime := et.ToTime().Add(60 * time.Second) + fn.Timer.Set(tp, nextTime) +} +{{< /highlight >}} + ## 12. Splittable `DoFns` {#splittable-dofns} diff --git a/website/www/site/content/en/documentation/runners/flink.md b/website/www/site/content/en/documentation/runners/flink.md index 8356b9067a6b..e924ccdb7bd6 100644 --- a/website/www/site/content/en/documentation/runners/flink.md +++ b/website/www/site/content/en/documentation/runners/flink.md @@ -330,6 +330,26 @@ To find out which version of Flink is compatible with Beam please see the table
    Artifact Id Supported Beam Versions
    2.2.xbeam-runners-flink-2.2≥ 2.75.0
    2.1.xbeam-runners-flink-2.1≥ 2.75.0
    2.0.xbeam-runners-flink-2.0≥ 2.72.0
    1.20.xbeam-runners-flink-1.20≥ 2.70.0
    1.19.x beam-runners-flink-1.19
    1.18.x beam-runners-flink-1.18≥ 2.57.02.57.0 - 2.74.0
    1.17.x beam-runners-flink-1.17≥ 2.56.02.56.0 - 2.74.0
    1.16.x
    82.x≤ 2.73.0
    \ No newline at end of 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/beam-overview.md b/website/www/site/content/en/get-started/beam-overview.md index 5a8fcd3b917c..c50ebdc39b54 100644 --- a/website/www/site/content/en/get-started/beam-overview.md +++ b/website/www/site/content/en/get-started/beam-overview.md @@ -50,12 +50,15 @@ Beam currently supports the following runners: - [Direct Runner](/documentation/runners/direct) - [Apache Flink Runner](/documentation/runners/flink) Apache Flink logo - [Apache Nemo Runner](/documentation/runners/nemo) -- [Apache Samza Runner](/documentation/runners/samza) Apache Samza logo - [Apache Spark Runner](/documentation/runners/spark) Apache Spark logo - [Google Cloud Dataflow Runner](/documentation/runners/dataflow) Google Cloud Dataflow logo - [Hazelcast Jet Runner](/documentation/runners/jet) Hazelcast Jet logo - [Twister2 Runner](/documentation/runners/twister2) Twister2 logo +Runners supported in older Beam versions + +- [Apache Samza Runner](/documentation/runners/samza) Apache Samza logo until Beam 2.74.0. + **Note:** You can always execute your pipeline locally for testing and debugging purposes. ## Get Started diff --git a/website/www/site/content/en/get-started/downloads.md b/website/www/site/content/en/get-started/downloads.md index cf37974973fe..61cc6bfe464b 100644 --- a/website/www/site/content/en/get-started/downloads.md +++ b/website/www/site/content/en/get-started/downloads.md @@ -95,23 +95,39 @@ versions denoted `0.x.y`. ### Current release -#### 2.72.0 (2026-03-30) +#### 2.74.0 (2026-06-02) -Official [source code download](https://www.apache.org/dyn/closer.lua/beam/2.72.0/apache-beam-2.72.0-source-release.zip). -[SHA-512](https://downloads.apache.org/beam/2.72.0/apache-beam-2.72.0-source-release.zip.sha512). -[signature](https://downloads.apache.org/beam/2.72.0/apache-beam-2.72.0-source-release.zip.asc). +Official [source code download](https://www.apache.org/dyn/closer.lua/beam/2.74.0/apache-beam-2.74.0-source-release.zip). +[SHA-512](https://downloads.apache.org/beam/2.74.0/apache-beam-2.74.0-source-release.zip.sha512). +[signature](https://downloads.apache.org/beam/2.74.0/apache-beam-2.74.0-source-release.zip.asc). -[Release notes](https://github.com/apache/beam/releases/tag/v2.71.0) +[Release notes](https://github.com/apache/beam/releases/tag/v2.74.0) ### Archived releases +#### 2.73.0 (2026-04-29) + +Official [source code download](https://archive.apache.org/dist/beam/2.73.0/apache-beam-2.73.0-source-release.zip). +[SHA-512](https://archive.apache.org/dist/beam/2.73.0/apache-beam-2.73.0-source-release.zip.sha512). +[signature](https://archive.apache.org/dist/beam/2.73.0/apache-beam-2.73.0-source-release.zip.asc). + +[Release notes](https://github.com/apache/beam/releases/tag/v2.73.0) + +#### 2.72.0 (2026-03-30) + +Official [source code download](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip). +[SHA-512](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip.sha512). +[signature](https://archive.apache.org/dist/beam/2.72.0/apache-beam-2.72.0-source-release.zip.asc). + +[Release notes](https://github.com/apache/beam/releases/tag/v2.72.0) + #### 2.71.0 (2026-01-22) Official [source code download](https://archive.apache.org/dist/beam/2.71.0/apache-beam-2.71.0-source-release.zip). [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 3152351facf5..d911918b9d2b 100644 --- a/website/www/site/content/en/get-started/quickstart-java.md +++ b/website/www/site/content/en/get-started/quickstart-java.md @@ -185,7 +185,6 @@ To run the WordCount pipeline: * [FlinkRunner](/documentation/runners/flink) * [SparkRunner](/documentation/runners/spark) * [DataflowRunner](/documentation/runners/dataflow) - * [SamzaRunner](/documentation/runners/samza) * [NemoRunner](/documentation/runners/nemo) * [JetRunner](/documentation/runners/jet) @@ -221,10 +220,6 @@ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ --inputFile=gs://apache-beam-samples/shakespeare/* --output=gs:///counts" \ -Pdataflow-runner {{< /runner >}} -{{< runner samza >}} -mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ - -Dexec.args="--inputFile=sample.txt --output=/tmp/counts --runner=SamzaRunner" -Psamza-runner -{{< /runner >}} {{< runner nemo >}} mvn package -Pnemo-runner && java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.WordCount \ --runner=NemoRunner --inputFile=`pwd`/sample.txt --output=counts @@ -262,10 +257,6 @@ mvn compile exec:java -D exec.mainClass=org.apache.beam.examples.WordCount ` --inputFile=gs://apache-beam-samples/shakespeare/* --output=gs:///counts" ` -P dataflow-runner {{< /runner >}} -{{< runner samza >}} -mvn compile exec:java -D exec.mainClass=org.apache.beam.examples.WordCount ` - -D exec.args="--inputFile=sample.txt --output=/tmp/counts --runner=SamzaRunner" -P samza-runner -{{< /runner >}} {{< runner nemo >}} mvn package -P nemo-runner -DskipTests java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.WordCount ` @@ -299,9 +290,6 @@ gradle clean execute -DmainClass=org.apache.beam.examples.WordCount \ --args="--project= --inputFile=gs://apache-beam-samples/shakespeare/* \ --output=gs:///counts --runner=DataflowRunner" -Pdataflow-runner {{< /runner >}} -{{< runner samza>}} -TODO: document Samza on Gradle: https://github.com/apache/beam/issues/21500 -{{< /runner >}} {{< runner nemo>}} TODO: document Nemo on Gradle: https://github.com/apache/beam/issues/21503 {{< /runner >}} @@ -329,9 +317,6 @@ TODO: document Spark on Gradle: https://github.com/apache/beam/issues/21502 --args="--project= --inputFile=gs://apache-beam-samples/shakespeare/* \ --output=gs:///counts --runner=DataflowRunner" -Pdataflow-runner {{< /runner >}} -{{< runner samza>}} -TODO: document Samza on Gradle: https://github.com/apache/beam/issues/21500 -{{< /runner >}} {{< runner nemo>}} TODO: document Nemo on Gradle: https://github.com/apache/beam/issues/21503 {{< /runner >}} @@ -360,9 +345,6 @@ ls counts* {{< /runner >}} {{< runner dataflow >}} gsutil ls gs:///counts* - {{< /runner >}} - {{< runner samza >}} -ls /tmp/counts* {{< /runner >}} {{< runner nemo >}} ls counts* @@ -387,9 +369,6 @@ more counts* {{< /runner >}} {{< runner dataflow >}} gsutil cat gs:///counts* - {{< /runner >}} - {{< runner samza >}} -more /tmp/counts* {{< /runner >}} {{< runner nemo >}} more counts* diff --git a/website/www/site/content/en/get-started/quickstart-py.md b/website/www/site/content/en/get-started/quickstart-py.md index d7f896153483..0d30bd94568c 100644 --- a/website/www/site/content/en/get-started/quickstart-py.md +++ b/website/www/site/content/en/get-started/quickstart-py.md @@ -23,7 +23,7 @@ If you're interested in contributing to the Apache Beam Python codebase, see the {{< toc >}} -The Python SDK supports Python 3.8, 3.9, 3.10, 3.11 and 3.12. Beam 2.48.0 was the last release with support for Python 3.7. +The Python SDK supports Python 3.10, 3.11, 3.12, 3.13 and 3.14. Beam 2.69.0 was the last release with support for Python 3.9. Beam 2.60.0 was the last release with support for Python 3.8. ## Set up your environment diff --git a/website/www/site/content/en/get-started/wordcount-example.md b/website/www/site/content/en/get-started/wordcount-example.md index e91226c247ca..157019230a65 100644 --- a/website/www/site/content/en/get-started/wordcount-example.md +++ b/website/www/site/content/en/get-started/wordcount-example.md @@ -376,11 +376,6 @@ $ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ -Pdataflow-runner {{< /runner >}} -{{< runner samza >}} -$ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WordCount \ - -Dexec.args="--inputFile=pom.xml --output=counts --runner=SamzaRunner" -Psamza-runner -{{< /runner >}} - {{< runner nemo >}} $ mvn package -Pnemo-runner && java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.WordCount \ --runner=NemoRunner --inputFile=`pwd`/pom.xml --output=counts @@ -428,10 +423,6 @@ python -m apache_beam.examples.wordcount --input gs://dataflow-samples/shakespea --temp_location gs://YOUR_GCS_BUCKET/tmp/ {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Python SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Python SDK. {{< /runner >}} @@ -476,10 +467,6 @@ $ wordcount --input gs://dataflow-samples/shakespeare/kinglear.txt \ --worker_harness_container_image=apache/beam_go_sdk:latest {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Go SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Go SDK. {{< /runner >}} @@ -716,11 +703,6 @@ $ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.DebuggingWordC -Pdataflow-runner {{< /runner >}} -{{< runner samza >}} -$ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.DebuggingWordCount \ - -Dexec.args="--runner=SamzaRunner --output=counts" -Psamza-runner -{{< /runner >}} - {{< runner nemo >}} $ mvn package -Pnemo-runner && java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.DebuggingWordCount \ --runner=NemoRunner --inputFile=`pwd`/pom.xml --output=counts @@ -762,10 +744,6 @@ python -m apache_beam.examples.wordcount_debugging --input gs://dataflow-samples --temp_location gs://YOUR_GCS_BUCKET/tmp/ {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Python SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Python SDK. {{< /runner >}} @@ -810,10 +788,6 @@ $ debugging_wordcount --input gs://dataflow-samples/shakespeare/kinglear.txt \ --worker_harness_container_image=apache-docker-beam-snapshots-docker.bintray.io/beam/go:20180515 {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Go SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Go SDK. {{< /runner >}} @@ -1048,11 +1022,6 @@ $ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WindowedWordCo -Pdataflow-runner {{< /runner >}} -{{< runner samza >}} -$ mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.WindowedWordCount \ - -Dexec.args="--runner=SamzaRunner --inputFile=pom.xml --output=counts" -Psamza-runner -{{< /runner >}} - {{< runner nemo >}} $ mvn package -Pnemo-runner && java -cp target/word-count-beam-bundled-0.1.jar org.apache.beam.examples.WindowedWordCount \ --runner=NemoRunner --inputFile=`pwd`/pom.xml --output=counts @@ -1098,10 +1067,6 @@ python -m apache_beam.examples.windowed_wordcount --input YOUR_INPUT_FILE \ --temp_location gs://YOUR_GCS_BUCKET/tmp/ {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Python SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Python SDK. {{< /runner >}} @@ -1145,10 +1110,6 @@ $ windowed_wordcount --input gs://dataflow-samples/shakespeare/kinglear.txt \ --worker_harness_container_image=apache-docker-beam-snapshots-docker.bintray.io/beam/go:20180515 {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Go SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Go SDK. {{< /runner >}} @@ -1410,10 +1371,6 @@ python -m apache_beam.examples.streaming_wordcount \ --streaming {{< /runner >}} -{{< runner samza >}} -This runner is not yet available for the Python SDK. -{{< /runner >}} - {{< runner nemo >}} This runner is not yet available for the Python SDK. {{< /runner >}} diff --git a/website/www/site/content/en/performance/_index.md b/website/www/site/content/en/performance/_index.md index 1624c58efe2e..a0eaba2aa0ec 100644 --- a/website/www/site/content/en/performance/_index.md +++ b/website/www/site/content/en/performance/_index.md @@ -59,3 +59,5 @@ See the following pages for performance measures recorded when running various B - [TensorFlow MNIST Image Classification](/performance/tensorflowmnist) - [VLLM Gemma Batch Completion Tesla T4 GPU](/performance/vllmgemmabatchtesla) - [Table Row Inference Sklearn Batch](/performance/tablerowinference) +- [MLTransform Generate Vocab (batch)](/performance/mltransformvocab) +- [MLTransform One-Hot Encoding](/performance/mltransformonehot) diff --git a/website/www/site/content/en/performance/mltransformonehot/_index.md b/website/www/site/content/en/performance/mltransformonehot/_index.md new file mode 100644 index 000000000000..2d641bcfb66f --- /dev/null +++ b/website/www/site/content/en/performance/mltransformonehot/_index.md @@ -0,0 +1,42 @@ +--- +title: "MLTransform One-Hot Encoding Performance" +--- + + + +# MLTransform One-Hot Encoding Performance + +**Pipeline**: MLTransform One-Hot Encoding for Categorical Features +**Type**: Batch only +**Host**: 50 × n1-standard-2 (2 vCPUs, 7.5 GB RAM) + +The following graphs show various metrics when running the MLTransform One-Hot +Encoding pipeline using Apache Beam's MLTransform TFT integration. +See the [glossary](/performance/glossary) for definitions. + +Full pipeline implementation is available +[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/ml_transform/mltransform_one_hot_encoding.py). + +## What is the estimated cost to run the pipeline? + +{{< performance_looks io="mltransformonehot" read_or_write="write" section="cost" >}} + +## How has various metrics changed when running the pipeline for different Beam SDK versions? + +{{< performance_looks io="mltransformonehot" read_or_write="write" section="version" >}} + +## How has various metrics changed over time when running the pipeline? + +{{< performance_looks io="mltransformonehot" read_or_write="write" section="date" >}} diff --git a/website/www/site/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/roadmap/_index.md b/website/www/site/content/en/roadmap/_index.md index 698b41dd798f..e764898d3f84 100644 --- a/website/www/site/content/en/roadmap/_index.md +++ b/website/www/site/content/en/roadmap/_index.md @@ -36,7 +36,7 @@ Beam 3 is the planned first major version upgrade. See https://s.apache.org/beam Portability is the primary Beam vision: running pipelines authored with _any SDK_ on _any runner_. This is a cross-cutting effort across Java, Python, and Go, and every Beam runner. Portability is currently supported on the -[DataFlow](/documentation/runners/dataflow), [Flink](/documentation/runners/flink/), [Jet](/documentation/runners/jet), [Nemo](/documentation/runners/nemo), [Prism](/documentation/runners/prism/), [Samza](/documentation/runners/samza), [Spark](/documentation/runners/spark/), and [Twister2](/documentation/runners/Twister2) +[DataFlow](/documentation/runners/dataflow), [Flink](/documentation/runners/flink/), [Jet](/documentation/runners/jet), [Nemo](/documentation/runners/nemo), [Prism](/documentation/runners/prism/), [Spark](/documentation/runners/spark/), and [Twister2](/documentation/runners/Twister2) runners. See the details on the [Portability Roadmap](/roadmap/portability/) diff --git a/website/www/site/content/en/roadmap/go-sdk.md b/website/www/site/content/en/roadmap/go-sdk.md index 4e2cc4ddba11..eb16b7936f73 100644 --- a/website/www/site/content/en/roadmap/go-sdk.md +++ b/website/www/site/content/en/roadmap/go-sdk.md @@ -20,7 +20,7 @@ limitations under the License. The Go SDK is [fully released as of v2.33.0](/blog/go-sdk-release/). The Go SDK the first SDK purely on the [Beam Portability Framework](/roadmap/portability/) -and can execute pipelines on portable runners, like Flink, Spark, Samza, and Google Cloud Dataflow. +and can execute pipelines on portable runners, like Flink, Spark, and Google Cloud Dataflow. Current roadmap: * continue building up unbounded pipeline facing features, as described on the [Beam Dev Wiki](https://cwiki.apache.org/confluence/display/BEAM/Supporting+Streaming+in+the+Go+SDK). diff --git a/website/www/site/content/en/roadmap/samza-runner.md b/website/www/site/content/en/roadmap/samza-runner.md index c27430601b09..801629222c68 100644 --- a/website/www/site/content/en/roadmap/samza-runner.md +++ b/website/www/site/content/en/roadmap/samza-runner.md @@ -17,7 +17,7 @@ limitations under the License. # Samza Runner Roadmap -**Note** Samza runner is deprecated and the support is planned to be removed in Beam 3.0 ([Issue](https://github.com/apache/beam/issues/35448)). +**Note** Samza runner is no longer supported since Beam 2.74.0 ([Issue](https://github.com/apache/beam/issues/35448)). For references, here are available resources: diff --git a/website/www/site/data/capability_matrix.yaml b/website/www/site/data/capability_matrix.yaml index c1da306b9cb7..a1afdc6f8abc 100644 --- a/website/www/site/data/capability_matrix.yaml +++ b/website/www/site/data/capability_matrix.yaml @@ -22,8 +22,6 @@ capability-matrix: name: Apache Spark (RDD/DStream based) - class: spark-dataset name: Apache Spark Structured Streaming (Dataset based) - - class: samza - name: Apache Samza - class: nemo name: Apache Nemo - class: jet @@ -32,8 +30,6 @@ capability-matrix: name: Twister2 - class: python direct name: Python Direct FnRunner - - class: go direct - name: Go Direct Runner categories: - description: What is being computed? @@ -68,10 +64,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: ParDo applies per-element transformations as Spark FlatMapFunction. - - class: samza - l1: "Yes" - l2: fully supported - l3: Supported with per-element transformation. - class: nemo l1: "Yes" l2: fully supported @@ -88,10 +80,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: GroupByKey description: Grouping of key-value pairs per key, window, and pane. (See also other tabs.) values: @@ -115,10 +103,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "Using Spark's groupByKey." - - class: samza - l1: "Yes" - l2: fully supported - l3: "Uses Samza's partitionBy for key grouping and Beam's logic for window aggregation and triggering." - class: nemo l1: "Yes" l2: fully supported @@ -135,10 +119,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Flatten description: Concatenates multiple homogenously typed collections together. values: @@ -162,10 +142,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: Some corner cases like flatten on empty collections are not yet supported. - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -182,10 +158,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Combine description: 'Application of an associative, commutative operation over all values ("globally") or over all values associated with each key ("per key"). Can be implemented using ParDo, but often more efficient implementations exist.' values: @@ -209,10 +181,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "Using Spark's Aggregator and agg function" - - class: samza - l1: "Yes" - l2: fully supported - l3: Use combiner for efficient pre-aggregation. - class: nemo l1: "Yes" l2: fully supported @@ -229,10 +197,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Composite Transforms description: Allows easy extensibility for library writers. In the near future, we expect there to be more information provided at this level -- customized metadata hooks for monitoring, additional runtime/environment hooks, etc. values: @@ -256,10 +220,6 @@ capability-matrix: l1: "Partially" l2: supported via inlining only in batch mode l3: "" - - class: samza - l1: "Partially" - l2: supported via inlining - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -276,10 +236,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Side Inputs description: Side inputs are additional PCollections whose contents are computed during pipeline execution and then made accessible to DoFn code. The exact shape of the side input depends both on the PCollectionView used to describe the access pattern (interable, map, singleton) and the window of the element from the main input that is currently being processed. values: @@ -303,10 +259,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "Using Spark's broadcast variables." - - class: samza - l1: "Yes" - l2: fully supported - l3: Uses Samza's broadcast operator to distribute the side inputs. - class: nemo l1: "Yes" l2: fully supported @@ -323,10 +275,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Source API description: Allows users to provide additional input sources. Supports both bounded and unbounded data. Includes hooks necessary to provide efficient parallelization (size estimation, progress information, dynamic splitting, etc). values: @@ -350,10 +298,6 @@ capability-matrix: l1: "Partially" l2: bounded source only l3: "Using Spark's DatasourceV2 API in microbatch mode (Continuous streaming mode is tagged experimental in spark and does not support aggregation)." - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -370,10 +314,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Metrics description: Allow transforms to gather simple metrics across bundles in a PTransform. Provide a mechanism to obtain both committed and attempted metrics. Semantically similar to using an additional output, but support partial results as the transform executes, and support both committed and attempted values. Will likely want to augment Metrics to be more useful for processing unbounded data by making them windowed. values: @@ -397,10 +337,6 @@ capability-matrix: l1: "Partially" l2: All metric types are supported in batch mode. l3: Only attempted values are supported. No committed values for metrics. - - class: samza - l1: "Partially" - l2: Counter and Gauge are supported. - l3: Only attempted values are supported. No committed values for metrics. - class: nemo l1: "No" l2: not implemented @@ -417,10 +353,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - name: Stateful Processing description: Allows fine-grained access to per-key, per-window persistent state. Necessary for certain use cases (e.g. high-volume windows which store large amounts of data, but typically only access small portions of it; complex state machines; etc.) that are not easily or efficiently addressed via Combine or GroupByKey+ParDo. values: @@ -444,10 +376,6 @@ capability-matrix: l1: "No" l2: not implemented l3: - - class: samza - l1: "Partially" - l2: non-merging windows - l3: "States are backed up by either rocksDb KV store or in-memory hash map, and persist using changelog." - class: nemo l1: "No" l2: not implemented @@ -464,10 +392,6 @@ capability-matrix: l1: "" l2: l3: "" - - class: go direct - l1: "" - l2: - l3: "" - description: Bounded Splittable DoFn Support Status anchor: what color-y: "fff" @@ -500,10 +424,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -520,10 +440,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "Yes" - l2: - l3: - name: Side Inputs description: "" values: @@ -547,10 +463,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -567,10 +479,6 @@ capability-matrix: l1: l2: l3: - - class: go direct - l1: "Yes" - l2: - l3: - name: Splittable DoFn Initiated Checkpointing description: "" values: @@ -594,10 +502,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -614,10 +518,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - name: Dynamic Splitting description: "" values: @@ -641,10 +541,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -661,10 +557,6 @@ capability-matrix: l1: "Yes" l2: Only with Python SDK l3: - - class: go direct - l1: "No" - l2: - l3: - name: Bundle Finalization description: "" values: @@ -688,10 +580,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -708,10 +596,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - description: Unbounded Splittable DoFn Support Status anchor: what color-y: "fff" @@ -744,10 +628,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -764,10 +644,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - name: Side Inputs description: "" values: @@ -791,10 +667,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -811,10 +683,6 @@ capability-matrix: l1: l2: l3: - - class: go direct - l1: "Yes" - l2: - l3: - name: Splittable DoFn Initiated Checkpointing description: "" values: @@ -838,10 +706,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -858,10 +722,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - name: Dynamic Splitting description: "" values: @@ -885,10 +745,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -905,10 +761,6 @@ capability-matrix: l1: "No" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - name: Bundle Finalization description: "" values: @@ -932,10 +784,6 @@ capability-matrix: l1: l2: l3: "" - - class: samza - l1: - l2: - l3: "" - class: nemo l1: l2: @@ -952,10 +800,6 @@ capability-matrix: l1: "Yes" l2: l3: - - class: go direct - l1: "No" - l2: - l3: - description: Where in event time? anchor: where color-y: "fff" @@ -988,10 +832,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1004,6 +844,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Fixed windows description: Fixed-size, timestamp-based windows. (Hourly, Daily, etc) values: @@ -1027,10 +871,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1043,6 +883,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Sliding windows description: Possibly overlapping fixed-size timestamp-based windows (Every minute, use the last ten minutes of data.) values: @@ -1066,10 +910,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1082,6 +922,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Session windows description: Based on bursts of activity separated by a gap size. Different per key. values: @@ -1105,10 +949,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1121,6 +961,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Custom windows description: All windows must implement BoundedWindow, which specifies a max timestamp. Each WindowFn assigns elements to an associated window. values: @@ -1144,10 +988,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1160,6 +1000,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Custom merging windows description: A custom WindowFn additionally specifies whether and how to merge windows. values: @@ -1183,10 +1027,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1199,6 +1039,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - name: Timestamp control description: For a grouping transform, such as GBK or Combine, an OutputTimeFn specifies (1) how to combine input timestamps within a window and (2) how to merge aggregated timestamps when windows merge. values: @@ -1222,10 +1066,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: supported - l3: "" - class: nemo l1: "Yes" l2: supported @@ -1238,6 +1078,10 @@ capability-matrix: l1: "Yes" l2: supported l3: "" + - class: python direct + l1: "Yes" + l2: supported + l3: "" - description: When in processing time? anchor: when @@ -1271,10 +1115,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1287,7 +1127,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" - + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Event-time triggers description: Triggers that fire in response to event-time completeness signals, such as watermarks progressing. values: @@ -1311,10 +1154,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1327,6 +1166,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Processing-time triggers description: Triggers that fire in response to processing-time advancing. @@ -1351,10 +1194,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1367,6 +1206,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Count triggers description: Triggers that fire after seeing at least N elements. @@ -1391,10 +1234,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1407,6 +1246,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Composite triggers description: Triggers which compose other triggers in more complex structures, such as logical AND, logical OR, early/on-time/late, etc. @@ -1431,10 +1274,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1447,6 +1286,10 @@ capability-matrix: l1: "Partially" l2: l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Allowed lateness description: A way to bound the useful lifetime of a window (in event time), after which any unemitted results may be materialized, the window contents may be garbage collected, and any addtional late data that arrive for the window may be discarded. @@ -1471,10 +1314,6 @@ capability-matrix: l1: "No" l2: no streaming support in the runner l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1487,6 +1326,10 @@ capability-matrix: l1: "Partially" l2: l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Timers description: A fine-grained mechanism for performing work at some point in the future, in either the event-time or processing-time domain. Useful for orchestrating delayed events, timeouts, etc in complex state per-key, per-window state machines. @@ -1511,10 +1354,6 @@ capability-matrix: l1: "No" l2: not implemented l3: "" - - class: samza - l1: "Partially" - l2: non-merging windows - l3: The Samza Runner supports timers in non-merging windows. - class: nemo l1: "No" l2: not implemented @@ -1527,6 +1366,10 @@ capability-matrix: l1: "Partially" l2: l3: "" + - class: python direct + l1: "Yes" + l2: "Partially" + l3: "" - description: How do refinements relate? anchor: how @@ -1560,10 +1403,6 @@ capability-matrix: l1: "Partially" l2: fully supported in batch mode l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1576,6 +1415,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - name: Accumulating description: Elements are accumulated in state across multiple pane firings for the same window. @@ -1600,10 +1443,6 @@ capability-matrix: l1: "No" l2: "" l3: "" - - class: samza - l1: "Yes" - l2: fully supported - l3: "" - class: nemo l1: "Yes" l2: fully supported @@ -1616,6 +1455,10 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" + - class: python direct + l1: "Yes" + l2: fully supported + l3: "" - description: Additional common features not yet part of the Beam model anchor: misc @@ -1632,7 +1475,7 @@ capability-matrix: - class: dataflow l1: "Partially" l2: - l3: Dataflow has a native drain operation, but it does not work in the presence of event time timer loops. Final implemention pending model support. + l3: Dataflow has a native drain operation, support for event time timer loops drain is limited to Non-portable runner. - class: prism l1: "No" l2: @@ -1649,10 +1492,6 @@ capability-matrix: l1: l2: l3: - - class: samza - l1: - l2: - l3: - class: nemo l1: l2: @@ -1665,6 +1504,10 @@ capability-matrix: l1: l2: l3: + - class: python direct + l1: + l2: + l3: - name: Checkpoint description: APIs and semantics for saving a pipeline checkpoint are under discussion. This would be a runner-specific materialization of the pipeline state required to resume or duplicate the pipeline. values: @@ -1688,10 +1531,6 @@ capability-matrix: l1: "No" l2: l3: not implemented - - class: samza - l1: "Partially" - l2: - l3: Samza has a native checkpoint capability. - class: nemo l1: l2: @@ -1704,6 +1543,10 @@ capability-matrix: l1: l2: l3: + - class: python direct + l1: + l2: + l3: - name: Key-ordered delivery description: The runner offers guarantees for the order in which elements are passed in between operations. See per-key ordering semantics. values: @@ -1715,10 +1558,6 @@ capability-matrix: l1: "Yes" l2: fully supported l3: "" - - class: prism - l1: "Unverified" - l2: - l3: - class: flink l1: "Partially" l2: @@ -1735,10 +1574,6 @@ capability-matrix: l1: "Unverified" l2: l3: - - class: samza - l1: "Partially" - l2: - l3: Samza may perform different shuffling algorithms for batch and streaming. Samza guarantees key-ordered delivery in streaming, though not in batch. - class: nemo l1: "Unverified" l2: diff --git a/website/www/site/data/performance.yaml b/website/www/site/data/performance.yaml index 8841bbb58ecb..42fb5bee92f5 100644 --- a/website/www/site/data/performance.yaml +++ b/website/www/site/data/performance.yaml @@ -283,3 +283,36 @@ looks: title: AvgThroughputBytesPerSec by Version - id: P7wKZy6tQFWbbDfm4HzfCJnsQrVgfGsJ title: AvgThroughputElementsPerSec by Version + mltransformvocab: + write: + folder: 107 + cost: + - id: CccNCHQ23Js2tmxTDYVnjxwfx2zSFSjY + title: RunTime and EstimatedCost + date: + - id: n7Gn7c7KSW52ZgXCQXndHgWtjVPK8W33 + title: AvgThroughputBytesPerSec by Date + - id: 3tcg6cSh6tBg6FmpTxgzyMtvFqWKkQn3 + title: AvgThroughputElementsPerSec by Date + version: + - id: tPYwJngPDBsKjK3DNgGHGsCyTpxdfmTB + title: AvgThroughputBytesPerSec by Version + - id: cC75NnCbQT3mQmKVHtDxzptXpwPb64qz + title: AvgThroughputElementsPerSec by Version + 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 + 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/roadmap.html b/website/www/site/layouts/partials/section-menu/en/roadmap.html index 8c8d14ecce28..09c326705d9f 100644 --- a/website/www/site/layouts/partials/section-menu/en/roadmap.html +++ b/website/www/site/layouts/partials/section-menu/en/roadmap.html @@ -33,7 +33,6 @@
  • Flink Runner
  • Nemo Runner
  • Spark Runner
  • -
  • Samza Runner
  • Twister2 Runner
  • diff --git a/website/www/site/layouts/partials/section-menu/en/runners.html b/website/www/site/layouts/partials/section-menu/en/runners.html index 58576bb5f963..337debf3ecec 100644 --- a/website/www/site/layouts/partials/section-menu/en/runners.html +++ b/website/www/site/layouts/partials/section-menu/en/runners.html @@ -16,7 +16,6 @@
  • Prism Runner
  • Apache Flink
  • Apache Nemo
  • -
  • Apache Samza
  • Apache Spark
  • Google Cloud Dataflow
  • Hazelcast Jet
  • 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/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