This is an automated email from the ASF dual-hosted git repository.
Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 5334dc1d92a Add Flink 2.2.1 runner support (#38980)
5334dc1d92a is described below
commit 5334dc1d92a73f8734627d863ac55effb6c0835e
Author: Manan Mangal <[email protected]>
AuthorDate: Fri Jun 19 19:24:04 2026 -0700
Add Flink 2.2.1 runner support (#38980)
---
.../test-properties.json | 2 +-
.../beam_PostCommit_Java_Examples_Flink.yml | 2 +-
.../beam_PostCommit_Java_Nexmark_Flink.yml | 4 +-
.../beam_PostCommit_Java_PVR_Flink_Batch.yml | 2 +-
.../beam_PostCommit_Java_PVR_Flink_Streaming.yml | 4 +-
.../workflows/beam_PostCommit_Java_Tpcds_Flink.yml | 2 +-
.../beam_PostCommit_Java_ValidatesRunner_Flink.yml | 4 +-
.github/workflows/beam_PostCommit_XVR_Flink.yml | 2 +-
.../beam_PreCommit_Java_PVR_Flink_Batch.yml | 2 +-
.../beam_PreCommit_Java_PVR_Flink_Docker.yml | 2 +-
.../workflows/beam_Publish_Docker_Snapshots.yml | 4 +-
.../run_rc_validation_java_quickstart.yml | 2 +-
CHANGES.md | 3 +
gradle.properties | 2 +-
release/build.gradle.kts | 2 +-
runners/flink/2.2/build.gradle | 71 +
.../flink/2.2/job-server-container/build.gradle | 26 +
runners/flink/2.2/job-server/build.gradle | 44 +
.../runners/flink/FlinkExecutionEnvironments.java | 507 ++++++
.../wrappers/streaming/DoFnOperator.java | 1786 ++++++++++++++++++++
.../runners/flink/FlinkPipelineOptionsTest.java | 205 +++
sdks/go/examples/wasm/README.md | 6 +-
.../python/apache_beam/options/pipeline_options.py | 2 +-
sdks/typescript/src/apache_beam/runners/flink.ts | 2 +-
.../site/content/en/documentation/runners/flink.md | 10 +
25 files changed, 2675 insertions(+), 23 deletions(-)
diff --git a/.github/actions/setup-default-test-properties/test-properties.json
b/.github/actions/setup-default-test-properties/test-properties.json
index ddb4a8b73ff..5a1057470b6 100644
--- a/.github/actions/setup-default-test-properties/test-properties.json
+++ b/.github/actions/setup-default-test-properties/test-properties.json
@@ -14,7 +14,7 @@
},
"JavaTestProperties": {
"SUPPORTED_VERSIONS": ["8", "11", "17", "21", "25"],
- "FLINK_VERSIONS": ["1.19", "1.20", "2.0", "2.1"],
+ "FLINK_VERSIONS": ["1.19", "1.20", "2.0", "2.1", "2.2"],
"SPARK_VERSIONS": ["3"]
},
"GoTestProperties": {
diff --git a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml
b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml
index 9f60e1529d1..de984a4b2bb 100644
--- a/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml
+++ b/.github/workflows/beam_PostCommit_Java_Examples_Flink.yml
@@ -80,7 +80,7 @@ jobs:
- name: run examplesIntegrationTest script
uses: ./.github/actions/gradle-command-self-hosted-action
with:
- gradle-command: :runners:flink:1.20:examplesIntegrationTest
+ gradle-command: :runners:flink:2.2:examplesIntegrationTest
- name: Archive JUnit Test Results
uses: actions/upload-artifact@v7
if: ${{ !success() }}
diff --git a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml
b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml
index 9d7ca2043d7..4a3574906a2 100644
--- a/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml
+++ b/.github/workflows/beam_PostCommit_Java_Nexmark_Flink.yml
@@ -102,7 +102,7 @@ jobs:
with:
gradle-command: :sdks:java:testing:nexmark:run
arguments: |
- -Pnexmark.runner=:runners:flink:1.20 \
+ -Pnexmark.runner=:runners:flink:2.2 \
"${{ env.GRADLE_COMMAND_ARGUMENTS }} --streaming=${{
matrix.streaming }} --queryLanguage=${{ matrix.queryLanguage }}" \
- name: run PostCommit Java Nexmark Flink (${{ matrix.streaming }})
script
if: matrix.queryLanguage == 'none'
@@ -110,5 +110,5 @@ jobs:
with:
gradle-command: :sdks:java:testing:nexmark:run
arguments: |
- -Pnexmark.runner=:runners:flink:1.19 \
+ -Pnexmark.runner=:runners:flink:2.2 \
"${{ env.GRADLE_COMMAND_ARGUMENTS }}--streaming=${{
matrix.streaming }}"
diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml
b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml
index 476793bfa8b..da8c29e4955 100644
--- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml
+++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Batch.yml
@@ -69,7 +69,7 @@ jobs:
job_name: ["beam_PostCommit_Java_PVR_Flink_Batch"]
job_phrase: ["Run Java_PVR_Flink_Batch PostCommit"]
# every major version
- flink_version: ['1.20', '2.0']
+ flink_version: ['1.20', '2.0', '2.1', '2.2']
timeout-minutes: 240
runs-on: [self-hosted, ubuntu-24.04, highmem]
if: |
diff --git a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml
b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml
index d5f7edeb11d..f3c6560f994 100644
--- a/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml
+++ b/.github/workflows/beam_PostCommit_Java_PVR_Flink_Streaming.yml
@@ -56,11 +56,11 @@ jobs:
runs-on: [self-hosted, ubuntu-24.04, main]
timeout-minutes: 120
strategy:
- matrix:
+ matrix:
job_name: [beam_PostCommit_Java_PVR_Flink_Streaming]
job_phrase: [Run Java Flink PortableValidatesRunner Streaming]
# every major version
- flink_version: [ '1.20', '2.0' ]
+ flink_version: [ '1.20', '2.0', '2.1', '2.2']
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'pull_request_target' ||
diff --git a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml
b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml
index ae3dcb7d6ad..49416266b48 100644
--- a/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml
+++ b/.github/workflows/beam_PostCommit_Java_Tpcds_Flink.yml
@@ -101,5 +101,5 @@ jobs:
with:
gradle-command: :sdks:java:testing:tpcds:run
arguments: |
- -Ptpcds.runner=:runners:flink:1.20 \
+ -Ptpcds.runner=:runners:flink:2.2 \
"-Ptpcds.args=${{env.tpcdsBigQueryArgs}}
${{env.tpcdsInfluxDBArgs}} ${{ env.GRADLE_COMMAND_ARGUMENTS }}
--queries=${{env.tpcdsQueriesArg}}" \
diff --git a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml
b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml
index febec1afa59..b55d6342cdb 100644
--- a/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml
+++ b/.github/workflows/beam_PostCommit_Java_ValidatesRunner_Flink.yml
@@ -49,7 +49,7 @@ env:
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }}
jobs:
- beam_PostCommit_Java_ValidatesRunner_Flink:
+ beam_PostCommit_Java_ValidatesRunner_Flink:
name: ${{ matrix.job_name }} (${{ matrix.flink_version }})
runs-on: [self-hosted, ubuntu-24.04, main]
timeout-minutes: 100
@@ -58,7 +58,7 @@ jobs:
job_name: [beam_PostCommit_Java_ValidatesRunner_Flink]
job_phrase: [Run Flink ValidatesRunner]
# every major version
- flink_version: ['1.20', '2.0']
+ flink_version: ['1.20', '2.0', '2.1', '2.2']
if: |
github.event_name == 'workflow_dispatch' ||
github.event_name == 'pull_request_target' ||
diff --git a/.github/workflows/beam_PostCommit_XVR_Flink.yml
b/.github/workflows/beam_PostCommit_XVR_Flink.yml
index 44cb264ad9d..e93fe82f48a 100644
--- a/.github/workflows/beam_PostCommit_XVR_Flink.yml
+++ b/.github/workflows/beam_PostCommit_XVR_Flink.yml
@@ -47,7 +47,7 @@ env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GE_CACHE_USERNAME }}
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GE_CACHE_PASSWORD }}
- FlinkVersion: 1.20
+ FlinkVersion: 2.2
jobs:
beam_PostCommit_XVR_Flink:
diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml
b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml
index 9e04f103a77..43549eb32c5 100644
--- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml
+++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Batch.yml
@@ -94,7 +94,7 @@ jobs:
- name: run validatesPortableRunnerBatch script
uses: ./.github/actions/gradle-command-self-hosted-action
with:
- gradle-command:
:runners:flink:1.20:job-server:validatesPortableRunnerBatch
+ gradle-command:
:runners:flink:2.2:job-server:validatesPortableRunnerBatch
env:
CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH }}
- name: Archive JUnit Test Results
diff --git a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml
b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml
index 3ad28549e59..9d95d830614 100644
--- a/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml
+++ b/.github/workflows/beam_PreCommit_Java_PVR_Flink_Docker.yml
@@ -99,7 +99,7 @@ jobs:
- name: run PreCommit Java PVR Flink Docker script
uses: ./.github/actions/gradle-command-self-hosted-action
with:
- gradle-command:
:runners:flink:1.20:job-server:validatesPortableRunnerDocker
+ gradle-command:
:runners:flink:2.2:job-server:validatesPortableRunnerDocker
env:
CLOUDSDK_CONFIG: ${{ env.KUBELET_GCLOUD_CONFIG_PATH}}
- name: Archive JUnit Test Results
diff --git a/.github/workflows/beam_Publish_Docker_Snapshots.yml
b/.github/workflows/beam_Publish_Docker_Snapshots.yml
index 5e4412b2650..0895c226d47 100644
--- a/.github/workflows/beam_Publish_Docker_Snapshots.yml
+++ b/.github/workflows/beam_Publish_Docker_Snapshots.yml
@@ -84,10 +84,10 @@ jobs:
-Pdocker-repository-root=gcr.io/apache-beam-testing/beam_portability \
-Pdocker-tag-list=${{ github.sha }}${LATEST_TAG} \
-Pdocker-pull-licenses
- - name: run Publish Docker Snapshots script for Flink 2.0
+ - name: run Publish Docker Snapshots script for Flink 2.2
uses: ./.github/actions/gradle-command-self-hosted-action
with:
- gradle-command: :runners:flink:2.0:job-server-container:dockerPush
+ gradle-command: :runners:flink:2.2:job-server-container:dockerPush
arguments: |
-Pdocker-repository-root=gcr.io/apache-beam-testing/beam_portability \
-Pdocker-tag-list=${{ github.sha }}${LATEST_TAG} \
diff --git a/.github/workflows/run_rc_validation_java_quickstart.yml
b/.github/workflows/run_rc_validation_java_quickstart.yml
index 41a6991d14e..0675d33c318 100644
--- a/.github/workflows/run_rc_validation_java_quickstart.yml
+++ b/.github/workflows/run_rc_validation_java_quickstart.yml
@@ -88,7 +88,7 @@ jobs:
- name: Run QuickStart Java Flink Runner
uses: ./.github/actions/gradle-command-self-hosted-action
with:
- gradle-command: :runners:flink:2.1:runQuickstartJavaFlinkLocal
+ gradle-command: :runners:flink:2.2:runQuickstartJavaFlinkLocal
arguments: |
-Prepourl=${{ env.APACHE_REPO_URL }} \
-Pver=${{ env.RELEASE_VERSION }}
diff --git a/CHANGES.md b/CHANGES.md
index 0139b8bbd3b..bd9c6ea246a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -74,6 +74,8 @@
* (Java) Enabled state tag encoding v2 by default for new Dataflow Streaming
Engine jobs. It can be disabled by passing
`--experiments=disable_streaming_engine_state_tag_encoding_v2` or
`--updateCompatibilityVersion=2.74.0` pipeline option. Note that the tag
encoding version cannot change during a job update. Jobs using tag encoding v2
(enabled by default for new jobs on 2.75.0+) cannot be downgraded to Beam
versions prior to 2.73.0, as only versions 2.73.0 and later support tag
encoding [...]
* (Python) Added instrumentation to support off-the-shelf profiling agents
when launching Python SDK Harness
([#38853](https://github.com/apache/beam/issues/38853)).
* (Java) Added support to the FnApi Data stream protocol allowing runners to
isolate bundles slowly processing input from other bundles.
([#39001](https://github.com/apache/beam/issues/39001)).
+* (Java) Flink 2.1 support added
([#38947](https://github.com/apache/beam/issues/38947)).
+* (Java) Flink 2.2 support added
([#38978](https://github.com/apache/beam/issues/38978)).
## Breaking Changes
@@ -99,6 +101,7 @@
[comment]: # ( When updating known issues after release, make sure also update
website blog in website/www/site/content/blog.)
* ([#X](https://github.com/apache/beam/issues/X)).
+* (Java) Projects using the Flink runner with Flink 2.1 or later alongside
libraries requiring `org.lz4:lz4-java` (e.g., Kafka clients) may encounter a
Gradle capability conflict, because Flink 2.1+ ships `at.yawk.lz4:lz4-java`
which declares the same capability. To resolve, add a `capabilitiesResolution`
rule to your `build.gradle` that selects `at.yawk.lz4:lz4-java`
([#38947](https://github.com/apache/beam/issues/38947)).
# [2.74.0] - 2026-06-02
diff --git a/gradle.properties b/gradle.properties
index c20a8373cb6..bc84397c63c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -39,7 +39,7 @@ docker_image_default_repo_root=apache
docker_image_default_repo_prefix=beam_
# supported flink versions
-flink_versions=1.19,1.20,2.0,2.1
+flink_versions=1.19,1.20,2.0,2.1,2.2
# supported spark versions
spark_versions=3,4
# supported python versions
diff --git a/release/build.gradle.kts b/release/build.gradle.kts
index 5be70742860..54165dc4965 100644
--- a/release/build.gradle.kts
+++ b/release/build.gradle.kts
@@ -39,7 +39,7 @@ task("runJavaExamplesValidationTask") {
dependsOn(":runners:direct-java:runQuickstartJavaDirect")
dependsOn(":runners:google-cloud-dataflow-java:runQuickstartJavaDataflow")
dependsOn(":runners:spark:3:runQuickstartJavaSpark")
- dependsOn(":runners:flink:2.0:runQuickstartJavaFlinkLocal")
+ dependsOn(":runners:flink:2.2:runQuickstartJavaFlinkLocal")
dependsOn(":runners:direct-java:runMobileGamingJavaDirect")
if (project.hasProperty("ver") ||
!project.version.toString().endsWith("SNAPSHOT")) {
// only run one variant of MobileGaming on Dataflow for nightly
diff --git a/runners/flink/2.2/build.gradle b/runners/flink/2.2/build.gradle
new file mode 100644
index 00000000000..3856d20a351
--- /dev/null
+++ b/runners/flink/2.2/build.gradle
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * License); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Flink 2.2 API changes addressed in this module's source overrides:
+// - CheckpointingMode moved (Flink 2.2):
org.apache.flink.streaming.api.CheckpointingMode
+// is now @Deprecated; canonical path is
org.apache.flink.core.execution.CheckpointingMode.
+// FlinkExecutionEnvironments.java, DoFnOperator.java, and
FlinkPipelineOptionsTest.java
+// are overridden here to use the non-deprecated import.
+//
+// Flink 2.2 breaking changes that are SQL/cluster-layer only (no runner code
change needed):
+// - TLS cipher suite default changed (FLINK-39022): JDK
11.0.30+/17.0.18+/21.0.10+/24+
+// disabled TLS_RSA_* ciphers; new default uses ECDHE suites. Verify cipher
support
+// on TLS-enabled clusters before upgrading.
+// - StreamingMultiJoinOperator state format change (FLINK-38209): savepoints
from Flink 2.1
+// with table.optimizer.multi-join.enabled=true are NOT compatible with 2.2.
+// - SQL row NOT NULL now enforced (FLINK-38181): use
table.legacy-nested-row-nullability
+// to restore prior (silent-ignore) behavior if needed.
+
+project.ext {
+ flink_major = '2.2'
+ flink_version = '2.2.1'
+ excluded_files = [
+ 'main': [
+ // Used by DataSet API only
+ "org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapter.java",
+ "org/apache/beam/runners/flink/FlinkBatchPipelineTranslator.java",
+
"org/apache/beam/runners/flink/FlinkBatchPortablePipelineTranslator.java",
+ "org/apache/beam/runners/flink/FlinkBatchTransformTranslators.java",
+
"org/apache/beam/runners/flink/translation/functions/FlinkNonMergingReduceFunction.java",
+ // Moved to org.apache.flink.runtime.state.StateBackendFactory
+ "org/apache/beam/runners/flink/FlinkStateBackendFactory.java",
+ ],
+ 'test': [
+ // Used by DataSet API only
+
"org/apache/beam/runners/flink/adapter/BeamFlinkDataSetAdapterTest.java",
+ "org/apache/beam/runners/flink/batch/NonMergingGroupByKeyTest.java",
+ "org/apache/beam/runners/flink/batch/ReshuffleTest.java",
+ ]
+ ]
+}
+
+// Load the main build script which contains all build logic.
+apply from: "../flink_runner.gradle"
+
+// Flink 2.2 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java
+// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java')
{
+ def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4')
}
+ if (candidate != null) {
+ select(candidate)
+ } else {
+ selectHighestVersion()
+ }
+ }
+}
diff --git a/runners/flink/2.2/job-server-container/build.gradle
b/runners/flink/2.2/job-server-container/build.gradle
new file mode 100644
index 00000000000..afdb68a0fc9
--- /dev/null
+++ b/runners/flink/2.2/job-server-container/build.gradle
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * License); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+def basePath = '../../job-server-container'
+
+project.ext {
+ resource_path = basePath
+}
+
+// Load the main build script which contains all build logic.
+apply from: "$basePath/flink_job_server_container.gradle"
diff --git a/runners/flink/2.2/job-server/build.gradle
b/runners/flink/2.2/job-server/build.gradle
new file mode 100644
index 00000000000..3f6b184e3fc
--- /dev/null
+++ b/runners/flink/2.2/job-server/build.gradle
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * License); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+def basePath = '../../job-server'
+
+project.ext {
+ // Look for the source code in the parent module
+ main_source_dirs = ["$basePath/src/main/java"]
+ test_source_dirs = ["$basePath/src/test/java"]
+ main_resources_dirs = ["$basePath/src/main/resources"]
+ test_resources_dirs = ["$basePath/src/test/resources"]
+ archives_base_name = 'beam-runners-flink-2.2-job-server'
+}
+
+// Load the main build script which contains all build logic.
+apply from: "$basePath/flink_job_server.gradle"
+
+// Flink 2.2 uses at.yawk.lz4:lz4-java instead of org.lz4:lz4-java
+// Explicitly prefer Flink's at.yawk.lz4 version to resolve capability conflict
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java')
{
+ def candidate = candidates.find { it.id.toString().contains('at.yawk.lz4')
}
+ if (candidate != null) {
+ select(candidate)
+ } else {
+ selectHighestVersion()
+ }
+ }
+}
diff --git
a/runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java
b/runners/flink/2.2/src/main/java/org/apache/beam/runners/flink/FlinkExecutionEnvironments.java
new file mode 100644
index 00000000000..b340632219a
--- /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<String> 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<String> 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<String, String> 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<String, String> jobOptions;
+
+ private GlobalJobParametersImpl(Map<String, String> jobOptions) {
+ this.jobOptions = jobOptions;
+ }
+
+ @Override
+ public Map<String, String> 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 00000000000..409797625db
--- /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 <InputT> the input type of the {@link DoFn}
+ * @param <OutputT> 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<PreInputT, InputT, OutputT>
+ extends AbstractStreamOperator<WindowedValue<OutputT>>
+ implements OneInputStreamOperator<WindowedValue<PreInputT>,
WindowedValue<OutputT>>,
+ TwoInputStreamOperator<WindowedValue<PreInputT>, RawUnionValue,
WindowedValue<OutputT>>,
+ Triggerable<FlinkKey, TimerData> {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(DoFnOperator.class);
+ private final boolean isStreaming;
+
+ protected DoFn<InputT, OutputT> doFn;
+
+ protected final SerializablePipelineOptions serializedOptions;
+
+ protected final TupleTag<OutputT> mainOutputTag;
+ protected final List<TupleTag<?>> additionalOutputTags;
+
+ protected final Collection<PCollectionView<?>> sideInputs;
+ protected final Map<Integer, PCollectionView<?>> sideInputTagMapping;
+
+ protected final WindowingStrategy<?, ?> windowingStrategy;
+
+ protected final OutputManagerFactory<OutputT> outputManagerFactory;
+
+ protected transient DoFnRunner<InputT, OutputT> doFnRunner;
+ protected transient PushbackSideInputDoFnRunner<InputT, OutputT>
pushbackDoFnRunner;
+ protected transient BufferingDoFnRunner<InputT, OutputT> bufferingDoFnRunner;
+
+ protected transient SideInputHandler sideInputHandler;
+
+ protected transient SideInputReader sideInputReader;
+
+ protected transient BufferedOutputManager<OutputT> outputManager;
+
+ private transient DoFnInvoker<InputT, OutputT> doFnInvoker;
+
+ protected transient FlinkStateInternals<?> keyedStateInternals;
+ protected transient FlinkTimerInternals timerInternals;
+
+ protected final String stepName;
+
+ final Coder<WindowedValue<InputT>> windowedInputCoder;
+
+ final Map<TupleTag<?>, Coder<?>> outputCoders;
+
+ final Coder<?> keyCoder;
+
+ final KeySelector<WindowedValue<InputT>, ?> 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<String, PCollectionView<?>> 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<Long,
List<InMemoryBundleFinalizer.Finalization>>
+ 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<TimerData> timerService;
+ // Flink 1.20 moved timeServiceManager to protected scope. No longer need
delegate
+ // private transient InternalTimeServiceManager<?> timeServiceManager;
+
+ private transient PushedBackElementsHandler<WindowedValue<InputT>>
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}.
+ *
+ * <p>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<InputT, OutputT> doFn,
+ String stepName,
+ Coder<WindowedValue<InputT>> inputWindowedCoder,
+ Map<TupleTag<?>, Coder<?>> outputCoders,
+ TupleTag<OutputT> mainOutputTag,
+ List<TupleTag<?>> additionalOutputTags,
+ OutputManagerFactory<OutputT> outputManagerFactory,
+ WindowingStrategy<?, ?> windowingStrategy,
+ Map<Integer, PCollectionView<?>> sideInputTagMapping,
+ Collection<PCollectionView<?>> sideInputs,
+ PipelineOptions options,
+ @Nullable Coder<?> keyCoder,
+ @Nullable KeySelector<WindowedValue<InputT>, ?> keySelector,
+ DoFnSchemaInformation doFnSchemaInformation,
+ Map<String, PCollectionView<?>> 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<InputT, OutputT> 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<InputT, OutputT> getDoFn() {
+ return doFn;
+ }
+
+ protected Iterable<WindowedValue<InputT>>
preProcess(WindowedValue<PreInputT> input) {
+ // Assume Input is PreInputT
+ return Collections.singletonList((WindowedValue<InputT>) 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<InputT, OutputT> createWrappingDoFnRunner(
+ DoFnRunner<InputT, OutputT> wrappedRunner, StepContext stepContext) {
+
+ if (keyCoder != null) {
+ StatefulDoFnRunner.CleanupTimer<InputT> cleanupTimer =
+ new StatefulDoFnRunner.TimeInternalsCleanupTimer<InputT>(
+ 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<StreamRecord<WindowedValue<OutputT>>> 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<WindowedValue<InputT>> pushedBackStateDescriptor =
+ new ListStateDescriptor<>(
+ "pushed-back-elements",
+ new CoderTypeSerializer<>(windowedInputCoder, serializedOptions));
+
+ if (keySelector != null) {
+ pushedBackElementsHandler =
+ KeyedPushedBackElementsHandler.create(
+ keySelector, getKeyedStateBackend(), pushedBackStateDescriptor);
+ } else {
+ ListState<WindowedValue<InputT>> 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<WindowedValue<InputT>> 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<InputT, OutputT> createBufferingDoFnRunnerIfNeeded(
+ DoFnRunner<InputT, OutputT> 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
+ */
+ <K> @Nullable KeyedStateBackend<K> 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<InputT, OutputT, BoundedWindow>) 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<WindowedValue<InputT>> 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<WindowedValue<PreInputT>>
streamRecord) {
+ for (WindowedValue<InputT> 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<WindowedValue<PreInputT>>
streamRecord)
+ throws Exception {
+ for (WindowedValue<InputT> e : preProcess(streamRecord.getValue())) {
+ checkInvokeStartBundle();
+ Iterable<WindowedValue<InputT>> justPushedBack =
+ pushbackDoFnRunner.processElementInReadyWindows(e);
+
+ long min = pushedBackWatermark;
+ for (WindowedValue<InputT> 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<RawUnionValue> streamRecord) {
+ @SuppressWarnings("unchecked")
+ WindowedValue<Iterable<?>> value =
+ (WindowedValue<Iterable<?>>) streamRecord.getValue().getValue();
+
+ PCollectionView<?> sideInput =
sideInputTagMapping.get(streamRecord.getValue().getUnionTag());
+ sideInputHandler.addSideInputValue(sideInput, value);
+ }
+
+ @Override
+ public final void processElement2(StreamRecord<RawUnionValue> 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<WindowedValue<InputT>> newPushedBack = new ArrayList<>();
+
+ Iterator<WindowedValue<InputT>> it =
pushedBackElementsHandler.getElements().iterator();
+
+ while (it.hasNext()) {
+ WindowedValue<InputT> 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<WindowedValue<InputT>> justPushedBack =
+ pushbackDoFnRunner.processElementInReadyWindows(element);
+ Iterables.addAll(newPushedBack, justPushedBack);
+ }
+
+ pushedBackElementsHandler.clear();
+ long min = Long.MAX_VALUE;
+ for (WindowedValue<InputT> 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<WindowedValue<InputT>> it =
pushedBackElementsHandler.getElements().iterator();
+
+ while (it.hasNext()) {
+ checkInvokeStartBundle();
+ WindowedValue<InputT> 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.
+ *
+ * <p>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<Long> 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<InMemoryBundleFinalizer.Finalization> 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<FlinkKey, TimerData> timer) {
+ checkInvokeStartBundle();
+ fireTimerInternal(timer.getKey(), timer.getNamespace());
+ }
+
+ @Override
+ public void onProcessingTime(InternalTimer<FlinkKey, TimerData> 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<InputT> getInputCoder() {
+ return (Coder<InputT>)
Iterables.getOnlyElement(windowedInputCoder.getCoderArguments());
+ }
+
+ /** Factory for creating an {@link BufferedOutputManager} from a Flink
{@link Output}. */
+ interface OutputManagerFactory<OutputT> extends Serializable {
+ BufferedOutputManager<OutputT> create(
+ Output<StreamRecord<WindowedValue<OutputT>>> 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.
+ *
+ * <p>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<OutputT> implements
WindowedValueMultiReceiver {
+
+ private final TupleTag<OutputT> mainTag;
+ private final Map<TupleTag<?>, OutputTag<WindowedValue<?>>>
tagsToOutputTags;
+ private final Map<TupleTag<?>, 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<Integer, TupleTag<?>> idsToTags;
+ /** Elements buffered during a snapshot, by output id. */
+ @VisibleForTesting
+ final PushedBackElementsHandler<KV<Integer, WindowedValue<?>>>
pushedBackElementsHandler;
+
+ protected final Output<StreamRecord<WindowedValue<OutputT>>> 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<StreamRecord<WindowedValue<OutputT>>> output,
+ TupleTag<OutputT> mainTag,
+ Map<TupleTag<?>, OutputTag<WindowedValue<?>>> tagsToOutputTags,
+ Map<TupleTag<?>, Integer> tagsToIds,
+ Lock bufferLock,
+ PushedBackElementsHandler<KV<Integer, WindowedValue<?>>>
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<TupleTag<?>, 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 <T> void output(TupleTag<T> tag, WindowedValue<T> 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<Integer, WindowedValue<?>> 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.
+ *
+ * <p>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 <T> void emit(TupleTag<T> tag, WindowedValue<T> 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<OutputT> castValue = (WindowedValue<OutputT>) value;
+ output.collect(new StreamRecord<>(castValue));
+ } else {
+ @SuppressWarnings("unchecked")
+ OutputTag<WindowedValue<T>> 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<KV<Integer,
WindowedValue<?>>> {
+
+ private final Map<Integer, Coder<WindowedValue<?>>> idsToCoders;
+
+ TaggedKvCoder(Map<Integer, Coder<WindowedValue<?>>> idsToCoders) {
+ this.idsToCoders = idsToCoders;
+ }
+
+ @Override
+ public void encode(KV<Integer, WindowedValue<?>> kv, OutputStream out)
throws IOException {
+ Coder<WindowedValue<?>> coder = idsToCoders.get(kv.getKey());
+ VarIntCoder.of().encode(kv.getKey(), out);
+ coder.encode(kv.getValue(), out);
+ }
+
+ @Override
+ public KV<Integer, WindowedValue<?>> decode(InputStream in) throws
IOException {
+ Integer id = VarIntCoder.of().decode(in);
+ Coder<WindowedValue<?>> coder = idsToCoders.get(id);
+ WindowedValue<?> value = coder.decode(in);
+ return KV.of(id, value);
+ }
+
+ @Override
+ public List<? extends Coder<?>> 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<OutputT>
+ implements OutputManagerFactory<OutputT> {
+
+ private final TupleTag<OutputT> mainTag;
+ private final Map<TupleTag<?>, Integer> tagsToIds;
+ private final Map<TupleTag<?>, OutputTag<WindowedValue<?>>>
tagsToOutputTags;
+ private final Map<TupleTag<?>, Coder<WindowedValue<?>>> tagsToCoders;
+ private final SerializablePipelineOptions pipelineOptions;
+ private final boolean isStreaming;
+
+ // There is no side output.
+ @SuppressWarnings("unchecked")
+ public MultiOutputOutputManagerFactory(
+ TupleTag<OutputT> mainTag,
+ Coder<WindowedValue<OutputT>> mainCoder,
+ SerializablePipelineOptions pipelineOptions) {
+ this(
+ mainTag,
+ new HashMap<>(),
+ ImmutableMap.<TupleTag<?>, Coder<WindowedValue<?>>>builder()
+ .put(mainTag, (Coder) mainCoder)
+ .build(),
+ ImmutableMap.<TupleTag<?>, Integer>builder().put(mainTag, 0).build(),
+ pipelineOptions);
+ }
+
+ public MultiOutputOutputManagerFactory(
+ TupleTag<OutputT> mainTag,
+ Map<TupleTag<?>, OutputTag<WindowedValue<?>>> tagsToOutputTags,
+ Map<TupleTag<?>, Coder<WindowedValue<?>>> tagsToCoders,
+ Map<TupleTag<?>, 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<OutputT> create(
+ Output<StreamRecord<WindowedValue<OutputT>>> output,
+ Lock bufferLock,
+ OperatorStateBackend operatorStateBackend)
+ throws Exception {
+ Preconditions.checkNotNull(output);
+ Preconditions.checkNotNull(bufferLock);
+ Preconditions.checkNotNull(operatorStateBackend);
+
+ TaggedKvCoder taggedKvCoder = buildTaggedKvCoder();
+ ListStateDescriptor<KV<Integer, WindowedValue<?>>>
taggedOutputPushbackStateDescriptor =
+ new ListStateDescriptor<>(
+ "bundle-buffer-tag", new CoderTypeSerializer<>(taggedKvCoder,
pipelineOptions));
+ ListState<KV<Integer, WindowedValue<?>>> listStateBuffer =
+
operatorStateBackend.getListState(taggedOutputPushbackStateDescriptor);
+ PushedBackElementsHandler<KV<Integer, WindowedValue<?>>>
pushedBackElementsHandler =
+ NonKeyedPushedBackElementsHandler.create(listStateBuffer);
+
+ return new BufferedOutputManager<>(
+ output,
+ mainTag,
+ tagsToOutputTags,
+ tagsToIds,
+ bufferLock,
+ pushedBackElementsHandler,
+ isStreaming);
+ }
+
+ private TaggedKvCoder buildTaggedKvCoder() {
+ ImmutableMap.Builder<Integer, Coder<WindowedValue<?>>>
idsToCodersBuilder =
+ ImmutableMap.builder();
+ for (Map.Entry<TupleTag<?>, 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.
+ *
+ * <p>CAUTION: This map is scoped by the current active key. Do not
attempt to perform any
+ * calculations which span across keys.
+ */
+ @VisibleForTesting final MapState<String, TimerData> pendingTimersById;
+
+ private final InternalTimerService<TimerData> timerService;
+
+ private FlinkTimerInternals(InternalTimerService<TimerData> timerService)
throws Exception {
+ MapStateDescriptor<String, TimerData> 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<Object> keyedStateBackend =
getKeyedStateBackend();
+ final InternalPriorityQueue<InternalTimer<Object, TimerData>>
processingTimeTimersQueue =
+ Workarounds.retrieveInternalProcessingTimerQueue(timerService);
+
+ InternalTimer<Object, TimerData> internalTimer;
+ while ((internalTimer = processingTimeTimersQueue.poll()) != null) {
+ keyedStateBackend.setCurrentKey(internalTimer.getKey());
+ TimerData timer = internalTimer.getNamespace();
+ checkInvokeStartBundle();
+ fireTimerInternal((FlinkKey) internalTimer.getKey(), timer);
+ }
+ }
+
+ private void populateOutputTimestampQueue(InternalTimerService<TimerData>
timerService)
+ throws Exception {
+
+ BiConsumerWithException<TimerData, Long, Exception> 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.
+ *
+ * <p>Flink's TimerService has different semantics because it only ensures
{@code T <=
+ * current_time}.
+ *
+ * <p>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.
+ *
+ * <p>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 00000000000..6cebadc49d5
--- /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}.
+ *
+ * <p>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<String> mainTag = new TupleTag<>("main-output");
+ Coder<WindowedValue<String>> 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<String> mainTag = new TupleTag<>("main-output");
+
+ Coder<WindowedValue<String>> coder =
WindowedValues.getValueOnlyCoder(StringUtf8Coder.of());
+ DoFnOperator<String, String, String> 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<Object, Object, Object> deserialized =
SerializationUtils.deserialize(serialized);
+
+ TypeInformation<WindowedValue<Object>> typeInformation =
+ TypeInformation.of(new TypeHint<WindowedValue<Object>>() {});
+
+ OneInputStreamOperatorTestHarness<WindowedValue<Object>,
WindowedValue<Object>> 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<String, String> {
+ @ProcessElement
+ public void processElement(ProcessContext c) throws Exception {
+ Assert.assertNotNull(c.getPipelineOptions());
+ Assert.assertEquals(
+ options.getTestOption(),
c.getPipelineOptions().as(MyOptions.class).getTestOption());
+ }
+ }
+}
diff --git a/sdks/go/examples/wasm/README.md b/sdks/go/examples/wasm/README.md
index deb2e9196f4..7a7b54befee 100644
--- a/sdks/go/examples/wasm/README.md
+++ b/sdks/go/examples/wasm/README.md
@@ -68,13 +68,13 @@ cd $BEAM_HOME
Expected output should include the following, from which you acquire the
latest flink runner version.
```shell
-'flink_versions: 1.19,1.20,2.0,2.1'
+'flink_versions: 1.19,1.20,2.0,2.1,2.2'
```
-#### 2. Set to the latest flink runner version i.e. 2.1
+#### 2. Set to the latest flink runner version i.e. 2.2
```shell
-FLINK_VERSION=2.1
+FLINK_VERSION=2.2
```
#### 3. In a separate terminal, start the flink runner (It should take a few
minutes on the first execution)
diff --git a/sdks/python/apache_beam/options/pipeline_options.py
b/sdks/python/apache_beam/options/pipeline_options.py
index 8bd22077631..4b06b8eda61 100644
--- a/sdks/python/apache_beam/options/pipeline_options.py
+++ b/sdks/python/apache_beam/options/pipeline_options.py
@@ -2106,7 +2106,7 @@ class JobServerOptions(PipelineOptions):
class FlinkRunnerOptions(PipelineOptions):
# These should stay in sync with gradle.properties.
- PUBLISHED_FLINK_VERSIONS = ['1.19', '1.20', '2.0', '2.1']
+ PUBLISHED_FLINK_VERSIONS = ['1.19', '1.20', '2.0', '2.1', '2.2']
@classmethod
def _add_argparse_args(cls, parser):
diff --git a/sdks/typescript/src/apache_beam/runners/flink.ts
b/sdks/typescript/src/apache_beam/runners/flink.ts
index bb5137b8f9b..3fa386cb58a 100644
--- a/sdks/typescript/src/apache_beam/runners/flink.ts
+++ b/sdks/typescript/src/apache_beam/runners/flink.ts
@@ -28,7 +28,7 @@ import { JavaJarService } from "../utils/service";
const MAGIC_HOST_NAMES = ["[local]", "[auto]"];
// These should stay in sync with gradle.properties.
-const PUBLISHED_FLINK_VERSIONS = ["1.19", "1.20", "2.0", "2.1"];
+const PUBLISHED_FLINK_VERSIONS = ["1.19", "1.20", "2.0", "2.1", "2.2"];
const defaultOptions = {
flinkMaster: "[local]",
diff --git a/website/www/site/content/en/documentation/runners/flink.md
b/website/www/site/content/en/documentation/runners/flink.md
index 1aafa41ecb9..d201d2546f8 100644
--- a/website/www/site/content/en/documentation/runners/flink.md
+++ b/website/www/site/content/en/documentation/runners/flink.md
@@ -330,6 +330,16 @@ To find out which version of Flink is compatible with Beam
please see the table
<th>Artifact Id</th>
<th>Supported Beam Versions</th>
</tr>
+<tr>
+ <td>2.2.x</td>
+ <td>beam-runners-flink-2.2</td>
+ <td>≥ 2.75.0</td>
+</tr>
+<tr>
+ <td>2.1.x</td>
+ <td>beam-runners-flink-2.1</td>
+ <td>≥ 2.75.0</td>
+</tr>
<tr>
<td>2.0.x</td>
<td>beam-runners-flink-2.0</td>