diff --git a/.asf.yaml b/.asf.yaml index 721b9f2d3dd7..8c067e7e4ee9 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -38,6 +38,7 @@ github: collaborators: - pcoet - olehborysevych + - rshamunov enabled_merge_buttons: squash: true diff --git a/.github/REVIEWERS.yml b/.github/REVIEWERS.yml index 4dc9b2b3de53..a0ec8f6eefde 100644 --- a/.github/REVIEWERS.yml +++ b/.github/REVIEWERS.yml @@ -33,7 +33,6 @@ labels: - ryanthompson591 - tvalentyn - pabloem - - y1chi exclusionList: [] - name: Java reviewers: diff --git a/.github/codecov.yml b/.github/codecov.yml index 0eaf91cdbdd6..c1c5dfb17bb4 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -64,6 +64,7 @@ ignore: - "**/*_test_py3*.py" - "**/*_microbenchmark.py" - "sdks/go/pkg/beam/register/register.go" + - "sdks/python/apache_beam/testing/benchmarks/nexmark/**" # See https://docs.codecov.com/docs/flags for options. flag_management: diff --git a/.github/workflows/build_playground_backend.yml b/.github/workflows/build_playground_backend.yml index 4aa0fd294931..c9e705d24c98 100644 --- a/.github/workflows/build_playground_backend.yml +++ b/.github/workflows/build_playground_backend.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Check out the repo uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v3.6.0 with: distribution: 'zulu' java-version: '8' diff --git a/.github/workflows/build_playground_frontend.yml b/.github/workflows/build_playground_frontend.yml index a27ce08d07ae..73e918f47005 100644 --- a/.github/workflows/build_playground_frontend.yml +++ b/.github/workflows/build_playground_frontend.yml @@ -43,7 +43,7 @@ jobs: steps: - name: Check out the repo uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v3.6.0 with: distribution: 'zulu' java-version: '8' diff --git a/.github/workflows/java_tests.yml b/.github/workflows/java_tests.yml index b8e64c3fdf46..1a587e7b4919 100644 --- a/.github/workflows/java_tests.yml +++ b/.github/workflows/java_tests.yml @@ -172,7 +172,7 @@ jobs: project_id: ${{ secrets.GCP_PROJECT_ID }} export_default_credentials: true - name: Set Java Version - uses: actions/setup-java@v3 + uses: actions/setup-java@v3.6.0 with: distribution: 'zulu' java-version: 8 diff --git a/.github/workflows/playground_deploy_examples.yml b/.github/workflows/playground_deploy_examples.yml index d75e9473126f..541da7907246 100644 --- a/.github/workflows/playground_deploy_examples.yml +++ b/.github/workflows/playground_deploy_examples.yml @@ -78,7 +78,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.8' - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v3.6.0 with: distribution: 'zulu' java-version: '8' diff --git a/.github/workflows/playground_examples_ci_reusable.yml b/.github/workflows/playground_examples_ci_reusable.yml index bde25c457404..43c72e7ec16c 100644 --- a/.github/workflows/playground_examples_ci_reusable.yml +++ b/.github/workflows/playground_examples_ci_reusable.yml @@ -100,7 +100,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.8' - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v3.6.0 with: distribution: 'zulu' java-version: '8' diff --git a/.github/workflows/run_rc_validation.yml b/.github/workflows/run_rc_validation.yml index 2407a0406168..e8d990912ffe 100644 --- a/.github/workflows/run_rc_validation.yml +++ b/.github/workflows/run_rc_validation.yml @@ -88,8 +88,7 @@ jobs: git config user.name $GITHUB_ACTOR git config user.email actions@"$RUNNER_NAME".local - name: Verify working branch name - run: - - sh ./ci_check_git_branch.sh $WORKING_BRANCH + run: ./scripts/ci/ci_check_git_branch.sh $WORKING_BRANCH - name: Create Pull Request run: | git checkout -b ${{env.WORKING_BRANCH}} ${{ env.RC_TAG }} --quiet @@ -121,7 +120,7 @@ jobs: - name: Setup Java JDK - uses: actions/setup-java@v3.5.1 + uses: actions/setup-java@v3.6.0 with: distribution: 'temurin' java-version: 11 @@ -188,7 +187,7 @@ jobs: uses: azure/setup-kubectl@v3 - name: Setup Java JDK - uses: actions/setup-java@v3.5.1 + uses: actions/setup-java@v3.6.0 with: distribution: 'temurin' java-version: 11 diff --git a/.github/workflows/tour_of_beam_backend_integration.yml b/.github/workflows/tour_of_beam_backend_integration.yml index 473088150840..47399e728dac 100644 --- a/.github/workflows/tour_of_beam_backend_integration.yml +++ b/.github/workflows/tour_of_beam_backend_integration.yml @@ -23,11 +23,15 @@ on: push: branches: ['master', 'release-*'] tags: 'v*' - paths: ['learning/tour-of-beam/backend/**'] + paths: + - 'learning/tour-of-beam/backend/**' + - 'playground/backend/**' pull_request: branches: ['master', 'release-*'] tags: 'v*' - paths: ['learning/tour-of-beam/backend/**'] + paths: + - 'learning/tour-of-beam/backend/**' + - 'playground/backend/**' # This allows a subsequently queued workflow run to interrupt previous runs concurrency: @@ -36,12 +40,23 @@ concurrency: env: TOB_LEARNING_ROOT: ./samples/learning-content - DATASTORE_PROJECT_ID: test-proj + # firebase + GOOGLE_CLOUD_PROJECT: demo-test-proj + FIREBASE_AUTH_EMULATOR_HOST: localhost:9099 + # datastore + DATASTORE_PROJECT_ID: demo-test-proj DATASTORE_EMULATOR_HOST: localhost:8081 DATASTORE_EMULATOR_DATADIR: ./datadir + # playground API + PLAYGROUND_ROUTER_HOST: localhost:8000 + + # GCF PORT_SDK_LIST: 8801 PORT_GET_CONTENT_TREE: 8802 PORT_GET_UNIT_CONTENT: 8803 + PORT_GET_USER_PROGRESS: 8804 + PORT_POST_UNIT_COMPLETE: 8805 + PORT_POST_USER_CODE: 8806 jobs: @@ -56,20 +71,12 @@ jobs: with: # pin to the biggest Go version supported by Cloud Functions runtime go-version: '1.16' - - # 1. Datastore emulator - - name: 'Set up Cloud SDK' - uses: 'google-github-actions/setup-gcloud@v0' - with: - version: 397.0.0 - project_id: ${{ env.DATASTORE_PROJECT_ID }} - install_components: 'beta,cloud-datastore-emulator' - - name: 'Start datastore emulator' - run: | - gcloud beta emulators datastore start \ - --data-dir=${{ env.DATASTORE_EMULATOR_DATADIR }} \ - --host-port=${{ env.DATASTORE_EMULATOR_HOST }} \ - --consistency=1 & + - name: Build Playground router image + run: ./gradlew playground:backend:containers:router:docker + working-directory: ${{ env.GITHUB_WORKSPACE }} + # 1. Start emulators + - name: Start emulators + run: docker-compose up -d # 2. start function-framework processes in BG - name: Compile CF @@ -80,14 +87,25 @@ jobs: run: PORT=${{ env.PORT_GET_CONTENT_TREE }} FUNCTION_TARGET=getContentTree ./tob_function & - name: Run getUnitContent in background run: PORT=${{ env.PORT_GET_UNIT_CONTENT }} FUNCTION_TARGET=getUnitContent ./tob_function & + - name: Run getUserProgress in background + run: PORT=${{ env.PORT_GET_USER_PROGRESS }} FUNCTION_TARGET=getUserProgress ./tob_function & + - name: Run postUnitComplete in background + run: PORT=${{ env.PORT_POST_UNIT_COMPLETE }} FUNCTION_TARGET=postUnitComplete ./tob_function & + - name: Run postUserCode in background + run: PORT=${{ env.PORT_POST_USER_CODE }} FUNCTION_TARGET=postUserCode ./tob_function & # 3. Load data in datastore: run CD step on samples/learning-content - name: Run CI/CD to populate datastore run: go run cmd/ci_cd/ci_cd.go - # 4. Check sdkList, getContentTree, getUnitContent: run integration tests + # 4. run integration tests - name: Go integration tests run: go test -v --tags integration ./integration_tests/... + + - name: Stop emulators + if: always() + run: docker-compose down + # 5. Compare storage/datastore/index.yml VS generated - name: Check index.yaml run: | diff --git a/.github/workflows/verify_release_build.yml b/.github/workflows/verify_release_build.yml index 5a128a4eec61..2ab76079e1d8 100644 --- a/.github/workflows/verify_release_build.yml +++ b/.github/workflows/verify_release_build.yml @@ -39,9 +39,8 @@ jobs: RELEASE_VER: ${{ github.event.inputs.RELEASE_VER }} steps: - name: Verify branch name - run: - - sh ./ci_check_git_branch.sh $WORKING_BRANCH - working-directory: 'scripts/ci' + run: ./scripts/ci/ci_check_git_branch.sh $WORKING_BRANCH + - name: Set RELEASE_BRANCH env variable run: | RELEASE_BRANCH=release-${{env.RELEASE_VER}} diff --git a/.test-infra/jenkins/Flink.groovy b/.test-infra/jenkins/Flink.groovy index 2aecf8ea8311..4aadf6943ed7 100644 --- a/.test-infra/jenkins/Flink.groovy +++ b/.test-infra/jenkins/Flink.groovy @@ -17,7 +17,7 @@ */ class Flink { - private static final String flinkDownloadUrl = 'https://archive.apache.org/dist/flink/flink-1.12.3/flink-1.12.3-bin-scala_2.11.tgz' + private static final String flinkDownloadUrl = 'https://archive.apache.org/dist/flink/flink-1.13.6/flink-1.13.6-bin-scala_2.12.tgz' private static final String hadoopDownloadUrl = 'https://repo.maven.apache.org/maven2/org/apache/flink/flink-shaded-hadoop-2-uber/2.8.3-10.0/flink-shaded-hadoop-2-uber-2.8.3-10.0.jar' private static final String FLINK_DIR = '"$WORKSPACE/src/.test-infra/dataproc"' private static final String FLINK_SCRIPT = 'flink_cluster.sh' @@ -75,7 +75,7 @@ class Flink { } /** - * Updates the number of worker nodes in a cluster. + * Updates the number of worker nodes in a cluster. * * @param workerCount - the new number of worker nodes in the cluster */ diff --git a/.test-infra/jenkins/README.md b/.test-infra/jenkins/README.md index c860435ea743..e53dae86c458 100644 --- a/.test-infra/jenkins/README.md +++ b/.test-infra/jenkins/README.md @@ -140,9 +140,9 @@ Beam Jenkins overview page: [link](https://ci-beam.apache.org/) | beam_PerformanceTests_AvroIOIT | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_AvroIOIT/), [hdfs_cron](https://ci-beam.apache.org/job/beam_PerformanceTests_AvroIOIT_HDFS/) | `Run Java AvroIO Performance Test` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_AvroIOIT/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_AvroIOIT) [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_AvroIOIT_HDFS/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_AvroIOIT_HDFS) | | beam_PerformanceTests_BiqQueryIO_Read_Python | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Read_Python/), [phrase](https://ci-beam.apache.org/view/PerformanceTests/job/beam_PerformanceTests_BiqQueryIO_Read_Python_PR/) | `Run BigQueryIO Read Performance Test Python` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Read_Python/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Read_Python) | | beam_PerformanceTests_BiqQueryIO_Write_Python_Batch | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch/), [phrase](https://ci-beam.apache.org/view/PerformanceTests/job/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch_PR/) | `Run BigQueryIO Write Performance Test Python Batch` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Write_Python_Batch) | -| beam_BiqQueryIO_Batch_Performance_Test_Java_Avro | [cron](https://ci-beam.apache.org/job/beam_BiqQueryIO_Batch_Performance_Test_Java_Avro/) | `Run BigQueryIO Batch Performance Test Java Avro` | [![Build Status](https://ci-beam.apache.org/job/beam_BiqQueryIO_Batch_Performance_Test_Java_Avro/badge/icon)](https://ci-beam.apache.org/job/beam_BiqQueryIO_Batch_Performance_Test_Java_Avro/) | -| beam_BiqQueryIO_Batch_Performance_Test_Java_Json | [cron](https://ci-beam.apache.org/job/beam_BiqQueryIO_Batch_Performance_Test_Java_Json/) | `Run BigQueryIO Batch Performance Test Java Json` | [![Build Status](https://ci-beam.apache.org/job/beam_BiqQueryIO_Batch_Performance_Test_Java_Json/badge/icon)](https://ci-beam.apache.org/job/beam_BiqQueryIO_Batch_Performance_Test_Java_Json/) | -| beam_BiqQueryIO_Streaming_Performance_Test_Java | [cron](https://ci-beam.apache.org/job/beam_BiqQueryIO_Streaming_Performance_Test_Java/) | `Run BigQueryIO Streaming Performance Test Java` | [![Build Status](https://ci-beam.apache.org/job/beam_BiqQueryIO_Streaming_Performance_Test_Java/badge/icon)](https://ci-beam.apache.org/job/beam_BiqQueryIO_Streaming_Performance_Test_Java/) | +| beam_PerformanceTests_BiqQueryIO_Batch_Java_Avro | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Batch_Java_Avro/) | `Run BigQueryIO Batch Performance Test Java Avro` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Batch_Java_Avro/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Batch_Java_Avro/) | +| beam_PerformanceTests_BiqQueryIO_Batch_Java_Json | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Batch_Java_Json/) | `Run BigQueryIO Batch Performance Test Java Json` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Batch_Java_Json/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Batch_Java_Json/) | +| beam_PerformanceTests_BiqQueryIO_Streaming_Java | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Streaming_Java/) | `Run BigQueryIO Streaming Performance Test Java` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Streaming_Java/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_BiqQueryIO_Streaming_Java/) | | beam_PerformanceTests_Cdap | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_Cdap/) | `Run Java CdapIO Performance Test` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_Cdap/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_Cdap) | | beam_PerformanceTests_Compressed_TextIOIT | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_Compressed_TextIOIT/), [hdfs_cron](https://ci-beam.apache.org/job/beam_PerformanceTests_Compressed_TextIOIT_HDFS/) | `Run Java CompressedTextIO Performance Test` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_Compressed_TextIOIT/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_Compressed_TextIOIT) [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_Compressed_TextIOIT_HDFS/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_Compressed_TextIOIT_HDFS) | | beam_PerformanceTests_HadoopFormat | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_HadoopFormat/) | `Run Java HadoopFormatIO Performance Test` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_HadoopFormat/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_HadoopFormat) | @@ -155,6 +155,7 @@ Beam Jenkins overview page: [link](https://ci-beam.apache.org/) | beam_PerformanceTests_PubsubIOIT_Python_Streaming | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_PubsubIOIT_Python_Streaming/), [phrase](https://ci-beam.apache.org/view/PerformanceTests/job/beam_PerformanceTests_PubsubIOIT_Python_Streaming_PR/) | `Run PubsubIO Performance Test Python` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_PubsubIOIT_Python_Streaming/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_PubsubIOIT_Python_Streaming) | | beam_PerformanceTests_SpannerIO_Read_2GB_Python | [cron](https://ci-beam.apache.org/view/PerformanceTests/job/beam_PerformanceTests_SpannerIO_Read_2GB_Python/), [phrase](https://ci-beam.apache.org/view/PerformanceTests/job/beam_PerformanceTests_SpannerIO_Read_2GB_Python_PR/) | `Run SpannerIO Read 2GB Performance Test Python Batch` | [![Build Status](https://ci-beam.apache.org/view/PerformanceTests/job/beam_PerformanceTests_SpannerIO_Read_2GB_Python/badge/icon)](https://ci-beam.apache.org/view/PerformanceTests/job/beam_PerformanceTests_SpannerIO_Read_2GB_Python/) | | beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch/), [phrase](https://ci-beam.apache.org/view/PerformanceTests/job/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch_PR/) | `Run SpannerIO Write 2GB Performance Test Python Batch` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_SpannerIO_Write_2GB_Python_Batch) | +| beam_PerformanceTests_SparkReceiverIOIT | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_SparkReceiverIOIT/) | `Run Java SparkReceiverIO Performance Test` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_SparkReceiverIO/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_SparkReceiverIO) | | beam_PerformanceTests_TFRecordIOIT | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_TFRecordIOIT/) | `Run Java TFRecordIO Performance Test` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_TFRecordIOIT/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_TFRecordIOIT) | | beam_PerformanceTests_TextIOIT | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_TextIOIT/), [hdfs_cron](https://ci-beam.apache.org/job/beam_PerformanceTests_TextIOIT_HDFS/) | `Run Java TextIO Performance Test` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_TextIOIT/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_TextIOIT) [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_TextIOIT_HDFS/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_TextIOIT_HDFS) | | beam_PerformanceTests_WordCountIT_Py37 | [cron](https://ci-beam.apache.org/job/beam_PerformanceTests_WordCountIT_Py37/) | `Run Python37 WordCountIT Performance Test` | [![Build Status](https://ci-beam.apache.org/job/beam_PerformanceTests_WordCountIT_Py37/badge/icon)](https://ci-beam.apache.org/job/beam_PerformanceTests_WordCountIT_Py37) | diff --git a/.test-infra/jenkins/job_LoadTests_Combine_Flink_Python.groovy b/.test-infra/jenkins/job_LoadTests_Combine_Flink_Python.groovy index 50863a0ddf1f..b88a3fafc2d4 100644 --- a/.test-infra/jenkins/job_LoadTests_Combine_Flink_Python.groovy +++ b/.test-infra/jenkins/job_LoadTests_Combine_Flink_Python.groovy @@ -132,7 +132,7 @@ def loadTestJob = { scope, triggeringContext, mode -> "${DOCKER_CONTAINER_REGISTRY}/${DOCKER_BEAM_SDK_IMAGE}" ], initialParallelism, - "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.12_job_server:latest") + "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.13_job_server:latest") // Execute all scenarios connected with initial parallelism. loadTestsBuilder.loadTests(scope, CommonTestProperties.SDK.PYTHON, initialScenarios, 'Combine', mode) diff --git a/.test-infra/jenkins/job_LoadTests_GBK_Flink_Python.groovy b/.test-infra/jenkins/job_LoadTests_GBK_Flink_Python.groovy index be395c829e49..ade6bc16a69b 100644 --- a/.test-infra/jenkins/job_LoadTests_GBK_Flink_Python.groovy +++ b/.test-infra/jenkins/job_LoadTests_GBK_Flink_Python.groovy @@ -146,7 +146,7 @@ def loadTest = { scope, triggeringContext -> "${DOCKER_CONTAINER_REGISTRY}/${DOCKER_BEAM_SDK_IMAGE}" ], numberOfWorkers, - "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.12_job_server:latest") + "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.13_job_server:latest") def configurations = testScenarios.findAll { it.pipelineOptions?.parallelism?.value == numberOfWorkers } loadTestsBuilder.loadTests(scope, sdk, configurations, "GBK", "batch") diff --git a/.test-infra/jenkins/job_LoadTests_ParDo_Flink_Python.groovy b/.test-infra/jenkins/job_LoadTests_ParDo_Flink_Python.groovy index 793e06109d45..d07964d0d448 100644 --- a/.test-infra/jenkins/job_LoadTests_ParDo_Flink_Python.groovy +++ b/.test-infra/jenkins/job_LoadTests_ParDo_Flink_Python.groovy @@ -320,7 +320,7 @@ def loadTestJob = { scope, triggeringContext, mode -> "${DOCKER_CONTAINER_REGISTRY}/${DOCKER_BEAM_SDK_IMAGE}" ], numberOfWorkers, - "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.12_job_server:latest") + "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.13_job_server:latest") loadTestsBuilder.loadTests(scope, CommonTestProperties.SDK.PYTHON, testScenarios, 'ParDo', mode) } diff --git a/.test-infra/jenkins/job_LoadTests_coGBK_Flink_Python.groovy b/.test-infra/jenkins/job_LoadTests_coGBK_Flink_Python.groovy index 3e7dbaa706aa..e1bb58cbdc85 100644 --- a/.test-infra/jenkins/job_LoadTests_coGBK_Flink_Python.groovy +++ b/.test-infra/jenkins/job_LoadTests_coGBK_Flink_Python.groovy @@ -137,7 +137,7 @@ def loadTest = { scope, triggeringContext -> "${DOCKER_CONTAINER_REGISTRY}/${DOCKER_BEAM_SDK_IMAGE}" ], numberOfWorkers, - "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.12_job_server:latest") + "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.13_job_server:latest") loadTestsBuilder.loadTests(scope, CommonTestProperties.SDK.PYTHON, testScenarios, 'CoGBK', 'batch') } diff --git a/.test-infra/jenkins/job_PerformanceTests_BigQueryIO_Java.groovy b/.test-infra/jenkins/job_PerformanceTests_BigQueryIO_Java.groovy index 1d8ce84ea12d..c3d0ae1f78cd 100644 --- a/.test-infra/jenkins/job_PerformanceTests_BigQueryIO_Java.groovy +++ b/.test-infra/jenkins/job_PerformanceTests_BigQueryIO_Java.groovy @@ -24,9 +24,9 @@ def now = new Date().format("MMddHHmmss", TimeZone.getTimeZone('UTC')) def jobConfigs = [ [ - title : 'BigQueryIO Streaming Performance Test Java 10 GB', + title : 'BigQueryIO Performance Test Streaming Java 10 GB', triggerPhrase: 'Run BigQueryIO Streaming Performance Test Java', - name : 'beam_BiqQueryIO_Streaming_Performance_Test_Java', + name : 'beam_PerformanceTests_BiqQueryIO_Streaming_Java', itClass : 'org.apache.beam.sdk.bigqueryioperftests.BigQueryIOIT', properties: [ project : 'apache-beam-testing', @@ -34,6 +34,7 @@ def jobConfigs = [ tempRoot : 'gs://temp-storage-for-perf-tests/loadtests', writeMethod : 'STREAMING_INSERTS', writeFormat : 'JSON', + pipelineTimeout : '1200', testBigQueryDataset : 'beam_performance', testBigQueryTable : 'bqio_write_10GB_java_stream_' + now, metricsBigQueryDataset: 'beam_performance', @@ -53,9 +54,9 @@ def jobConfigs = [ ] ], [ - title : 'BigQueryIO Batch Performance Test Java 10 GB JSON', + title : 'BigQueryIO Performance Test Batch Java 10 GB JSON', triggerPhrase: 'Run BigQueryIO Batch Performance Test Java Json', - name : 'beam_BiqQueryIO_Batch_Performance_Test_Java_Json', + name : 'beam_PerformanceTests_BiqQueryIO_Batch_Java_Json', itClass : 'org.apache.beam.sdk.bigqueryioperftests.BigQueryIOIT', properties: [ project : 'apache-beam-testing', @@ -82,9 +83,9 @@ def jobConfigs = [ ] ], [ - title : 'BigQueryIO Batch Performance Test Java 10 GB AVRO', + title : 'BigQueryIO Performance Test Batch Java 10 GB AVRO', triggerPhrase: 'Run BigQueryIO Batch Performance Test Java Avro', - name : 'beam_BiqQueryIO_Batch_Performance_Test_Java_Avro', + name : 'beam_PerformanceTests_BiqQueryIO_Batch_Java_Avro', itClass : 'org.apache.beam.sdk.bigqueryioperftests.BigQueryIOIT', properties: [ project : 'apache-beam-testing', diff --git a/.test-infra/jenkins/job_PerformanceTests_SparkReceiverIO_IT.groovy b/.test-infra/jenkins/job_PerformanceTests_SparkReceiverIO_IT.groovy new file mode 100644 index 000000000000..0bfb01b43ce7 --- /dev/null +++ b/.test-infra/jenkins/job_PerformanceTests_SparkReceiverIO_IT.groovy @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import CommonJobProperties as common +import Kubernetes +import InfluxDBCredentialsHelper + +String jobName = "beam_PerformanceTests_SparkReceiver_IO" + +/** + * This job runs the SparkReceiver IO performance tests. + It runs on a RabbitMQ cluster that is build by applying the folder .test-infra/kubernetes/rabbit, + in an existing kubernetes cluster (DEFAULT_CLUSTER in Kubernetes.groovy). + The services created to run this test are: + Pods: 1 RabbitMq pods. + Services: 1 broker + When the performance tests finish all resources are cleaned up by a postBuild step in Kubernetes.groovy + */ +job(jobName) { + common.setTopLevelMainJobProperties(delegate, 'master', 120) + common.setAutoJob(delegate, 'H H/6 * * *') + common.enablePhraseTriggeringFromPullRequest( + delegate, + 'Java SparkReceiverIO Performance Test', + 'Run Java SparkReceiverIO Performance Test') + InfluxDBCredentialsHelper.useCredentials(delegate) + + String namespace = common.getKubernetesNamespace(jobName) + String kubeconfig = common.getKubeconfigLocationForNamespace(namespace) + Kubernetes k8s = Kubernetes.create(delegate, kubeconfig, namespace) + + k8s.apply(common.makePathAbsolute("src/.test-infra/kubernetes/rabbit/rabbitmq.yaml")) + String rabbitMqHostName = "LOAD_BALANCER_IP" + k8s.loadBalancerIP("rabbitmq", rabbitMqHostName) + + Map pipelineOptions = [ + tempRoot : 'gs://temp-storage-for-perf-tests', + project : 'apache-beam-testing', + runner : 'DataflowRunner', + sourceOptions : """ + { + "numRecords": "600000", + "keySizeBytes": "1", + "valueSizeBytes": "90" + } + """.trim().replaceAll("\\s", ""), + bigQueryDataset : 'beam_performance', + bigQueryTable : 'sparkreceiverioit_results', + influxMeasurement : 'sparkreceiverioit_results', + influxDatabase : InfluxDBCredentialsHelper.InfluxDBDatabaseName, + influxHost : InfluxDBCredentialsHelper.InfluxDBHostUrl, + rabbitMqBootstrapServerAddress: "amqp://guest:guest@\$${rabbitMqHostName}:5672", + streamName : 'rabbitMqTestStream', + readTimeout : '900', + numWorkers : '5', + autoscalingAlgorithm : 'NONE' + ] + + steps { + gradle { + rootBuildScriptDir(common.checkoutDir) + common.setGradleSwitches(delegate) + switches("--info") + switches("-DintegrationTestPipelineOptions=\'${common.joinOptionsWithNestedJsonValues(pipelineOptions)}\'") + switches("-DintegrationTestRunner=dataflow") + tasks(":sdks:java:io:sparkreceiver:integrationTest --tests org.apache.beam.sdk.io.sparkreceiver.SparkReceiverIOIT") + } + } +} diff --git a/.test-infra/jenkins/job_PostCommit_Python_Chicago_Taxi_Example_Flink.groovy b/.test-infra/jenkins/job_PostCommit_Python_Chicago_Taxi_Example_Flink.groovy index 2874fc3bad3a..516bf028714c 100644 --- a/.test-infra/jenkins/job_PostCommit_Python_Chicago_Taxi_Example_Flink.groovy +++ b/.test-infra/jenkins/job_PostCommit_Python_Chicago_Taxi_Example_Flink.groovy @@ -38,7 +38,7 @@ def chicagoTaxiJob = { scope -> "${DOCKER_CONTAINER_REGISTRY}/${beamSdkDockerImage}" ], numberOfWorkers, - "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.12_job_server:latest") + "${DOCKER_CONTAINER_REGISTRY}/beam_flink1.13_job_server:latest") def pipelineOptions = [ parallelism : numberOfWorkers, diff --git a/.test-infra/kubernetes/rabbit/rabbitmq.yaml b/.test-infra/kubernetes/rabbit/rabbitmq.yaml new file mode 100644 index 000000000000..72bd41d5a92c --- /dev/null +++ b/.test-infra/kubernetes/rabbit/rabbitmq.yaml @@ -0,0 +1,187 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: rabbitmq +--- +kind: Service +apiVersion: v1 +metadata: + name: rabbitmq-internal + labels: + app: rabbitmq +spec: + clusterIP: None + ports: + - name: http + protocol: TCP + port: 15672 + - name: amqp + protocol: TCP + port: 5672 + selector: + app: rabbitmq +--- +kind: Service +apiVersion: v1 +metadata: + name: rabbitmq + labels: + app: rabbitmq + type: LoadBalancer +spec: + type: LoadBalancer + ports: + - name: http + protocol: TCP + port: 15672 + - name: amqp + protocol: TCP + port: 5672 + selector: + app: rabbitmq +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: rabbitmq-config +data: + enabled_plugins: | + [rabbitmq_management,rabbitmq_peer_discovery_k8s]. + + rabbitmq.conf: | + loopback_users = none + cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s + cluster_formation.k8s.host = kubernetes.default.svc.cluster.local + cluster_formation.k8s.port = 443 + ### cluster_formation.k8s.address_type = ip + cluster_formation.k8s.address_type = hostname + cluster_formation.node_cleanup.interval = 10 + cluster_formation.node_cleanup.only_log_warning = true + cluster_partition_handling = autoheal + queue_master_locator=min-masters + cluster_formation.randomized_startup_delay_range.min = 0 + cluster_formation.randomized_startup_delay_range.max = 2 + cluster_formation.k8s.service_name = rabbitmq-internal + cluster_formation.k8s.hostname_suffix = .rabbitmq-internal.our-namespace.svc.cluster.local +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: rabbitmq +spec: + selector: + matchLabels: + app: "rabbitmq" + serviceName: rabbitmq-internal + replicas: 1 + volumeClaimTemplates: + - metadata: + name: rabbitmq-data + namespace: rabbit-test + spec: + storageClassName: standard + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "3Gi" + template: + metadata: + labels: + app: rabbitmq + annotations: + scheduler.alpha.kubernetes.io/affinity: > + { + "podAntiAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": [{ + "labelSelector": { + "matchExpressions": [{ + "key": "app", + "operator": "In", + "values": ["rabbitmq"] + }] + }, + "topologyKey": "kubernetes.io/hostname" + }] + } + } + spec: + serviceAccountName: rabbitmq + terminationGracePeriodSeconds: 10 + containers: + - name: rabbitmq-k8s + image: rabbitmq:3.7 + volumeMounts: + - name: config-volume + mountPath: /etc/rabbitmq + - name: rabbitmq-data + mountPath: /var/lib/rabbitmq/mnesia + ports: + - name: http + protocol: TCP + containerPort: 15672 + - name: amqp + protocol: TCP + containerPort: 5672 + livenessProbe: + exec: + command: ["rabbitmqctl", "status"] + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 10 + readinessProbe: + exec: + command: ["rabbitmqctl", "status"] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 10 + imagePullPolicy: Always + env: + - name: MY_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: HOSTNAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: RABBITMQ_USE_LONGNAME + value: "true" + - name: RABBITMQ_NODENAME + value: "rabbit@$(HOSTNAME).rabbitmq-internal.$(NAMESPACE).svc.cluster.local" + - name: K8S_SERVICE_NAME + value: "rabbitmq-internal" + - name: RABBITMQ_ERLANG_COOKIE + value: "cookie" + volumes: + - name: config-volume + configMap: + name: rabbitmq-config + items: + - key: rabbitmq.conf + path: rabbitmq.conf + - key: enabled_plugins + path: enabled_plugins + - name: rabbitmq-data + persistentVolumeClaim: + claimName: rabbitmq-data diff --git a/.test-infra/metrics/grafana/dashboards/perftests_metrics/Java_IO_IT_Tests_Dataflow.json b/.test-infra/metrics/grafana/dashboards/perftests_metrics/Java_IO_IT_Tests_Dataflow.json index 1c6cde8bbb79..54eba316631f 100644 --- a/.test-infra/metrics/grafana/dashboards/perftests_metrics/Java_IO_IT_Tests_Dataflow.json +++ b/.test-infra/metrics/grafana/dashboards/perftests_metrics/Java_IO_IT_Tests_Dataflow.json @@ -103,7 +103,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "TextIOIT | 1 GB | GCS", + "title": "TextIOIT | GCS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -225,7 +225,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "TextIOIT | 1 GB | HDFS", + "title": "TextIOIT | HDFS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -347,7 +347,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "TextIOIT GZIP | 1 GB | GCS", + "title": "TextIOIT | GZIP GCS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -469,7 +469,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "TextIOIT GZIP | 1 GB | HDFS", + "title": "TextIOIT | GZIP HDFS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -591,7 +591,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "TextIOIT | 1 GB | GCP | \"Many files\"", + "title": "TextIOIT | \"Many files\" GCP | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -713,7 +713,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "TextIOIT | 1 GB | HDFS | \"Many files\"", + "title": "TextIOIT | \"Many files\" HDFS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -837,7 +837,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "TextIOIT | 1 GB | GCS | \"Many files\" | GCS Rename", + "title": "TextIOIT | \"Many files\" GCS Rename | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -855,7 +855,7 @@ "yaxes": [ { "$$hashKey": "object:403", - "format": "s", + "format": "none", "label": null, "logBase": 1, "max": null, @@ -959,7 +959,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "AvroIOIT | 1 GB | GCS", + "title": "AvroIOIT | GCS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -1081,7 +1081,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "AvroIOIT | 1 GB | HDFS", + "title": "AvroIOIT | HDFS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -1203,7 +1203,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "XmlIOIT | 1 GB | GCS", + "title": "XmlIOIT | GCS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -1325,7 +1325,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "XmlIOIT | 1 GB | HDFS", + "title": "XmlIOIT | HDFS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -1372,7 +1372,6 @@ "dashLength": 10, "dashes": false, "datasource": "BeamInfluxDB", - "description": "TODO: https://issues.apache.org/jira/browse/BEAM-7115", "fill": 1, "fillGradient": 0, "gridPos": { @@ -1448,7 +1447,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "TFRecordIO | 1 GB | GCS", + "title": "TFRecordIO | GCS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -1571,7 +1570,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "ParquetIO | 1 GB | GCS", + "title": "ParquetIO | GCS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -1694,7 +1693,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "ParquetIO | 1 GB | HDFS", + "title": "ParquetIO | HDFS | 1 GB", "tooltip": { "shared": true, "sort": 0, @@ -1817,7 +1816,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "MongoDBIO", + "title": "MongoDBIO | 10M records", "tooltip": { "shared": true, "sort": 0, @@ -1940,7 +1939,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "JdbcIO", + "title": "JdbcIO | 5M records", "tooltip": { "shared": true, "sort": 0, @@ -2063,7 +2062,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "HadoopFormatIO", + "title": "HadoopFormatIO | 600k records", "tooltip": { "shared": true, "sort": 0, @@ -2186,7 +2185,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "KafkaIO | 1GB", + "title": "KafkaIO | 100M records, 10 GB", "tooltip": { "shared": true, "sort": 0, @@ -2309,7 +2308,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "BigQueryIO | batch | JSON", + "title": "BigQueryIO batch JSON | 10M records, 10 GB", "tooltip": { "shared": true, "sort": 0, @@ -2432,7 +2431,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "BigQueryIO | streaming | JSON", + "title": "BigQueryIO | streaming JSON | 10M records, 10 GB", "tooltip": { "shared": true, "sort": 0, @@ -2484,7 +2483,7 @@ "fillGradient": 0, "gridPos": { "h": 9, - "w": 24, + "w": 12, "x": 0, "y": 97 }, @@ -2555,7 +2554,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "BigQueryIO | batch | Avro", + "title": "BigQueryIO | batch Avro | 10M records, 10 GB", "tooltip": { "shared": true, "sort": 0, @@ -2608,8 +2607,8 @@ "gridPos": { "h": 9, "w": 12, - "x": 0, - "y": 106 + "x": 12, + "y": 97 }, "hiddenSeries": false, "id": 26, @@ -2678,7 +2677,130 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "CdapIO", + "title": "CdapIO | 600k records", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "transparent": true, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:403", + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:404", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "cacheTimeout": null, + "dashLength": 10, + "dashes": false, + "datasource": "BeamInfluxDB", + "description": "", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 106 + }, + "hiddenSeries": false, + "id": 27, + "interval": "6h", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "6.7.2", + "pointradius": 2, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "$tag_metric", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + } + ], + "measurement": "", + "orderByTime": "ASC", + "policy": "default", + "query": "SELECT mean(\"value\") FROM \"sparkreceiverioit_results\" WHERE \"metric\" =~ /time/ AND $timeFilter GROUP BY time($__interval), \"metric\"", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "SparkReceiverIO | 600k Records", "tooltip": { "shared": true, "sort": 0, @@ -2746,6 +2868,7 @@ }, "timezone": "", "title": "Java IO IT Tests | Dataflow", + "description": "Shows performance test metrics on Dataflow of Beam Java SDK.\nTests are named after 'IO Connector | Specifications | data size'.", "uid": "bnlHKP3Wz", "variables": { "list": [] diff --git a/.test-infra/metrics/grafana/dashboards/perftests_metrics/Python_IO_IT_Tests_Dataflow.json b/.test-infra/metrics/grafana/dashboards/perftests_metrics/Python_IO_IT_Tests_Dataflow.json index 570dc82e3d4b..5b1ff2b8103b 100644 --- a/.test-infra/metrics/grafana/dashboards/perftests_metrics/Python_IO_IT_Tests_Dataflow.json +++ b/.test-infra/metrics/grafana/dashboards/perftests_metrics/Python_IO_IT_Tests_Dataflow.json @@ -94,96 +94,7 @@ ] ], "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Reading 10GB of data | BigQuery native Dataflow IO", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "transparent": true, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:403", - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true }, - { - "$$hashKey": "object:404", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "cacheTimeout": null, - "dashLength": 10, - "dashes": false, - "datasource": "BeamInfluxDB", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 0 - }, - "hiddenSeries": false, - "id": 3, - "interval": "24h", - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": false, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pluginVersion": "6.7.2", - "pointradius": 2, - "points": true, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ { "alias": "write_time", "groupBy": [ @@ -222,7 +133,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Writing 10GB of data | BigQuery native Dataflow IO", + "title": "BigQueryIO | Batch | 10 GB", "tooltip": { "shared": true, "sort": 0, @@ -274,8 +185,8 @@ "gridPos": { "h": 9, "w": 12, - "x": 0, - "y": 9 + "x": 12, + "y": 0 }, "hiddenSeries": false, "id": 4, @@ -338,96 +249,7 @@ ] ], "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Reading 2GB of data | Pubsub native Dataflow IO | streaming", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "transparent": true, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:403", - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true }, - { - "$$hashKey": "object:404", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "cacheTimeout": null, - "dashLength": 10, - "dashes": false, - "datasource": "BeamInfluxDB", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 9 - }, - "hiddenSeries": false, - "id": 5, - "interval": "24h", - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": false, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pluginVersion": "6.7.2", - "pointradius": 2, - "points": true, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ { "alias": "write_time", "groupBy": [ @@ -466,7 +288,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Writing 2GB of data | Pubsub native Dataflow IO | streaming", + "title": "PubsubIO | Streaming | 2 GB", "tooltip": { "shared": true, "sort": 0, @@ -519,7 +341,7 @@ "h": 9, "w": 12, "x": 0, - "y": 18 + "y": 9 }, "hiddenSeries": false, "id": 6, @@ -582,96 +404,7 @@ ] ], "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Reading 2GB of data | Spanner native Dataflow IO", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "transparent": true, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:403", - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true }, - { - "$$hashKey": "object:404", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "cacheTimeout": null, - "dashLength": 10, - "dashes": false, - "datasource": "BeamInfluxDB", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 18 - }, - "hiddenSeries": false, - "id": 7, - "interval": "24h", - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": false, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pluginVersion": "6.7.2", - "pointradius": 2, - "points": true, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ { "alias": "write_time", "groupBy": [ @@ -710,7 +443,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Writing 2GB of data | Spanner native Dataflow IO", + "title": "SpannerIO | native | 2 GB", "tooltip": { "shared": true, "sort": 0, @@ -777,6 +510,7 @@ }, "timezone": "", "title": "Python IO IT Tests | Dataflow", + "description": "Shows performance test metrics on Dataflow of Beam Python SDK.\nTests are named after 'IO Connector | Specifications | data size'.", "uid": "gP7vMPqZz", "variables": { "list": [] diff --git a/.test-infra/metrics/sync/github/sync.py b/.test-infra/metrics/sync/github/sync.py index d48fd7e43f29..543b19476c10 100644 --- a/.test-infra/metrics/sync/github/sync.py +++ b/.test-infra/metrics/sync/github/sync.py @@ -207,11 +207,16 @@ def fetchGHData(timestamp, ghQuery): query = ghQuery.replace('', tsString) return executeGHGraphqlQuery(query) +def extractUserLogin(user): + # user could be missing + if not user: + return "Unknown" + return user.get("login", "Unknown") def extractRequestedReviewers(pr): reviewEdges = pr["reviewRequests"]["edges"] return list( - map(lambda x: x["node"]["requestedReviewer"]["login"], reviewEdges)) + map(lambda x: extractUserLogin(x["node"]["requestedReviewer"]), reviewEdges)) def extractMentions(pr): @@ -238,24 +243,24 @@ def extractFirstNAActivity(pr): Returns timestamp and login of author on first activity on pull request done by non-author. ''' - author = pr["author"]["login"] + author = extractUserLogin(pr["author"]) commentEdges = None commentEdges = [ edge for edge in pr["comments"]["edges"] - if edge["node"]["author"]["login"] != author + if extractUserLogin(edge["node"]["author"]) != author ] reviewEdges = [ edge for edge in pr["reviews"]["edges"] - if edge["node"]["author"]["login"] != author + if extractUserLogin(edge["node"]["author"]) != author ] merged = pr["merged"] mergedAt = pr["mergedAt"] - mergedBy = None if not merged else pr["mergedBy"]["login"] + mergedBy = None if not merged else extractUserLogin(pr["mergedBy"]) commentTimestamps = list( - map(lambda x: (x["node"]["createdAt"], x["node"]["author"]["login"]), + map(lambda x: (x["node"]["createdAt"], extractUserLogin(x["node"]["author"])), commentEdges)) reviewTimestamps = list( - map(lambda x: (x["node"]["createdAt"], x["node"]["author"]["login"]), + map(lambda x: (x["node"]["createdAt"], extractUserLogin(x["node"]["author"])), reviewEdges)) allTimestamps = commentTimestamps + reviewTimestamps if merged: @@ -266,18 +271,18 @@ def extractFirstNAActivity(pr): def extractBeamReviewers(pr): '''Extract logins of users defined by Beam as reviewers.''' - author = pr['author']['login'] + author = extractUserLogin(pr['author']) # All the direct GitHub indicators of reviewers reviewers = [] for r in pr['assignees']['edges']: - reviewers.append(r['node']['login']) + reviewers.append(extractUserLogin(r['node'])) for r in pr['reviewRequests']['edges']: - reviewers.append(r['node']['requestedReviewer']['login']) + reviewers.append(extractUserLogin(r['node']['requestedReviewer'])) # GitHub users that have performed reviews. for r in pr['reviews']['edges']: - reviewers.append(r['node']['author']['login']) + reviewers.append(extractUserLogin(r['node']['author'])) # @r1, @r2 ... look/PTAL/ptal? beam_reviewer_regex = r'(@\w+).*?(?:PTAL|ptal|look)' @@ -303,7 +308,7 @@ def extractBeamReviewers(pr): def extractReviewers(pr): '''Extracts reviewers logins from PR.''' - return [edge["node"]["author"]["login"] for edge in pr["reviews"]["edges"]] + return [extractUserLogin(edge["node"]["author"]) for edge in pr["reviews"]["edges"]] def extractRowValuesFromPr(pr): @@ -318,7 +323,7 @@ def extractRowValuesFromPr(pr): reviewedBy = extractReviewers(pr) result = [ - pr["number"], pr["author"]["login"], pr["createdAt"], pr["updatedAt"], + pr["number"], extractUserLogin(pr["author"]), pr["createdAt"], pr["updatedAt"], pr["closedAt"], pr["merged"], firstNAActivity, firstNAAAuthor, requestedReviewers, mentions, beamReviewers, reviewedBy ] @@ -333,13 +338,13 @@ def extractRowValuesFromIssue(issue): ''' assignees = [] for a in issue['assignees']['edges']: - assignees.append(a['node']['login']) + assignees.append(extractUserLogin(a['node'])) labels = [] for l in issue['labels']['edges']: labels.append(l['node']['name']) result = [ - issue["number"], issue["author"]["login"], issue["createdAt"], issue["updatedAt"], + issue["number"], extractUserLogin(issue["author"]), issue["createdAt"], issue["updatedAt"], issue["closedAt"], issue["title"], assignees, labels ] diff --git a/CHANGES.md b/CHANGES.md index 1df3cb35b2bf..724a57e59aab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,18 +43,51 @@ ## Bugfixes -* Fixed JmsIO acknowledgment issue (https://github.com/apache/beam/issues/20814) * Fixed X (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). ## Known Issues * ([#X](https://github.com/apache/beam/issues/X)). --> +# [2.44.0] - Unreleased + +## Highlights + +* New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). +* New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). + +## I/Os + +* Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). + +## New Features / Improvements + +* Local packages can now be used as dependencies in the requirements.txt file, rather + than requiring them to be passed separately via the `--extra_package` option. + ([#23684](https://github.com/apache/beam/pull/23684)) + +## Breaking Changes + +* `ParquetIO.withSplit` was removed since splittable reading has been the default behavior since 2.35.0. The effect of + this change is to drop support for non-splittable reading ([#23832](https://github.com/apache/beam/issues/23832)). + +## Deprecations + +* X behavior is deprecated and will be removed in X versions ([#X](https://github.com/apache/beam/issues/X)). + +## Bugfixes + +* Fixed X (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). +* Fixed JmsIO acknowledgment issue (https://github.com/apache/beam/issues/20814) +* Fixed Beam SQL CalciteUtils (Java) and Cross-language JdbcIO (Python) did not support JDBC CHAR/VARCHAR, BINARY/VARBINARY logical types ([#23747](https://github.com/apache/beam/issues/23747), [#23526](https://github.com/apache/beam/issues/23526)). +* Ensure iterated and emitted types are used with the generic register package are registered with the type and schema registries.(Go) ([#23889](https://github.com/apache/beam/pull/23889)) + # [2.43.0] - Unreleased ## Highlights * Python 3.10 support in Apache Beam ([#21458](https://github.com/apache/beam/issues/21458)). +* An initial implementation of a runner that allows us to run Beam pipelines on Dask. Try it out and give us feedback! (Python) ([#18962](https://github.com/apache/beam/issues/18962)). ## I/Os @@ -72,10 +105,13 @@ * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * Dataframe wrapper added in Go SDK via Cross-Language (with automatic expansion service). (Go) ([#23384](https://github.com/apache/beam/issues/23384)). * Name all Java threads to aid in debugging ([#23049](https://github.com/apache/beam/issues/23049)). +* An initial implementation of a runner that allows us to run Beam pipelines on Dask. (Python) ([#18962](https://github.com/apache/beam/issues/18962)). ## Breaking Changes * Python SDK CoGroupByKey outputs an iterable allowing for arbitrarily large results. [#21556](https://github.com/apache/beam/issues/21556) Beam users may see an error on transforms downstream from CoGroupByKey. Users must change methods expecting a List to expect an Iterable going forward. See [document](https://docs.google.com/document/d/1RIzm8-g-0CyVsPb6yasjwokJQFoKHG4NjRUcKHKINu0) for information and fixes. +* The PortableRunner for Spark assumes Spark 3 as default Spark major version unless configured otherwise using `--spark_version`. + Spark 2 support is deprecated and will be removed soon ([#23728](https://github.com/apache/beam/issues/23728)). ## Deprecations diff --git a/build.gradle.kts b/build.gradle.kts index 72d2a8e92584..38d2971303b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -117,7 +117,11 @@ tasks.rat { // Tour Of Beam backend autogenerated Datastore indexes "learning/tour-of-beam/backend/internal/storage/index.yaml", - + + // Tour Of Beam backend autogenerated Playground GRPC API stubs and mocks + "learning/tour-of-beam/backend/playground_api/api.pb.go", + "learning/tour-of-beam/backend/playground_api/api_grpc.pb.go", + "learning/tour-of-beam/backend/playground_api/mock.go", // test p8 file for SnowflakeIO "sdks/java/io/snowflake/src/test/resources/invalid_test_rsa_key.p8", diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy index 1f1fe4589ffc..6aa2e4859c59 100644 --- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy +++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy @@ -546,6 +546,7 @@ class BeamModulePlugin implements Plugin { aws_java_sdk2_http_client_spi : "software.amazon.awssdk:http-client-spi:$aws_java_sdk2_version", aws_java_sdk2_regions : "software.amazon.awssdk:regions:$aws_java_sdk2_version", aws_java_sdk2_utils : "software.amazon.awssdk:utils:$aws_java_sdk2_version", + aws_java_sdk2_profiles : "software.amazon.awssdk:profiles:$aws_java_sdk2_version", bigdataoss_gcsio : "com.google.cloud.bigdataoss:gcsio:$google_cloud_bigdataoss_version", bigdataoss_util : "com.google.cloud.bigdataoss:util:$google_cloud_bigdataoss_version", byte_buddy : "net.bytebuddy:byte-buddy:1.12.14", @@ -603,7 +604,7 @@ class BeamModulePlugin implements Plugin { google_cloud_pubsub : "com.google.cloud:google-cloud-pubsub", // google_cloud_platform_libraries_bom sets version google_cloud_pubsublite : "com.google.cloud:google-cloud-pubsublite", // google_cloud_platform_libraries_bom sets version // The GCP Libraries BOM dashboard shows the versions set by the BOM: - // https://storage.googleapis.com/cloud-opensource-java-dashboard/com.google.cloud/libraries-bom/25.2.0/artifact_details.html + // https://storage.googleapis.com/cloud-opensource-java-dashboard/com.google.cloud/libraries-bom/26.1.3/artifact_details.html // Update libraries-bom version on sdks/java/container/license_scripts/dep_urls_java.yaml google_cloud_platform_libraries_bom : "com.google.cloud:libraries-bom:26.1.3", google_cloud_spanner : "com.google.cloud:google-cloud-spanner", // google_cloud_platform_libraries_bom sets version @@ -725,6 +726,7 @@ class BeamModulePlugin implements Plugin { testcontainers_postgresql : "org.testcontainers:postgresql:$testcontainers_version", testcontainers_mysql : "org.testcontainers:mysql:$testcontainers_version", testcontainers_gcloud : "org.testcontainers:gcloud:$testcontainers_version", + testcontainers_rabbitmq : "org.testcontainers:rabbitmq:$testcontainers_version", vendored_grpc_1_48_1 : "org.apache.beam:beam-vendor-grpc-1_48_1:0.1", vendored_guava_26_0_jre : "org.apache.beam:beam-vendor-guava-26_0-jre:0.1", vendored_calcite_1_28_0 : "org.apache.beam:beam-vendor-calcite-1_28_0:0.2", @@ -913,6 +915,18 @@ class BeamModulePlugin implements Plugin { project.tasks.withType(JavaCompile).configureEach { options.encoding = "UTF-8" + // Use --release 8 when targeting Java 8 and running on JDK > 8 + // + // Consider migrating compilation and testing to use JDK 9+ and setting '--release=8' as + // the default allowing 'applyJavaNature' to override it for the few modules that need JDK 9+ + // artifacts. See https://stackoverflow.com/a/43103038/4368200 for additional details. + if (JavaVersion.VERSION_1_8.compareTo(JavaVersion.toVersion(project.javaVersion)) == 0 + && JavaVersion.VERSION_1_8.compareTo(JavaVersion.current()) < 0) { + options.compilerArgs += ['--release', '8'] + // TODO(https://github.com/apache/beam/issues/23901): Fix + // optimizerOuterThis breakage + options.compilerArgs += ['-XDoptimizeOuterThis=false'] + } // As we want to add '-Xlint:-deprecation' we intentionally remove '-Xlint:deprecation' from compilerArgs here, // as intellij is adding this, see https://youtrack.jetbrains.com/issue/IDEA-196615 options.compilerArgs -= [ @@ -981,10 +995,10 @@ class BeamModulePlugin implements Plugin { 'org.checkerframework.checker.nullness.NullnessChecker' ] - if (parseBooleanProperty(project, 'enableCheckerFramework') || project.jenkins.isCIBuild) { - skipCheckerFramework = false - } else { + if (!parseBooleanProperty(project, 'enableCheckerFramework') && !project.jenkins.isCIBuild) { skipCheckerFramework = true + } else { + skipCheckerFramework = false } // Always exclude checkerframework on tests. It's slow, and it often @@ -1919,9 +1933,7 @@ class BeamModulePlugin implements Plugin { } if (runner?.equalsIgnoreCase('spark')) { - testRuntimeOnly it.project(path: ":runners:spark:2", configuration: "testRuntimeMigration") - testRuntimeOnly project.library.java.spark_core - testRuntimeOnly project.library.java.spark_streaming + testRuntimeOnly it.project(path: ":runners:spark:3", configuration: "testRuntimeMigration") // Testing the Spark runner causes a StackOverflowError if slf4j-jdk14 is on the classpath project.configurations.testRuntimeClasspath { @@ -2679,7 +2691,7 @@ class BeamModulePlugin implements Plugin { dependsOn = [installGcpTest] mustRunAfter = [ ":runners:flink:${project.ext.latestFlinkVersion}:job-server:shadowJar", - ':runners:spark:2:job-server:shadowJar', + ':runners:spark:3:job-server:shadowJar', ':sdks:python:container:py37:docker', ':sdks:python:container:py38:docker', ':sdks:python:container:py39:docker', @@ -2695,7 +2707,7 @@ class BeamModulePlugin implements Plugin { "--parallelism=2", "--sdk_worker_parallelism=1", "--flink_job_server_jar=${project.project(flinkJobServerProject).shadowJar.archivePath}", - "--spark_job_server_jar=${project.project(':runners:spark:2:job-server').shadowJar.archivePath}", + "--spark_job_server_jar=${project.project(':runners:spark:3:job-server').shadowJar.archivePath}", ] if (isStreaming) options += [ diff --git a/examples/java/build.gradle b/examples/java/build.gradle index 13b2518bf382..aa51dcfeae85 100644 --- a/examples/java/build.gradle +++ b/examples/java/build.gradle @@ -109,13 +109,8 @@ dependencies { } directRunnerPreCommit project(path: ":runners:direct-java", configuration: "shadow") flinkRunnerPreCommit project(":runners:flink:${project.ext.latestFlinkVersion}") - // TODO: Make the netty version used configurable, we add netty-all 4.1.17.Final so it appears on the classpath - // before 4.1.8.Final defined by Apache Beam - sparkRunnerPreCommit "io.netty:netty-all:4.1.17.Final" - sparkRunnerPreCommit project(":runners:spark:2") + sparkRunnerPreCommit project(":runners:spark:3") sparkRunnerPreCommit project(":sdks:java:io:hadoop-file-system") - sparkRunnerPreCommit library.java.spark_streaming - sparkRunnerPreCommit library.java.spark_core } /* diff --git a/examples/multi-language/src/main/java/org/apache/beam/examples/multilanguage/PythonDataframeWordCount.java b/examples/java/src/main/java/org/apache/beam/examples/multilanguage/PythonDataframeWordCount.java similarity index 100% rename from examples/multi-language/src/main/java/org/apache/beam/examples/multilanguage/PythonDataframeWordCount.java rename to examples/java/src/main/java/org/apache/beam/examples/multilanguage/PythonDataframeWordCount.java diff --git a/examples/kotlin/build.gradle b/examples/kotlin/build.gradle index 0aa3dc257b09..79a1248712d0 100644 --- a/examples/kotlin/build.gradle +++ b/examples/kotlin/build.gradle @@ -81,13 +81,8 @@ dependencies { } directRunnerPreCommit project(path: ":runners:direct-java", configuration: "shadow") flinkRunnerPreCommit project(":runners:flink:${project.ext.latestFlinkVersion}") - // TODO: Make the netty version used configurable, we add netty-all 4.1.17.Final so it appears on the classpath - // before 4.1.8.Final defined by Apache Beam - sparkRunnerPreCommit "io.netty:netty-all:4.1.17.Final" - sparkRunnerPreCommit project(":runners:spark:2") + sparkRunnerPreCommit project(":runners:spark:3") sparkRunnerPreCommit project(":sdks:java:io:hadoop-file-system") - sparkRunnerPreCommit library.java.spark_streaming - sparkRunnerPreCommit library.java.spark_core } /* diff --git a/examples/multi-language/README.md b/examples/multi-language/README.md index 127ab8c30eb2..072052b4cd56 100644 --- a/examples/multi-language/README.md +++ b/examples/multi-language/README.md @@ -126,9 +126,25 @@ gsutil cat gs://$GCP_BUCKET/multi-language-beam/output* #### Instructions for running the Java pipeline at HEAD (Beam 2.41.0 and 2.42.0). +* Activate a new virtual environment following +[these instructions](https://beam.apache.org/get-started/quickstart-py/#create-and-activate-a-virtual-environment). + +* 2. Install Apache Beam package with gcp support and the `sklearn` package. + +``` +pip install apache-beam[gcp] +pip install sklearn +``` + +* Startup the expansion service + +``` +python -m apache_beam.runners.portability.expansion_service_main -p --fully_qualified_name_glob "*" +``` + * Make sure that Docker is installed and available on your system. -* Build and push Python and Java Docker containers. +* In a different shell, build and push Python and Java Docker containers. ``` export DOCKER_ROOT= @@ -137,7 +153,7 @@ export DOCKER_ROOT= docker push $DOCKER_ROOT/beam_python3.8_sdk:latest -./gradlew :sdks:java:container:java11:docker -Pdocker-repository-root=$DOCKER_ROOT -Pdocker-tag=latest +./gradlew :sdks:java:container:java11:docker -Pdocker-repository-root=$DOCKER_ROOT -Pdocker-tag=latest -Pjava11Home=$JAVA_HOME docker push $DOCKER_ROOT/beam_java11_sdk:latest ``` @@ -149,6 +165,10 @@ Note that we override both the Java and Python SDK harness containers here. export GCP_PROJECT= export GCP_BUCKET= export GCP_REGION= +export EXPANSION_SERVICE_PORT= + +# This removes any existing output. +gsutil rm gs://$GCP_BUCKET/multi-language-beam/output* ./gradlew :examples:multi-language:sklearnMinstClassification --args=" \ --runner=DataflowRunner \ @@ -157,6 +177,7 @@ export GCP_REGION= --output=gs://$GCP_BUCKET/multi-language-beam/output \ --sdkContainerImage=$DOCKER_ROOT/beam_java11_sdk:latest \ --sdkHarnessContainerImageOverrides=.*python.*,$DOCKER_ROOT/beam_python3.8_sdk:latest \ +--expansionService=localhost:$EXPANSION_SERVICE_PORT \ --region=${GCP_REGION}" ``` @@ -166,3 +187,9 @@ of the digit. The second item is the predicted label of the digit. ``` gsutil cat gs://$GCP_BUCKET/multi-language-beam/output* ``` + +### Python Dataframe Wordcount + +This example is covered in the [Java multi-language pipelines quickstart](https://beam.apache.org/documentation/sdks/java-multi-language-pipelines/). +The pipeline source code is available at +[PythonDataframeWordCount.java](https://github.com/apache/beam/tree/master/examples/java/src/main/java/org/apache/beam/examples/multilanguage/PythonDataframeWordCount.java). diff --git a/examples/multi-language/build.gradle b/examples/multi-language/build.gradle index 61fdb686f4eb..b266faeb8f17 100644 --- a/examples/multi-language/build.gradle +++ b/examples/multi-language/build.gradle @@ -40,7 +40,6 @@ dependencies { runtimeOnly project(path: ":runners:portability:java") implementation library.java.vendored_guava_26_0_jre implementation project(":sdks:java:expansion-service") - implementation project(":sdks:java:extensions:python") permitUnusedDeclared project(":sdks:java:expansion-service") // BEAM-11761 } diff --git a/examples/notebooks/beam-ml/custom_remote_inference.ipynb b/examples/notebooks/beam-ml/custom_remote_inference.ipynb new file mode 100644 index 000000000000..713c65599656 --- /dev/null +++ b/examples/notebooks/beam-ml/custom_remote_inference.ipynb @@ -0,0 +1,625 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "paYiulysGrwR" + }, + "outputs": [], + "source": [ + "# @title ###### Licensed to the Apache Software Foundation (ASF), Version 2.0 (the \"License\")\n", + "\n", + "# Licensed to the Apache Software Foundation (ASF) under one\n", + "# or more contributor license agreements. See the NOTICE file\n", + "# distributed with this work for additional information\n", + "# regarding copyright ownership. The ASF licenses this file\n", + "# to you under the Apache License, Version 2.0 (the\n", + "# \"License\"); you may not use this file except in compliance\n", + "# with the License. You may obtain a copy of the License at\n", + "#\n", + "# http://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing,\n", + "# software distributed under the License is distributed on an\n", + "# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n", + "# KIND, either express or implied. See the License for the\n", + "# specific language governing permissions and limitations\n", + "# under the License" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0UGzzndTBPWQ" + }, + "source": [ + "# Remote inference in Beam\n", + "\n", + "The prefered way of running inference in Beam is by using the [RunInference API](https://beam.apache.org/documentation/sdks/python-machine-learning/). The RunInference API enables you to run your models as part of your pipeline in a way that is optimized for machine learning inference. It supports features such as batching, so that you do not need to take care of it yourself. For more info on the RunInference API you can check out the [RunInference notebook](https://github.com/apache/beam/blob/master/examples/notebooks/beam-ml/run_inference_pytorch_tensorflow_sklearn.ipynb), which demonstrates how you can implement model inference in pytorch, scikit-learn and tensorflow.\n", + "\n", + "As of now, RunInference API doesn't support making remote inference calls (e.g. Natural Language API, Cloud Vision API and others). Therefore, in order to use these remote APIs with Beam, one needs to write custom inference call. \n", + "\n", + "This notebook shows how you can implement such a custom inference call in Beam. We are using Cloud Vision API for demonstration. \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GNbarEZsalS1" + }, + "source": [ + "## Use case: run Cloud Vision API\n", + "\n", + "The Cloud Vision API can be used to retrieve labels that describe an image.\n", + "For example:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q-jVQn3maZ81" + }, + "source": [ + "![Capture.PNG](data:image/png;base64,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)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4io1vzkzF683" + }, + "source": [ + "We want to run the Google Cloud Vision API on a large set of images. Beam is the ideal tool to handle this. In this notebook we will show how to retrieve image labels with this API on a small set of images.\n", + "\n", + "The steps needed to implement this are shown in the notebook:\n", + "* read the images\n", + "* batch your images together to optimize your model call\n", + "* send your images to an external API to run inference\n", + "* post-process the results of your API\n", + "\n", + "⚠️ beware of API quotas and the heavy load you might incur on your external API. Make sure you have set up your pipeline and API correctly for your use case.\n", + "\n", + "For optimizing the calls to external API, you can confgure [PipelineOptions](https://beam.apache.org/documentation/programming-guide/#configuring-pipeline-options) to limit the parallel calls to the external remote API. Different Runners in Beam provide options to handle the parallelism, for example:\n", + "* [DirectRunner](https://beam.apache.org/documentation/runners/direct/) provides `direct_num_workers`.\n", + "* [DataflowRunner](https://beam.apache.org/documentation/runners/dataflow/) provides `max_num_workers`.\n", + "\n", + "You can find details about other runners here: [Link](https://beam.apache.org/documentation/runners/capability-matrix/) " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FAawWOaiIYaS" + }, + "source": [ + "## Installation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XhpKOxINrIqz" + }, + "source": [ + "Install dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bA7MLR8OptJw" + }, + "outputs": [], + "source": [ + "!pip install --upgrade pip\n", + "!pip install protobuf==3.19.4\n", + "!pip install apache-beam[interactive,gcp]>=2.40.0\n", + "!pip install google-cloud-vision==3.1.1\n", + "!pip install requests\n", + "\n", + "# restart the runtime in order to use newly installed versions\n", + "exit() " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C-RVR2eprc0r" + }, + "source": [ + "Authenticate with Google so that you will be able to use the Cloud Vision API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qGDJCbxgTprh" + }, + "outputs": [], + "source": [ + "# Follow the steps to configure your GCP setup\n", + "!gcloud init --console-only" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "74acX7AlT91N" + }, + "outputs": [], + "source": [ + "\n", + "!gcloud auth application-default login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mL4MaHm_XOVd" + }, + "source": [ + "## Remote inference on Google Cloud vision API" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gE0go8CpnTy3" + }, + "outputs": [], + "source": [ + "from typing import List\n", + "import io\n", + "import os\n", + "import requests\n", + "\n", + "from google.cloud import vision\n", + "from google.cloud.vision_v1.types import Feature\n", + "import apache_beam as beam" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "09k08IYlLmON" + }, + "source": [ + "For this use case we have selected some images part of the [MSCoco dataset](https://cocodataset.org/#explore), as a list of image urls. This is what we will use as input for our pipeline." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_89eN_1QeYEd" + }, + "outputs": [], + "source": [ + "image_urls = [\n", + " \"http://farm3.staticflickr.com/2824/10213933686_6936eb402b_z.jpg\",\n", + " \"http://farm8.staticflickr.com/7026/6388965173_92664a0d78_z.jpg\",\n", + " \"http://farm8.staticflickr.com/7003/6528937031_10e1ce0960_z.jpg\",\n", + " \"http://farm6.staticflickr.com/5207/5304302785_7b5f763190_z.jpg\",\n", + " \"http://farm6.staticflickr.com/5207/5304302785_7b5f763190_z.jpg\",\n", + " \"http://farm8.staticflickr.com/7026/6388965173_92664a0d78_z.jpg\",\n", + " \"http://farm8.staticflickr.com/7026/6388965173_92664a0d78_z.jpg\",\n", + "]\n", + "\n", + "def read_image(image_url):\n", + " \"\"\"Read image from url and return image_url, image bytes\"\"\"\n", + " response = requests.get(image_url)\n", + " image_bytes = io.BytesIO(response.content).read()\n", + " return image_url, image_bytes " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HLy7VKJhLrmT" + }, + "source": [ + "### Custom DoFn\n", + "\n", + "In order to implement remote inference, we must create our own DoFn class. This class will be responsible to send a batch of images to the Cloud vision API.\n", + "\n", + "The custom DoFn allows us to initialize our API, or in case of a custom model, a model can also be loaded in the `setup` function. \n", + "\n", + "The `process` function is the most interesting part. In this function we need to implement the actual model call and return its results.\n", + "\n", + "⚠️ When running remote inference, you must be prepared to encounter, identify, and handle failure as gracefully as possible. We recommend using the following techniques: \n", + "\n", + "* Exponential backoff: Retrying failed remote calls with exponentially growing pauses between retries. Using exponential backoff ensures that failures don't lead to an overwhelming number of retries in quick succession. \n", + "\n", + "* Dead letter queues: Routing failed inferences to a separate PCollection without failing the whole transform. This allows you to continue execution without failing the job (batch jobs' default behavior) or retrying indefinitely (streaming jobs' default behavior). You can then run custom pipeline logic on the deadletter queue to log the failure, alert, and push the failed message to temporary storage so that it can eventually be reprocessed. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LnaisJ_JiY_Q" + }, + "outputs": [], + "source": [ + "class RemoteBatchInference(beam.DoFn):\n", + " \"\"\"DoFn that accepts a batch of images as bytearray\n", + " and sends that batch to the Cloud vision API for remote inference.\"\"\"\n", + " def setup(self):\n", + " \"\"\"Init the Google Vision API client.\"\"\"\n", + " self._client = vision.ImageAnnotatorClient()\n", + " \n", + " def process(self, images_batch):\n", + " feature = Feature()\n", + " feature.type_ = Feature.Type.LABEL_DETECTION\n", + "\n", + " # list of image_urls\n", + " image_urls = [image_url for (image_url, image_bytes) in images_batch]\n", + "\n", + " # create a batch request for all images in the batch\n", + " images = [vision.Image(content=image_bytes) for (image_url, image_bytes) in images_batch]\n", + " image_requests = [vision.AnnotateImageRequest(image=image, features=[feature]) for image in images]\n", + " batch_image_request = vision.BatchAnnotateImagesRequest(requests=image_requests)\n", + "\n", + " # send batch request to remote endpoint\n", + " responses = self._client.batch_annotate_images(request=batch_image_request).responses\n", + " \n", + " return list(zip(image_urls, responses))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lHJuyHhvL0-a" + }, + "source": [ + "### Batching\n", + "\n", + "Before we can chain all the different steps together in a pipeline, there is one more thing we need to understand: batching. When running inference with your model (both in Beam itself or in an external API), you can batch your input together to allow for more efficient execution of your model. When using a custom DoFn, you need to take care of the batching yourself, in contrast with the RunInference API which takes care of this for you.\n", + "\n", + "In order to achieve this in our pipeline: we will introduce one more step in our pipeline, a `BatchElements` transform that will group elements together to form a batch of the desired size.\n", + "\n", + "⚠️ If you have a streaming pipeline, you may considering using [GroupIntoBatches](https://beam.apache.org/documentation/transforms/python/aggregation/groupintobatches/) as `BatchElements` doesn't batch things across bundles. `GroupIntoBatches` requires choosing a key within which things are batched.\n", + "\n", + "⚠️ When batching make sure that the input batch matches the max payload of the external API. \n", + "\n", + "⚠️ If you are designing your own API endpoint, then make sure that it can handle batches. \n", + "\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4sXHwZk9Url2" + }, + "source": [ + "### Create pipeline\n", + "\n", + "Now we can chain the different steps all together to read data, transform it to fit the model input, run remote inference and finally process and display the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LLg0OTvNkqo4", + "outputId": "7250b11d-a805-436a-990b-0a864404a536" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('http://farm3.staticflickr.com/2824/10213933686_6936eb402b_z.jpg', label_annotations {\n", + " mid: \"/m/083wq\"\n", + " description: \"Wheel\"\n", + " score: 0.9790800213813782\n", + " topicality: 0.9790800213813782\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0h9mv\"\n", + " description: \"Tire\"\n", + " score: 0.9781236052513123\n", + " topicality: 0.9781236052513123\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/043g5f\"\n", + " description: \"Fuel tank\"\n", + " score: 0.9584090113639832\n", + " topicality: 0.9584090113639832\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/05s2s\"\n", + " description: \"Plant\"\n", + " score: 0.956047534942627\n", + " topicality: 0.956047534942627\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0h8lk_j\"\n", + " description: \"Automotive fuel system\"\n", + " score: 0.9403533339500427\n", + " topicality: 0.9403533339500427\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/07yv9\"\n", + " description: \"Vehicle\"\n", + " score: 0.9362041354179382\n", + " topicality: 0.9362041354179382\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02qwkrn\"\n", + " description: \"Vehicle brake\"\n", + " score: 0.9050074815750122\n", + " topicality: 0.9050074815750122\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0h8pb3l\"\n", + " description: \"Automotive tire\"\n", + " score: 0.8968825936317444\n", + " topicality: 0.8968825936317444\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0768fx\"\n", + " description: \"Automotive lighting\"\n", + " score: 0.8944322466850281\n", + " topicality: 0.8944322466850281\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/04tkfx\"\n", + " description: \"Tread\"\n", + " score: 0.878828227519989\n", + " topicality: 0.878828227519989\n", + "}\n", + ")\n", + "('http://farm8.staticflickr.com/7026/6388965173_92664a0d78_z.jpg', label_annotations {\n", + " mid: \"/m/054_l\"\n", + " description: \"Mirror\"\n", + " score: 0.9682560563087463\n", + " topicality: 0.9682560563087463\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02jz0l\"\n", + " description: \"Tap\"\n", + " score: 0.9611372947692871\n", + " topicality: 0.9611372947692871\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0130jx\"\n", + " description: \"Sink\"\n", + " score: 0.9328749775886536\n", + " topicality: 0.9328749775886536\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0h8lr5r\"\n", + " description: \"Bathroom sink\"\n", + " score: 0.9324912428855896\n", + " topicality: 0.9324912428855896\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02pkr5\"\n", + " description: \"Plumbing fixture\"\n", + " score: 0.9191171526908875\n", + " topicality: 0.9191171526908875\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02dgv\"\n", + " description: \"Door\"\n", + " score: 0.8910166621208191\n", + " topicality: 0.8910166621208191\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/09ggk\"\n", + " description: \"Purple\"\n", + " score: 0.8799519538879395\n", + " topicality: 0.8799519538879395\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/01j2bj\"\n", + " description: \"Bathroom\"\n", + " score: 0.8725592494010925\n", + " topicality: 0.8725592494010925\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/04wnmd\"\n", + " description: \"Fixture\"\n", + " score: 0.8603869080543518\n", + " topicality: 0.8603869080543518\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/04y4h8h\"\n", + " description: \"Bathroom cabinet\"\n", + " score: 0.80011385679245\n", + " topicality: 0.80011385679245\n", + "}\n", + ")\n", + "('http://farm8.staticflickr.com/7003/6528937031_10e1ce0960_z.jpg', error {\n", + " code: 3\n", + " message: \"Bad image data.\"\n", + "}\n", + ")\n", + "('http://farm6.staticflickr.com/5207/5304302785_7b5f763190_z.jpg', error {\n", + " code: 3\n", + " message: \"Bad image data.\"\n", + "}\n", + ")\n", + "('http://farm6.staticflickr.com/5207/5304302785_7b5f763190_z.jpg', error {\n", + " code: 3\n", + " message: \"Bad image data.\"\n", + "}\n", + ")\n", + "('http://farm8.staticflickr.com/7026/6388965173_92664a0d78_z.jpg', label_annotations {\n", + " mid: \"/m/054_l\"\n", + " description: \"Mirror\"\n", + " score: 0.9682560563087463\n", + " topicality: 0.9682560563087463\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02jz0l\"\n", + " description: \"Tap\"\n", + " score: 0.9611372947692871\n", + " topicality: 0.9611372947692871\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0130jx\"\n", + " description: \"Sink\"\n", + " score: 0.9328749775886536\n", + " topicality: 0.9328749775886536\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0h8lr5r\"\n", + " description: \"Bathroom sink\"\n", + " score: 0.9324912428855896\n", + " topicality: 0.9324912428855896\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02pkr5\"\n", + " description: \"Plumbing fixture\"\n", + " score: 0.9191171526908875\n", + " topicality: 0.9191171526908875\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02dgv\"\n", + " description: \"Door\"\n", + " score: 0.8910166621208191\n", + " topicality: 0.8910166621208191\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/09ggk\"\n", + " description: \"Purple\"\n", + " score: 0.8799519538879395\n", + " topicality: 0.8799519538879395\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/01j2bj\"\n", + " description: \"Bathroom\"\n", + " score: 0.8725592494010925\n", + " topicality: 0.8725592494010925\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/04wnmd\"\n", + " description: \"Fixture\"\n", + " score: 0.8603869080543518\n", + " topicality: 0.8603869080543518\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/04y4h8h\"\n", + " description: \"Bathroom cabinet\"\n", + " score: 0.80011385679245\n", + " topicality: 0.80011385679245\n", + "}\n", + ")\n", + "('http://farm8.staticflickr.com/7026/6388965173_92664a0d78_z.jpg', label_annotations {\n", + " mid: \"/m/054_l\"\n", + " description: \"Mirror\"\n", + " score: 0.9682560563087463\n", + " topicality: 0.9682560563087463\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02jz0l\"\n", + " description: \"Tap\"\n", + " score: 0.9611372947692871\n", + " topicality: 0.9611372947692871\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0130jx\"\n", + " description: \"Sink\"\n", + " score: 0.9328749775886536\n", + " topicality: 0.9328749775886536\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/0h8lr5r\"\n", + " description: \"Bathroom sink\"\n", + " score: 0.9324912428855896\n", + " topicality: 0.9324912428855896\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02pkr5\"\n", + " description: \"Plumbing fixture\"\n", + " score: 0.9191171526908875\n", + " topicality: 0.9191171526908875\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/02dgv\"\n", + " description: \"Door\"\n", + " score: 0.8910166621208191\n", + " topicality: 0.8910166621208191\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/09ggk\"\n", + " description: \"Purple\"\n", + " score: 0.8799519538879395\n", + " topicality: 0.8799519538879395\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/01j2bj\"\n", + " description: \"Bathroom\"\n", + " score: 0.8725592494010925\n", + " topicality: 0.8725592494010925\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/04wnmd\"\n", + " description: \"Fixture\"\n", + " score: 0.8603869080543518\n", + " topicality: 0.8603869080543518\n", + "}\n", + "label_annotations {\n", + " mid: \"/m/04y4h8h\"\n", + " description: \"Bathroom cabinet\"\n", + " score: 0.80011385679245\n", + " topicality: 0.80011385679245\n", + "}\n", + ")\n" + ] + } + ], + "source": [ + "with beam.Pipeline() as pipeline:\n", + " _ = (pipeline | \"Create inputs\" >> beam.Create(image_urls)\n", + " | \"Read images\" >> beam.Map(read_image)\n", + " | \"Batch images\" >> beam.BatchElements(min_batch_size=2, max_batch_size=4)\n", + " | \"Inference\" >> beam.ParDo(RemoteBatchInference())\n", + " | \"Print image_url and annotation\" >> beam.Map(print)\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7gwn5bF1XaDm" + }, + "source": [ + "### Metrics\n", + "\n", + "You should consider monitoring and measuring performance of a pipeline when deploying since monitoring can provide insight into the status and health of the application. See [RunInference Metrics](https://beam.apache.org/documentation/ml/runinference-metrics/) for an example of the types of metrics you may want to consider tracking." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/notebooks/beam-ml/dataframe_api_preprocessing.ipynb b/examples/notebooks/beam-ml/dataframe_api_preprocessing.ipynb new file mode 100644 index 000000000000..0dbd0e66ddf8 --- /dev/null +++ b/examples/notebooks/beam-ml/dataframe_api_preprocessing.ipynb @@ -0,0 +1,3496 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#@title ###### Licensed to the Apache Software Foundation (ASF), Version 2.0 (the \"License\")\n", + "\n", + "# Licensed to the Apache Software Foundation (ASF) under one\n", + "# or more contributor license agreements. See the NOTICE file\n", + "# distributed with this work for additional information\n", + "# regarding copyright ownership. The ASF licenses this file\n", + "# to you under the Apache License, Version 2.0 (the\n", + "# \"License\"); you may not use this file except in compliance\n", + "# with the License. You may obtain a copy of the License at\n", + "#\n", + "# http://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing,\n", + "# software distributed under the License is distributed on an\n", + "# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n", + "# KIND, either express or implied. See the License for the\n", + "# specific language governing permissions and limitations\n", + "# under the License." + ], + "metadata": { + "id": "sARMhsXz8yR1", + "cellView": "form" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Overview\n", + "\n", + "One of the most common tools used for data exploration and pre-processing is [pandas DataFrames](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html). Pandas has become very popular for its ease of use. It has very intuitive methods to perform common analytical tasks and data pre-processing. \n", + "\n", + "Pandas loads all of the data into memory on a single machine (one node) for rapid execution. This works well when dealing with small-scale datasets. However, many projects involve datasets that can grow too big to fit in memory. These use cases generally require the usage of parallel data processing frameworks such as Apache Beam.\n", + "\n", + "\n", + "## Beam DataFrames\n", + "\n", + "\n", + "Beam DataFrames provide a pandas-like\n", + "API to declare and define Beam processing pipelines. It provides a familiar interface for machine learning practioners to build complex data-processing pipelines by only invoking standard pandas commands.\n", + "\n", + "> ℹ️ To learn more about Beam DataFrames, take a look at the\n", + "[Beam DataFrames overview](https://beam.apache.org/documentation/dsls/dataframes/overview) page.\n", + "\n", + "## Goal\n", + "The goal of this notebook is to explore a dataset preprocessed it for machine learning model training using the Beam DataFrames API.\n", + "\n", + "\n", + "## Tutorial outline\n", + "\n", + "In this notebook, we walk through the use of the Beam DataFrames API to perform common data exploration as well as pre-processing steps that are necessary to prepare your dataset for machine learning model training and inference, such as: \n", + "\n", + "* Removing unwanted columns.\n", + "* One-hot encoding categorical columns.\n", + "* Normalizing numerical columns.\n", + "\n", + "\n" + ], + "metadata": { + "id": "iFZC1inKuUCy" + } + }, + { + "cell_type": "markdown", + "source": [ + "# Installation\n", + "\n", + "As we want to explore the elements within a `PCollection`, we can make use of the the Interactive runner by installing Apache Beam with the `interactive` component. The latest implemented DataFrames API methods invoked in this notebook are available in Beam 2.43 or later.\n" + ], + "metadata": { + "id": "A0f2HJ22D4lt" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pCjwrwNWnuqI" + }, + "source": [ + "Install latest version" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-OJC0Xn5Um-C", + "beam:comment": "TODO(https://github.com/apache/issues/23961): Just install 2.43.0 once it's released, [`issue 23276`](https://github.com/apache/beam/issues/23276) is currently not implemented for Beam 2.42 (required fix for implementing `str.get_dummies()`" + }, + "outputs": [], + "source": [ + "!git clone https://github.com/apache/beam.git\n", + "\n", + "!cd beam/sdks/python && pip3 install -r build-requirements.txt \n", + "\n", + "%pip install -e beam/sdks/python/.[interactive,gcp]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "# Part I : Local exploration with the Interactive Beam runner\n", + "We first use the [Interactive Beam](https://beam.apache.org/releases/pydoc/2.20.0/apache_beam.runners.interactive.interactive_beam.html) to explore and develop our pipeline.\n", + "This allows us to test our code interactively, building out the pipeline as we go before deploying it on a distributed runner. \n", + "\n", + "\n", + "> ℹ️ In this section, we will only be working with a subset of the original dataset since we're only using the the compute resources of the notebook instance.\n" + ], + "metadata": { + "id": "3NO6RgB7GkkE" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5I3G094hoB1P" + }, + "source": [ + "# Loading the data\n", + "\n", + "Pandas has the\n", + "[`pandas.read_csv`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html)\n", + "function to easily read CSV files into DataFrames.\n", + "We're using the beam\n", + "[`beam.dataframe.io.read_csv`](https://beam.apache.org/releases/pydoc/current/apache_beam.dataframe.io.html#apache_beam.dataframe.io.read_csv)\n", + "function that emulates `pandas.read_csv`. The main difference between them is that the beam method returns a deferred Beam DataFrame while pandas return a standard DataFrame.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "X3_OB9cAULav" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import numpy as np\n", + "import pandas as pd \n", + "import apache_beam as beam\n", + "import apache_beam.runners.interactive.interactive_beam as ib\n", + "from apache_beam import dataframe\n", + "from apache_beam.runners.interactive.interactive_runner import InteractiveRunner\n", + "from apache_beam.runners.dataflow import DataflowRunner\n", + "\n", + "# Available options: [sample_1000, sample_10000, sample_100000, full] where\n", + "# sample contains all of the dataset (around 1000000 samples)\n", + "\n", + "source_csv_file = 'gs://apache-beam-samples/nasa_jpl_asteroid/sample_10000.csv'\n", + "\n", + "# Initialize pipline\n", + "p = beam.Pipeline(InteractiveRunner())\n", + "\n", + "beam_df = p | beam.dataframe.io.read_csv(source_csv_file)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "paf7yf3YpCh8" + }, + "source": [ + "# Data pre-processing\n", + "\n", + "## Dataset description \n", + "\n", + "### [NASA - Nearest Earth Objects dataset](https://cneos.jpl.nasa.gov/ca/)\n", + "There are an innumerable number of objects in the outer space. Some of them are closer than we think. Even though we might think that a distance of 70,000 Km can not potentially harm us, but at an astronomical scale, this is a very small distance and can disrupt many natural phenomena. \n", + "\n", + "These objects/asteroids can thus prove to be harmful. Hence, it is wise to know what is surrounding us and what can harm us amongst those. Thus, this dataset compiles the list of NASA certified asteroids that are classified as the nearest earth object." + ] + }, + { + "cell_type": "markdown", + "source": [ + "\n", + "Let's first inspect the columns of our dataset and their types" + ], + "metadata": { + "id": "cvAu5T0ENjuQ" + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LwW77ixE-pjR", + "outputId": "3dfba30d-165e-46a6-b0b9-f12519db1c27" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "spk_id int64\n", + "full_name object\n", + "near_earth_object object\n", + "absolute_magnitude float64\n", + "diameter float64\n", + "albedo float64\n", + "diameter_sigma float64\n", + "eccentricity float64\n", + "inclination float64\n", + "moid_ld float64\n", + "object_class object\n", + "semi_major_axis_au_unit float64\n", + "hazardous_flag object\n", + "dtype: object" + ] + }, + "metadata": {}, + "execution_count": 27 + } + ], + "source": [ + "beam_df.dtypes" + ] + }, + { + "cell_type": "markdown", + "source": [ + "When using Interactive Beam, we can use `ib.collect()` to bring a Beam DataFrame into local memory as a Pandas DataFrame." + ], + "metadata": { + "id": "1Wa6fpbyQige" + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 746 + }, + "id": "DPxkAmkpq4Xv", + "outputId": "3f89126d-f6fb-43fc-d87b-5daf8563e057" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_79206f341d7de09f6cacdd05be309575\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_79206f341d7de09f6cacdd05be309575\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " spk_id full_name near_earth_object \\\n", + "0 2000001 1 Ceres N \n", + "1 2000002 2 Pallas N \n", + "2 2000003 3 Juno N \n", + "3 2000004 4 Vesta N \n", + "4 2000005 5 Astraea N \n", + "... ... ... ... \n", + "9994 2009995 9995 Alouette (4805 P-L) N \n", + "9995 2009996 9996 ANS (9070 P-L) N \n", + "9996 2009997 9997 COBE (1217 T-1) N \n", + "9997 2009998 9998 ISO (1293 T-1) N \n", + "9998 2009999 9999 Wiles (4196 T-2) N \n", + "\n", + " absolute_magnitude diameter albedo diameter_sigma eccentricity \\\n", + "0 3.40 939.400 0.0900 0.200 0.076009 \n", + "1 4.20 545.000 0.1010 18.000 0.229972 \n", + "2 5.33 246.596 0.2140 10.594 0.256936 \n", + "3 3.00 525.400 0.4228 0.200 0.088721 \n", + "4 6.90 106.699 0.2740 3.140 0.190913 \n", + "... ... ... ... ... ... \n", + "9994 15.10 2.564 0.2450 0.550 0.160610 \n", + "9995 13.60 8.978 0.1130 0.376 0.235174 \n", + "9996 14.30 NaN NaN NaN 0.113059 \n", + "9997 15.10 2.235 0.3880 0.373 0.093852 \n", + "9998 13.00 7.148 0.2620 0.065 0.071351 \n", + "\n", + " inclination moid_ld object_class semi_major_axis_au_unit \\\n", + "0 10.594067 620.640533 MBA 2.769165 \n", + "1 34.832932 480.348639 MBA 2.773841 \n", + "2 12.991043 402.514639 MBA 2.668285 \n", + "3 7.141771 443.451432 MBA 2.361418 \n", + "4 5.367427 426.433027 MBA 2.574037 \n", + "... ... ... ... ... \n", + "9994 2.311731 388.723233 MBA 2.390249 \n", + "9995 7.657713 444.194746 MBA 2.796605 \n", + "9996 2.459643 495.460110 MBA 2.545674 \n", + "9997 3.912263 373.848377 MBA 2.160961 \n", + "9998 3.198839 632.144398 MBA 2.839917 \n", + "\n", + " hazardous_flag \n", + "0 N \n", + "1 N \n", + "2 N \n", + "3 N \n", + "4 N \n", + "... ... \n", + "9994 N \n", + "9995 N \n", + "9996 N \n", + "9997 N \n", + "9998 N \n", + "\n", + "[9999 rows x 13 columns]" + ], + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
spk_idfull_namenear_earth_objectabsolute_magnitudediameteralbedodiameter_sigmaeccentricityinclinationmoid_ldobject_classsemi_major_axis_au_unithazardous_flag
020000011 CeresN3.40939.4000.09000.2000.07600910.594067620.640533MBA2.769165N
120000022 PallasN4.20545.0000.101018.0000.22997234.832932480.348639MBA2.773841N
220000033 JunoN5.33246.5960.214010.5940.25693612.991043402.514639MBA2.668285N
320000044 VestaN3.00525.4000.42280.2000.0887217.141771443.451432MBA2.361418N
420000055 AstraeaN6.90106.6990.27403.1400.1909135.367427426.433027MBA2.574037N
..........................................
999420099959995 Alouette (4805 P-L)N15.102.5640.24500.5500.1606102.311731388.723233MBA2.390249N
999520099969996 ANS (9070 P-L)N13.608.9780.11300.3760.2351747.657713444.194746MBA2.796605N
999620099979997 COBE (1217 T-1)N14.30NaNNaNNaN0.1130592.459643495.460110MBA2.545674N
999720099989998 ISO (1293 T-1)N15.102.2350.38800.3730.0938523.912263373.848377MBA2.160961N
999820099999999 Wiles (4196 T-2)N13.007.1480.26200.0650.0713513.198839632.144398MBA2.839917N
\n", + "

9999 rows × 13 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 28 + } + ], + "source": [ + "ib.collect(beam_df)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "We can see that our datasets consists of both:\n", + "\n", + "* **Numerical columns:** These columns need to be transformed through [normalization](https://developers.google.com/machine-learning/data-prep/transform/normalization) before they can be used for training a machine learning model.\n", + "\n", + "* **Categorical columns:** We need to transform those columns with [one-hot encoding](https://developers.google.com/machine-learning/data-prep/transform/transform-categorical) to use them during training. \n" + ], + "metadata": { + "id": "8jV9odKhNyF2" + } + }, + { + "cell_type": "markdown", + "source": [ + "We can also explore use the standard pandas command `DataFrame.describe()` to generate descriptive statistics for the numerical columns like percentile, mean, std, etc. " + ], + "metadata": { + "id": "MGAErO0lAYws" + } + }, + { + "cell_type": "code", + "source": [ + "with dataframe.allow_non_parallel_operations():\n", + " beam_df_description = ib.collect(beam_df.describe())\n", + "\n", + "beam_df_description" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 378 + }, + "id": "Befv697VBGM7", + "outputId": "bb465020-94e4-4b3c-fda6-6e43da199be1" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_98687cb0060a8077a8abab6e464e4a75\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_98687cb0060a8077a8abab6e464e4a75\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " spk_id absolute_magnitude diameter albedo \\\n", + "count 9.999000e+03 9999.000000 8688.000000 8672.000000 \n", + "mean 2.005000e+06 12.675380 19.245446 0.197723 \n", + "std 2.886607e+03 1.639609 30.190191 0.138819 \n", + "min 2.000001e+06 3.000000 0.300000 0.008000 \n", + "25% 2.002500e+06 11.900000 5.614000 0.074000 \n", + "50% 2.005000e+06 12.900000 9.814000 0.187000 \n", + "75% 2.007500e+06 13.700000 19.156750 0.283000 \n", + "max 2.009999e+06 20.700000 939.400000 1.000000 \n", + "\n", + " diameter_sigma eccentricity inclination moid_ld \\\n", + "count 8591.000000 9999.000000 9999.000000 9999.000000 \n", + "mean 0.454072 0.148716 7.890742 509.805237 \n", + "std 1.093676 0.083803 6.336244 205.046582 \n", + "min 0.006000 0.001003 0.042716 0.131028 \n", + "25% 0.120000 0.093780 3.220137 377.829197 \n", + "50% 0.201000 0.140335 6.018836 470.650523 \n", + "75% 0.375000 0.187092 10.918176 636.010802 \n", + "max 39.297000 0.889831 68.018875 4241.524913 \n", + "\n", + " semi_major_axis_au_unit \n", + "count 9999.000000 \n", + "mean 2.689836 \n", + "std 0.607190 \n", + "min 0.832048 \n", + "25% 2.340816 \n", + "50% 2.614468 \n", + "75% 3.005449 \n", + "max 24.667968 " + ], + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
spk_idabsolute_magnitudediameteralbedodiameter_sigmaeccentricityinclinationmoid_ldsemi_major_axis_au_unit
count9.999000e+039999.0000008688.0000008672.0000008591.0000009999.0000009999.0000009999.0000009999.000000
mean2.005000e+0612.67538019.2454460.1977230.4540720.1487167.890742509.8052372.689836
std2.886607e+031.63960930.1901910.1388191.0936760.0838036.336244205.0465820.607190
min2.000001e+063.0000000.3000000.0080000.0060000.0010030.0427160.1310280.832048
25%2.002500e+0611.9000005.6140000.0740000.1200000.0937803.220137377.8291972.340816
50%2.005000e+0612.9000009.8140000.1870000.2010000.1403356.018836470.6505232.614468
75%2.007500e+0613.70000019.1567500.2830000.3750000.18709210.918176636.0108023.005449
max2.009999e+0620.700000939.4000001.00000039.2970000.88983168.0188754241.52491324.667968
\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 21 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D9uJtHLSSAMC" + }, + "source": [ + "Before executing any transformations, we need to check if all the columns need to be used for model training. Let's first have a look at the column description as provided by the [JPL website](https://ssd.jpl.nasa.gov/sbdb_query.cgi):\n", + "\n", + "* **spk_id:** Object primary SPK-ID\n", + "* **full_name:** Asteroid name\n", + "* **near_earth_object:** Near-earth object flag\n", + "* **absolute_magnitude:** the apparent magnitude an object would have if it were located at a distance of 10 parsecs.\n", + "* **diameter:** object diameter (from equivalent sphere) km Unit\n", + "* **albedo:** a measure of the diffuse reflection of solar radiation out of the total solar radiation and measured on a scale from 0 to 1.\n", + "* **diameter_sigma:** 1-sigma uncertainty in object diameter km Unit.\n", + "* **eccentricity:** value between 0 and 1 that referes to how flat or round the shape of the asteroid is \n", + "* **inclination:** angle with respect to x-y ecliptic plane\n", + "* **moid_ld:** Earth Minimum Orbit Intersection Distance au Unit\n", + "* **object_class:** the classification of the asteroid. Checkout this [link](https://pdssbn.astro.umd.edu/data_other/objclass.shtml) for a more detailed description.\n", + "* **Semi-major axis au Unit:** the length of half of the long axis in AU unit\n", + "* **hazardous_flag:** Hazardous Asteroid Flag" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DzYVKbwTp72d" + }, + "source": [ + "Columns **'spk_id'** and **'full_name'** are unique for each row. These columns can be removed since they are not needed for model training." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "piRPwH2aqT06" + }, + "outputs": [], + "source": [ + "beam_df = beam_df.drop(['spk_id', 'full_name'], axis='columns', inplace=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fRvNyahSuX_y" + }, + "source": [ + "Let's have a look at the number of missing values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 358 + }, + "id": "A2PLchW8vXvt", + "outputId": "14a4ac64-5b54-4ed4-959d-daea65bb6457" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/content/beam/sdks/python/apache_beam/dataframe/frame_base.py:145: RuntimeWarning: invalid value encountered in long_scalars\n", + " lambda left, right: getattr(left, op)(right), name=op, args=[other])\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_868f8ad001ab00c7013b65472a513917\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_868f8ad001ab00c7013b65472a513917\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "near_earth_object 0.000000\n", + "absolute_magnitude 0.000000\n", + "diameter 13.111311\n", + "albedo 13.271327\n", + "diameter_sigma 14.081408\n", + "eccentricity 0.000000\n", + "inclination 0.000000\n", + "moid_ld 0.000000\n", + "object_class 0.000000\n", + "semi_major_axis_au_unit 0.000000\n", + "hazardous_flag 0.000000\n", + "dtype: float64" + ] + }, + "metadata": {}, + "execution_count": 30 + } + ], + "source": [ + "ib.collect(beam_df.isnull().mean() * 100)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "00MRdFGLwQiD" + }, + "source": [ + "It can be observed that most of the columns do not have missing values. However, columns **'diameter'**, **'albedo'** and **'diameter_sigma'** have many missing values. Since these values cannot be measured or derived, we can remove them since they will not be required for training the machine learning model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tHYeCHREwvyB", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 538 + }, + "outputId": "3be686d0-f56a-4054-a71a-d3019bf379e8" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_f88b77f183371d1a45fa87bed4a545f6\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_f88b77f183371d1a45fa87bed4a545f6\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " near_earth_object absolute_magnitude eccentricity inclination \\\n", + "0 N 3.40 0.076009 10.594067 \n", + "1 N 4.20 0.229972 34.832932 \n", + "2 N 5.33 0.256936 12.991043 \n", + "3 N 3.00 0.088721 7.141771 \n", + "4 N 6.90 0.190913 5.367427 \n", + "... ... ... ... ... \n", + "9994 N 15.10 0.160610 2.311731 \n", + "9995 N 13.60 0.235174 7.657713 \n", + "9996 N 14.30 0.113059 2.459643 \n", + "9997 N 15.10 0.093852 3.912263 \n", + "9998 N 13.00 0.071351 3.198839 \n", + "\n", + " moid_ld object_class semi_major_axis_au_unit hazardous_flag \n", + "0 620.640533 MBA 2.769165 N \n", + "1 480.348639 MBA 2.773841 N \n", + "2 402.514639 MBA 2.668285 N \n", + "3 443.451432 MBA 2.361418 N \n", + "4 426.433027 MBA 2.574037 N \n", + "... ... ... ... ... \n", + "9994 388.723233 MBA 2.390249 N \n", + "9995 444.194746 MBA 2.796605 N \n", + "9996 495.460110 MBA 2.545674 N \n", + "9997 373.848377 MBA 2.160961 N \n", + "9998 632.144398 MBA 2.839917 N \n", + "\n", + "[9999 rows x 8 columns]" + ], + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
near_earth_objectabsolute_magnitudeeccentricityinclinationmoid_ldobject_classsemi_major_axis_au_unithazardous_flag
0N3.400.07600910.594067620.640533MBA2.769165N
1N4.200.22997234.832932480.348639MBA2.773841N
2N5.330.25693612.991043402.514639MBA2.668285N
3N3.000.0887217.141771443.451432MBA2.361418N
4N6.900.1909135.367427426.433027MBA2.574037N
...........................
9994N15.100.1606102.311731388.723233MBA2.390249N
9995N13.600.2351747.657713444.194746MBA2.796605N
9996N14.300.1130592.459643495.460110MBA2.545674N
9997N15.100.0938523.912263373.848377MBA2.160961N
9998N13.000.0713513.198839632.144398MBA2.839917N
\n", + "

9999 rows × 8 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 31 + } + ], + "source": [ + "beam_df = beam_df.drop(['diameter', 'albedo', 'diameter_sigma'], axis='columns', inplace=False)\n", + "ib.collect(beam_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a3PojL3WBqgE" + }, + "source": [ + "Next, we need to normalize the numerical columns before using them to train a model. A common method of standarization is to subtract the mean and divide by standard deviation (a.k.a [z-score](https://developers.google.com/machine-learning/data-prep/transform/normalization#z-score)). This improves the performance and training stability of the model during training and inference.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sZ2_gB8wENF1" + }, + "source": [ + "Let's first get both the the numerical columns and categorical columns" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vsWY8xW5d_Wn" + }, + "outputs": [], + "source": [ + "numerical_cols = beam_df.select_dtypes(include=np.number).columns.tolist()\n", + "categorical_cols = list(set(beam_df.columns) - set(numerical_cols))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v03ABuXJKEmv" + }, + "source": [ + "Normalizing the data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 587 + }, + "id": "PD_DTxPCP4hs", + "outputId": "16fede03-f67e-4c26-8714-fd3fc6892109" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/content/beam/sdks/python/apache_beam/dataframe/frame_base.py:145: RuntimeWarning: invalid value encountered in double_scalars\n", + " lambda left, right: getattr(left, op)(right), name=op, args=[other])\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_55302fa5950ce6ceb9f99ff9a168097a\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_55302fa5950ce6ceb9f99ff9a168097a\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " absolute_magnitude eccentricity inclination moid_ld \\\n", + "306 -1.570727 -0.062543 -0.278518 0.373194 \n", + "310 -1.631718 -1.724526 -0.736389 1.087833 \n", + "546 -1.753698 1.028793 1.415303 -0.339489 \n", + "635 -1.875678 0.244869 0.005905 0.214107 \n", + "701 -3.278451 -1.570523 2.006145 1.542754 \n", + "... ... ... ... ... \n", + "9697 0.807888 -1.151809 -0.082944 -0.129556 \n", + "9813 1.722740 0.844551 -0.583247 -1.006447 \n", + "9868 0.807888 -0.207399 -0.784665 -0.462136 \n", + "9903 0.868878 0.460086 0.092258 -0.107597 \n", + "9956 0.746898 -0.234132 -0.161116 -0.601379 \n", + "\n", + " semi_major_axis_au_unit \n", + "306 0.357201 \n", + "310 0.344233 \n", + "546 0.139080 \n", + "635 0.367559 \n", + "701 0.829337 \n", + "... ... \n", + "9697 -0.533538 \n", + "9813 -0.677961 \n", + "9868 -0.539794 \n", + "9903 0.071794 \n", + "9956 -0.664887 \n", + "\n", + "[9999 rows x 5 columns]" + ], + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
absolute_magnitudeeccentricityinclinationmoid_ldsemi_major_axis_au_unit
306-1.570727-0.062543-0.2785180.3731940.357201
310-1.631718-1.724526-0.7363891.0878330.344233
546-1.7536981.0287931.415303-0.3394890.139080
635-1.8756780.2448690.0059050.2141070.367559
701-3.278451-1.5705232.0061451.5427540.829337
..................
96970.807888-1.151809-0.082944-0.129556-0.533538
98131.7227400.844551-0.583247-1.006447-0.677961
98680.807888-0.207399-0.784665-0.462136-0.539794
99030.8688780.4600860.092258-0.1075970.071794
99560.746898-0.234132-0.161116-0.601379-0.664887
\n", + "

9999 rows × 5 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 33 + } + ], + "source": [ + "# Get numerical columns\n", + "beam_df_numericals = beam_df.filter(items=numerical_cols)\n", + "\n", + "# Standarize dataframes only with numerical columns\n", + "beam_df_numericals = (beam_df_numericals - beam_df_numericals.mean())/beam_df_numericals.std()\n", + "\n", + "ib.collect(beam_df_numericals)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qdNILsajFvex" + }, + "source": [ + "Next, we need to convert the categorical columns into one-hot encoded variables to use them during training. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ngoxg0rSywVd" + }, + "outputs": [], + "source": [ + "def get_one_hot_encoding(df: pd.DataFrame, categorical_col:list) -> pd.DataFrame:\n", + " beam_df_categorical= beam_df[categorical_col]\n", + " # Get unique values\n", + " with dataframe.allow_non_parallel_operations():\n", + " unique_classes = pd.CategoricalDtype(ib.collect(beam_df_categorical.unique(as_series=True)))\n", + " # Use `str.get_dummies()` to get the one-hot encoded representation of the categorical columns\n", + " beam_df_categorical = beam_df_categorical.astype(unique_classes).str.get_dummies()\n", + " # Add column name prefix to the newly created categorical columns\n", + " beam_df_categorical = beam_df_categorical.add_prefix(f'{categorical_col}_')\n", + "\n", + " return beam_df_categorical" + ] + }, + { + "cell_type": "code", + "source": [ + "for categorical_col in categorical_cols:\n", + " beam_df_categorical = get_one_hot_encoding(df=beam_df, categorical_col=categorical_col)\n", + " beam_df_numericals = beam_df_numericals.merge(beam_df_categorical, left_index = True, right_index = True)\n", + "ib.collect(beam_df_numericals)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 602 + }, + "id": "k9rvtWqHf6Qw", + "outputId": "b8d8ae57-6dba-45b4-e7ae-e4b14084eede" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_6b2563c7f661bc0fc5729c2577d6f232\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_6b2563c7f661bc0fc5729c2577d6f232\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_6fa896083b128ad99059af69a3d7fc7e\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_6fa896083b128ad99059af69a3d7fc7e\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_6339347de9805da541eba53abaee2d5e\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_6339347de9805da541eba53abaee2d5e\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_1af5b908898a1e5949dcc20549f650eb\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_1af5b908898a1e5949dcc20549f650eb\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " absolute_magnitude eccentricity inclination moid_ld \\\n", + "0 -5.657067 -0.867596 0.426645 0.540537 \n", + "12 -3.583402 -0.756931 1.364340 0.238610 \n", + "47 -3.400432 -0.912290 -0.211925 1.136060 \n", + "381 -2.363599 0.271412 -0.078826 0.535299 \n", + "515 -2.729540 1.469775 0.799915 -0.602881 \n", + "... ... ... ... ... \n", + "9146 0.563927 -0.508757 -0.327512 -0.637391 \n", + "9657 1.478779 0.487849 -0.637779 -0.648240 \n", + "9704 0.380957 -0.238383 0.443053 0.670490 \n", + "9879 1.295809 -0.442966 -0.698505 -0.494818 \n", + "9980 0.746898 -1.455992 -0.849144 0.592902 \n", + "\n", + " semi_major_axis_au_unit near_earth_object_N near_earth_object_Y \\\n", + "0 0.130649 1 0 \n", + "12 -0.187375 1 0 \n", + "47 0.691182 1 0 \n", + "381 0.712755 1 0 \n", + "515 -0.014654 1 0 \n", + "... ... ... ... \n", + "9146 -0.820638 1 0 \n", + "9657 -0.468778 1 0 \n", + "9704 0.587128 1 0 \n", + "9879 -0.662602 1 0 \n", + "9980 -0.022726 1 0 \n", + "\n", + " near_earth_object_nan object_class_AMO object_class_APO ... \\\n", + "0 0 0 0 ... \n", + "12 0 0 0 ... \n", + "47 0 0 0 ... \n", + "381 0 0 0 ... \n", + "515 0 0 0 ... \n", + "... ... ... ... ... \n", + "9146 0 0 0 ... \n", + "9657 0 0 0 ... \n", + "9704 0 0 0 ... \n", + "9879 0 0 0 ... \n", + "9980 0 0 0 ... \n", + "\n", + " object_class_CEN object_class_IMB object_class_MBA object_class_MCA \\\n", + "0 0 0 1 0 \n", + "12 0 0 1 0 \n", + "47 0 0 1 0 \n", + "381 0 0 1 0 \n", + "515 0 0 1 0 \n", + "... ... ... ... ... \n", + "9146 0 0 1 0 \n", + "9657 0 0 1 0 \n", + "9704 0 0 1 0 \n", + "9879 0 0 1 0 \n", + "9980 0 0 1 0 \n", + "\n", + " object_class_OMB object_class_TJN object_class_nan hazardous_flag_N \\\n", + "0 0 0 0 1 \n", + "12 0 0 0 1 \n", + "47 0 0 0 1 \n", + "381 0 0 0 1 \n", + "515 0 0 0 1 \n", + "... ... ... ... ... \n", + "9146 0 0 0 1 \n", + "9657 0 0 0 1 \n", + "9704 0 0 0 1 \n", + "9879 0 0 0 1 \n", + "9980 0 0 0 1 \n", + "\n", + " hazardous_flag_Y hazardous_flag_nan \n", + "0 0 0 \n", + "12 0 0 \n", + "47 0 0 \n", + "381 0 0 \n", + "515 0 0 \n", + "... ... ... \n", + "9146 0 0 \n", + "9657 0 0 \n", + "9704 0 0 \n", + "9879 0 0 \n", + "9980 0 0 \n", + "\n", + "[9999 rows x 22 columns]" + ], + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
absolute_magnitudeeccentricityinclinationmoid_ldsemi_major_axis_au_unitnear_earth_object_Nnear_earth_object_Ynear_earth_object_nanobject_class_AMOobject_class_APO...object_class_CENobject_class_IMBobject_class_MBAobject_class_MCAobject_class_OMBobject_class_TJNobject_class_nanhazardous_flag_Nhazardous_flag_Yhazardous_flag_nan
0-5.657067-0.8675960.4266450.5405370.13064910000...0010000100
12-3.583402-0.7569311.3643400.238610-0.18737510000...0010000100
47-3.400432-0.912290-0.2119251.1360600.69118210000...0010000100
381-2.3635990.271412-0.0788260.5352990.71275510000...0010000100
515-2.7295401.4697750.799915-0.602881-0.01465410000...0010000100
..................................................................
91460.563927-0.508757-0.327512-0.637391-0.82063810000...0010000100
96571.4787790.487849-0.637779-0.648240-0.46877810000...0010000100
97040.380957-0.2383830.4430530.6704900.58712810000...0010000100
98791.295809-0.442966-0.698505-0.494818-0.66260210000...0010000100
99800.746898-1.455992-0.8491440.592902-0.02272610000...0010000100
\n", + "

9999 rows × 22 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 35 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rVdSIyCB0spw" + }, + "source": [ + "# Putting it all together\n", + "\n", + "Let's now try to summarize all the steps that we've executed above into a full pipeline implementation and visualize our pre-processed data.\n", + "\n", + "> ℹ️ Note that the only standard Beam method invoked here is the `pipeline` instance. The rest of the pre-processing commands are all based on native pandas methods that have been integrated with the Beam DataFrame API. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ndaSNond0v8Q", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 651 + }, + "outputId": "b265e915-e649-44e4-a31a-95ac85c0ebf6" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/content/beam/sdks/python/apache_beam/dataframe/frame_base.py:145: RuntimeWarning: invalid value encountered in double_scalars\n", + " lambda left, right: getattr(left, op)(right), name=op, args=[other])\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_cb06c945824aa1bb68aa31ad7e601b74\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_cb06c945824aa1bb68aa31ad7e601b74\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_fb923f80fecb72b4fa55e5cfdba16d23\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_fb923f80fecb72b4fa55e5cfdba16d23\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_3f4b1a0f483cd017e004e11816a91d3b\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_3f4b1a0f483cd017e004e11816a91d3b\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + "
\n", + "
\n", + " Processing... collect\n", + "
\n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "display_data", + "data": { + "application/javascript": [ + "\n", + " if (typeof window.interactive_beam_jquery == 'undefined') {\n", + " var jqueryScript = document.createElement('script');\n", + " jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n", + " jqueryScript.type = 'text/javascript';\n", + " jqueryScript.onload = function() {\n", + " var datatableScript = document.createElement('script');\n", + " datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n", + " datatableScript.type = 'text/javascript';\n", + " datatableScript.onload = function() {\n", + " window.interactive_beam_jquery = jQuery.noConflict(true);\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_fce8902eccbfaa17e32ba0c7c242ccec\").remove();\n", + " });\n", + " }\n", + " document.head.appendChild(datatableScript);\n", + " };\n", + " document.head.appendChild(jqueryScript);\n", + " } else {\n", + " window.interactive_beam_jquery(document).ready(function($){\n", + " \n", + " $(\"#progress_indicator_fce8902eccbfaa17e32ba0c7c242ccec\").remove();\n", + " });\n", + " }" + ] + }, + "metadata": {} + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " absolute_magnitude eccentricity inclination moid_ld \\\n", + "0 -5.657067 -0.867596 0.426645 0.540537 \n", + "12 -3.583402 -0.756931 1.364340 0.238610 \n", + "47 -3.400432 -0.912290 -0.211925 1.136060 \n", + "381 -2.363599 0.271412 -0.078826 0.535299 \n", + "515 -2.729540 1.469775 0.799915 -0.602881 \n", + "... ... ... ... ... \n", + "9146 0.563927 -0.508757 -0.327512 -0.637391 \n", + "9657 1.478779 0.487849 -0.637779 -0.648240 \n", + "9704 0.380957 -0.238383 0.443053 0.670490 \n", + "9879 1.295809 -0.442966 -0.698505 -0.494818 \n", + "9980 0.746898 -1.455992 -0.849144 0.592902 \n", + "\n", + " semi_major_axis_au_unit near_earth_object_N near_earth_object_Y \\\n", + "0 0.130649 1 0 \n", + "12 -0.187375 1 0 \n", + "47 0.691182 1 0 \n", + "381 0.712755 1 0 \n", + "515 -0.014654 1 0 \n", + "... ... ... ... \n", + "9146 -0.820638 1 0 \n", + "9657 -0.468778 1 0 \n", + "9704 0.587128 1 0 \n", + "9879 -0.662602 1 0 \n", + "9980 -0.022726 1 0 \n", + "\n", + " near_earth_object_nan object_class_AMO object_class_APO ... \\\n", + "0 0 0 0 ... \n", + "12 0 0 0 ... \n", + "47 0 0 0 ... \n", + "381 0 0 0 ... \n", + "515 0 0 0 ... \n", + "... ... ... ... ... \n", + "9146 0 0 0 ... \n", + "9657 0 0 0 ... \n", + "9704 0 0 0 ... \n", + "9879 0 0 0 ... \n", + "9980 0 0 0 ... \n", + "\n", + " object_class_CEN object_class_IMB object_class_MBA object_class_MCA \\\n", + "0 0 0 1 0 \n", + "12 0 0 1 0 \n", + "47 0 0 1 0 \n", + "381 0 0 1 0 \n", + "515 0 0 1 0 \n", + "... ... ... ... ... \n", + "9146 0 0 1 0 \n", + "9657 0 0 1 0 \n", + "9704 0 0 1 0 \n", + "9879 0 0 1 0 \n", + "9980 0 0 1 0 \n", + "\n", + " object_class_OMB object_class_TJN object_class_nan hazardous_flag_N \\\n", + "0 0 0 0 1 \n", + "12 0 0 0 1 \n", + "47 0 0 0 1 \n", + "381 0 0 0 1 \n", + "515 0 0 0 1 \n", + "... ... ... ... ... \n", + "9146 0 0 0 1 \n", + "9657 0 0 0 1 \n", + "9704 0 0 0 1 \n", + "9879 0 0 0 1 \n", + "9980 0 0 0 1 \n", + "\n", + " hazardous_flag_Y hazardous_flag_nan \n", + "0 0 0 \n", + "12 0 0 \n", + "47 0 0 \n", + "381 0 0 \n", + "515 0 0 \n", + "... ... ... \n", + "9146 0 0 \n", + "9657 0 0 \n", + "9704 0 0 \n", + "9879 0 0 \n", + "9980 0 0 \n", + "\n", + "[9999 rows x 22 columns]" + ], + "text/html": [ + "\n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
absolute_magnitudeeccentricityinclinationmoid_ldsemi_major_axis_au_unitnear_earth_object_Nnear_earth_object_Ynear_earth_object_nanobject_class_AMOobject_class_APO...object_class_CENobject_class_IMBobject_class_MBAobject_class_MCAobject_class_OMBobject_class_TJNobject_class_nanhazardous_flag_Nhazardous_flag_Yhazardous_flag_nan
0-5.657067-0.8675960.4266450.5405370.13064910000...0010000100
12-3.583402-0.7569311.3643400.238610-0.18737510000...0010000100
47-3.400432-0.912290-0.2119251.1360600.69118210000...0010000100
381-2.3635990.271412-0.0788260.5352990.71275510000...0010000100
515-2.7295401.4697750.799915-0.602881-0.01465410000...0010000100
..................................................................
91460.563927-0.508757-0.327512-0.637391-0.82063810000...0010000100
96571.4787790.487849-0.637779-0.648240-0.46877810000...0010000100
97040.380957-0.2383830.4430530.6704900.58712810000...0010000100
98791.295809-0.442966-0.698505-0.494818-0.66260210000...0010000100
99800.746898-1.455992-0.8491440.592902-0.02272610000...0010000100
\n", + "

9999 rows × 22 columns

\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ] + }, + "metadata": {}, + "execution_count": 36 + } + ], + "source": [ + "# Specify the location of source csv file to be processed\n", + "source_csv_file = 'gs://apache-beam-samples/nasa_jpl_asteroid/sample_10000.csv'\n", + "\n", + "# Initialize pipline\n", + "p = beam.Pipeline(InteractiveRunner())\n", + "\n", + "# Create a deferred Beam DataFrame with the contents of our csv file.\n", + "beam_df = p | beam.dataframe.io.read_csv(source_csv_file)\n", + "\n", + "# Drop irrelavant columns/columns with missing values\n", + "beam_df = beam_df.drop(['spk_id', 'full_name','diameter', 'albedo', 'diameter_sigma'], axis='columns', inplace=False)\n", + "\n", + "# Get numerical columns/columns with categorical variables\n", + "numerical_cols = beam_df.select_dtypes(include=np.number).columns.tolist()\n", + "categorical_cols = list(set(beam_df.columns) - set(numerical_cols))\n", + "\n", + "# Normalize the numerical variables \n", + "beam_df_numericals = beam_df.filter(items=numerical_cols)\n", + "beam_df_numericals = (beam_df_numericals - beam_df_numericals.mean())/beam_df_numericals.std()\n", + "\n", + "\n", + "# One-hot encode the categorical variables \n", + "for categorical_col in categorical_cols:\n", + " beam_df_categorical= get_one_hot_encoding(df=beam_df, categorical_col=categorical_col)\n", + " beam_df_numericals = beam_df_numericals.merge(beam_df_categorical, left_index = True, right_index = True)\n", + "\n", + "ib.collect(beam_df_numericals)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xZvJTqa3XKI_" + }, + "source": [ + "# Part II : Process the full dataset with the Distributed Runner\n", + "Now that we've showcased how to build and execute the pipeline locally using the Interactive Runner. It's time to execute our pipeline on our full dataset by switching to a distributed runner. For this example, we will exectue our pipeline on [Dataflow](https://cloud.google.com/dataflow/docs/guides/deploying-a-pipeline)." + ] + }, + { + "cell_type": "code", + "source": [ + "PROJECT_ID = \"\"\n", + "REGION = \"us-central1\"\n", + "TEMP_DIR = \"gs:///tmp\"\n", + "OUTPUT_DIR = \"gs:///dataframe-result\"" + ], + "metadata": { + "id": "dDBYbMEWbL4t" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "> ℹ️ Note that we are now processing the full dataset `full.csv` that containts approximately 1 million rows. We're also writing the results to a `csv` file instead of using `ib.collect()` to materialize the deferred dataframe.\n", + "\n", + "> ℹ️ The only things we need to change to switch from an interactive runner towards a distributed one are the pipeline options. The rest of the pipeline steps are exactly identical." + ], + "metadata": { + "id": "Qk1GaYoSc9-1" + } + }, + { + "cell_type": "code", + "source": [ + "# Specify the location of source csv file to be processed (full dataset)\n", + "source_csv_file = 'gs://apache-beam-samples/nasa_jpl_asteroid/full.csv'\n", + "\n", + "# Build a new pipeline that will execute on Dataflow.\n", + "p = beam.Pipeline(DataflowRunner(),\n", + " options=beam.options.pipeline_options.PipelineOptions(\n", + " project=PROJECT_ID,\n", + " region=REGION,\n", + " temp_location=TEMP_DIR,\n", + " # Disable autoscaling for a quicker demo\n", + " autoscaling_algorithm='NONE',\n", + " num_workers=10))\n", + "\n", + "# Create a deferred Beam DataFrame with the contents of our csv file.\n", + "beam_df = p | beam.dataframe.io.read_csv(source_csv_file)\n", + "\n", + "# Drop irrelavant columns/columns with missing values\n", + "beam_df = beam_df.drop(['spk_id', 'full_name','diameter', 'albedo', 'diameter_sigma'], axis='columns', inplace=False)\n", + "\n", + "# Get numerical columns/columns with categorical variables\n", + "numerical_cols = beam_df.select_dtypes(include=np.number).columns.tolist()\n", + "categorical_cols = list(set(beam_df.columns) - set(numerical_cols))\n", + "\n", + "# Normalize the numerical variables \n", + "beam_df_numericals = beam_df.filter(items=numerical_cols)\n", + "beam_df_numericals = (beam_df_numericals - beam_df_numericals.mean())/beam_df_numericals.std()\n", + "\n", + "\n", + "# One-hot encode the categorical variables \n", + "for categorical_col in categorical_cols:\n", + " beam_df_categorical= get_one_hot_encoding(df=beam_df, categorical_col=categorical_col)\n", + " beam_df_numericals = beam_df_numericals.merge(beam_df_categorical, left_index = True, right_index = True\n", + "\n", + "# Write the pre-processed dataset to csv\n", + "beam_df_numericals.to_csv(os.path.join(OUTPUT_DIR, \"preprocessed_data.csv\"))" + ], + "metadata": { + "id": "1XovR0gKbMlK" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Let's now submit and execute our pipeline." + ], + "metadata": { + "id": "a789u4Yecs_g" + } + }, + { + "cell_type": "code", + "source": [ + "p.run().wait_until_finish()" + ], + "metadata": { + "id": "pbUlC102bPaZ" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "The execution of the pipeline job will take some time until it finishes." + ], + "metadata": { + "id": "dzdqmzKzTOng" + } + }, + { + "cell_type": "markdown", + "source": [ + "# What's next \n", + "\n", + "Now that we've seen how we can analyze and preprocess a large-scale dataset with the Beam DataFrames API, we can now train a model on a classification task on our preprocessed dataset. \n", + "\n", + "To learn more on how to get started with classifying structured data, refer to:\n", + "\n", + "* [Structred data classification from scratch](https://keras.io/examples/structured_data/structured_data_classification_from_scratch/)\n", + "\n", + "We suggest finding another dataset to try out the Beam DataFrames API processing with. Make sure think carefully about which features to include in your model and how they should be represented.\n", + "\n" + ], + "metadata": { + "id": "UOLr6YgOOSVQ" + } + }, + { + "cell_type": "markdown", + "source": [ + "# References\n", + "\n", + "* [Beam DataFrames overview](https://beam.apache.org/documentation/dsls/dataframes/overview) -- an overview of the Beam DataFrames API.\n", + "* [Differences from pandas](https://beam.apache.org/documentation/dsls/dataframes/differences-from-pandas) -- goes through some of the differences between Beam DataFrames and Pandas DataFrames, as well as some of the workarounds for unsupported operations.\n", + "* [10 minutes to Pandas](https://pandas.pydata.org/pandas-docs/stable/user_guide/10min.html) -- a quickstart guide to Pandas DataFrames.\n", + "* [Pandas DataFrame API](https://pandas.pydata.org/pandas-docs/stable/reference/frame.html) -- the API reference for Pandas DataFrames.\n", + "* [Data preparation and feature training in ML](https://developers.google.com/machine-learning/data-prep) -- A guideline on data transformation for ML training." + ], + "metadata": { + "id": "nG9WXXVcMCe_" + } + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/examples/notebooks/beam-ml/run_inference_pytorch_tensorflow_sklearn.ipynb b/examples/notebooks/beam-ml/run_inference_pytorch_tensorflow_sklearn.ipynb new file mode 100644 index 000000000000..cbca4a1e896b --- /dev/null +++ b/examples/notebooks/beam-ml/run_inference_pytorch_tensorflow_sklearn.ipynb @@ -0,0 +1,1178 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LzOTNrs_P6Vv" + }, + "outputs": [], + "source": [ + "# @title ###### Licensed to the Apache Software Foundation (ASF), Version 2.0 (the \"License\")\n", + "\n", + "# Licensed to the Apache Software Foundation (ASF) under one\n", + "# or more contributor license agreements. See the NOTICE file\n", + "# distributed with this work for additional information\n", + "# regarding copyright ownership. The ASF licenses this file\n", + "# to you under the Apache License, Version 2.0 (the\n", + "# \"License\"); you may not use this file except in compliance\n", + "# with the License. You may obtain a copy of the License at\n", + "#\n", + "# http://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing,\n", + "# software distributed under the License is distributed on an\n", + "# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n", + "# KIND, either express or implied. See the License for the\n", + "# specific language governing permissions and limitations\n", + "# under the License" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "faayYQYrQzY3" + }, + "source": [ + "## RunInference in Beam" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JjAt1GesQ9sg" + }, + "source": [ + "Starting with Apache Beam 2.40.0, a new API called RunInference can be used for using machine learning (ML) models to do local and remote inference with batch and streaming pipelines. RunInference API leverages Apache Beam concepts such as the BatchElements transform and the Shared class, to enable you to use models in your pipelines to create transforms optimized for machine learning inferences.\n", + "\n", + "One can find more details about RunInference API, here:https://beam.apache.org/documentation/sdks/python-machine-learning/" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A8xNRyZMW1yK" + }, + "source": [ + "In this notebook, we show how to use RunInference with three different popular ML frameworks: PyTorch, TensorFlow and Scikit-learn. We showcase three pipelines that uses a text classification model for generating prediction.\n", + "\n", + "The different steps needed to build this pipeline can be summarized as follows:\n", + "* Read the images.\n", + "* Preprocess the text if needed\n", + "* Inference with PyTorch/TensorFlow/Scikit-learn Model\n", + "* PostProcess the output from RunInference if needed " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CTtBTpsHZFCk" + }, + "source": [ + "### RunInference with a PyTorch Model\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5kkjbcIzZIf6" + }, + "source": [ + "#### Install Dependency" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "MRASwRTxY-2u", + "outputId": "28760c59-c4dc-4486-dbd2-e7ac2c92c3b8" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Requirement already satisfied: pip in /usr/local/lib/python3.7/dist-packages (22.3)\n", + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0m\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0mLooking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Requirement already satisfied: transformers in /usr/local/lib/python3.7/dist-packages (4.23.1)\n", + "Requirement already satisfied: tqdm>=4.27 in /usr/local/lib/python3.7/dist-packages (from transformers) (4.64.1)\n", + "Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.7/dist-packages (from transformers) (1.21.6)\n", + "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.7/dist-packages (from transformers) (21.3)\n", + "Requirement already satisfied: huggingface-hub<1.0,>=0.10.0 in /usr/local/lib/python3.7/dist-packages (from transformers) (0.10.1)\n", + "Requirement already satisfied: importlib-metadata in /usr/local/lib/python3.7/dist-packages (from transformers) (4.13.0)\n", + "Requirement already satisfied: pyyaml>=5.1 in /usr/local/lib/python3.7/dist-packages (from transformers) (6.0)\n", + "Requirement already satisfied: tokenizers!=0.11.3,<0.14,>=0.11.1 in /usr/local/lib/python3.7/dist-packages (from transformers) (0.13.1)\n", + "Requirement already satisfied: regex!=2019.12.17 in /usr/local/lib/python3.7/dist-packages (from transformers) (2022.6.2)\n", + "Requirement already satisfied: filelock in /usr/local/lib/python3.7/dist-packages (from transformers) (3.8.0)\n", + "Requirement already satisfied: requests in /usr/local/lib/python3.7/dist-packages (from transformers) (2.28.1)\n", + "Requirement already satisfied: typing-extensions>=3.7.4.3 in /usr/local/lib/python3.7/dist-packages (from huggingface-hub<1.0,>=0.10.0->transformers) (4.1.1)\n", + "Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /usr/local/lib/python3.7/dist-packages (from packaging>=20.0->transformers) (3.0.9)\n", + "Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata->transformers) (3.9.0)\n", + "Requirement already satisfied: charset-normalizer<3,>=2 in /usr/local/lib/python3.7/dist-packages (from requests->transformers) (2.1.1)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests->transformers) (2022.9.24)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests->transformers) (2.10)\n", + "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests->transformers) (1.24.3)\n", + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0mLooking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Collecting google-api-core==1.32\n", + " Using cached google_api_core-1.32.0-py2.py3-none-any.whl (93 kB)\n", + "Requirement already satisfied: protobuf<4.0.0dev,>=3.12.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core==1.32) (3.20.3)\n", + "Requirement already satisfied: pytz in /usr/local/lib/python3.7/dist-packages (from google-api-core==1.32) (2022.4)\n", + "Requirement already satisfied: google-auth<2.0dev,>=1.25.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core==1.32) (1.35.0)\n", + "Requirement already satisfied: googleapis-common-protos<2.0dev,>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core==1.32) (1.56.4)\n", + "Requirement already satisfied: setuptools>=40.3.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core==1.32) (57.4.0)\n", + "Requirement already satisfied: six>=1.13.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core==1.32) (1.15.0)\n", + "Requirement already satisfied: packaging>=14.3 in /usr/local/lib/python3.7/dist-packages (from google-api-core==1.32) (21.3)\n", + "Requirement already satisfied: requests<3.0.0dev,>=2.18.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core==1.32) (2.28.1)\n", + "Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.7/dist-packages (from google-auth<2.0dev,>=1.25.0->google-api-core==1.32) (4.9)\n", + "Requirement already satisfied: cachetools<5.0,>=2.0.0 in /usr/local/lib/python3.7/dist-packages (from google-auth<2.0dev,>=1.25.0->google-api-core==1.32) (4.2.4)\n", + "Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from google-auth<2.0dev,>=1.25.0->google-api-core==1.32) (0.2.8)\n", + "Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /usr/local/lib/python3.7/dist-packages (from packaging>=14.3->google-api-core==1.32) (3.0.9)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0dev,>=2.18.0->google-api-core==1.32) (2.10)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0dev,>=2.18.0->google-api-core==1.32) (2022.9.24)\n", + "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0dev,>=2.18.0->google-api-core==1.32) (1.24.3)\n", + "Requirement already satisfied: charset-normalizer<3,>=2 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0dev,>=2.18.0->google-api-core==1.32) (2.1.1)\n", + "Requirement already satisfied: pyasn1<0.5.0,>=0.4.6 in /usr/local/lib/python3.7/dist-packages (from pyasn1-modules>=0.2.1->google-auth<2.0dev,>=1.25.0->google-api-core==1.32) (0.4.8)\n", + "Installing collected packages: google-api-core\n", + " Attempting uninstall: google-api-core\n", + " Found existing installation: google-api-core 1.33.2\n", + " Uninstalling google-api-core-1.33.2:\n", + " Successfully uninstalled google-api-core-1.33.2\n", + "Successfully installed google-api-core-1.32.0\n", + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0m" + ] + }, + { + "data": { + "application/vnd.colab-display-data+json": { + "pip_warning": { + "packages": [ + "google" + ] + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "!pip install --upgrade pip\n", + "!pip install apache_beam[gcp]>=2.40.0\n", + "!pip install transformers\n", + "!pip install google-api-core==1.32" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ObRPUrlEbjHj" + }, + "source": [ + "#### Model\n", + "\n", + "We are using a pretrained text classification model, [distilbert-base-uncased-finetuned-sst-2-english](https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english?text=I+like+you.+I+love+you). This model is a fine-tune checkpoint of DistilBERT-base-uncased, fine-tuned on SST-2 dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "vfDyy4WNQaJM", + "outputId": "75683116-f415-4956-f44c-baa953c564e1" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error: Failed to call git rev-parse --git-dir --show-toplevel: \"fatal: not a git repository (or any of the parent directories): .git\\n\"\n", + "Git LFS initialized.\n", + "fatal: destination path 'distilbert-base-uncased-finetuned-sst-2-english' already exists and is not an empty directory.\n", + "'=2.40.0' distilbert-base-uncased-finetuned-sst-2-english sample_data\n" + ] + } + ], + "source": [ + "! git lfs install\n", + "! git clone https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english\n", + "! ls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vA1UmbFRb5C-" + }, + "source": [ + "#### Helper Functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c4ZwN8wsbvgK" + }, + "outputs": [], + "source": [ + "from collections import defaultdict\n", + "\n", + "import torch\n", + "from transformers import DistilBertForSequenceClassification, DistilBertTokenizer, DistilBertConfig\n", + "\n", + "import apache_beam as beam\n", + "from apache_beam.ml.inference import RunInference\n", + "from apache_beam.ml.inference.base import PredictionResult, KeyedModelHandler\n", + "from apache_beam.ml.inference.pytorch_inference import PytorchModelHandlerKeyedTensor\n", + "\n", + "\n", + "class HuggingFaceStripBatchingWrapper(DistilBertForSequenceClassification):\n", + " \"\"\"Wrapper around HugginFace model because RunInference requires a batch\n", + " as a list of dicts instead of a dict of lists. Another workaround can be found\n", + " here where they disable batching instead.\n", + " https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_language_modeling.py\"\"\"\n", + " def forward(self, **kwargs):\n", + " output = super().forward(**kwargs)\n", + " return [dict(zip(output, v)) for v in zip(*output.values())]\n", + "\n", + "\n", + "\n", + "class Tokenize(beam.DoFn):\n", + " def __init__(self, model_name: str):\n", + " self._model_name = model_name\n", + "\n", + " def setup(self):\n", + " self._tokenizer = DistilBertTokenizer.from_pretrained(self._model_name)\n", + " \n", + " def process(self, text_input: str):\n", + " # We need to pad the tokens tensors to max length to make sure that all the tensors\n", + " # are of the same length and hence stack-able by the RunInference API, normally you would batch first\n", + " # and tokenize the batch after and pad each tensor the the max length in the batch.\n", + " # see: https://beam.apache.org/documentation/sdks/python-machine-learning/#unable-to-batch-tensor-elements\n", + " tokens = self._tokenizer(text_input, return_tensors='pt', padding='max_length', max_length=512)\n", + " # squeeze because tokenization adds an extra dimension, which is empty\n", + " # in this case because we're tokenizing one element at a time.\n", + " tokens = {key: torch.squeeze(val) for key, val in tokens.items()}\n", + " return [(text_input, tokens)]\n", + "\n", + "class PostProcessor(beam.DoFn):\n", + " def process(self, tuple_):\n", + " text_input, prediction_result = tuple_\n", + " softmax = torch.nn.Softmax(dim=-1)(prediction_result.inference['logits']).detach().numpy()\n", + " return [{\"input\": text_input, \"softmax\": softmax}]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WYYbQTMWctkW" + }, + "source": [ + "#### RunInference Pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lLb8D2n2n09n" + }, + "outputs": [], + "source": [ + "inputs = [\n", + " \"This is the worst food I have ever eaten\",\n", + " \"In my soul and in my heart, I’m convinced I’m wrong!\",\n", + " \"Be with me always—take any form—drive me mad! only do not leave me in this abyss, where I cannot find you!\",\n", + " \"Do I want to live? Would you like to live with your soul in the grave?\",\n", + " \"Honest people don’t hide their deeds.\",\n", + " \"Nelly, I am Heathcliff! He’s always, always in my mind: not as a pleasure, any more than I am always a pleasure to myself, but as my own being.\",\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 269 + }, + "id": "TDmMARxGb751", + "outputId": "437e168a-b4c5-463b-ce5f-09a8cb8d8191" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:10: FutureWarning: PytorchModelHandlerKeyedTensor is experimental. No backwards-compatibility guarantees.\n", + " # Remove the CWD from sys.path while we load stuff.\n", + "WARNING:apache_beam.runners.interactive.interactive_environment:Dependencies required for Interactive Beam PCollection visualization are not available, please use: `pip install apache-beam[interactive]` to install necessary dependencies to enable all data visualization features.\n" + ] + }, + { + "data": { + "application/javascript": "\n if (typeof window.interactive_beam_jquery == 'undefined') {\n var jqueryScript = document.createElement('script');\n jqueryScript.src = 'https://code.jquery.com/jquery-3.4.1.slim.min.js';\n jqueryScript.type = 'text/javascript';\n jqueryScript.onload = function() {\n var datatableScript = document.createElement('script');\n datatableScript.src = 'https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js';\n datatableScript.type = 'text/javascript';\n datatableScript.onload = function() {\n window.interactive_beam_jquery = jQuery.noConflict(true);\n window.interactive_beam_jquery(document).ready(function($){\n \n });\n }\n document.head.appendChild(datatableScript);\n };\n document.head.appendChild(jqueryScript);\n } else {\n window.interactive_beam_jquery(document).ready(function($){\n \n });\n }" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.7/dist-packages/dill/_dill.py:472: FutureWarning: PytorchModelHandlerKeyedTensor is experimental. No backwards-compatibility guarantees.\n", + " obj = StockUnpickler.load(self)\n", + "/usr/local/lib/python3.7/dist-packages/dill/_dill.py:472: FutureWarning: PytorchModelHandlerKeyedTensor is experimental. No backwards-compatibility guarantees.\n", + " obj = StockUnpickler.load(self)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input: This is the worst food I have ever eaten -> negative=99.9777%/positive=0.0223%\n", + "Input: In my soul and in my heart, I’m convinced I’m wrong! -> negative=1.6313%/positive=98.3687%\n", + "Input: Be with me always—take any form—drive me mad! only do not leave me in this abyss, where I cannot find you! -> negative=62.1188%/positive=37.8812%\n", + "Input: Do I want to live? Would you like to live with your soul in the grave? -> negative=73.6841%/positive=26.3159%\n", + "Input: Honest people don’t hide their deeds. -> negative=0.2377%/positive=99.7623%\n", + "Input: Nelly, I am Heathcliff! He’s always, always in my mind: not as a pleasure, any more than I am always a pleasure to myself, but as my own being. -> negative=0.0672%/positive=99.9328%\n" + ] + } + ], + "source": [ + "model_handler = PytorchModelHandlerKeyedTensor(\n", + " state_dict_path=\"./distilbert-base-uncased-finetuned-sst-2-english/pytorch_model.bin\",\n", + " model_class=HuggingFaceStripBatchingWrapper,\n", + " model_params={\"config\": DistilBertConfig.from_pretrained(\"./distilbert-base-uncased-finetuned-sst-2-english/config.json\")},\n", + " device='cuda:0')\n", + "\n", + "keyed_model_handler = KeyedModelHandler(model_handler)\n", + "\n", + "with beam.Pipeline() as pipeline:\n", + " _ = (pipeline | \"Create inputs\" >> beam.Create(inputs)\n", + " | \"Tokenize\" >> beam.ParDo(Tokenize(\"distilbert-base-uncased-finetuned-sst-2-english\"))\n", + " | \"Inference\" >> RunInference(model_handler=keyed_model_handler)\n", + " | \"Postprocess\" >> beam.ParDo(PostProcessor())\n", + " | \"Print\" >> beam.Map(lambda x: print(f\"Input: {x['input']} -> negative={100 * x['softmax'][0]:.4f}%/positive={100 * x['softmax'][1]:.4f}%\"))\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7KXeaQg3eCcp" + }, + "source": [ + "### RunInference with a TensorFlow Model\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hEHxNka4eOhC" + }, + "source": [ + "Note: Tensorflow models are supported through tfx-bsl." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8KyXULYbeYlD" + }, + "source": [ + "#### Install Dependency" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "uqWJhQBlc4oT", + "outputId": "2a17a966-fe2d-45d8-b6b9-02534f40c9a8" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Requirement already satisfied: pip in /usr/local/lib/python3.7/dist-packages (22.3)\n", + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0mLooking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Collecting apache_beam[gcp]==2.41.0\n", + " Downloading apache_beam-2.41.0-cp37-cp37m-manylinux2010_x86_64.whl (10.9 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m10.9/10.9 MB\u001b[0m \u001b[31m42.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: proto-plus<2,>=1.7.1 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.22.1)\n", + "Requirement already satisfied: pydot<2,>=1.2.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.3.0)\n", + "Requirement already satisfied: numpy<1.23.0,>=1.14.3 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.21.6)\n", + "Requirement already satisfied: pyarrow<8.0.0,>=0.15.1 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (6.0.1)\n", + "Requirement already satisfied: fastavro<2,>=0.23.6 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.6.1)\n", + "Requirement already satisfied: hdfs<3.0.0,>=2.1.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (2.7.0)\n", + "Requirement already satisfied: dill<0.3.2,>=0.3.1.1 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (0.3.1.1)\n", + "Requirement already satisfied: requests<3.0.0,>=2.24.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (2.28.1)\n", + "Requirement already satisfied: python-dateutil<3,>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (2.8.2)\n", + "Requirement already satisfied: pytz>=2018.3 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (2022.4)\n", + "Requirement already satisfied: crcmod<2.0,>=1.7 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.7)\n", + "Requirement already satisfied: protobuf<4,>=3.12.2 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (3.20.3)\n", + "Requirement already satisfied: cloudpickle<3,>=2.1.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (2.1.0)\n", + "Requirement already satisfied: orjson<4.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (3.8.0)\n", + "Requirement already satisfied: pymongo<4.0.0,>=3.8.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (3.12.3)\n", + "Requirement already satisfied: grpcio<2,>=1.33.1 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.49.1)\n", + "Requirement already satisfied: typing-extensions>=3.7.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (4.1.1)\n", + "Requirement already satisfied: httplib2<0.21.0,>=0.8 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (0.17.4)\n", + "Requirement already satisfied: google-cloud-language<2,>=1.3.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.3.2)\n", + "Requirement already satisfied: google-cloud-pubsub<3,>=2.1.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (2.13.10)\n", + "Requirement already satisfied: google-apitools<0.5.32,>=0.5.31 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (0.5.31)\n", + "Requirement already satisfied: google-cloud-recommendations-ai<0.8.0,>=0.1.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (0.7.1)\n", + "Requirement already satisfied: cachetools<5,>=3.1.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (4.2.4)\n", + "Requirement already satisfied: google-cloud-bigtable<2,>=0.31.1 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.7.2)\n", + "Requirement already satisfied: google-cloud-dlp<4,>=3.0.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (3.9.2)\n", + "Requirement already satisfied: google-auth-httplib2<0.2.0,>=0.1.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (0.1.0)\n", + "Requirement already satisfied: google-cloud-datastore<2,>=1.8.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.8.0)\n", + "Requirement already satisfied: google-cloud-spanner<2,>=1.13.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.19.3)\n", + "Requirement already satisfied: google-cloud-bigquery-storage<2.14,>=2.6.3 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (2.13.2)\n", + "Requirement already satisfied: google-cloud-vision<2,>=0.38.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.0.2)\n", + "Requirement already satisfied: google-cloud-core<3,>=0.28.1 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.7.3)\n", + "Requirement already satisfied: google-cloud-videointelligence<2,>=1.8.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.16.3)\n", + "Requirement already satisfied: grpcio-gcp<1,>=0.2.2 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (0.2.2)\n", + "Requirement already satisfied: google-cloud-pubsublite<2,>=1.2.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.6.0)\n", + "Requirement already satisfied: google-auth<3,>=1.18.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.35.0)\n", + "Requirement already satisfied: google-cloud-bigquery<3,>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.21.0)\n", + "Requirement already satisfied: google-api-core!=2.8.2,<3 in /usr/local/lib/python3.7/dist-packages (from apache_beam[gcp]==2.41.0) (1.32.0)\n", + "Requirement already satisfied: six>=1.13.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core!=2.8.2,<3->apache_beam[gcp]==2.41.0) (1.15.0)\n", + "Requirement already satisfied: packaging>=14.3 in /usr/local/lib/python3.7/dist-packages (from google-api-core!=2.8.2,<3->apache_beam[gcp]==2.41.0) (21.3)\n", + "Requirement already satisfied: googleapis-common-protos<2.0dev,>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core!=2.8.2,<3->apache_beam[gcp]==2.41.0) (1.56.4)\n", + "Requirement already satisfied: setuptools>=40.3.0 in /usr/local/lib/python3.7/dist-packages (from google-api-core!=2.8.2,<3->apache_beam[gcp]==2.41.0) (57.4.0)\n", + "Requirement already satisfied: fasteners>=0.14 in /usr/local/lib/python3.7/dist-packages (from google-apitools<0.5.32,>=0.5.31->apache_beam[gcp]==2.41.0) (0.18)\n", + "Requirement already satisfied: oauth2client>=1.4.12 in /usr/local/lib/python3.7/dist-packages (from google-apitools<0.5.32,>=0.5.31->apache_beam[gcp]==2.41.0) (4.1.3)\n", + "Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.18.0->apache_beam[gcp]==2.41.0) (4.9)\n", + "Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.18.0->apache_beam[gcp]==2.41.0) (0.2.8)\n", + "Requirement already satisfied: google-resumable-media!=0.4.0,<0.5.0dev,>=0.3.1 in /usr/local/lib/python3.7/dist-packages (from google-cloud-bigquery<3,>=1.6.0->apache_beam[gcp]==2.41.0) (0.4.1)\n", + "Requirement already satisfied: grpc-google-iam-v1<0.13dev,>=0.12.3 in /usr/local/lib/python3.7/dist-packages (from google-cloud-bigtable<2,>=0.31.1->apache_beam[gcp]==2.41.0) (0.12.4)\n", + "Requirement already satisfied: grpcio-status>=1.16.0 in /usr/local/lib/python3.7/dist-packages (from google-cloud-pubsub<3,>=2.1.0->apache_beam[gcp]==2.41.0) (1.48.2)\n", + "Requirement already satisfied: overrides<7.0.0,>=6.0.1 in /usr/local/lib/python3.7/dist-packages (from google-cloud-pubsublite<2,>=1.2.0->apache_beam[gcp]==2.41.0) (6.5.0)\n", + "Requirement already satisfied: docopt in /usr/local/lib/python3.7/dist-packages (from hdfs<3.0.0,>=2.1.0->apache_beam[gcp]==2.41.0) (0.6.2)\n", + "Requirement already satisfied: pyparsing>=2.1.4 in /usr/local/lib/python3.7/dist-packages (from pydot<2,>=1.2.0->apache_beam[gcp]==2.41.0) (3.0.9)\n", + "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0,>=2.24.0->apache_beam[gcp]==2.41.0) (1.24.3)\n", + "Requirement already satisfied: charset-normalizer<3,>=2 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0,>=2.24.0->apache_beam[gcp]==2.41.0) (2.1.1)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0,>=2.24.0->apache_beam[gcp]==2.41.0) (2.10)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0,>=2.24.0->apache_beam[gcp]==2.41.0) (2022.9.24)\n", + "Requirement already satisfied: pyasn1>=0.1.7 in /usr/local/lib/python3.7/dist-packages (from oauth2client>=1.4.12->google-apitools<0.5.32,>=0.5.31->apache_beam[gcp]==2.41.0) (0.4.8)\n", + "Installing collected packages: apache_beam\n", + " Attempting uninstall: apache_beam\n", + " Found existing installation: apache-beam 2.42.0\n", + " Uninstalling apache-beam-2.42.0:\n", + " Successfully uninstalled apache-beam-2.42.0\n", + "Successfully installed apache_beam-2.41.0\n", + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0m" + ] + }, + { + "data": { + "application/vnd.colab-display-data+json": { + "pip_warning": { + "packages": [ + "apache_beam" + ] + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Collecting tensorflow==2.8\n", + " Downloading https://us-python.pkg.dev/colab-wheels/public/tensorflow/tensorflow-2.8.0%2Bzzzcolab20220506162203-cp37-cp37m-linux_x86_64.whl (668.3 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m668.3/668.3 MB\u001b[0m \u001b[31m2.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: libclang>=9.0.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (14.0.6)\n", + "Requirement already satisfied: h5py>=2.9.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (3.1.0)\n", + "Requirement already satisfied: google-pasta>=0.1.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (0.2.0)\n", + "Collecting keras<2.9,>=2.8.0rc0\n", + " Downloading keras-2.8.0-py2.py3-none-any.whl (1.4 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.4/1.4 MB\u001b[0m \u001b[31m17.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: flatbuffers>=1.12 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (1.12)\n", + "Requirement already satisfied: gast>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (0.4.0)\n", + "Requirement already satisfied: opt-einsum>=2.3.2 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (3.3.0)\n", + "Requirement already satisfied: six>=1.12.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (1.15.0)\n", + "Requirement already satisfied: grpcio<2.0,>=1.24.3 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (1.49.1)\n", + "Requirement already satisfied: typing-extensions>=3.6.6 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (4.1.1)\n", + "Requirement already satisfied: tensorflow-io-gcs-filesystem>=0.23.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (0.27.0)\n", + "Requirement already satisfied: setuptools in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (57.4.0)\n", + "Requirement already satisfied: keras-preprocessing>=1.1.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (1.1.2)\n", + "Requirement already satisfied: absl-py>=0.4.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (1.3.0)\n", + "Requirement already satisfied: protobuf>=3.9.2 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (3.20.3)\n", + "Requirement already satisfied: astunparse>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (1.6.3)\n", + "Requirement already satisfied: termcolor>=1.1.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (2.0.1)\n", + "Collecting tf-estimator-nightly==2.8.0.dev2021122109\n", + " Downloading tf_estimator_nightly-2.8.0.dev2021122109-py2.py3-none-any.whl (462 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m462.5/462.5 kB\u001b[0m \u001b[31m19.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting tensorboard<2.9,>=2.8\n", + " Downloading tensorboard-2.8.0-py3-none-any.whl (5.8 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m5.8/5.8 MB\u001b[0m \u001b[31m62.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: wrapt>=1.11.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (1.14.1)\n", + "Requirement already satisfied: numpy>=1.20 in /usr/local/lib/python3.7/dist-packages (from tensorflow==2.8) (1.21.6)\n", + "Requirement already satisfied: wheel<1.0,>=0.23.0 in /usr/local/lib/python3.7/dist-packages (from astunparse>=1.6.0->tensorflow==2.8) (0.37.1)\n", + "Requirement already satisfied: cached-property in /usr/local/lib/python3.7/dist-packages (from h5py>=2.9.0->tensorflow==2.8) (1.5.2)\n", + "Requirement already satisfied: markdown>=2.6.8 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow==2.8) (3.4.1)\n", + "Requirement already satisfied: requests<3,>=2.21.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow==2.8) (2.28.1)\n", + "Requirement already satisfied: google-auth-oauthlib<0.5,>=0.4.1 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow==2.8) (0.4.6)\n", + "Requirement already satisfied: werkzeug>=0.11.15 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow==2.8) (1.0.1)\n", + "Requirement already satisfied: tensorboard-plugin-wit>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow==2.8) (1.8.1)\n", + "Requirement already satisfied: google-auth<3,>=1.6.3 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow==2.8) (1.35.0)\n", + "Requirement already satisfied: tensorboard-data-server<0.7.0,>=0.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow==2.8) (0.6.1)\n", + "Requirement already satisfied: cachetools<5.0,>=2.0.0 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow==2.8) (4.2.4)\n", + "Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow==2.8) (0.2.8)\n", + "Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow==2.8) (4.9)\n", + "Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/lib/python3.7/dist-packages (from google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.9,>=2.8->tensorflow==2.8) (1.3.1)\n", + "Requirement already satisfied: importlib-metadata>=4.4 in /usr/local/lib/python3.7/dist-packages (from markdown>=2.6.8->tensorboard<2.9,>=2.8->tensorflow==2.8) (4.13.0)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow==2.8) (2.10)\n", + "Requirement already satisfied: charset-normalizer<3,>=2 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow==2.8) (2.1.1)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow==2.8) (2022.9.24)\n", + "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow==2.8) (1.24.3)\n", + "Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata>=4.4->markdown>=2.6.8->tensorboard<2.9,>=2.8->tensorflow==2.8) (3.9.0)\n", + "Requirement already satisfied: pyasn1<0.5.0,>=0.4.6 in /usr/local/lib/python3.7/dist-packages (from pyasn1-modules>=0.2.1->google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow==2.8) (0.4.8)\n", + "Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/lib/python3.7/dist-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.9,>=2.8->tensorflow==2.8) (3.2.1)\n", + "Installing collected packages: tf-estimator-nightly, keras, tensorboard, tensorflow\n", + " Attempting uninstall: keras\n", + " Found existing installation: keras 2.9.0\n", + " Uninstalling keras-2.9.0:\n", + " Successfully uninstalled keras-2.9.0\n", + " Attempting uninstall: tensorboard\n", + " Found existing installation: tensorboard 2.9.1\n", + " Uninstalling tensorboard-2.9.1:\n", + " Successfully uninstalled tensorboard-2.9.1\n", + " Attempting uninstall: tensorflow\n", + " Found existing installation: tensorflow 2.9.2\n", + " Uninstalling tensorflow-2.9.2:\n", + " Successfully uninstalled tensorflow-2.9.2\n", + "Successfully installed keras-2.8.0 tensorboard-2.8.0 tensorflow-2.8.0+zzzcolab20220506162203 tf-estimator-nightly-2.8.0.dev2021122109\n", + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0mLooking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Collecting tfx_bsl\n", + " Downloading tfx_bsl-1.10.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (21.6 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m21.6/21.6 MB\u001b[0m \u001b[31m49.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: tensorflow-metadata<1.11.0,>=1.10.0 in /usr/local/lib/python3.7/dist-packages (from tfx_bsl) (1.10.0)\n", + "Collecting tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5\n", + " Downloading tensorflow-2.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (578.0 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m578.0/578.0 MB\u001b[0m \u001b[31m2.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: google-api-python-client<2,>=1.7.11 in /usr/local/lib/python3.7/dist-packages (from tfx_bsl) (1.12.11)\n", + "Collecting tensorflow-serving-api!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15\n", + " Downloading tensorflow_serving_api-2.10.0-py2.py3-none-any.whl (37 kB)\n", + "Requirement already satisfied: numpy<2,>=1.16 in /usr/local/lib/python3.7/dist-packages (from tfx_bsl) (1.21.6)\n", + "Requirement already satisfied: apache-beam[gcp]<3,>=2.40 in /usr/local/lib/python3.7/dist-packages (from tfx_bsl) (2.41.0)\n", + "Requirement already satisfied: absl-py<2.0.0,>=0.9 in /usr/local/lib/python3.7/dist-packages (from tfx_bsl) (1.3.0)\n", + "Requirement already satisfied: protobuf<3.21,>=3.13 in /usr/local/lib/python3.7/dist-packages (from tfx_bsl) (3.20.3)\n", + "Requirement already satisfied: pyarrow<7,>=6 in /usr/local/lib/python3.7/dist-packages (from tfx_bsl) (6.0.1)\n", + "Requirement already satisfied: pandas<2,>=1.0 in /usr/local/lib/python3.7/dist-packages (from tfx_bsl) (1.3.5)\n", + "Requirement already satisfied: requests<3.0.0,>=2.24.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (2.28.1)\n", + "Requirement already satisfied: dill<0.3.2,>=0.3.1.1 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.3.1.1)\n", + "Requirement already satisfied: pymongo<4.0.0,>=3.8.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (3.12.3)\n", + "Requirement already satisfied: cloudpickle<3,>=2.1.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (2.1.0)\n", + "Requirement already satisfied: fastavro<2,>=0.23.6 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.6.1)\n", + "Requirement already satisfied: pydot<2,>=1.2.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.3.0)\n", + "Requirement already satisfied: pytz>=2018.3 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (2022.4)\n", + "Requirement already satisfied: grpcio<2,>=1.33.1 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.49.1)\n", + "Requirement already satisfied: crcmod<2.0,>=1.7 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.7)\n", + "Requirement already satisfied: httplib2<0.21.0,>=0.8 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.17.4)\n", + "Requirement already satisfied: hdfs<3.0.0,>=2.1.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (2.7.0)\n", + "Requirement already satisfied: proto-plus<2,>=1.7.1 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.22.1)\n", + "Requirement already satisfied: typing-extensions>=3.7.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (4.1.1)\n", + "Requirement already satisfied: orjson<4.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (3.8.0)\n", + "Requirement already satisfied: python-dateutil<3,>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (2.8.2)\n", + "Requirement already satisfied: cachetools<5,>=3.1.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (4.2.4)\n", + "Requirement already satisfied: google-cloud-spanner<2,>=1.13.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.19.3)\n", + "Requirement already satisfied: grpcio-gcp<1,>=0.2.2 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.2.2)\n", + "Requirement already satisfied: google-cloud-videointelligence<2,>=1.8.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.16.3)\n", + "Requirement already satisfied: google-cloud-language<2,>=1.3.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.3.2)\n", + "Requirement already satisfied: google-cloud-pubsub<3,>=2.1.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (2.13.10)\n", + "Requirement already satisfied: google-cloud-core<3,>=0.28.1 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.7.3)\n", + "Requirement already satisfied: google-cloud-dlp<4,>=3.0.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (3.9.2)\n", + "Requirement already satisfied: google-auth<3,>=1.18.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.35.0)\n", + "Requirement already satisfied: google-auth-httplib2<0.2.0,>=0.1.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.1.0)\n", + "Requirement already satisfied: google-cloud-bigtable<2,>=0.31.1 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.7.2)\n", + "Requirement already satisfied: google-cloud-bigquery-storage<2.14,>=2.6.3 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (2.13.2)\n", + "Requirement already satisfied: google-api-core!=2.8.2,<3 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.32.0)\n", + "Requirement already satisfied: google-cloud-datastore<2,>=1.8.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.8.0)\n", + "Requirement already satisfied: google-cloud-recommendations-ai<0.8.0,>=0.1.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.7.1)\n", + "Requirement already satisfied: google-apitools<0.5.32,>=0.5.31 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.5.31)\n", + "Requirement already satisfied: google-cloud-vision<2,>=0.38.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.0.2)\n", + "Requirement already satisfied: google-cloud-bigquery<3,>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.21.0)\n", + "Requirement already satisfied: google-cloud-pubsublite<2,>=1.2.0 in /usr/local/lib/python3.7/dist-packages (from apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.6.0)\n", + "Requirement already satisfied: six<2dev,>=1.13.0 in /usr/local/lib/python3.7/dist-packages (from google-api-python-client<2,>=1.7.11->tfx_bsl) (1.15.0)\n", + "Requirement already satisfied: uritemplate<4dev,>=3.0.0 in /usr/local/lib/python3.7/dist-packages (from google-api-python-client<2,>=1.7.11->tfx_bsl) (3.0.1)\n", + "Collecting tensorflow-estimator<2.11,>=2.10.0\n", + " Downloading tensorflow_estimator-2.10.0-py2.py3-none-any.whl (438 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m438.7/438.7 kB\u001b[0m \u001b[31m31.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: opt-einsum>=2.3.2 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (3.3.0)\n", + "Requirement already satisfied: h5py>=2.9.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (3.1.0)\n", + "Requirement already satisfied: packaging in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (21.3)\n", + "Requirement already satisfied: gast<=0.4.0,>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (0.4.0)\n", + "Requirement already satisfied: astunparse>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (1.6.3)\n", + "Requirement already satisfied: termcolor>=1.1.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (2.0.1)\n", + "Requirement already satisfied: setuptools in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (57.4.0)\n", + "Collecting protobuf<3.21,>=3.13\n", + " Downloading protobuf-3.19.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.1/1.1 MB\u001b[0m \u001b[31m24.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: google-pasta>=0.1.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (0.2.0)\n", + "Requirement already satisfied: wrapt>=1.11.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (1.14.1)\n", + "Requirement already satisfied: tensorflow-io-gcs-filesystem>=0.23.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (0.27.0)\n", + "Collecting flatbuffers>=2.0\n", + " Downloading flatbuffers-22.9.24-py2.py3-none-any.whl (26 kB)\n", + "Collecting tensorboard<2.11,>=2.10\n", + " Downloading tensorboard-2.10.1-py3-none-any.whl (5.9 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m5.9/5.9 MB\u001b[0m \u001b[31m58.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting keras<2.11,>=2.10.0\n", + " Downloading keras-2.10.0-py2.py3-none-any.whl (1.7 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.7/1.7 MB\u001b[0m \u001b[31m38.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: libclang>=13.0.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (14.0.6)\n", + "Requirement already satisfied: keras-preprocessing>=1.1.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (1.1.2)\n", + "Requirement already satisfied: googleapis-common-protos<2,>=1.52.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow-metadata<1.11.0,>=1.10.0->tfx_bsl) (1.56.4)\n", + "Requirement already satisfied: wheel<1.0,>=0.23.0 in /usr/local/lib/python3.7/dist-packages (from astunparse>=1.6.0->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (0.37.1)\n", + "Requirement already satisfied: fasteners>=0.14 in /usr/local/lib/python3.7/dist-packages (from google-apitools<0.5.32,>=0.5.31->apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.18)\n", + "Requirement already satisfied: oauth2client>=1.4.12 in /usr/local/lib/python3.7/dist-packages (from google-apitools<0.5.32,>=0.5.31->apache-beam[gcp]<3,>=2.40->tfx_bsl) (4.1.3)\n", + "Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.18.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.2.8)\n", + "Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.18.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (4.9)\n", + "Requirement already satisfied: google-resumable-media!=0.4.0,<0.5.0dev,>=0.3.1 in /usr/local/lib/python3.7/dist-packages (from google-cloud-bigquery<3,>=1.6.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.4.1)\n", + "Requirement already satisfied: grpc-google-iam-v1<0.13dev,>=0.12.3 in /usr/local/lib/python3.7/dist-packages (from google-cloud-bigtable<2,>=0.31.1->apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.12.4)\n", + "Requirement already satisfied: grpcio-status>=1.16.0 in /usr/local/lib/python3.7/dist-packages (from google-cloud-pubsub<3,>=2.1.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.48.2)\n", + "Requirement already satisfied: overrides<7.0.0,>=6.0.1 in /usr/local/lib/python3.7/dist-packages (from google-cloud-pubsublite<2,>=1.2.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (6.5.0)\n", + "Requirement already satisfied: cached-property in /usr/local/lib/python3.7/dist-packages (from h5py>=2.9.0->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (1.5.2)\n", + "Requirement already satisfied: docopt in /usr/local/lib/python3.7/dist-packages (from hdfs<3.0.0,>=2.1.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.6.2)\n", + "Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /usr/local/lib/python3.7/dist-packages (from packaging->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (3.0.9)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0,>=2.24.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (2022.9.24)\n", + "Requirement already satisfied: charset-normalizer<3,>=2 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0,>=2.24.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (2.1.1)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0,>=2.24.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (2.10)\n", + "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3.0.0,>=2.24.0->apache-beam[gcp]<3,>=2.40->tfx_bsl) (1.24.3)\n", + "Requirement already satisfied: werkzeug>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (1.0.1)\n", + "Requirement already satisfied: google-auth-oauthlib<0.5,>=0.4.1 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (0.4.6)\n", + "Requirement already satisfied: tensorboard-plugin-wit>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (1.8.1)\n", + "Requirement already satisfied: tensorboard-data-server<0.7.0,>=0.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (0.6.1)\n", + "Requirement already satisfied: markdown>=2.6.8 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (3.4.1)\n", + "Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/lib/python3.7/dist-packages (from google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (1.3.1)\n", + "Requirement already satisfied: importlib-metadata>=4.4 in /usr/local/lib/python3.7/dist-packages (from markdown>=2.6.8->tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (4.13.0)\n", + "Requirement already satisfied: pyasn1>=0.1.7 in /usr/local/lib/python3.7/dist-packages (from oauth2client>=1.4.12->google-apitools<0.5.32,>=0.5.31->apache-beam[gcp]<3,>=2.40->tfx_bsl) (0.4.8)\n", + "Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata>=4.4->markdown>=2.6.8->tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (3.9.0)\n", + "Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/lib/python3.7/dist-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.11,>=2.10->tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5->tfx_bsl) (3.2.1)\n", + "Installing collected packages: keras, flatbuffers, tensorflow-estimator, protobuf, tensorboard, tensorflow, tensorflow-serving-api, tfx_bsl\n", + " Attempting uninstall: keras\n", + " Found existing installation: keras 2.8.0\n", + " Uninstalling keras-2.8.0:\n", + " Successfully uninstalled keras-2.8.0\n", + " Attempting uninstall: flatbuffers\n", + " Found existing installation: flatbuffers 1.12\n", + " Uninstalling flatbuffers-1.12:\n", + " Successfully uninstalled flatbuffers-1.12\n", + " Attempting uninstall: tensorflow-estimator\n", + " Found existing installation: tensorflow-estimator 2.9.0\n", + " Uninstalling tensorflow-estimator-2.9.0:\n", + " Successfully uninstalled tensorflow-estimator-2.9.0\n", + " Attempting uninstall: protobuf\n", + " Found existing installation: protobuf 3.20.3\n", + " Uninstalling protobuf-3.20.3:\n", + " Successfully uninstalled protobuf-3.20.3\n", + " Attempting uninstall: tensorboard\n", + " Found existing installation: tensorboard 2.8.0\n", + " Uninstalling tensorboard-2.8.0:\n", + " Successfully uninstalled tensorboard-2.8.0\n", + " Attempting uninstall: tensorflow\n", + " Found existing installation: tensorflow 2.8.0+zzzcolab20220506162203\n", + " Uninstalling tensorflow-2.8.0+zzzcolab20220506162203:\n", + " Successfully uninstalled tensorflow-2.8.0+zzzcolab20220506162203\n", + "Successfully installed flatbuffers-22.9.24 keras-2.10.0 protobuf-3.19.6 tensorboard-2.10.1 tensorflow-2.10.0 tensorflow-estimator-2.10.0 tensorflow-serving-api-2.10.0 tfx_bsl-1.10.1\n", + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0m" + ] + }, + { + "data": { + "application/vnd.colab-display-data+json": { + "pip_warning": { + "packages": [ + "google" + ] + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Collecting tensorflow-text==2.8.1\n", + " Downloading tensorflow_text-2.8.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (4.9 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.9/4.9 MB\u001b[0m \u001b[31m39.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: tensorflow-hub>=0.8.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow-text==2.8.1) (0.12.0)\n", + "Collecting tensorflow<2.9,>=2.8.0\n", + " Downloading tensorflow-2.8.3-cp37-cp37m-manylinux2010_x86_64.whl (497.9 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m497.9/497.9 MB\u001b[0m \u001b[31m2.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: termcolor>=1.1.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (2.0.1)\n", + "Requirement already satisfied: protobuf<3.20,>=3.9.2 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (3.19.6)\n", + "Requirement already satisfied: flatbuffers>=1.12 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (22.9.24)\n", + "Requirement already satisfied: typing-extensions>=3.6.6 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (4.1.1)\n", + "Requirement already satisfied: setuptools in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (57.4.0)\n", + "Requirement already satisfied: opt-einsum>=2.3.2 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (3.3.0)\n", + "Requirement already satisfied: grpcio<2.0,>=1.24.3 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.49.1)\n", + "Requirement already satisfied: absl-py>=0.4.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.3.0)\n", + "Collecting tensorboard<2.9,>=2.8\n", + " Using cached tensorboard-2.8.0-py3-none-any.whl (5.8 MB)\n", + "Collecting tensorflow-estimator<2.9,>=2.8\n", + " Downloading tensorflow_estimator-2.8.0-py2.py3-none-any.whl (462 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m462.3/462.3 kB\u001b[0m \u001b[31m25.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: numpy>=1.20 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.21.6)\n", + "Requirement already satisfied: tensorflow-io-gcs-filesystem>=0.23.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (0.27.0)\n", + "Requirement already satisfied: libclang>=9.0.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (14.0.6)\n", + "Requirement already satisfied: wrapt>=1.11.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.14.1)\n", + "Requirement already satisfied: google-pasta>=0.1.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (0.2.0)\n", + "Collecting keras<2.9,>=2.8.0rc0\n", + " Using cached keras-2.8.0-py2.py3-none-any.whl (1.4 MB)\n", + "Requirement already satisfied: gast>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (0.4.0)\n", + "Requirement already satisfied: h5py>=2.9.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (3.1.0)\n", + "Requirement already satisfied: six>=1.12.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.15.0)\n", + "Requirement already satisfied: keras-preprocessing>=1.1.1 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.1.2)\n", + "Requirement already satisfied: astunparse>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.6.3)\n", + "Requirement already satisfied: wheel<1.0,>=0.23.0 in /usr/local/lib/python3.7/dist-packages (from astunparse>=1.6.0->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (0.37.1)\n", + "Requirement already satisfied: cached-property in /usr/local/lib/python3.7/dist-packages (from h5py>=2.9.0->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.5.2)\n", + "Requirement already satisfied: markdown>=2.6.8 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (3.4.1)\n", + "Requirement already satisfied: google-auth-oauthlib<0.5,>=0.4.1 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (0.4.6)\n", + "Requirement already satisfied: tensorboard-plugin-wit>=1.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.8.1)\n", + "Requirement already satisfied: requests<3,>=2.21.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (2.28.1)\n", + "Requirement already satisfied: google-auth<3,>=1.6.3 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.35.0)\n", + "Requirement already satisfied: werkzeug>=0.11.15 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.0.1)\n", + "Requirement already satisfied: tensorboard-data-server<0.7.0,>=0.6.0 in /usr/local/lib/python3.7/dist-packages (from tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (0.6.1)\n", + "Requirement already satisfied: rsa<5,>=3.1.4 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (4.9)\n", + "Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (0.2.8)\n", + "Requirement already satisfied: cachetools<5.0,>=2.0.0 in /usr/local/lib/python3.7/dist-packages (from google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (4.2.4)\n", + "Requirement already satisfied: requests-oauthlib>=0.7.0 in /usr/local/lib/python3.7/dist-packages (from google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.3.1)\n", + "Requirement already satisfied: importlib-metadata>=4.4 in /usr/local/lib/python3.7/dist-packages (from markdown>=2.6.8->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (4.13.0)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (2022.9.24)\n", + "Requirement already satisfied: charset-normalizer<3,>=2 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (2.1.1)\n", + "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (2.10)\n", + "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.21.0->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (1.24.3)\n", + "Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata>=4.4->markdown>=2.6.8->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (3.9.0)\n", + "Requirement already satisfied: pyasn1<0.5.0,>=0.4.6 in /usr/local/lib/python3.7/dist-packages (from pyasn1-modules>=0.2.1->google-auth<3,>=1.6.3->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (0.4.8)\n", + "Requirement already satisfied: oauthlib>=3.0.0 in /usr/local/lib/python3.7/dist-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.9,>=2.8->tensorflow<2.9,>=2.8.0->tensorflow-text==2.8.1) (3.2.1)\n", + "Installing collected packages: tensorflow-estimator, keras, tensorboard, tensorflow, tensorflow-text\n", + " Attempting uninstall: tensorflow-estimator\n", + " Found existing installation: tensorflow-estimator 2.10.0\n", + " Uninstalling tensorflow-estimator-2.10.0:\n", + " Successfully uninstalled tensorflow-estimator-2.10.0\n", + " Attempting uninstall: keras\n", + " Found existing installation: keras 2.10.0\n", + " Uninstalling keras-2.10.0:\n", + " Successfully uninstalled keras-2.10.0\n", + " Attempting uninstall: tensorboard\n", + " Found existing installation: tensorboard 2.10.1\n", + " Uninstalling tensorboard-2.10.1:\n", + " Successfully uninstalled tensorboard-2.10.1\n", + " Attempting uninstall: tensorflow\n", + " Found existing installation: tensorflow 2.10.0\n", + " Uninstalling tensorflow-2.10.0:\n", + " Successfully uninstalled tensorflow-2.10.0\n", + "\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", + "tfx-bsl 1.10.1 requires tensorflow!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,<3,>=1.15.5, but you have tensorflow 2.8.3 which is incompatible.\n", + "tensorflow-serving-api 2.10.0 requires tensorflow<3,>=2.10.0, but you have tensorflow 2.8.3 which is incompatible.\u001b[0m\u001b[31m\n", + "\u001b[0mSuccessfully installed keras-2.8.0 tensorboard-2.8.0 tensorflow-2.8.3 tensorflow-estimator-2.8.0 tensorflow-text-2.8.1\n", + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0m" + ] + } + ], + "source": [ + "!pip install --upgrade pip\n", + "!pip install google-api-core==1.32\n", + "!pip install apache_beam[gcp]==2.41.0\n", + "!pip install tensorflow==2.8\n", + "!pip install tfx_bsl\n", + "!pip install tensorflow-text==2.8.1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "642maF_redwC" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import tensorflow as tf\n", + "import tensorflow_text as text\n", + "from scipy.special import expit\n", + "\n", + "import apache_beam as beam\n", + "import tfx_bsl\n", + "from tfx_bsl.public.beam import RunInference\n", + "from tfx_bsl.public import tfxio\n", + "from tfx_bsl.public.proto import model_spec_pb2\n", + "from tfx_bsl.public.tfxio import TFExampleRecord\n", + "from tensorflow_serving.apis import prediction_log_pb2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h2JP7zsqerCT" + }, + "source": [ + "#### Model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ydYQ_5EyfeEM" + }, + "source": [ + "Download a pretrained binary classifier to perform sentiment analysis on an IMDB dataset from GCS. This model was trained by following this [tutorial](https://www.tensorflow.org/tutorials/keras/text_classification)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BucRWly0flz8" + }, + "outputs": [], + "source": [ + "model_dir = \"gs://apache-beam-testing-ml-examples/imdb_bert\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GZ-Ioc8ZfyIT" + }, + "source": [ + "#### Helper Functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pZ0LNtHUfsRq" + }, + "outputs": [], + "source": [ + "class ExampleProcessor:\n", + " \"\"\"\n", + " Process the raw text input to a format suitable for RunInference.\n", + " TensorFlow model handler expects a serialized tf.Example as input\n", + " \"\"\"\n", + " def create_example(self, feature):\n", + " return tf.train.Example(\n", + " features=tf.train.Features(\n", + " feature={'x' : self.create_feature(feature)})\n", + " )\n", + "\n", + " def create_feature(self, element):\n", + " return tf.train.Feature(bytes_list=tf.train.BytesList(value=[element]))\n", + "\n", + "class PredictionProcessor(beam.DoFn):\n", + " \"\"\"\n", + " Process the RunInference output to return the input text and the softmax probability\n", + " \"\"\"\n", + " def process(\n", + " self,\n", + " element: prediction_log_pb2.PredictionLog):\n", + " predict_log = element.predict_log\n", + " input_value = tf.train.Example.FromString(predict_log.request.inputs['text'].string_val[0])\n", + " output_value = predict_log.response.outputs\n", + " # print(output_value)\n", + " yield (f\"input is [{input_value.features.feature['x'].bytes_list.value}] output is {expit(output_value['classifier'].float_val)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PZVwI4BbgaAI" + }, + "source": [ + "#### Prepare the Input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TOXX1KMKi_mm" + }, + "outputs": [], + "source": [ + "inputs = np.array([\n", + " b\"this is such an amazing movie\",\n", + " b\"The movie was great\",\n", + " b\"The movie was okish\",\n", + " b\"The movie was terrible\"\n", + "])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "O2Y15WmfgZXQ" + }, + "outputs": [], + "source": [ + "input_strings_file = 'input_strings.tfrecord'\n", + "\n", + "# Preprocess the input as RunInference is expecting a serialized tf.example as an input\n", + "# Write the processed input to a file \n", + "# One can also do it as a pipeline step by using beam.Map() \n", + "\n", + "with tf.io.TFRecordWriter(input_strings_file) as writer:\n", + " for i in inputs:\n", + " example = ExampleProcessor().create_example(feature=i)\n", + " writer.write(example.SerializeToString())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BYkQl_l8gRgo" + }, + "source": [ + "#### RunInference Pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "uh5bMhxdgA7Q", + "outputId": "2a22059f-519c-44f7-e36f-59e09b1cb24a" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:tensorflow:From /usr/local/lib/python3.7/dist-packages/tfx_bsl/beam/run_inference.py:615: load (from tensorflow.python.saved_model.loader_impl) is deprecated and will be removed in a future version.\n", + "Instructions for updating:\n", + "This function will only be available through the v1 compatibility library as tf.compat.v1.saved_model.loader.load or tf.compat.v1.saved_model.load. There will be a new function for importing SavedModels in Tensorflow 2.0.\n", + "WARNING:apache_beam.io.tfrecordio:Couldn't find python-snappy so the implementation of _TFRecordUtil._masked_crc32c is not as fast as it could be.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "input is [[b'this is such an amazing movie']] output is [0.99906057]\n", + "input is [[b'The movie was great']] output is [0.99307914]\n", + "input is [[b'The movie was okish']] output is [0.03274685]\n", + "input is [[b'The movie was terrible']] output is [0.00680008]\n" + ] + } + ], + "source": [ + "saved_model_spec = model_spec_pb2.SavedModelSpec(model_path=model_dir)\n", + "inference_spec_type = model_spec_pb2.InferenceSpecType(saved_model_spec=saved_model_spec)\n", + "\n", + "#A Beam IO that reads a file of serialized tf.Examples\n", + "tfexample_beam_record = TFExampleRecord(file_pattern='input_strings.tfrecord')\n", + "\n", + "with beam.Pipeline() as pipeline:\n", + " _ = ( pipeline | \"Create Input PCollection\" >> tfexample_beam_record.RawRecordBeamSource()\n", + " | \"Do Inference\" >> RunInference(model_spec_pb2.InferenceSpecType(\n", + " saved_model_spec=model_spec_pb2.SavedModelSpec(model_path=model_dir)))\n", + " | \"Post Process\" >> beam.ParDo(PredictionProcessor())\n", + " | beam.Map(print)\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8wBUckzHjGV6" + }, + "source": [ + "### RunInference with Scikit-Learn\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6ArL_55kjxkO" + }, + "source": [ + "#### Install Dependency" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "R4p6Mil0jxSy" + }, + "outputs": [], + "source": [ + "!pip install --upgrade pip\n", + "!pip install google-api-core==1.32\n", + "!pip install apache_beam[gcp]==2.41.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_YtRRxh1hLag" + }, + "outputs": [], + "source": [ + "import pickle\n", + "\n", + "import apache_beam as beam\n", + "from apache_beam.ml.inference import RunInference\n", + "from apache_beam.ml.inference.sklearn_inference import SklearnModelHandlerNumpy, ModelFileType" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-7ABKlZvkFHy" + }, + "source": [ + "#### Model\n", + "\n", + "Train and save a sentiment analysis pipeline on movie reviews to classify movie reviews as either positive or negative" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WI_UXluPkRYq" + }, + "source": [ + "This model was trained by following this [tutorial](https://scikit-learn.org/stable/tutorial/text_analytics/working_with_text_data.html#exercise-2-sentiment-analysis-on-movie-reviews)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model_dir = \"gs://apache-beam-testing-ml-examples/sklearn-text-classification/sklearn_sentiment_analysis_pipeline.pkl\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KL4Cx8s0mBqn" + }, + "source": [ + "#### RunInference Pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kyN2Aco8l7SR" + }, + "outputs": [], + "source": [ + "inputs = [\n", + " \"In my soul and in my heart, I’m convinced I’m wrong!\",\n", + " \"Be with me always—take any form—drive me mad! only do not leave me in this abyss, where I cannot find you!\",\n", + " \"Do I want to live? Would you like to live with your soul in the grave?\",\n", + " \"Honest people don’t hide their deeds.\",\n", + " \"Nelly, I am Heathcliff! He’s always, always in my mind: not as a pleasure, any more than I am always a pleasure to myself, but as my own being.\",\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "QnQ6ePcgmEeR", + "outputId": "b0d4d31a-76c1-49e4-aa5a-8003a95bbb47" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "input: In my soul and in my heart, I’m convinced I’m wrong! -> negative\n", + "input: Be with me always—take any form—drive me mad! only do not leave me in this abyss, where I cannot find you! -> positive\n", + "input: Do I want to live? Would you like to live with your soul in the grave? -> positive\n", + "input: Honest people don’t hide their deeds. -> negative\n", + "input: Nelly, I am Heathcliff! He’s always, always in my mind: not as a pleasure, any more than I am always a pleasure to myself, but as my own being. -> negative\n" + ] + } + ], + "source": [ + "# One can choose a Sklearn model handler based on their input data type:\n", + "# 1. SklearnModelHandlerNumpy: For using numpy arrays as an input\n", + "# 2. SklearnModelHandlerPandas: For using pandas dataframes as an input\n", + "\n", + "# Sklearn model handler supports loading of two serialized format: \n", + "# 1. ModelFileType.PICKLE: For models saved using pickle\n", + "# 2. ModelFileType.JOBLIB: For models saved using Joblib\n", + "\n", + "model_handler = SklearnModelHandlerNumpy(model_uri=model_dir, model_file_type=ModelFileType.PICKLE)\n", + "\n", + "with beam.Pipeline() as pipeline:\n", + " _ = (pipeline | \"Create inputs\" >> beam.Create(inputs)\n", + " | \"Inference\" >> RunInference(model_handler=model_handler)\n", + " | \"Print\" >> beam.Map(lambda x: print(f\"input: {x.example} -> {'positive' if x.inference == 0 else 'negative'}\"))\n", + " )" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "5kkjbcIzZIf6", + "vA1UmbFRb5C-", + "-7ABKlZvkFHy" + ], + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/notebooks/tour-of-beam/dataframes.ipynb b/examples/notebooks/tour-of-beam/dataframes.ipynb index e1ae406f668f..06330ade3ada 100644 --- a/examples/notebooks/tour-of-beam/dataframes.ipynb +++ b/examples/notebooks/tour-of-beam/dataframes.ipynb @@ -1,6 +1,6 @@ { "nbformat": 4, - "nbformat_minor": 2, + "nbformat_minor": 0, "metadata": { "colab": { "name": "Beam DataFrames", @@ -64,8 +64,7 @@ "> ℹ️ To learn more about Beam DataFrames, take a look at the\n", "[Beam DataFrames overview](https://beam.apache.org/documentation/dsls/dataframes/overview) page.\n", "\n", - "First, we need to install Apache Beam with the `interactive` extra for the Interactive runner.", - "We also need to install a version of `pandas` supported by the DataFrame API, which we can get with the `dataframe` extra in Beam 2.34.0 and newer." + "First, we need to install Apache Beam with the `interactive` extra for the Interactive runner.We also need to install a version of `pandas` supported by the DataFrame API, which we can get with the `dataframe` extra in Beam 2.34.0 and newer." ], "metadata": { "id": "hDuXLLSZnI1D" @@ -135,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "source": [ "import apache_beam as beam\n", "import apache_beam.runners.interactive.interactive_beam as ib\n", @@ -283,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "source": [ "import apache_beam.runners.interactive.interactive_beam as ib\n", "\n", @@ -408,7 +407,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "source": [ "import apache_beam as beam\n", "from apache_beam.dataframe import convert\n", @@ -470,7 +469,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "source": [ "import pandas as pd\n", "import apache_beam as beam\n", @@ -533,7 +532,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "source": [ "import pandas as pd\n", "import apache_beam as beam\n", @@ -600,7 +599,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "source": [ "import csv\n", "import apache_beam as beam\n", @@ -676,7 +675,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "source": [ "import csv\n", "import pandas as pd\n", @@ -738,11 +737,12 @@ "* [Beam DataFrames overview](https://beam.apache.org/documentation/dsls/dataframes/overview) -- an overview of the Beam DataFrames API.\n", "* [Differences from pandas](https://beam.apache.org/documentation/dsls/dataframes/differences-from-pandas) -- goes through some of the differences between Beam DataFrames and Pandas DataFrames, as well as some of the workarounds for unsupported operations.\n", "* [10 minutes to Pandas](https://pandas.pydata.org/pandas-docs/stable/user_guide/10min.html) -- a quickstart guide to Pandas DataFrames.\n", - "* [Pandas DataFrame API](https://pandas.pydata.org/pandas-docs/stable/reference/frame.html) -- the API reference for Pandas DataFrames" + "* [Pandas DataFrame API](https://pandas.pydata.org/pandas-docs/stable/reference/frame.html) -- the API reference for Pandas DataFrames\n", + "* [Preprocessing with Beam Dataframes](https://colab.research.google.com/github/apache/beam/blob/master/examples/notebooks/beam-ml/dataframe_api_preprocessing.ipynb) -- an example of data preprocessing for ML training using Beam DataFrames API\n" ], "metadata": { "id": "UflW6AJp6-ss" } } ] -} +} \ No newline at end of file diff --git a/learning/katas/python/Common Transforms/Aggregation/Count/task.py b/learning/katas/python/Common Transforms/Aggregation/Count/task.py index 188360e5a258..a4e5b0cb53ee 100644 --- a/learning/katas/python/Common Transforms/Aggregation/Count/task.py +++ b/learning/katas/python/Common Transforms/Aggregation/Count/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(range(1, 11)) | beam.combiners.Count.Globally() - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Common Transforms/Aggregation/Largest/task.py b/learning/katas/python/Common Transforms/Aggregation/Largest/task.py index 5798a2366714..fbbe17223742 100644 --- a/learning/katas/python/Common Transforms/Aggregation/Largest/task.py +++ b/learning/katas/python/Common Transforms/Aggregation/Largest/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(range(1, 11)) | beam.combiners.Top.Largest(2) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Common Transforms/Aggregation/Mean/task.py b/learning/katas/python/Common Transforms/Aggregation/Mean/task.py index 6b05b1d25cec..024f1b02d14c 100644 --- a/learning/katas/python/Common Transforms/Aggregation/Mean/task.py +++ b/learning/katas/python/Common Transforms/Aggregation/Mean/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(range(1, 11)) | beam.combiners.Mean.Globally() - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Common Transforms/Aggregation/Smallest/task.py b/learning/katas/python/Common Transforms/Aggregation/Smallest/task.py index c2f2f54ca48d..9b2ec87586de 100644 --- a/learning/katas/python/Common Transforms/Aggregation/Smallest/task.py +++ b/learning/katas/python/Common Transforms/Aggregation/Smallest/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(range(1, 11)) | beam.combiners.Top.Smallest(1) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Common Transforms/Aggregation/Sum/task.py b/learning/katas/python/Common Transforms/Aggregation/Sum/task.py index e857c73a9334..a5c8c997279f 100644 --- a/learning/katas/python/Common Transforms/Aggregation/Sum/task.py +++ b/learning/katas/python/Common Transforms/Aggregation/Sum/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(range(1, 11)) | beam.CombineGlobally(sum) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Common Transforms/Filter/Filter/task.py b/learning/katas/python/Common Transforms/Filter/Filter/task.py index 2024eaf41840..756e7a7d22a9 100644 --- a/learning/katas/python/Common Transforms/Filter/Filter/task.py +++ b/learning/katas/python/Common Transforms/Filter/Filter/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(range(1, 11)) | beam.Filter(lambda num: num % 2 == 0) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Common Transforms/Filter/ParDo/task.py b/learning/katas/python/Common Transforms/Filter/ParDo/task.py index 38c18bdf0e2a..f6f148342072 100644 --- a/learning/katas/python/Common Transforms/Filter/ParDo/task.py +++ b/learning/katas/python/Common Transforms/Filter/ParDo/task.py @@ -28,8 +28,6 @@ import apache_beam as beam -from log_elements import LogElements - class FilterOutEvenNumber(beam.DoFn): @@ -41,4 +39,4 @@ def process(self, element): with beam.Pipeline() as p: (p | beam.Create(range(1, 11)) | beam.ParDo(FilterOutEvenNumber()) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Common Transforms/WithKeys/WithKeys/task.py b/learning/katas/python/Common Transforms/WithKeys/WithKeys/task.py index 9d5ea7d51f39..35c44e7b3043 100644 --- a/learning/katas/python/Common Transforms/WithKeys/WithKeys/task.py +++ b/learning/katas/python/Common Transforms/WithKeys/WithKeys/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(['apple', 'banana', 'cherry', 'durian', 'guava', 'melon']) | beam.WithKeys(lambda word: word[0:1]) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Branching/Branching/task.py b/learning/katas/python/Core Transforms/Branching/Branching/task.py index bc69caa8c96e..53ffdf0723ac 100644 --- a/learning/katas/python/Core Transforms/Branching/Branching/task.py +++ b/learning/katas/python/Core Transforms/Branching/Branching/task.py @@ -30,8 +30,6 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: numbers = p | beam.Create([1, 2, 3, 4, 5]) @@ -39,5 +37,5 @@ mult5_results = numbers | beam.Map(lambda num: num * 5) mult10_results = numbers | beam.Map(lambda num: num * 10) - mult5_results | 'Log multiply 5' >> LogElements(prefix='Multiplied by 5: ') - mult10_results | 'Log multiply 10' >> LogElements(prefix='Multiplied by 10: ') + mult5_results | 'Log multiply 5' >> beam.LogElements(prefix='Multiplied by 5: ') + mult10_results | 'Log multiply 10' >> beam.LogElements(prefix='Multiplied by 10: ') diff --git a/learning/katas/python/Core Transforms/CoGroupByKey/CoGroupByKey/task.py b/learning/katas/python/Core Transforms/CoGroupByKey/CoGroupByKey/task.py index 2c83e7b23857..636cc79d17bc 100644 --- a/learning/katas/python/Core Transforms/CoGroupByKey/CoGroupByKey/task.py +++ b/learning/katas/python/Core Transforms/CoGroupByKey/CoGroupByKey/task.py @@ -31,8 +31,6 @@ import apache_beam as beam -from log_elements import LogElements - class WordsAlphabet: @@ -67,4 +65,4 @@ def cogbk_result_to_wordsalphabet(cgbk_result): countries = p | 'Countries' >> beam.Create(['australia', 'brazil', 'canada']) (apply_transforms(fruits, countries) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Combine/Combine PerKey/task.py b/learning/katas/python/Core Transforms/Combine/Combine PerKey/task.py index 8066c01ea34d..6ab01d208728 100644 --- a/learning/katas/python/Core Transforms/Combine/Combine PerKey/task.py +++ b/learning/katas/python/Core Transforms/Combine/Combine PerKey/task.py @@ -30,8 +30,6 @@ import apache_beam as beam -from log_elements import LogElements - PLAYER_1 = 'Player 1' PLAYER_2 = 'Player 2' PLAYER_3 = 'Player 3' @@ -41,4 +39,4 @@ (p | beam.Create([(PLAYER_1, 15), (PLAYER_2, 10), (PLAYER_1, 100), (PLAYER_3, 25), (PLAYER_2, 75)]) | beam.CombinePerKey(sum) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Combine/CombineFn/task.py b/learning/katas/python/Core Transforms/Combine/CombineFn/task.py index 396a82caa65d..1ff506247765 100644 --- a/learning/katas/python/Core Transforms/Combine/CombineFn/task.py +++ b/learning/katas/python/Core Transforms/Combine/CombineFn/task.py @@ -28,8 +28,6 @@ import apache_beam as beam -from log_elements import LogElements - class AverageFn(beam.CombineFn): @@ -53,4 +51,4 @@ def extract_output(self, accumulator): (p | beam.Create([10, 20, 50, 70, 90]) | beam.CombineGlobally(AverageFn()) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Combine/Simple Function/task.py b/learning/katas/python/Core Transforms/Combine/Simple Function/task.py index 1ccc335b42eb..7ac6768c1d34 100644 --- a/learning/katas/python/Core Transforms/Combine/Simple Function/task.py +++ b/learning/katas/python/Core Transforms/Combine/Simple Function/task.py @@ -29,8 +29,6 @@ import apache_beam as beam -from log_elements import LogElements - def sum(numbers): total = 0 @@ -45,4 +43,4 @@ def sum(numbers): (p | beam.Create([1, 2, 3, 4, 5]) | beam.CombineGlobally(sum) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Composite Transform/Composite Transform/task.py b/learning/katas/python/Core Transforms/Composite Transform/Composite Transform/task.py index 87e13fc0bd8b..66960ab1c4b3 100644 --- a/learning/katas/python/Core Transforms/Composite Transform/Composite Transform/task.py +++ b/learning/katas/python/Core Transforms/Composite Transform/Composite Transform/task.py @@ -30,8 +30,6 @@ import apache_beam as beam -from log_elements import LogElements - class ExtractAndMultiplyNumbers(beam.PTransform): @@ -46,4 +44,4 @@ def expand(self, pcoll): (p | beam.Create(['1,2,3,4,5', '6,7,8,9,10']) | ExtractAndMultiplyNumbers() - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Flatten/Flatten/task.py b/learning/katas/python/Core Transforms/Flatten/Flatten/task.py index e13017a7a31a..0d3cfb26be46 100644 --- a/learning/katas/python/Core Transforms/Flatten/Flatten/task.py +++ b/learning/katas/python/Core Transforms/Flatten/Flatten/task.py @@ -28,8 +28,6 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: wordsStartingWithA = \ @@ -40,4 +38,4 @@ ((wordsStartingWithA, wordsStartingWithB) | beam.Flatten() - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/GroupByKey/GroupByKey/task.py b/learning/katas/python/Core Transforms/GroupByKey/GroupByKey/task.py index 36f944ce6b80..e47136554538 100644 --- a/learning/katas/python/Core Transforms/GroupByKey/GroupByKey/task.py +++ b/learning/katas/python/Core Transforms/GroupByKey/GroupByKey/task.py @@ -29,11 +29,9 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(['apple', 'ball', 'car', 'bear', 'cheetah', 'ant']) | beam.Map(lambda word: (word[0], word)) | beam.GroupByKey() - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Map/FlatMap/task.py b/learning/katas/python/Core Transforms/Map/FlatMap/task.py index 5d6b382ad634..3b9e876d6f20 100644 --- a/learning/katas/python/Core Transforms/Map/FlatMap/task.py +++ b/learning/katas/python/Core Transforms/Map/FlatMap/task.py @@ -29,10 +29,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(['Apache Beam', 'Unified Batch and Streaming']) | beam.FlatMap(lambda sentence: sentence.split()) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Map/Map/task.py b/learning/katas/python/Core Transforms/Map/Map/task.py index 4fddc4394a58..aa6714ba9489 100644 --- a/learning/katas/python/Core Transforms/Map/Map/task.py +++ b/learning/katas/python/Core Transforms/Map/Map/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create([10, 20, 30, 40, 50]) | beam.Map(lambda num: num * 5) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Map/ParDo OneToMany/task.py b/learning/katas/python/Core Transforms/Map/ParDo OneToMany/task.py index fc28fb9adc47..cadd56b53021 100644 --- a/learning/katas/python/Core Transforms/Map/ParDo OneToMany/task.py +++ b/learning/katas/python/Core Transforms/Map/ParDo OneToMany/task.py @@ -29,8 +29,6 @@ import apache_beam as beam -from log_elements import LogElements - class BreakIntoWordsDoFn(beam.DoFn): @@ -43,5 +41,5 @@ def process(self, element): (p | beam.Create(['Hello Beam', 'It is awesome']) | beam.ParDo(BreakIntoWordsDoFn()) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Map/ParDo/task.py b/learning/katas/python/Core Transforms/Map/ParDo/task.py index e54faf2daa16..b6a0aed8d99d 100644 --- a/learning/katas/python/Core Transforms/Map/ParDo/task.py +++ b/learning/katas/python/Core Transforms/Map/ParDo/task.py @@ -28,8 +28,6 @@ import apache_beam as beam -from log_elements import LogElements - class MultiplyByTenDoFn(beam.DoFn): @@ -41,5 +39,5 @@ def process(self, element): (p | beam.Create([1, 2, 3, 4, 5]) | beam.ParDo(MultiplyByTenDoFn()) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Partition/Partition/task.py b/learning/katas/python/Core Transforms/Partition/Partition/task.py index cd28eba307a9..b06a1662e55c 100644 --- a/learning/katas/python/Core Transforms/Partition/Partition/task.py +++ b/learning/katas/python/Core Transforms/Partition/Partition/task.py @@ -29,8 +29,6 @@ import apache_beam as beam -from log_elements import LogElements - def partition_fn(number, num_partitions): if number > 100: @@ -45,5 +43,5 @@ def partition_fn(number, num_partitions): (p | beam.Create([1, 2, 3, 4, 5, 100, 110, 150, 250]) | beam.Partition(partition_fn, 2)) - results[0] | 'Log numbers > 100' >> LogElements(prefix='Number > 100: ') - results[1] | 'Log numbers <= 100' >> LogElements(prefix='Number <= 100: ') + results[0] | 'Log numbers > 100' >> beam.LogElements(prefix='Number > 100: ') + results[1] | 'Log numbers <= 100' >> beam.LogElements(prefix='Number <= 100: ') diff --git a/learning/katas/python/Core Transforms/Side Input/Side Input/task.py b/learning/katas/python/Core Transforms/Side Input/Side Input/task.py index edda30d6308e..5943907b5ab3 100644 --- a/learning/katas/python/Core Transforms/Side Input/Side Input/task.py +++ b/learning/katas/python/Core Transforms/Side Input/Side Input/task.py @@ -29,8 +29,6 @@ import apache_beam as beam -from log_elements import LogElements - class Person: def __init__(self, name, city, country=''): @@ -64,4 +62,4 @@ def process(self, element, cities_to_countries): (p | beam.Create(persons) | beam.ParDo(EnrichCountryDoFn(), beam.pvalue.AsDict(cities_to_countries)) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Core Transforms/Side Output/Side Output/task.py b/learning/katas/python/Core Transforms/Side Output/Side Output/task.py index b29f1cd99d17..f6dad94a5671 100644 --- a/learning/katas/python/Core Transforms/Side Output/Side Output/task.py +++ b/learning/katas/python/Core Transforms/Side Output/Side Output/task.py @@ -32,8 +32,6 @@ import apache_beam as beam from apache_beam import pvalue -from log_elements import LogElements - num_below_100_tag = 'num_below_100' num_above_100_tag = 'num_above_100' @@ -54,5 +52,5 @@ def process(self, element): | beam.ParDo(ProcessNumbersDoFn()) .with_outputs(num_above_100_tag, main=num_below_100_tag)) - results[num_below_100_tag] | 'Log numbers <= 100' >> LogElements(prefix='Number <= 100: ') - results[num_above_100_tag] | 'Log numbers > 100' >> LogElements(prefix='Number > 100: ') + results[num_below_100_tag] | 'Log numbers <= 100' >> beam.LogElements(prefix='Number <= 100: ') + results[num_above_100_tag] | 'Log numbers > 100' >> beam.LogElements(prefix='Number > 100: ') diff --git a/learning/katas/python/Examples/Word Count/Word Count/task.py b/learning/katas/python/Examples/Word Count/Word Count/task.py index af0df927fb4e..4c605c401ef8 100644 --- a/learning/katas/python/Examples/Word Count/Word Count/task.py +++ b/learning/katas/python/Examples/Word Count/Word Count/task.py @@ -31,8 +31,6 @@ import apache_beam as beam -from log_elements import LogElements - lines = [ "apple orange grape banana apple banana", "banana orange banana papaya" @@ -44,4 +42,4 @@ | beam.FlatMap(lambda sentence: sentence.split()) | beam.combiners.Count.PerElement() | beam.MapTuple(lambda k, v: k + ":" + str(v)) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/IO/TextIO/ReadFromText/task.py b/learning/katas/python/IO/TextIO/ReadFromText/task.py index 2e764c0e836e..720d9214abc8 100644 --- a/learning/katas/python/IO/TextIO/ReadFromText/task.py +++ b/learning/katas/python/IO/TextIO/ReadFromText/task.py @@ -30,11 +30,9 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: file_path = 'countries.txt' (p | beam.io.ReadFromText(file_path) | beam.Map(lambda country: country.upper()) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Introduction/Hello Beam/Hello Beam/task.py b/learning/katas/python/Introduction/Hello Beam/Hello Beam/task.py index a9be129bc37f..bed032039ef7 100644 --- a/learning/katas/python/Introduction/Hello Beam/Hello Beam/task.py +++ b/learning/katas/python/Introduction/Hello Beam/Hello Beam/task.py @@ -28,10 +28,8 @@ import apache_beam as beam -from log_elements import LogElements - with beam.Pipeline() as p: (p | beam.Create(['Hello Beam']) - | LogElements()) + | beam.LogElements()) diff --git a/learning/katas/python/Streaming/Timestamps/Add Timestamps/task.py b/learning/katas/python/Streaming/Timestamps/Add Timestamps/task.py index abbaa3c2851e..51e53dd74e38 100644 --- a/learning/katas/python/Streaming/Timestamps/Add Timestamps/task.py +++ b/learning/katas/python/Streaming/Timestamps/Add Timestamps/task.py @@ -32,8 +32,6 @@ import apache_beam as beam from apache_beam.transforms import window -from log_elements import LogElements - class Event: def __init__(self, id, event, timestamp): @@ -60,5 +58,5 @@ def process(self, element, **kwargs): Event('5', 'book-order', datetime.datetime(2020, 3, 8, 0, 0, 0, 0, tzinfo=pytz.UTC)), ]) | beam.ParDo(AddTimestampDoFn()) - | LogElements(with_timestamp=True)) + | beam.LogElements(with_timestamp=True)) diff --git a/learning/katas/python/Streaming/Triggers/Early Triggers/task.py b/learning/katas/python/Streaming/Triggers/Early Triggers/task.py index 0817560ce1cf..830e612a4307 100644 --- a/learning/katas/python/Streaming/Triggers/Early Triggers/task.py +++ b/learning/katas/python/Streaming/Triggers/Early Triggers/task.py @@ -41,7 +41,7 @@ from apache_beam.transforms.trigger import AfterCount from apache_beam.transforms.trigger import AccumulationMode from apache_beam.utils.timestamp import Duration -from log_elements import LogElements +from apache_beam.transforms.util import LogElements class CountEventsWithEarlyTrigger(beam.PTransform): diff --git a/learning/katas/python/Streaming/Triggers/Event Time Triggers/task.py b/learning/katas/python/Streaming/Triggers/Event Time Triggers/task.py index 4476721ec83a..283648499e1a 100644 --- a/learning/katas/python/Streaming/Triggers/Event Time Triggers/task.py +++ b/learning/katas/python/Streaming/Triggers/Event Time Triggers/task.py @@ -37,7 +37,7 @@ from apache_beam.transforms.trigger import AccumulationMode from apache_beam.transforms.trigger import AfterWatermark from apache_beam.utils.timestamp import Duration -from log_elements import LogElements +from apache_beam.transforms.util import LogElements class CountEvents(beam.PTransform): diff --git a/learning/katas/python/Streaming/Triggers/Window Accumulation Modes/task.py b/learning/katas/python/Streaming/Triggers/Window Accumulation Modes/task.py index 5e7881b1e94c..51f592722a92 100644 --- a/learning/katas/python/Streaming/Triggers/Window Accumulation Modes/task.py +++ b/learning/katas/python/Streaming/Triggers/Window Accumulation Modes/task.py @@ -40,7 +40,7 @@ from apache_beam.utils.timestamp import Duration from apache_beam.options.pipeline_options import PipelineOptions from apache_beam.options.pipeline_options import StandardOptions -from log_elements import LogElements +from apache_beam.transforms.util import LogElements class CountEventsWithAccumulating(beam.PTransform): diff --git a/learning/katas/python/Streaming/Windows/Fixed Windows/task.py b/learning/katas/python/Streaming/Windows/Fixed Windows/task.py index 627326342917..3bf218e6b5e6 100644 --- a/learning/katas/python/Streaming/Windows/Fixed Windows/task.py +++ b/learning/katas/python/Streaming/Windows/Fixed Windows/task.py @@ -35,8 +35,6 @@ import apache_beam as beam from apache_beam.transforms import window -from log_elements import LogElements - with beam.Pipeline() as p: @@ -54,4 +52,4 @@ ]) | beam.WindowInto(window.FixedWindows(24*60*60)) | beam.combiners.Count.PerElement() - | LogElements(with_window=True)) + | beam.LogElements(with_window=True)) diff --git a/learning/katas/python/log_elements.py b/learning/katas/python/log_elements.py deleted file mode 100644 index 4477256da7d9..000000000000 --- a/learning/katas/python/log_elements.py +++ /dev/null @@ -1,54 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import apache_beam as beam - - -class LogElements(beam.PTransform): - - class _LoggingFn(beam.DoFn): - - def __init__(self, prefix='', with_timestamp=False, with_window=False): - super().__init__() - self.prefix = prefix - self.with_timestamp = with_timestamp - self.with_window = with_window - - def process(self, element, timestamp=beam.DoFn.TimestampParam, - window=beam.DoFn.WindowParam, **kwargs): - log_line = self.prefix + str(element) - - if self.with_timestamp: - log_line += ', timestamp=' + repr(timestamp.to_rfc3339()) - - if self.with_window: - log_line += ', window(start=' + window.start.to_rfc3339() - log_line += ', end=' + window.end.to_rfc3339() + ')' - - print(log_line) - yield element - - def __init__(self, label=None, prefix='', - with_timestamp=False, with_window=False): - super().__init__(label) - self.prefix = prefix - self.with_timestamp = with_timestamp - self.with_window = with_window - - def expand(self, input): - input | beam.ParDo( - self._LoggingFn(self.prefix, self.with_timestamp, - self.with_window)) diff --git a/learning/tour-of-beam/backend/README.md b/learning/tour-of-beam/backend/README.md index 61b0a93be338..6432c854b6fa 100644 --- a/learning/tour-of-beam/backend/README.md +++ b/learning/tour-of-beam/backend/README.md @@ -19,12 +19,25 @@ and currently logged-in user's snippets and progress. Currently it supports Java, Python, and Go Beam SDK. It is comprised of several Cloud Functions, with Firerstore in Datastore mode as a storage. -* get-sdk-list -* get-content-tree?sdk=(java|go|python) -* get-unit-content?unitId= -TODO: add response schemas -TODO: add save functions info -TODO: add user token info +Public endpoints: + +* getSdkList +* getContentTree?sdk=(java|go|python) +* getUnitContent?sdk=&id= + +Authorized endpoints also consume `Authorization: Bearer ` header + +* getUserProgress?sdk= +* postUnitContent?sdk=&id= + +### Playground GRPC API + +We use Playground GRPC to save/get user snippets, so we keep the generated stubs in [playground_api](playground_api) +To re-generate: +``` +$ go generate -x ./... +``` + ### Datastore schema @@ -55,37 +68,45 @@ __Kinds__ parentKey: parent module/group key +- tb_user + + key: `uid` from IDToken + +- tb_user_progress + + key: `_` + + parentKey: tb_user entity key + ### Deployment Prerequisites: - - GCP project with enabled Billing API & Cloud Functions API + - GCP project with enabled + * Billing API + * Cloud Functions API + * Firebase Admin API - set environment variables: * PROJECT_ID: GCP id * REGION: the region, "us-central1" fe - existing setup of Playground backend in a project -1. Deploy Datastore indexes +1. Deploy Datastore indexes (but don't delete existing Playground indexes!) ``` gcloud datastore indexes create ./internal/storage/index.yaml ``` 2. Deploy cloud functions ``` -$ gcloud functions deploy getSdkList --entry-point getSdkList \ - --region $REGION --runtime go116 --allow-unauthenticated \ - --trigger-http --set-env-vars="DATASTORE_PROJECT_ID=$PROJECT_ID" - -$ gcloud functions deploy getContentTree --entry-point getContentTree \ +for endpoint in "getSdkList getContentTree getUnitComplete getUserProgress postUnitComplete"; do +gcloud functions deploy $endpoint --entry-point $endpoint \ --region $REGION --runtime go116 --allow-unauthenticated \ - --trigger-http --set-env-vars="DATASTORE_PROJECT_ID=$PROJECT_ID" + --trigger-http --set-env-vars="DATASTORE_PROJECT_ID=$PROJECT_ID,GOOGLE_PROJECT_ID=$PROJECT_ID" -$ gcloud functions deploy getUnitContent --entry-point getUnitContent \ - --region $REGION --runtime go116 --allow-unauthenticated \ - --trigger-http --set-env-vars="DATASTORE_PROJECT_ID=$PROJECT_ID" ``` 3. Set environment variables: - TOB_MOCK: set to 1 to deliver mock responses from samples/api - DATASTORE_PROJECT_ID: Google Cloud PROJECT_ID +- GOOGLE_PROJECT_ID: Google Cloud PROJECT_ID (consumed by Firebase Admin SDK) - GOOGLE_APPLICATION_CREDENTIALS: path to json auth key - TOB_LEARNING_PATH: path the content tree root @@ -94,23 +115,36 @@ $ gcloud functions deploy getUnitContent --entry-point getUnitContent \ $ go run cmd/ci_cd/ci_cd.go ``` -### Sample usage +## Sample usage Entry point: list sdk names ``` -$ curl -X GET https://$REGION-$PROJECT_ID.cloudfunctions.net/getSdkList | json_pp +$ curl -X GET "https://$REGION-$PROJECT_ID.cloudfunctions.net/getSdkList" | json_pp ``` [response](./samples/api/get_sdk_list.json) -Get content tree by sdk name (SDK name == SDK id) +### Get content tree by sdk name (SDK name == SDK id) ``` -$ curl -X GET 'https://$REGION-$PROJECT_ID.cloudfunctions.net/getContentTree?sdk=python' +$ curl -X GET "https://$REGION-$PROJECT_ID.cloudfunctions.net/getContentTree?sdk=python" ``` [response](./samples/api/get_content_tree.json) -Get unit content tree by sdk name and unitId +### Get unit content by sdk name and unitId ``` -$ curl -X GET 'https://$REGION-$PROJECT_ID.cloudfunctions.net/getContentTree?sdk=python&id=challenge1' +$ curl -X GET "https://$REGION-$PROJECT_ID.cloudfunctions.net/getUnitContent?sdk=python&id=challenge1" ``` [response](./samples/api/get_unit_content.json) + +### Set unit as complete +``` +$ curl -X POST -H "Authorization: Bearer $token" \ + "https://$REGION-$PROJECT_ID.cloudfunctions.net/postUnitComplete?sdk=python&id=challenge1" -d '{}' +``` + +### Get user progress by sdk name +``` +$ curl -X GET -H "Authorization: Bearer $token" \ + "https://$REGION-$PROJECT_ID.cloudfunctions.net/getUserProgress?sdk=python" +``` +[response](./samples/api/get_user_progress.json) diff --git a/learning/tour-of-beam/backend/auth.go b/learning/tour-of-beam/backend/auth.go new file mode 100644 index 000000000000..e307c00fe293 --- /dev/null +++ b/learning/tour-of-beam/backend/auth.go @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package tob + +import ( + "context" + "log" + "net/http" + "strings" + + tob "beam.apache.org/learning/tour-of-beam/backend/internal" + "beam.apache.org/learning/tour-of-beam/backend/internal/storage" + firebase "firebase.google.com/go/v4" +) + +// HandleFunc enriched with sdk and authenticated user uid. +type HandlerFuncAuthWithSdk func(w http.ResponseWriter, r *http.Request, sdk tob.Sdk, uid string) + +const BEARER_SCHEMA = "Bearer " + +type Authorizer struct { + fbApp *firebase.App + repo storage.Iface +} + +func MakeAuthorizer(ctx context.Context, repo storage.Iface) *Authorizer { + // setup authorizer + // consumes: + // GOOGLE_PROJECT_ID + // GOOGLE_APPLICATION_CREDENTIALS + // OR + // FIREBASE_AUTH_EMULATOR_HOST + fbApp, err := firebase.NewApp(ctx, nil) + if err != nil { + log.Fatalf("error initializing firebase: %v", err) + } + return &Authorizer{fbApp, repo} +} + +// middleware to parse authorization header, verify the ID token and extract uid. +func (a *Authorizer) ParseAuthHeader(next HandlerFuncAuthWithSdk) HandlerFuncWithSdk { + return func(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) { + ctx := r.Context() + header := r.Header.Get("authorization") // returns "" if no header + if !strings.HasPrefix(header, BEARER_SCHEMA) { + log.Printf("Bad authorization header") + finalizeErrResponse(w, http.StatusUnauthorized, UNAUTHORIZED, "bad auth header") + return + } + + client, err := a.fbApp.Auth(ctx) + if err != nil { + log.Println("Failed to get auth client:", err) + finalizeErrResponse(w, http.StatusInternalServerError, INTERNAL_ERROR, "auth client failed") + return + } + + tokenEncoded := header[len(BEARER_SCHEMA):] + token, err := client.VerifyIDTokenAndCheckRevoked(ctx, tokenEncoded) + if err != nil { + log.Println("Failed to verify token:", err) + finalizeErrResponse(w, http.StatusUnauthorized, UNAUTHORIZED, "failed to verify token") + return + } + + uid := token.UID + // store in tb_user + // TODO: implement IDToken caching in tb_user to optimize calls to Firebase API + if err = a.repo.SaveUser(ctx, uid); err != nil { + log.Println("Failed to store user info:", err) + finalizeErrResponse(w, http.StatusInternalServerError, INTERNAL_ERROR, "failed to store user") + return + } + + next(w, r, sdk, uid) + } +} diff --git a/learning/tour-of-beam/backend/docker-compose.yml b/learning/tour-of-beam/backend/docker-compose.yml index 67a289f1ac3f..50823ab1bfef 100644 --- a/learning/tour-of-beam/backend/docker-compose.yml +++ b/learning/tour-of-beam/backend/docker-compose.yml @@ -17,12 +17,32 @@ version: "3" services: datastore: - build: internal/storage/image + build: integration_tests/emulators/datastore volumes: - ${DATASTORE_EMULATOR_DATADIR}:/opt/data environment: - - DATASTORE_PROJECT_ID=project-test + - DATASTORE_PROJECT_ID - DATASTORE_LISTEN_ADDRESS=0.0.0.0:8081 ports: - "8081:8081" command: --consistency=1.0 --store-on-disk + + firebase_auth: + build: integration_tests/emulators/firebase + environment: + - PROJECT_ID=${GOOGLE_CLOUD_PROJECT} + ports: + - "9099:9099" + + playground-router: + image: apache/beam_playground-backend-router + environment: + - GOOGLE_CLOUD_PROJECT + - DATASTORE_EMULATOR_HOST=datastore:8081 + - CACHE_TYPE=local + - SDK_CONFIG=/opt/playground/backend/sdks-emulator.yaml + - PROTOCOL_TYPE=TCP + ports: + - "8000:8080" + depends_on: + - datastore diff --git a/learning/tour-of-beam/backend/function.go b/learning/tour-of-beam/backend/function.go index 363c1585b928..2138ba19da0a 100644 --- a/learning/tour-of-beam/backend/function.go +++ b/learning/tour-of-beam/backend/function.go @@ -15,11 +15,17 @@ // specific language governing permissions and limitations // under the License. +//go:generate protoc -I ../../../playground/api/v1 --go_out=playground_api --go_opt=paths=source_relative api.proto +//go:generate protoc -I ../../../playground/api/v1 --go-grpc_out=playground_api --go-grpc_opt=paths=source_relative api.proto +//go:generate moq -rm -out playground_api/mock.go playground_api PlaygroundServiceClient + package tob import ( "context" "encoding/json" + "errors" + "fmt" "log" "net/http" "os" @@ -27,14 +33,18 @@ import ( tob "beam.apache.org/learning/tour-of-beam/backend/internal" "beam.apache.org/learning/tour-of-beam/backend/internal/service" "beam.apache.org/learning/tour-of-beam/backend/internal/storage" + pb "beam.apache.org/learning/tour-of-beam/backend/playground_api" "cloud.google.com/go/datastore" "github.com/GoogleCloudPlatform/functions-framework-go/functions" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + grpc_status "google.golang.org/grpc/status" ) -const ( - BAD_FORMAT = "BAD_FORMAT" - INTERNAL_ERROR = "INTERNAL_ERROR" - NOT_FOUND = "NOT_FOUND" +var ( + svc service.IContent + auth *Authorizer + pgClient pb.PlaygroundServiceClient ) // Helper to format http error messages. @@ -45,31 +55,66 @@ func finalizeErrResponse(w http.ResponseWriter, status int, code, message string _ = json.NewEncoder(w).Encode(resp) } -var svc service.IContent - -func init() { +func MakeRepo(ctx context.Context) storage.Iface { // dependencies // required: // * TOB_MOCK: respond with static samples // OR + // * GOOGLE_APPLICATION_CREDENTIALS: json file path to cloud credentials // * DATASTORE_PROJECT_ID: cloud project id // optional: // * DATASTORE_EMULATOR_HOST: emulator host/port (ex. 0.0.0.0:8888) if os.Getenv("TOB_MOCK") > "" { - svc = &service.Mock{} + fmt.Println("Initialize mock storage") + return &storage.Mock{} } else { // consumes DATASTORE_* env variables - client, err := datastore.NewClient(context.Background(), "") + client, err := datastore.NewClient(ctx, "") if err != nil { log.Fatalf("new datastore client: %v", err) } - svc = &service.Svc{Repo: &storage.DatastoreDb{Client: client}} + + return &storage.DatastoreDb{Client: client} + } +} + +func MakePlaygroundClient(ctx context.Context) pb.PlaygroundServiceClient { + // dependencies + // required: + // * TOB_MOCK: use mock implementation + // OR + // * PLAYGROUND_ROUTER_HOST: playground API host/port + if os.Getenv("TOB_MOCK") > "" { + fmt.Println("Using mock playground client") + return pb.GetMockClient() + } else { + host := os.Getenv("PLAYGROUND_ROUTER_HOST") + cc, err := grpc.Dial(host, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("fail to dial playground: %v", err) + } + return pb.NewPlaygroundServiceClient(cc) } +} + +func init() { + ctx := context.Background() + + repo := MakeRepo(ctx) + pgClient = MakePlaygroundClient(ctx) + svc = &service.Svc{Repo: repo, PgClient: pgClient} + auth = MakeAuthorizer(ctx, repo) + commonGet := Common(http.MethodGet) + commonPost := Common(http.MethodPost) // functions framework - functions.HTTP("getSdkList", Common(getSdkList)) - functions.HTTP("getContentTree", Common(ParseSdkParam(getContentTree))) - functions.HTTP("getUnitContent", Common(ParseSdkParam(getUnitContent))) + functions.HTTP("getSdkList", commonGet(getSdkList)) + functions.HTTP("getContentTree", commonGet(ParseSdkParam(getContentTree))) + functions.HTTP("getUnitContent", commonGet(ParseSdkParam(getUnitContent))) + + functions.HTTP("getUserProgress", commonGet(ParseSdkParam(auth.ParseAuthHeader(getUserProgress)))) + functions.HTTP("postUnitComplete", commonPost(ParseSdkParam(auth.ParseAuthHeader(postUnitComplete)))) + functions.HTTP("postUserCode", commonPost(ParseSdkParam(auth.ParseAuthHeader(postUserCode)))) } // Get list of SDK names @@ -85,12 +130,10 @@ func getSdkList(w http.ResponseWriter, r *http.Request) { } } -// Get the content tree for a given SDK and user -// Merges info from the default tree and per-user information: -// user code snippets and progress +// Get the content tree for a given SDK // Required to be wrapped into ParseSdkParam middleware. func getContentTree(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) { - tree, err := svc.GetContentTree(r.Context(), sdk, nil /*TODO userId*/) + tree, err := svc.GetContentTree(r.Context(), sdk) if err != nil { log.Println("Get content tree error:", err) finalizeErrResponse(w, http.StatusInternalServerError, INTERNAL_ERROR, "storage error") @@ -112,8 +155,8 @@ func getContentTree(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) { func getUnitContent(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) { unitId := r.URL.Query().Get("id") - unit, err := svc.GetUnitContent(r.Context(), sdk, unitId, nil /*TODO userId*/) - if err == service.ErrNoUnit { + unit, err := svc.GetUnitContent(r.Context(), sdk, unitId) + if errors.Is(err, tob.ErrNoUnit) { finalizeErrResponse(w, http.StatusNotFound, NOT_FOUND, "unit not found") return } @@ -130,3 +173,61 @@ func getUnitContent(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) { return } } + +// Get user progress +func getUserProgress(w http.ResponseWriter, r *http.Request, sdk tob.Sdk, uid string) { + progress, err := svc.GetUserProgress(r.Context(), sdk, uid) + + if err != nil { + log.Println("Get user progress error:", err) + finalizeErrResponse(w, http.StatusInternalServerError, INTERNAL_ERROR, "storage error") + return + } + + err = json.NewEncoder(w).Encode(progress) + if err != nil { + log.Println("Format user progress error:", err) + finalizeErrResponse(w, http.StatusInternalServerError, INTERNAL_ERROR, "format user progress content") + return + } +} + +// Mark unit completed +func postUnitComplete(w http.ResponseWriter, r *http.Request, sdk tob.Sdk, uid string) { + unitId := r.URL.Query().Get("id") + + err := svc.SetUnitComplete(r.Context(), sdk, unitId, uid) + if err != nil { + log.Println("Set unit complete error:", err) + finalizeErrResponse(w, http.StatusInternalServerError, INTERNAL_ERROR, "storage error") + return + } + + fmt.Fprint(w, "{}") +} + +// Save user code for unit +func postUserCode(w http.ResponseWriter, r *http.Request, sdk tob.Sdk, uid string) { + unitId := r.URL.Query().Get("id") + + var userCodeRequest tob.UserCodeRequest + err := json.NewDecoder(r.Body).Decode(&userCodeRequest) + if err != nil { + log.Println("body decode error:", err) + finalizeErrResponse(w, http.StatusBadRequest, BAD_FORMAT, "bad request body") + return + } + + err = svc.SaveUserCode(r.Context(), sdk, unitId, uid, userCodeRequest) + if err != nil { + log.Println("Save user code error:", err) + message := "storage error" + if st, ok := grpc_status.FromError(err); ok { + message = fmt.Sprintf("playground api error: %s", st) + } + finalizeErrResponse(w, http.StatusInternalServerError, INTERNAL_ERROR, message) + return + } + + fmt.Fprint(w, "{}") +} diff --git a/learning/tour-of-beam/backend/go.mod b/learning/tour-of-beam/backend/go.mod index 6601abeee276..d796f60bc9d4 100644 --- a/learning/tour-of-beam/backend/go.mod +++ b/learning/tour-of-beam/backend/go.mod @@ -24,5 +24,9 @@ require ( require ( cloud.google.com/go/datastore v1.8.0 + cloud.google.com/go/firestore v1.7.0 // indirect + firebase.google.com/go/v4 v4.9.0 github.com/stretchr/testify v1.8.0 + google.golang.org/grpc v1.49.0 + google.golang.org/protobuf v1.28.1 ) diff --git a/learning/tour-of-beam/backend/go.sum b/learning/tour-of-beam/backend/go.sum index 45dd6a5e1909..ba9fb37df5d1 100644 --- a/learning/tour-of-beam/backend/go.sum +++ b/learning/tour-of-beam/backend/go.sum @@ -27,38 +27,104 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.1 h1:vpK6iQWv/2uUeFJth4/cBHsQAGjn1iIE6AAlxipRaA0= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.8.0 h1:2qo2G7hABSeqswa+5Ga3+QB8/ZwKOJmDsCISM9scmsU= cloud.google.com/go/datastore v1.8.0/go.mod h1:q1CpHVByTlXppdqTcu4LIhCsTn3fhtZ5R7+TajciO+M= -cloud.google.com/go/functions v1.0.0 h1:cOFEDJ3sgAFRjRULSUJ0Q8cw9qFa5JdpXIBWoNX5uDw= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/firestore v1.7.0 h1:cNkQyruzd5v7FjmL6eeDqwqgX+FbPCjbHxz7vsMhGoo= +cloud.google.com/go/firestore v1.7.0/go.mod h1:0b8DxQkXhbg/PmsjhCUAg4EExIuifAvbHj5Z/iX3BYI= cloud.google.com/go/functions v1.0.0/go.mod h1:O9KS8UweFVo6GbbbCBKh5yEzbW08PVkg2spe3RfPMd4= +cloud.google.com/go/functions v1.6.0 h1:Oveqoadoi2f+yMpJtf1/OrwhTIzaR38l+6Q8/RPyM18= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.26.0 h1:lYAGjknyDJirSzfwUlkv4Nsnj7od7foxQNH/fqZqles= +cloud.google.com/go/storage v1.26.0/go.mod h1:mk/N7YwIKEWyTvXAWQCIeiCTdLoRH6Pd5xmSnolQLTI= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +firebase.google.com/go/v4 v4.9.0 h1:VCagv+hYOxUGeuyu7J+o2rKJkDp5JQBbA3Bzlof+LMk= +firebase.google.com/go/v4 v4.9.0/go.mod h1:bHhRkM3VtGJx19rQdW7GDNLdnA8/T6SsnN5nXk/xdw8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/functions-framework-go v1.5.3 h1:Xx8uWT4hjgbjuXexbpU6V0yawWOdrbcAzZVyMYJvX8Q= @@ -152,9 +218,11 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -172,18 +240,21 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa h1:7MYGT2XEMam7Mtzv1yDUYXANedWvwk3HKkR3MyGowy8= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1 h1:kBRZU0PSuI7PspsSb/ChWoVResUcwNVIdpB049pKTiw= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -326,8 +397,12 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -343,12 +418,16 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb h1:8tDJ3aechhddbdPAxpycgXHJRMLpk/Ab+aa4OgdN5/g= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -360,6 +439,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -407,6 +487,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -416,9 +497,13 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -434,6 +519,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -490,6 +577,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -521,6 +609,7 @@ google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6 google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= @@ -528,9 +617,17 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.84.0 h1:NMB9J4cCxs9xEm+1Z9QiO3eFvn7EnQj3Eo3hN6ugVlg= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.94.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0 h1:F60cuQPJq7K7FzsxMYHAUJSiXh2oKctHxBMbDygxhfM= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -539,6 +636,8 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= +google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -597,6 +696,8 @@ google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -613,11 +714,30 @@ google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad h1:kqrS+lhvaMHCxul6sKQvKJ8nAAhlVItmZV822hYFH/U= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220810155839-1856144b1d9c/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 h1:mmbq5q8M1t7dhkLw320YK4PsOXm6jdnUAkErImaIqOg= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -647,8 +767,11 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -663,8 +786,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= diff --git a/learning/tour-of-beam/backend/integration_tests/api.go b/learning/tour-of-beam/backend/integration_tests/api.go index 4bb4f6743656..d8d684ca368f 100644 --- a/learning/tour-of-beam/backend/integration_tests/api.go +++ b/learning/tour-of-beam/backend/integration_tests/api.go @@ -67,3 +67,22 @@ type ErrorResponse struct { Code string `json:"code"` Message string `json:"message,omitempty"` } + +type UnitProgress struct { + Id string `json:"id"` + IsCompleted bool `json:"isCompleted"` + UserSnippetId string `json:"userSnippetId,omitempty"` +} +type SdkProgress struct { + Units []UnitProgress `json:"units"` +} + +type UserCodeFile struct { + Name string `json:"name"` + Content string `json:"content"` + IsMain bool `json:"isMain,omitempty"` +} +type UserCodeRequest struct { + Files []UserCodeFile `json:"files"` + PipelineOptions string `json:"pipelineOptions"` +} diff --git a/learning/tour-of-beam/backend/integration_tests/auth_emulator.go b/learning/tour-of-beam/backend/integration_tests/auth_emulator.go new file mode 100644 index 000000000000..9273153efcd8 --- /dev/null +++ b/learning/tour-of-beam/backend/integration_tests/auth_emulator.go @@ -0,0 +1,134 @@ +//go:build integration +// +build integration + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "os" + "time" +) + +const ( + TIMEOUT_HTTP = 10 * time.Second + TIMEOUT_STARTUP = 30 * time.Second +) + +type EmulatorClient struct { + host string + client *http.Client +} + +func makeEmulatorCiient() *EmulatorClient { + return &EmulatorClient{ + os.Getenv("FIREBASE_AUTH_EMULATOR_HOST"), + &http.Client{Timeout: TIMEOUT_HTTP}, + } +} + +func (e *EmulatorClient) waitApi() { + terminate := time.NewTimer(TIMEOUT_STARTUP) + tick := time.NewTicker(5 * time.Second) + for { + select { + case <-terminate.C: + log.Fatalf("timeout waiting for emulator") + case <-tick.C: + resp, err := e.do(http.MethodGet, "", nil) + if err != nil { + log.Println("emulator API:", err) + continue + } + parsed := struct { + AuthEmulator struct { + Ready bool `json:"ready"` + } `json:"authEmulator"` + }{} + err = json.Unmarshal(resp, &parsed) + if err != nil { + log.Println("emulator API bad response:", err) + continue + } + if parsed.AuthEmulator.Ready { + return + } + } + } +} + +func (e *EmulatorClient) do(method, endpoint string, jsonBody map[string]string) ([]byte, error) { + url := "http://" + e.host + if endpoint > "" { + url += "/" + endpoint + } + var buf []byte + // handle nil jsonBody as no body + if jsonBody != nil { + buf, _ = json.Marshal(jsonBody) + } + + req, err := http.NewRequest(method, url, bytes.NewBuffer(buf)) + if err != nil { + return nil, err + } + req.Header.Add("content-type", "application/json") + + response, err := e.client.Do(req) + if err != nil { + return nil, err + } + + // Close the connection to reuse it + defer response.Body.Close() + // show the response in stdout + tee := io.TeeReader(response.Body, os.Stdout) + defer os.Stdout.WriteString("\n") + + var out []byte + out, err = io.ReadAll(tee) + if err != nil { + return nil, err + } + + return out, nil +} + +// Get valid Firebase ID token +// Simulate Frontend client authorization logic +// Here, we use the simplest possible authorization: email/password +// Firebase Admin SDK lacks methods to create a user and get ID token +func (e *EmulatorClient) getIDToken() string { + // create a user (sign-up with dummy email/password) + endpoint := "identitytoolkit.googleapis.com/v1/accounts:signUp?key=anything_goes" + body := map[string]string{"email": "a@b.c", "password": "1q2w3e"} + resp, err := e.do(http.MethodPost, endpoint, body) + if err != nil { + log.Fatalf("emulator request error: %+v", err) + } + + var parsed struct { + IdToken string `json:"idToken"` + } + err = json.Unmarshal(resp, &parsed) + if err != nil { + log.Fatalf("failed to parse output: %+v", err) + } + + return parsed.IdToken +} diff --git a/learning/tour-of-beam/backend/integration_tests/auth_test.go b/learning/tour-of-beam/backend/integration_tests/auth_test.go new file mode 100644 index 000000000000..abfecd5e4fa6 --- /dev/null +++ b/learning/tour-of-beam/backend/integration_tests/auth_test.go @@ -0,0 +1,114 @@ +//go:build integration +// +build integration + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "flag" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +var emulator *EmulatorClient + +func TestMain(m *testing.M) { + // to parse go test * flags m.Run consumes + flag.Parse() + + emulator = makeEmulatorCiient() + emulator.waitApi() + + os.Exit(m.Run()) +} + +func TestSaveGetProgress(t *testing.T) { + idToken := emulator.getIDToken() + + t.Run("save_complete", func(t *testing.T) { + port := os.Getenv(PORT_POST_UNIT_COMPLETE) + if port == "" { + t.Fatal(PORT_POST_UNIT_COMPLETE, "env not set") + } + url := "http://localhost:" + port + + err := PostUnitComplete(url, "python", "unit_id_1", idToken) + if err != nil { + t.Fatal(err) + } + }) + t.Run("save_code", func(t *testing.T) { + port := os.Getenv(PORT_POST_USER_CODE) + if port == "" { + t.Fatal(PORT_POST_USER_CODE, "env not set") + } + url := "http://localhost:" + port + req := UserCodeRequest{ + Files: []UserCodeFile{ + {Name: "main.py", Content: "import sys; sys.exit(0)", IsMain: true}, + }, + PipelineOptions: "some opts", + } + + _, err := PostUserCode(url, "python", "unit_id_2", idToken, req) + if err != nil { + t.Fatal(err) + } + }) + t.Run("save_code_fail", func(t *testing.T) { + port := os.Getenv(PORT_POST_USER_CODE) + if port == "" { + t.Fatal(PORT_POST_USER_CODE, "env not set") + } + url := "http://localhost:" + port + req := UserCodeRequest{ + Files: []UserCodeFile{ + // empty content doesn't pass validation + {Name: "main.py", Content: "", IsMain: true}, + }, + PipelineOptions: "some opts", + } + + resp, err := PostUserCode(url, "python", "unit_id_1", idToken, req) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "INTERNAL_ERROR", resp.Code) + msg := "playground api error" + assert.Equal(t, msg, resp.Message[:len(msg)]) + + }) + t.Run("get", func(t *testing.T) { + port := os.Getenv(PORT_GET_USER_PROGRESS) + if port == "" { + t.Fatal(PORT_GET_USER_PROGRESS, "env not set") + } + url := "http://localhost:" + port + + mock_path := filepath.Join("..", "samples", "api", "get_user_progress.json") + var exp SdkProgress + if err := loadJson(mock_path, &exp); err != nil { + t.Fatal(err) + } + + resp, err := GetUserProgress(url, "python", idToken) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, exp, resp) + }) +} diff --git a/learning/tour-of-beam/backend/integration_tests/client.go b/learning/tour-of-beam/backend/integration_tests/client.go index 5d43f454d495..4d4c23d52441 100644 --- a/learning/tour-of-beam/backend/integration_tests/client.go +++ b/learning/tour-of-beam/backend/integration_tests/client.go @@ -13,6 +13,7 @@ package main import ( + "bytes" "encoding/json" "fmt" "io" @@ -39,33 +40,67 @@ func verifyHeaders(header http.Header) error { func GetSdkList(url string) (SdkList, error) { var result SdkList - err := Get(&result, url, nil) + err := Get(&result, url, nil, nil) return result, err } func GetContentTree(url, sdk string) (ContentTree, error) { var result ContentTree - err := Get(&result, url, map[string]string{"sdk": sdk}) + err := Get(&result, url, map[string]string{"sdk": sdk}, nil) return result, err } func GetUnitContent(url, sdk, unitId string) (Unit, error) { var result Unit - err := Get(&result, url, map[string]string{"sdk": sdk, "id": unitId}) + err := Get(&result, url, map[string]string{"sdk": sdk, "id": unitId}, nil) return result, err } +func GetUserProgress(url, sdk, token string) (SdkProgress, error) { + var result SdkProgress + err := Get(&result, url, map[string]string{"sdk": sdk}, + map[string]string{"Authorization": "Bearer " + token}) + return result, err +} + +func PostUnitComplete(url, sdk, unitId, token string) error { + var result interface{} + err := Do(&result, http.MethodPost, url, map[string]string{"sdk": sdk, "id": unitId}, + map[string]string{"Authorization": "Bearer " + token}, nil) + return err +} + +func PostUserCode(url, sdk, unitId, token string, body UserCodeRequest) (ErrorResponse, error) { + raw, err := json.Marshal(body) + if err != nil { + return ErrorResponse{}, err + } + + var result ErrorResponse + err = Do(&result, http.MethodPost, url, map[string]string{"sdk": sdk, "id": unitId}, + map[string]string{"Authorization": "Bearer " + token}, bytes.NewReader(raw)) + return result, err +} + +func Get(dst interface{}, url string, queryParams, headers map[string]string) error { + return Do(dst, http.MethodGet, url, queryParams, headers, nil) +} + // Generic HTTP call wrapper // params: // * dst: response struct pointer // * url: request url // * query_params: url query params, as a map (we don't use multiple-valued params) -func Get(dst interface{}, url string, queryParams map[string]string) error { - req, err := http.NewRequest(http.MethodGet, url, nil) +func Do(dst interface{}, method, url string, queryParams, headers map[string]string, body io.Reader) error { + req, err := http.NewRequest(method, url, body) if err != nil { return err } req.Header.Add("Content-Type", "application/json") + for k, v := range headers { + req.Header.Add(k, v) + } + if len(queryParams) > 0 { q := req.URL.Query() for k, v := range queryParams { @@ -85,5 +120,6 @@ func Get(dst interface{}, url string, queryParams map[string]string) error { } tee := io.TeeReader(resp.Body, os.Stdout) + defer os.Stdout.WriteString("\n") return json.NewDecoder(tee).Decode(dst) } diff --git a/learning/tour-of-beam/backend/internal/storage/image/Dockerfile b/learning/tour-of-beam/backend/integration_tests/emulators/datastore/Dockerfile similarity index 100% rename from learning/tour-of-beam/backend/internal/storage/image/Dockerfile rename to learning/tour-of-beam/backend/integration_tests/emulators/datastore/Dockerfile diff --git a/learning/tour-of-beam/backend/internal/storage/image/start-datastore.sh b/learning/tour-of-beam/backend/integration_tests/emulators/datastore/start-datastore.sh similarity index 100% rename from learning/tour-of-beam/backend/internal/storage/image/start-datastore.sh rename to learning/tour-of-beam/backend/integration_tests/emulators/datastore/start-datastore.sh diff --git a/learning/tour-of-beam/backend/integration_tests/emulators/firebase/Dockerfile b/learning/tour-of-beam/backend/integration_tests/emulators/firebase/Dockerfile new file mode 100644 index 000000000000..7fb924ba6a82 --- /dev/null +++ b/learning/tour-of-beam/backend/integration_tests/emulators/firebase/Dockerfile @@ -0,0 +1,21 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM alpine:3.16 + +RUN apk add openjdk11 npm bash + +RUN npm install -g firebase-tools + +COPY firebase.json / + +CMD firebase emulators:start --only auth --project $PROJECT_ID \ No newline at end of file diff --git a/learning/tour-of-beam/backend/integration_tests/emulators/firebase/firebase.json b/learning/tour-of-beam/backend/integration_tests/emulators/firebase/firebase.json new file mode 100644 index 000000000000..e4fda75c2d88 --- /dev/null +++ b/learning/tour-of-beam/backend/integration_tests/emulators/firebase/firebase.json @@ -0,0 +1,12 @@ +{ + "emulators": { + "auth": { + "host": "0.0.0.0", + "port": 9099 + }, + "ui": { + "enabled": false + }, + "singleProjectMode": true + } +} diff --git a/learning/tour-of-beam/backend/integration_tests/function_test.go b/learning/tour-of-beam/backend/integration_tests/function_test.go index 06ed66d2a7e3..fcda014ec33e 100644 --- a/learning/tour-of-beam/backend/integration_tests/function_test.go +++ b/learning/tour-of-beam/backend/integration_tests/function_test.go @@ -25,9 +25,12 @@ import ( ) const ( - PORT_SDK_LIST = "PORT_SDK_LIST" - PORT_GET_CONTENT_TREE = "PORT_GET_CONTENT_TREE" - PORT_GET_UNIT_CONTENT = "PORT_GET_UNIT_CONTENT" + PORT_SDK_LIST = "PORT_SDK_LIST" + PORT_GET_CONTENT_TREE = "PORT_GET_CONTENT_TREE" + PORT_GET_UNIT_CONTENT = "PORT_GET_UNIT_CONTENT" + PORT_GET_USER_PROGRESS = "PORT_GET_USER_PROGRESS" + PORT_POST_UNIT_COMPLETE = "PORT_POST_UNIT_COMPLETE" + PORT_POST_USER_CODE = "PORT_POST_USER_CODE" ) // scenarios: @@ -36,12 +39,9 @@ const ( // + Get content tree for non-existing SDK: 404 Not Found // + Get unit content for existing SDK, existing unitId // + Get unit content for non-existing SDK/unitId: 404 Not Found -// TODO: -// - Get content tree for a registered user -// - Get unit content for a registered user -// - Save user code/progress for a registered user -// - (negative) Save user code/progress w/o user token/bad token -// - (negative) Save user code/progress for non-existing SDK/unitId: 404 Not Found +// + Save user code/progress for a registered user +// + (negative) Save user code/progress w/o user token/bad token +// + (negative) Save user code/progress for non-existing SDK/unitId: 404 Not Found func loadJson(path string, dst interface{}) error { fh, err := os.Open(path) @@ -115,24 +115,36 @@ func TestNegative(t *testing.T) { for i, params := range []struct { portEnvName string queryParams map[string]string + headers map[string]string expected ErrorResponse }{ - {PORT_GET_CONTENT_TREE, nil, + {PORT_GET_CONTENT_TREE, nil, nil, ErrorResponse{ Code: "BAD_FORMAT", Message: "unknown sdk", }, }, - {PORT_GET_CONTENT_TREE, map[string]string{"sdk": "scio"}, + {PORT_GET_CONTENT_TREE, map[string]string{"sdk": "scio"}, nil, // TODO: actually here should be a NOT_FOUND error ErrorResponse{Code: "INTERNAL_ERROR", Message: "storage error"}, }, - {PORT_GET_UNIT_CONTENT, map[string]string{"sdk": "python", "unitId": "unknown_unitId"}, + {PORT_GET_UNIT_CONTENT, map[string]string{"sdk": "python", "id": "unknown_unitId"}, + nil, ErrorResponse{ Code: "NOT_FOUND", Message: "unit not found", }, }, + // bad authorization header we can test w/o Firebase auth emulator + // for functional tests see auth_test.go + {PORT_GET_USER_PROGRESS, + map[string]string{"sdk": "python"}, + map[string]string{"authorization": "bad_header"}, + ErrorResponse{ + Code: "UNAUTHORIZED", + Message: "bad auth header", + }, + }, } { t.Log("Scenario", i) port := os.Getenv(params.portEnvName) @@ -142,7 +154,7 @@ func TestNegative(t *testing.T) { url := "http://localhost:" + port var resp ErrorResponse - err := Get(&resp, url, params.queryParams) + err := Get(&resp, url, params.queryParams, params.headers) if err != nil { t.Fatal(err) } diff --git a/learning/tour-of-beam/backend/integration_tests/local.sh b/learning/tour-of-beam/backend/integration_tests/local.sh index 6ebebd20f3e3..a28032ac0cbc 100644 --- a/learning/tour-of-beam/backend/integration_tests/local.sh +++ b/learning/tour-of-beam/backend/integration_tests/local.sh @@ -12,14 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -export DATASTORE_PROJECT_ID=test-proj +# demo- prefix makes firebase emulator thinking we're in a local-only environment +export GOOGLE_CLOUD_PROJECT=demo-test-proj +export FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 + +# Enable TOB_MOCK to mock out datastore +#export TOB_MOCK=1 +export DATASTORE_PROJECT_ID=$GOOGLE_CLOUD_PROJECT export DATASTORE_EMULATOR_HOST=localhost:8081 export DATASTORE_EMULATOR_DATADIR=./datadir-$(date '+%H-%M-%S') +export PLAYGROUND_ROUTER_HOST=localhost:8000 + export TOB_LEARNING_ROOT=./samples/learning-content export PORT_SDK_LIST=8801 export PORT_GET_CONTENT_TREE=8802 export PORT_GET_UNIT_CONTENT=8803 +export PORT_GET_USER_PROGRESS=8804 +export PORT_POST_UNIT_COMPLETE=8805 +export PORT_POST_USER_CODE=8806 mkdir "$DATASTORE_EMULATOR_DATADIR" @@ -30,14 +41,17 @@ go build -o tob_function cmd/main.go PORT=$PORT_SDK_LIST FUNCTION_TARGET=getSdkList ./tob_function & PORT=$PORT_GET_CONTENT_TREE FUNCTION_TARGET=getContentTree ./tob_function & PORT=$PORT_GET_UNIT_CONTENT FUNCTION_TARGET=getUnitContent ./tob_function & +PORT=$PORT_GET_USER_PROGRESS FUNCTION_TARGET=getUserProgress ./tob_function & +PORT=$PORT_POST_UNIT_COMPLETE FUNCTION_TARGET=postUnitComplete ./tob_function & +PORT=$PORT_POST_USER_CODE FUNCTION_TARGET=postUserCode ./tob_function & sleep 5 go run cmd/ci_cd/ci_cd.go - -go test -v --tags integration ./integration_tests/... +# -count=1 is an idiomatic way to disable test caching +go test -v -count=1 --tags integration ./integration_tests/... pkill -P $$ diff --git a/learning/tour-of-beam/backend/internal/entity.go b/learning/tour-of-beam/backend/internal/entity.go index 45f5c15bdbb7..62c33722f930 100644 --- a/learning/tour-of-beam/backend/internal/entity.go +++ b/learning/tour-of-beam/backend/internal/entity.go @@ -15,6 +15,14 @@ package internal +import "errors" + +var ( + ErrNoUnit = errors.New("unit not found") + ErrNoUser = errors.New("user not found") + ErrPlayground = errors.New("playground error") +) + type SdkItem struct { Id string `json:"id"` Title string `json:"title"` @@ -75,3 +83,22 @@ type CodeMessage struct { Code string `json:"code"` Message string `json:"message,omitempty"` } + +type UnitProgress struct { + Id string `json:"id"` + IsCompleted bool `json:"isCompleted"` + UserSnippetId string `json:"userSnippetId,omitempty"` +} +type SdkProgress struct { + Units []UnitProgress `json:"units"` +} + +type UserCodeFile struct { + Name string `json:"name"` + Content string `json:"content"` + IsMain bool `json:"isMain,omitempty"` +} +type UserCodeRequest struct { + Files []UserCodeFile `json:"files"` + PipelineOptions string `json:"pipelineOptions"` +} diff --git a/learning/tour-of-beam/backend/internal/service/content.go b/learning/tour-of-beam/backend/internal/service/content.go index 2edf2e87048b..675addbf17fe 100644 --- a/learning/tour-of-beam/backend/internal/service/content.go +++ b/learning/tour-of-beam/backend/internal/service/content.go @@ -18,35 +18,67 @@ package service import ( "context" "errors" + "fmt" tob "beam.apache.org/learning/tour-of-beam/backend/internal" "beam.apache.org/learning/tour-of-beam/backend/internal/storage" + pb "beam.apache.org/learning/tour-of-beam/backend/playground_api" ) -var ErrNoUnit = errors.New("unit not found") - type IContent interface { - GetContentTree(ctx context.Context, sdk tob.Sdk, userId *string) (tob.ContentTree, error) - GetUnitContent(ctx context.Context, sdk tob.Sdk, unitId string, userId *string) (tob.Unit, error) + GetContentTree(ctx context.Context, sdk tob.Sdk) (tob.ContentTree, error) + GetUnitContent(ctx context.Context, sdk tob.Sdk, unitId string) (tob.Unit, error) + GetUserProgress(ctx context.Context, sdk tob.Sdk, userId string) (tob.SdkProgress, error) + SetUnitComplete(ctx context.Context, sdk tob.Sdk, unitId, uid string) error + SaveUserCode(ctx context.Context, sdk tob.Sdk, unitId, uid string, userRequest tob.UserCodeRequest) error } type Svc struct { - Repo storage.Iface + Repo storage.Iface + PgClient pb.PlaygroundServiceClient } -func (s *Svc) GetContentTree(ctx context.Context, sdk tob.Sdk, userId *string) (ct tob.ContentTree, err error) { - // TODO enrich tree with user-specific state (isCompleted) +func (s *Svc) GetContentTree(ctx context.Context, sdk tob.Sdk) (ct tob.ContentTree, err error) { return s.Repo.GetContentTree(ctx, sdk) } -func (s *Svc) GetUnitContent(ctx context.Context, sdk tob.Sdk, unitId string, userId *string) (tob.Unit, error) { - // TODO enrich unit with user-specific state: isCompleted, userSnippetId +func (s *Svc) GetUnitContent(ctx context.Context, sdk tob.Sdk, unitId string) (tob.Unit, error) { unit, err := s.Repo.GetUnitContent(ctx, sdk, unitId) if err != nil { return tob.Unit{}, err } if unit == nil { - return tob.Unit{}, ErrNoUnit + return tob.Unit{}, tob.ErrNoUnit } return *unit, nil } + +func (s *Svc) GetUserProgress(ctx context.Context, sdk tob.Sdk, userId string) (tob.SdkProgress, error) { + progress, err := s.Repo.GetUserProgress(ctx, sdk, userId) + if errors.Is(err, tob.ErrNoUser) { + // make an empty list a default response + return tob.SdkProgress{Units: make([]tob.UnitProgress, 0)}, nil + } + if err != nil { + return tob.SdkProgress{}, err + } + if progress == nil { + panic("progress is nil, no err") + } + + return *progress, nil +} + +func (s *Svc) SetUnitComplete(ctx context.Context, sdk tob.Sdk, unitId, uid string) error { + return s.Repo.SetUnitComplete(ctx, sdk, unitId, uid) +} + +func (s *Svc) SaveUserCode(ctx context.Context, sdk tob.Sdk, unitId, uid string, userRequest tob.UserCodeRequest) error { + req := MakePgSaveRequest(userRequest, sdk) + resp, err := s.PgClient.SaveSnippet(ctx, &req) + if err != nil { + return err + } + fmt.Println("SaveSnippet response:", resp) + return s.Repo.SaveUserSnippetId(ctx, sdk, unitId, uid, resp.GetId()) +} diff --git a/learning/tour-of-beam/backend/internal/service/pg_adapter.go b/learning/tour-of-beam/backend/internal/service/pg_adapter.go new file mode 100644 index 000000000000..ca28b260ebd9 --- /dev/null +++ b/learning/tour-of-beam/backend/internal/service/pg_adapter.go @@ -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. + +package service + +import ( + "log" + + tob "beam.apache.org/learning/tour-of-beam/backend/internal" + pb "beam.apache.org/learning/tour-of-beam/backend/playground_api" +) + +func MakePgSaveRequest(userRequest tob.UserCodeRequest, sdk tob.Sdk) pb.SaveSnippetRequest { + filesProto := make([]*pb.SnippetFile, 0) + for _, file := range userRequest.Files { + filesProto = append(filesProto, + &pb.SnippetFile{ + Name: file.Name, + Content: file.Content, + IsMain: file.IsMain, + }) + } + sdkIdx, ok := pb.Sdk_value[sdk.StorageID()] + if !ok { + log.Panicf("Playground SDK undefined for: %v", sdk) + } + return pb.SaveSnippetRequest{ + Sdk: pb.Sdk(sdkIdx), + Files: filesProto, + PipelineOptions: userRequest.PipelineOptions, + } +} diff --git a/learning/tour-of-beam/backend/internal/storage/adapter.go b/learning/tour-of-beam/backend/internal/storage/adapter.go index 28240dcc09a7..51c1be10f539 100644 --- a/learning/tour-of-beam/backend/internal/storage/adapter.go +++ b/learning/tour-of-beam/backend/internal/storage/adapter.go @@ -35,6 +35,10 @@ func datastoreKey(kind string, sdk tob.Sdk, id string, parent *datastore.Key) *d return pgNameKey(kind, name, parent) } +func rootSdkKey(sdk tob.Sdk) *datastore.Key { + return pgNameKey(PgSdksKind, sdk.StorageID(), nil) +} + func MakeUnitNode(unit *tob.Unit, order, level int) *TbLearningNode { if unit == nil { return nil @@ -129,3 +133,11 @@ func MakeDatastoreModule(mod *tob.Module, order int) *TbLearningModule { Order: order, } } + +func FromDatastoreUserProgress(tbUP TbUnitProgress) tob.UnitProgress { + return tob.UnitProgress{ + Id: tbUP.UnitID, + IsCompleted: tbUP.IsCompleted, + UserSnippetId: tbUP.SnippetId, + } +} diff --git a/learning/tour-of-beam/backend/internal/storage/datastore.go b/learning/tour-of-beam/backend/internal/storage/datastore.go index 840983a9dc91..477dc368012e 100644 --- a/learning/tour-of-beam/backend/internal/storage/datastore.go +++ b/learning/tour-of-beam/backend/internal/storage/datastore.go @@ -17,8 +17,10 @@ package storage import ( "context" + "errors" "fmt" "log" + "time" tob "beam.apache.org/learning/tour-of-beam/backend/internal" "cloud.google.com/go/datastore" @@ -256,5 +258,82 @@ func (d *DatastoreDb) GetUnitContent(ctx context.Context, sdk tob.Sdk, unitId st return node.Unit, nil } +func (d *DatastoreDb) SaveUser(ctx context.Context, uid string) error { + userKey := pgNameKey(TbUserKind, uid, nil) + + _, err := d.Client.Put(ctx, userKey, &TbUser{UID: uid, LastVisitAt: time.Now()}) + if err != nil { + return fmt.Errorf("failed to create tb_user: %w", err) + } + + return nil +} + +func (d *DatastoreDb) GetUserProgress(ctx context.Context, sdk tob.Sdk, uid string) (*tob.SdkProgress, error) { + userKey := pgNameKey(TbUserKind, uid, nil) + err := d.Client.Get(ctx, userKey, &TbUser{}) + if errors.Is(err, datastore.ErrNoSuchEntity) { + return nil, tob.ErrNoUser + } + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + var tbUnits []TbUnitProgress + query := datastore.NewQuery(TbUserProgressKind). + Namespace(PgNamespace). + Ancestor(userKey). + FilterField("sdk", "=", rootSdkKey(sdk)) + + _, err = d.Client.GetAll(ctx, query, &tbUnits) + if err != nil { + return nil, fmt.Errorf("query progress failed: %w", err) + } + + sdkProgress := &tob.SdkProgress{Units: make([]tob.UnitProgress, 0)} + for _, up := range tbUnits { + sdkProgress.Units = append(sdkProgress.Units, FromDatastoreUserProgress(up)) + } + + return sdkProgress, nil +} + +func (d *DatastoreDb) upsertUnitProgress(ctx context.Context, sdk tob.Sdk, unitId, uid string, applyChanges func(*TbUnitProgress)) error { + userKey := pgNameKey(TbUserKind, uid, nil) + progressKey := datastoreKey(TbUserProgressKind, sdk, unitId, userKey) + + _, err := d.Client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + // default entity values + progress := TbUnitProgress{ + Sdk: rootSdkKey(sdk), + UnitID: unitId, + } + if err := tx.Get(progressKey, &progress); err != nil && err != datastore.ErrNoSuchEntity { + return err + } + applyChanges(&progress) + if _, err := tx.Put(progressKey, &progress); err != nil { + return err + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to upsert tb_user_progress: %w", err) + } + return nil +} + +func (d *DatastoreDb) SetUnitComplete(ctx context.Context, sdk tob.Sdk, unitId, uid string) error { + return d.upsertUnitProgress(ctx, sdk, unitId, uid, func(p *TbUnitProgress) { + p.IsCompleted = true + }) +} + +func (d *DatastoreDb) SaveUserSnippetId(ctx context.Context, sdk tob.Sdk, unitId, uid, snippetId string) error { + return d.upsertUnitProgress(ctx, sdk, unitId, uid, func(p *TbUnitProgress) { + p.SnippetId = snippetId + }) +} + // check if the interface is implemented. var _ Iface = &DatastoreDb{} diff --git a/learning/tour-of-beam/backend/internal/storage/iface.go b/learning/tour-of-beam/backend/internal/storage/iface.go index f81a28e4ce52..c18e093ca7aa 100644 --- a/learning/tour-of-beam/backend/internal/storage/iface.go +++ b/learning/tour-of-beam/backend/internal/storage/iface.go @@ -26,4 +26,9 @@ type Iface interface { SaveContentTrees(ctx context.Context, trees []tob.ContentTree) error GetUnitContent(ctx context.Context, sdk tob.Sdk, unitId string) (*tob.Unit, error) + + SaveUser(ctx context.Context, uid string) error + GetUserProgress(ctx context.Context, sdk tob.Sdk, uid string) (*tob.SdkProgress, error) + SetUnitComplete(ctx context.Context, sdk tob.Sdk, unitId, uid string) error + SaveUserSnippetId(ctx context.Context, sdk tob.Sdk, unitId, uid, snippetId string) error } diff --git a/learning/tour-of-beam/backend/internal/service/mock.go b/learning/tour-of-beam/backend/internal/storage/mock.go similarity index 58% rename from learning/tour-of-beam/backend/internal/service/mock.go rename to learning/tour-of-beam/backend/internal/storage/mock.go index dd9fac6cc958..1a8d4193bd11 100644 --- a/learning/tour-of-beam/backend/internal/service/mock.go +++ b/learning/tour-of-beam/backend/internal/storage/mock.go @@ -13,14 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package service +package storage import ( "context" "encoding/json" + "errors" "io/ioutil" "path" "runtime" + "strings" tob "beam.apache.org/learning/tour-of-beam/backend/internal" ) @@ -33,16 +35,45 @@ func getSamplesPath() string { type Mock struct{} // check if the interface is implemented. -var _ IContent = &Mock{} +var _ Iface = &Mock{} -func (d *Mock) GetContentTree(_ context.Context, sdk tob.Sdk, userId *string) (ct tob.ContentTree, err error) { +func (d *Mock) GetContentTree(_ context.Context, sdk tob.Sdk) (ct tob.ContentTree, err error) { + // this sdk is special: we use it as an empty learning path + if sdk == tob.SDK_SCIO { + return ct, errors.New("empty sdk tree") + } content, _ := ioutil.ReadFile(path.Join(getSamplesPath(), "get_content_tree.json")) _ = json.Unmarshal(content, &ct) return ct, nil } -func (d *Mock) GetUnitContent(_ context.Context, sdk tob.Sdk, unitId string, userId *string) (u tob.Unit, err error) { +func (d *Mock) SaveContentTrees(_ context.Context, _ []tob.ContentTree) error { + return nil +} + +func (d *Mock) GetUnitContent(_ context.Context, sdk tob.Sdk, unitId string) (u *tob.Unit, err error) { + if strings.HasPrefix(unitId, "unknown_") { + return u, tob.ErrNoUnit + } content, _ := ioutil.ReadFile(path.Join(getSamplesPath(), "get_unit_content.json")) err = json.Unmarshal(content, &u) return u, err } + +func (d *Mock) SaveUser(ctx context.Context, uid string) error { + return nil +} + +func (d *Mock) GetUserProgress(_ context.Context, sdk tob.Sdk, userId string) (sp *tob.SdkProgress, err error) { + content, _ := ioutil.ReadFile(path.Join(getSamplesPath(), "get_user_progress.json")) + _ = json.Unmarshal(content, &sp) + return sp, nil +} + +func (d *Mock) SetUnitComplete(ctx context.Context, sdk tob.Sdk, unitId, uid string) error { + return nil +} + +func (d *Mock) SaveUserSnippetId(ctx context.Context, sdk tob.Sdk, unitId, uid, snippetId string) error { + return nil +} diff --git a/learning/tour-of-beam/backend/internal/storage/schema.go b/learning/tour-of-beam/backend/internal/storage/schema.go index 5e36c86f4907..e0d05f0ab4f9 100644 --- a/learning/tour-of-beam/backend/internal/storage/schema.go +++ b/learning/tour-of-beam/backend/internal/storage/schema.go @@ -16,6 +16,8 @@ package storage import ( + "time" + tob "beam.apache.org/learning/tour-of-beam/backend/internal" "cloud.google.com/go/datastore" ) @@ -33,6 +35,8 @@ const ( TbLearningPathKind = "tb_learning_path" TbLearningModuleKind = "tb_learning_module" TbLearningNodeKind = "tb_learning_node" + TbUserKind = "tb_user" + TbUserProgressKind = "tb_user_progress" PgSnippetsKind = "pg_snippets" PgSdksKind = "pg_sdks" @@ -95,6 +99,21 @@ type TbLearningNode struct { Level int `datastore:"level"` } +type TbUser struct { + Key *datastore.Key `datastore:"__key__"` + UID string `datastore:"uid"` + LastVisitAt time.Time `datastore:"lastVisitAt"` +} + +type TbUnitProgress struct { + Key *datastore.Key `datastore:"__key__"` + Sdk *datastore.Key `datastore:"sdk"` + + UnitID string `datastore:"unitId"` + IsCompleted bool `datastore:"isCompleted"` + SnippetId string `datastore:"snippetId"` +} + type PgSnippets struct { Key *datastore.Key `datastore:"__key__"` Origin string `datastore:"origin"` diff --git a/learning/tour-of-beam/backend/middleware.go b/learning/tour-of-beam/backend/middleware.go index 87c98bd6e14d..b328fbe89ea8 100644 --- a/learning/tour-of-beam/backend/middleware.go +++ b/learning/tour-of-beam/backend/middleware.go @@ -24,6 +24,13 @@ import ( tob "beam.apache.org/learning/tour-of-beam/backend/internal" ) +const ( + BAD_FORMAT = "BAD_FORMAT" + INTERNAL_ERROR = "INTERNAL_ERROR" + NOT_FOUND = "NOT_FOUND" + UNAUTHORIZED = "UNAUTHORIZED" +) + // Middleware-maker for setting a header // We also make this less generic: it works with HandlerFunc's // so that to be convertible to func(w http ResponseWriter, r *http.Request) @@ -51,12 +58,14 @@ func EnsureMethod(method string) func(http.HandlerFunc) http.HandlerFunc { } // Helper common AIO middleware -func Common(next http.HandlerFunc) http.HandlerFunc { - addContentType := AddHeader("Content-Type", "application/json") - addCORS := AddHeader("Access-Control-Allow-Origin", "*") - ensureGet := EnsureMethod(http.MethodGet) +func Common(method string) func(http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { + addContentType := AddHeader("Content-Type", "application/json") + addCORS := AddHeader("Access-Control-Allow-Origin", "*") + ensureGet := EnsureMethod(method) - return ensureGet(addCORS(addContentType(next))) + return ensureGet(addCORS(addContentType(next))) + } } // HandleFunc enriched with sdk. diff --git a/learning/tour-of-beam/backend/playground_api/api.pb.go b/learning/tour-of-beam/backend/playground_api/api.pb.go new file mode 100644 index 000000000000..f9f402704b5b --- /dev/null +++ b/learning/tour-of-beam/backend/playground_api/api.pb.go @@ -0,0 +1,3507 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.12.4 +// source: api.proto + +package playground + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Sdk int32 + +const ( + Sdk_SDK_UNSPECIFIED Sdk = 0 + Sdk_SDK_JAVA Sdk = 1 + Sdk_SDK_GO Sdk = 2 + Sdk_SDK_PYTHON Sdk = 3 + Sdk_SDK_SCIO Sdk = 4 +) + +// Enum value maps for Sdk. +var ( + Sdk_name = map[int32]string{ + 0: "SDK_UNSPECIFIED", + 1: "SDK_JAVA", + 2: "SDK_GO", + 3: "SDK_PYTHON", + 4: "SDK_SCIO", + } + Sdk_value = map[string]int32{ + "SDK_UNSPECIFIED": 0, + "SDK_JAVA": 1, + "SDK_GO": 2, + "SDK_PYTHON": 3, + "SDK_SCIO": 4, + } +) + +func (x Sdk) Enum() *Sdk { + p := new(Sdk) + *p = x + return p +} + +func (x Sdk) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Sdk) Descriptor() protoreflect.EnumDescriptor { + return file_api_proto_enumTypes[0].Descriptor() +} + +func (Sdk) Type() protoreflect.EnumType { + return &file_api_proto_enumTypes[0] +} + +func (x Sdk) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Sdk.Descriptor instead. +func (Sdk) EnumDescriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{0} +} + +type Status int32 + +const ( + Status_STATUS_UNSPECIFIED Status = 0 + Status_STATUS_VALIDATING Status = 1 + Status_STATUS_VALIDATION_ERROR Status = 2 + Status_STATUS_PREPARING Status = 3 + Status_STATUS_PREPARATION_ERROR Status = 4 + Status_STATUS_COMPILING Status = 5 + Status_STATUS_COMPILE_ERROR Status = 6 + Status_STATUS_EXECUTING Status = 7 + Status_STATUS_FINISHED Status = 8 + Status_STATUS_RUN_ERROR Status = 9 + Status_STATUS_ERROR Status = 10 + Status_STATUS_RUN_TIMEOUT Status = 11 + Status_STATUS_CANCELED Status = 12 +) + +// Enum value maps for Status. +var ( + Status_name = map[int32]string{ + 0: "STATUS_UNSPECIFIED", + 1: "STATUS_VALIDATING", + 2: "STATUS_VALIDATION_ERROR", + 3: "STATUS_PREPARING", + 4: "STATUS_PREPARATION_ERROR", + 5: "STATUS_COMPILING", + 6: "STATUS_COMPILE_ERROR", + 7: "STATUS_EXECUTING", + 8: "STATUS_FINISHED", + 9: "STATUS_RUN_ERROR", + 10: "STATUS_ERROR", + 11: "STATUS_RUN_TIMEOUT", + 12: "STATUS_CANCELED", + } + Status_value = map[string]int32{ + "STATUS_UNSPECIFIED": 0, + "STATUS_VALIDATING": 1, + "STATUS_VALIDATION_ERROR": 2, + "STATUS_PREPARING": 3, + "STATUS_PREPARATION_ERROR": 4, + "STATUS_COMPILING": 5, + "STATUS_COMPILE_ERROR": 6, + "STATUS_EXECUTING": 7, + "STATUS_FINISHED": 8, + "STATUS_RUN_ERROR": 9, + "STATUS_ERROR": 10, + "STATUS_RUN_TIMEOUT": 11, + "STATUS_CANCELED": 12, + } +) + +func (x Status) Enum() *Status { + p := new(Status) + *p = x + return p +} + +func (x Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Status) Descriptor() protoreflect.EnumDescriptor { + return file_api_proto_enumTypes[1].Descriptor() +} + +func (Status) Type() protoreflect.EnumType { + return &file_api_proto_enumTypes[1] +} + +func (x Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Status.Descriptor instead. +func (Status) EnumDescriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{1} +} + +type PrecompiledObjectType int32 + +const ( + PrecompiledObjectType_PRECOMPILED_OBJECT_TYPE_UNSPECIFIED PrecompiledObjectType = 0 + PrecompiledObjectType_PRECOMPILED_OBJECT_TYPE_EXAMPLE PrecompiledObjectType = 1 + PrecompiledObjectType_PRECOMPILED_OBJECT_TYPE_KATA PrecompiledObjectType = 2 + PrecompiledObjectType_PRECOMPILED_OBJECT_TYPE_UNIT_TEST PrecompiledObjectType = 3 +) + +// Enum value maps for PrecompiledObjectType. +var ( + PrecompiledObjectType_name = map[int32]string{ + 0: "PRECOMPILED_OBJECT_TYPE_UNSPECIFIED", + 1: "PRECOMPILED_OBJECT_TYPE_EXAMPLE", + 2: "PRECOMPILED_OBJECT_TYPE_KATA", + 3: "PRECOMPILED_OBJECT_TYPE_UNIT_TEST", + } + PrecompiledObjectType_value = map[string]int32{ + "PRECOMPILED_OBJECT_TYPE_UNSPECIFIED": 0, + "PRECOMPILED_OBJECT_TYPE_EXAMPLE": 1, + "PRECOMPILED_OBJECT_TYPE_KATA": 2, + "PRECOMPILED_OBJECT_TYPE_UNIT_TEST": 3, + } +) + +func (x PrecompiledObjectType) Enum() *PrecompiledObjectType { + p := new(PrecompiledObjectType) + *p = x + return p +} + +func (x PrecompiledObjectType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PrecompiledObjectType) Descriptor() protoreflect.EnumDescriptor { + return file_api_proto_enumTypes[2].Descriptor() +} + +func (PrecompiledObjectType) Type() protoreflect.EnumType { + return &file_api_proto_enumTypes[2] +} + +func (x PrecompiledObjectType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PrecompiledObjectType.Descriptor instead. +func (PrecompiledObjectType) EnumDescriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{2} +} + +type Complexity int32 + +const ( + Complexity_COMPLEXITY_UNSPECIFIED Complexity = 0 + Complexity_COMPLEXITY_BASIC Complexity = 1 + Complexity_COMPLEXITY_MEDIUM Complexity = 2 + Complexity_COMPLEXITY_ADVANCED Complexity = 3 +) + +// Enum value maps for Complexity. +var ( + Complexity_name = map[int32]string{ + 0: "COMPLEXITY_UNSPECIFIED", + 1: "COMPLEXITY_BASIC", + 2: "COMPLEXITY_MEDIUM", + 3: "COMPLEXITY_ADVANCED", + } + Complexity_value = map[string]int32{ + "COMPLEXITY_UNSPECIFIED": 0, + "COMPLEXITY_BASIC": 1, + "COMPLEXITY_MEDIUM": 2, + "COMPLEXITY_ADVANCED": 3, + } +) + +func (x Complexity) Enum() *Complexity { + p := new(Complexity) + *p = x + return p +} + +func (x Complexity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Complexity) Descriptor() protoreflect.EnumDescriptor { + return file_api_proto_enumTypes[3].Descriptor() +} + +func (Complexity) Type() protoreflect.EnumType { + return &file_api_proto_enumTypes[3] +} + +func (x Complexity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Complexity.Descriptor instead. +func (Complexity) EnumDescriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{3} +} + +// RunCodeRequest represents a code text and options of SDK which executes the code. +type RunCodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` + Sdk Sdk `protobuf:"varint,2,opt,name=sdk,proto3,enum=api.v1.Sdk" json:"sdk,omitempty"` + // The pipeline options as they would be passed to the program (e.g. "--option1 value1 --option2 value2") + PipelineOptions string `protobuf:"bytes,3,opt,name=pipeline_options,json=pipelineOptions,proto3" json:"pipeline_options,omitempty"` +} + +func (x *RunCodeRequest) Reset() { + *x = RunCodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RunCodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunCodeRequest) ProtoMessage() {} + +func (x *RunCodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunCodeRequest.ProtoReflect.Descriptor instead. +func (*RunCodeRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{0} +} + +func (x *RunCodeRequest) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *RunCodeRequest) GetSdk() Sdk { + if x != nil { + return x.Sdk + } + return Sdk_SDK_UNSPECIFIED +} + +func (x *RunCodeRequest) GetPipelineOptions() string { + if x != nil { + return x.PipelineOptions + } + return "" +} + +// RunCodeResponse contains information of the pipeline uuid. +type RunCodeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *RunCodeResponse) Reset() { + *x = RunCodeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RunCodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RunCodeResponse) ProtoMessage() {} + +func (x *RunCodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RunCodeResponse.ProtoReflect.Descriptor instead. +func (*RunCodeResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{1} +} + +func (x *RunCodeResponse) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// CheckStatusRequest contains information of the pipeline uuid. +type CheckStatusRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *CheckStatusRequest) Reset() { + *x = CheckStatusRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckStatusRequest) ProtoMessage() {} + +func (x *CheckStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckStatusRequest.ProtoReflect.Descriptor instead. +func (*CheckStatusRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{2} +} + +func (x *CheckStatusRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// StatusInfo contains information about the status of the code execution. +type CheckStatusResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status Status `protobuf:"varint,1,opt,name=status,proto3,enum=api.v1.Status" json:"status,omitempty"` +} + +func (x *CheckStatusResponse) Reset() { + *x = CheckStatusResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CheckStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckStatusResponse) ProtoMessage() {} + +func (x *CheckStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckStatusResponse.ProtoReflect.Descriptor instead. +func (*CheckStatusResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{3} +} + +func (x *CheckStatusResponse) GetStatus() Status { + if x != nil { + return x.Status + } + return Status_STATUS_UNSPECIFIED +} + +// GetValidationOutputRequest contains information of the pipeline uuid. +type GetValidationOutputRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *GetValidationOutputRequest) Reset() { + *x = GetValidationOutputRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetValidationOutputRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetValidationOutputRequest) ProtoMessage() {} + +func (x *GetValidationOutputRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetValidationOutputRequest.ProtoReflect.Descriptor instead. +func (*GetValidationOutputRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{4} +} + +func (x *GetValidationOutputRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// GetValidationOutputResponse represents the result of the code validation. +type GetValidationOutputResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *GetValidationOutputResponse) Reset() { + *x = GetValidationOutputResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetValidationOutputResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetValidationOutputResponse) ProtoMessage() {} + +func (x *GetValidationOutputResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetValidationOutputResponse.ProtoReflect.Descriptor instead. +func (*GetValidationOutputResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{5} +} + +func (x *GetValidationOutputResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// GetPreparationOutputRequest contains information of the pipeline uuid. +type GetPreparationOutputRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *GetPreparationOutputRequest) Reset() { + *x = GetPreparationOutputRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPreparationOutputRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPreparationOutputRequest) ProtoMessage() {} + +func (x *GetPreparationOutputRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPreparationOutputRequest.ProtoReflect.Descriptor instead. +func (*GetPreparationOutputRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{6} +} + +func (x *GetPreparationOutputRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// GetPreparationOutputResponse represents the result of the code preparation. +type GetPreparationOutputResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *GetPreparationOutputResponse) Reset() { + *x = GetPreparationOutputResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPreparationOutputResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPreparationOutputResponse) ProtoMessage() {} + +func (x *GetPreparationOutputResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPreparationOutputResponse.ProtoReflect.Descriptor instead. +func (*GetPreparationOutputResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{7} +} + +func (x *GetPreparationOutputResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// GetCompileOutputRequest contains information of the pipeline uuid. +type GetCompileOutputRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *GetCompileOutputRequest) Reset() { + *x = GetCompileOutputRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetCompileOutputRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCompileOutputRequest) ProtoMessage() {} + +func (x *GetCompileOutputRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCompileOutputRequest.ProtoReflect.Descriptor instead. +func (*GetCompileOutputRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{8} +} + +func (x *GetCompileOutputRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// GetCompileOutputResponse represents the result of the compiled code. +type GetCompileOutputResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *GetCompileOutputResponse) Reset() { + *x = GetCompileOutputResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetCompileOutputResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCompileOutputResponse) ProtoMessage() {} + +func (x *GetCompileOutputResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCompileOutputResponse.ProtoReflect.Descriptor instead. +func (*GetCompileOutputResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{9} +} + +func (x *GetCompileOutputResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// GetRunOutputRequest contains information of the pipeline uuid. +type GetRunOutputRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *GetRunOutputRequest) Reset() { + *x = GetRunOutputRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetRunOutputRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRunOutputRequest) ProtoMessage() {} + +func (x *GetRunOutputRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRunOutputRequest.ProtoReflect.Descriptor instead. +func (*GetRunOutputRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{10} +} + +func (x *GetRunOutputRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// RunOutputResponse represents the result of the executed code. +type GetRunOutputResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *GetRunOutputResponse) Reset() { + *x = GetRunOutputResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetRunOutputResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRunOutputResponse) ProtoMessage() {} + +func (x *GetRunOutputResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRunOutputResponse.ProtoReflect.Descriptor instead. +func (*GetRunOutputResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{11} +} + +func (x *GetRunOutputResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// GetRunErrorRequest contains information of the pipeline uuid. +type GetRunErrorRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *GetRunErrorRequest) Reset() { + *x = GetRunErrorRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetRunErrorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRunErrorRequest) ProtoMessage() {} + +func (x *GetRunErrorRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRunErrorRequest.ProtoReflect.Descriptor instead. +func (*GetRunErrorRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{12} +} + +func (x *GetRunErrorRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// GetRunErrorResponse represents the error of the executed code. +type GetRunErrorResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *GetRunErrorResponse) Reset() { + *x = GetRunErrorResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetRunErrorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRunErrorResponse) ProtoMessage() {} + +func (x *GetRunErrorResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRunErrorResponse.ProtoReflect.Descriptor instead. +func (*GetRunErrorResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{13} +} + +func (x *GetRunErrorResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// GetLogsRequest contains information of the pipeline uuid. +type GetLogsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *GetLogsRequest) Reset() { + *x = GetLogsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetLogsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLogsRequest) ProtoMessage() {} + +func (x *GetLogsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLogsRequest.ProtoReflect.Descriptor instead. +func (*GetLogsRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{14} +} + +func (x *GetLogsRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// RunOutputResponse represents the logs of the executed code. +type GetLogsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *GetLogsResponse) Reset() { + *x = GetLogsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetLogsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLogsResponse) ProtoMessage() {} + +func (x *GetLogsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLogsResponse.ProtoReflect.Descriptor instead. +func (*GetLogsResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{15} +} + +func (x *GetLogsResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// GetGraphRequest contains information of the pipeline uuid. +type GetGraphRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *GetGraphRequest) Reset() { + *x = GetGraphRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetGraphRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGraphRequest) ProtoMessage() {} + +func (x *GetGraphRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGraphRequest.ProtoReflect.Descriptor instead. +func (*GetGraphRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{16} +} + +func (x *GetGraphRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// GetGraphResponse represents the string representation of pipeline execution graph in DOT format. +type GetGraphResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Graph string `protobuf:"bytes,1,opt,name=graph,proto3" json:"graph,omitempty"` +} + +func (x *GetGraphResponse) Reset() { + *x = GetGraphResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetGraphResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGraphResponse) ProtoMessage() {} + +func (x *GetGraphResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGraphResponse.ProtoReflect.Descriptor instead. +func (*GetGraphResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{17} +} + +func (x *GetGraphResponse) GetGraph() string { + if x != nil { + return x.Graph + } + return "" +} + +// CancelRequest request to cancel code processing +type CancelRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PipelineUuid string `protobuf:"bytes,1,opt,name=pipeline_uuid,json=pipelineUuid,proto3" json:"pipeline_uuid,omitempty"` +} + +func (x *CancelRequest) Reset() { + *x = CancelRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CancelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelRequest) ProtoMessage() {} + +func (x *CancelRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelRequest.ProtoReflect.Descriptor instead. +func (*CancelRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{18} +} + +func (x *CancelRequest) GetPipelineUuid() string { + if x != nil { + return x.PipelineUuid + } + return "" +} + +// CancelResponse response for cancel request +type CancelResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *CancelResponse) Reset() { + *x = CancelResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CancelResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelResponse) ProtoMessage() {} + +func (x *CancelResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelResponse.ProtoReflect.Descriptor instead. +func (*CancelResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{19} +} + +// PrecompiledObject represents one PrecompiledObject with its information +type PrecompiledObject struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CloudPath string `protobuf:"bytes,1,opt,name=cloud_path,json=cloudPath,proto3" json:"cloud_path,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Type PrecompiledObjectType `protobuf:"varint,4,opt,name=type,proto3,enum=api.v1.PrecompiledObjectType" json:"type,omitempty"` + PipelineOptions string `protobuf:"bytes,5,opt,name=pipeline_options,json=pipelineOptions,proto3" json:"pipeline_options,omitempty"` + // Link to the example in the Beam repository + Link string `protobuf:"bytes,6,opt,name=link,proto3" json:"link,omitempty"` + Multifile bool `protobuf:"varint,7,opt,name=multifile,proto3" json:"multifile,omitempty"` + ContextLine int32 `protobuf:"varint,8,opt,name=context_line,json=contextLine,proto3" json:"context_line,omitempty"` + DefaultExample bool `protobuf:"varint,9,opt,name=default_example,json=defaultExample,proto3" json:"default_example,omitempty"` + Sdk Sdk `protobuf:"varint,10,opt,name=sdk,proto3,enum=api.v1.Sdk" json:"sdk,omitempty"` + Complexity Complexity `protobuf:"varint,11,opt,name=complexity,proto3,enum=api.v1.Complexity" json:"complexity,omitempty"` + Tags []string `protobuf:"bytes,12,rep,name=tags,proto3" json:"tags,omitempty"` +} + +func (x *PrecompiledObject) Reset() { + *x = PrecompiledObject{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrecompiledObject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrecompiledObject) ProtoMessage() {} + +func (x *PrecompiledObject) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrecompiledObject.ProtoReflect.Descriptor instead. +func (*PrecompiledObject) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{20} +} + +func (x *PrecompiledObject) GetCloudPath() string { + if x != nil { + return x.CloudPath + } + return "" +} + +func (x *PrecompiledObject) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PrecompiledObject) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *PrecompiledObject) GetType() PrecompiledObjectType { + if x != nil { + return x.Type + } + return PrecompiledObjectType_PRECOMPILED_OBJECT_TYPE_UNSPECIFIED +} + +func (x *PrecompiledObject) GetPipelineOptions() string { + if x != nil { + return x.PipelineOptions + } + return "" +} + +func (x *PrecompiledObject) GetLink() string { + if x != nil { + return x.Link + } + return "" +} + +func (x *PrecompiledObject) GetMultifile() bool { + if x != nil { + return x.Multifile + } + return false +} + +func (x *PrecompiledObject) GetContextLine() int32 { + if x != nil { + return x.ContextLine + } + return 0 +} + +func (x *PrecompiledObject) GetDefaultExample() bool { + if x != nil { + return x.DefaultExample + } + return false +} + +func (x *PrecompiledObject) GetSdk() Sdk { + if x != nil { + return x.Sdk + } + return Sdk_SDK_UNSPECIFIED +} + +func (x *PrecompiledObject) GetComplexity() Complexity { + if x != nil { + return x.Complexity + } + return Complexity_COMPLEXITY_UNSPECIFIED +} + +func (x *PrecompiledObject) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +// Categories represent the array of messages with sdk and categories at this sdk +type Categories struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sdk Sdk `protobuf:"varint,1,opt,name=sdk,proto3,enum=api.v1.Sdk" json:"sdk,omitempty"` + Categories []*Categories_Category `protobuf:"bytes,2,rep,name=categories,proto3" json:"categories,omitempty"` +} + +func (x *Categories) Reset() { + *x = Categories{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Categories) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Categories) ProtoMessage() {} + +func (x *Categories) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Categories.ProtoReflect.Descriptor instead. +func (*Categories) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{21} +} + +func (x *Categories) GetSdk() Sdk { + if x != nil { + return x.Sdk + } + return Sdk_SDK_UNSPECIFIED +} + +func (x *Categories) GetCategories() []*Categories_Category { + if x != nil { + return x.Categories + } + return nil +} + +// GetPrecompiledObjectsRequest contains information of the needed PrecompiledObjects sdk and categories. +type GetPrecompiledObjectsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sdk Sdk `protobuf:"varint,1,opt,name=sdk,proto3,enum=api.v1.Sdk" json:"sdk,omitempty"` + Category string `protobuf:"bytes,2,opt,name=category,proto3" json:"category,omitempty"` +} + +func (x *GetPrecompiledObjectsRequest) Reset() { + *x = GetPrecompiledObjectsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectsRequest) ProtoMessage() {} + +func (x *GetPrecompiledObjectsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectsRequest.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectsRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{22} +} + +func (x *GetPrecompiledObjectsRequest) GetSdk() Sdk { + if x != nil { + return x.Sdk + } + return Sdk_SDK_UNSPECIFIED +} + +func (x *GetPrecompiledObjectsRequest) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +// GetPrecompiledObjectRequest contains information of the needed PrecompiledObject. +type GetPrecompiledObjectRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CloudPath string `protobuf:"bytes,1,opt,name=cloud_path,json=cloudPath,proto3" json:"cloud_path,omitempty"` +} + +func (x *GetPrecompiledObjectRequest) Reset() { + *x = GetPrecompiledObjectRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectRequest) ProtoMessage() {} + +func (x *GetPrecompiledObjectRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectRequest.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{23} +} + +func (x *GetPrecompiledObjectRequest) GetCloudPath() string { + if x != nil { + return x.CloudPath + } + return "" +} + +// GetPrecompiledObjectCodeRequest contains information of the PrecompiledObject uuid. +type GetPrecompiledObjectCodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CloudPath string `protobuf:"bytes,1,opt,name=cloud_path,json=cloudPath,proto3" json:"cloud_path,omitempty"` +} + +func (x *GetPrecompiledObjectCodeRequest) Reset() { + *x = GetPrecompiledObjectCodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectCodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectCodeRequest) ProtoMessage() {} + +func (x *GetPrecompiledObjectCodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectCodeRequest.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectCodeRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{24} +} + +func (x *GetPrecompiledObjectCodeRequest) GetCloudPath() string { + if x != nil { + return x.CloudPath + } + return "" +} + +// GetPrecompiledObjectOutputRequest contains information of the PrecompiledObject uuid. +type GetPrecompiledObjectOutputRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CloudPath string `protobuf:"bytes,1,opt,name=cloud_path,json=cloudPath,proto3" json:"cloud_path,omitempty"` +} + +func (x *GetPrecompiledObjectOutputRequest) Reset() { + *x = GetPrecompiledObjectOutputRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectOutputRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectOutputRequest) ProtoMessage() {} + +func (x *GetPrecompiledObjectOutputRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectOutputRequest.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectOutputRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{25} +} + +func (x *GetPrecompiledObjectOutputRequest) GetCloudPath() string { + if x != nil { + return x.CloudPath + } + return "" +} + +// GetPrecompiledObjectLogsRequest contains information of the PrecompiledObject uuid. +type GetPrecompiledObjectLogsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CloudPath string `protobuf:"bytes,1,opt,name=cloud_path,json=cloudPath,proto3" json:"cloud_path,omitempty"` +} + +func (x *GetPrecompiledObjectLogsRequest) Reset() { + *x = GetPrecompiledObjectLogsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectLogsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectLogsRequest) ProtoMessage() {} + +func (x *GetPrecompiledObjectLogsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[26] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectLogsRequest.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectLogsRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{26} +} + +func (x *GetPrecompiledObjectLogsRequest) GetCloudPath() string { + if x != nil { + return x.CloudPath + } + return "" +} + +// GetPrecompiledObjectGraphRequest contains information of the PrecompiledObject cloud path. +type GetPrecompiledObjectGraphRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CloudPath string `protobuf:"bytes,1,opt,name=cloud_path,json=cloudPath,proto3" json:"cloud_path,omitempty"` +} + +func (x *GetPrecompiledObjectGraphRequest) Reset() { + *x = GetPrecompiledObjectGraphRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectGraphRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectGraphRequest) ProtoMessage() {} + +func (x *GetPrecompiledObjectGraphRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[27] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectGraphRequest.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectGraphRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{27} +} + +func (x *GetPrecompiledObjectGraphRequest) GetCloudPath() string { + if x != nil { + return x.CloudPath + } + return "" +} + +// GetDefaultPrecompiledObjectRequest contains information of the needed PrecompiledObject sdk. +type GetDefaultPrecompiledObjectRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sdk Sdk `protobuf:"varint,1,opt,name=sdk,proto3,enum=api.v1.Sdk" json:"sdk,omitempty"` +} + +func (x *GetDefaultPrecompiledObjectRequest) Reset() { + *x = GetDefaultPrecompiledObjectRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetDefaultPrecompiledObjectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDefaultPrecompiledObjectRequest) ProtoMessage() {} + +func (x *GetDefaultPrecompiledObjectRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[28] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDefaultPrecompiledObjectRequest.ProtoReflect.Descriptor instead. +func (*GetDefaultPrecompiledObjectRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{28} +} + +func (x *GetDefaultPrecompiledObjectRequest) GetSdk() Sdk { + if x != nil { + return x.Sdk + } + return Sdk_SDK_UNSPECIFIED +} + +// GetPrecompiledObjectsResponse represent the map between sdk and categories for the sdk. +type GetPrecompiledObjectsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SdkCategories []*Categories `protobuf:"bytes,1,rep,name=sdk_categories,json=sdkCategories,proto3" json:"sdk_categories,omitempty"` +} + +func (x *GetPrecompiledObjectsResponse) Reset() { + *x = GetPrecompiledObjectsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectsResponse) ProtoMessage() {} + +func (x *GetPrecompiledObjectsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[29] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectsResponse.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectsResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{29} +} + +func (x *GetPrecompiledObjectsResponse) GetSdkCategories() []*Categories { + if x != nil { + return x.SdkCategories + } + return nil +} + +// GetPrecompiledObjectResponse represent the PrecompiledObject. +type GetPrecompiledObjectResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PrecompiledObject *PrecompiledObject `protobuf:"bytes,1,opt,name=precompiled_object,json=precompiledObject,proto3" json:"precompiled_object,omitempty"` +} + +func (x *GetPrecompiledObjectResponse) Reset() { + *x = GetPrecompiledObjectResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectResponse) ProtoMessage() {} + +func (x *GetPrecompiledObjectResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[30] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectResponse.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{30} +} + +func (x *GetPrecompiledObjectResponse) GetPrecompiledObject() *PrecompiledObject { + if x != nil { + return x.PrecompiledObject + } + return nil +} + +// GetPrecompiledObjectResponse represents the source code of the PrecompiledObject. +type GetPrecompiledObjectCodeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` +} + +func (x *GetPrecompiledObjectCodeResponse) Reset() { + *x = GetPrecompiledObjectCodeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectCodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectCodeResponse) ProtoMessage() {} + +func (x *GetPrecompiledObjectCodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[31] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectCodeResponse.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectCodeResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{31} +} + +func (x *GetPrecompiledObjectCodeResponse) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +// GetPrecompiledObjectOutputResponse represents the result of the executed code. +type GetPrecompiledObjectOutputResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *GetPrecompiledObjectOutputResponse) Reset() { + *x = GetPrecompiledObjectOutputResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectOutputResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectOutputResponse) ProtoMessage() {} + +func (x *GetPrecompiledObjectOutputResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[32] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectOutputResponse.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectOutputResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{32} +} + +func (x *GetPrecompiledObjectOutputResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// GetPrecompiledObjectLogsResponse represents the result of the executed code. +type GetPrecompiledObjectLogsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` +} + +func (x *GetPrecompiledObjectLogsResponse) Reset() { + *x = GetPrecompiledObjectLogsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectLogsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectLogsResponse) ProtoMessage() {} + +func (x *GetPrecompiledObjectLogsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[33] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectLogsResponse.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectLogsResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{33} +} + +func (x *GetPrecompiledObjectLogsResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// GetPrecompiledObjectGraphResponse represents the string representation of the executed code graph in DOT format. +type GetPrecompiledObjectGraphResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Graph string `protobuf:"bytes,1,opt,name=graph,proto3" json:"graph,omitempty"` +} + +func (x *GetPrecompiledObjectGraphResponse) Reset() { + *x = GetPrecompiledObjectGraphResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetPrecompiledObjectGraphResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPrecompiledObjectGraphResponse) ProtoMessage() {} + +func (x *GetPrecompiledObjectGraphResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[34] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPrecompiledObjectGraphResponse.ProtoReflect.Descriptor instead. +func (*GetPrecompiledObjectGraphResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{34} +} + +func (x *GetPrecompiledObjectGraphResponse) GetGraph() string { + if x != nil { + return x.Graph + } + return "" +} + +// GetDefaultPrecompiledObjectResponse represents the default PrecompiledObject and his category for the sdk. +type GetDefaultPrecompiledObjectResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PrecompiledObject *PrecompiledObject `protobuf:"bytes,1,opt,name=precompiled_object,json=precompiledObject,proto3" json:"precompiled_object,omitempty"` +} + +func (x *GetDefaultPrecompiledObjectResponse) Reset() { + *x = GetDefaultPrecompiledObjectResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetDefaultPrecompiledObjectResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDefaultPrecompiledObjectResponse) ProtoMessage() {} + +func (x *GetDefaultPrecompiledObjectResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[35] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDefaultPrecompiledObjectResponse.ProtoReflect.Descriptor instead. +func (*GetDefaultPrecompiledObjectResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{35} +} + +func (x *GetDefaultPrecompiledObjectResponse) GetPrecompiledObject() *PrecompiledObject { + if x != nil { + return x.PrecompiledObject + } + return nil +} + +// SnippetFile represents the snippet file content and its name to save. +type SnippetFile struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + IsMain bool `protobuf:"varint,3,opt,name=is_main,json=isMain,proto3" json:"is_main,omitempty"` +} + +func (x *SnippetFile) Reset() { + *x = SnippetFile{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SnippetFile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SnippetFile) ProtoMessage() {} + +func (x *SnippetFile) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[36] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SnippetFile.ProtoReflect.Descriptor instead. +func (*SnippetFile) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{36} +} + +func (x *SnippetFile) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SnippetFile) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *SnippetFile) GetIsMain() bool { + if x != nil { + return x.IsMain + } + return false +} + +// SaveSnippetRequest represents a snippet content and options of SDK which executes the snippet. +type SaveSnippetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Files []*SnippetFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` + Sdk Sdk `protobuf:"varint,2,opt,name=sdk,proto3,enum=api.v1.Sdk" json:"sdk,omitempty"` + // The pipeline options as they would be passed to the program (e.g. "--option1 value1 --option2 value2") + PipelineOptions string `protobuf:"bytes,3,opt,name=pipeline_options,json=pipelineOptions,proto3" json:"pipeline_options,omitempty"` + Complexity Complexity `protobuf:"varint,4,opt,name=complexity,proto3,enum=api.v1.Complexity" json:"complexity,omitempty"` +} + +func (x *SaveSnippetRequest) Reset() { + *x = SaveSnippetRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SaveSnippetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SaveSnippetRequest) ProtoMessage() {} + +func (x *SaveSnippetRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[37] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SaveSnippetRequest.ProtoReflect.Descriptor instead. +func (*SaveSnippetRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{37} +} + +func (x *SaveSnippetRequest) GetFiles() []*SnippetFile { + if x != nil { + return x.Files + } + return nil +} + +func (x *SaveSnippetRequest) GetSdk() Sdk { + if x != nil { + return x.Sdk + } + return Sdk_SDK_UNSPECIFIED +} + +func (x *SaveSnippetRequest) GetPipelineOptions() string { + if x != nil { + return x.PipelineOptions + } + return "" +} + +func (x *SaveSnippetRequest) GetComplexity() Complexity { + if x != nil { + return x.Complexity + } + return Complexity_COMPLEXITY_UNSPECIFIED +} + +// SaveSnippetResponse contains information of the generated ID. +type SaveSnippetResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *SaveSnippetResponse) Reset() { + *x = SaveSnippetResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SaveSnippetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SaveSnippetResponse) ProtoMessage() {} + +func (x *SaveSnippetResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[38] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SaveSnippetResponse.ProtoReflect.Descriptor instead. +func (*SaveSnippetResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{38} +} + +func (x *SaveSnippetResponse) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// GetSnippetRequest represents the generated ID. +type GetSnippetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *GetSnippetRequest) Reset() { + *x = GetSnippetRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetSnippetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSnippetRequest) ProtoMessage() {} + +func (x *GetSnippetRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[39] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSnippetRequest.ProtoReflect.Descriptor instead. +func (*GetSnippetRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{39} +} + +func (x *GetSnippetRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// GetSnippetResponse contains information of a snippet content and options of SDK which executes the snippet. +type GetSnippetResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Files []*SnippetFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty"` + Sdk Sdk `protobuf:"varint,2,opt,name=sdk,proto3,enum=api.v1.Sdk" json:"sdk,omitempty"` + // The pipeline options as they would be passed to the program (e.g. "--option1 value1 --option2 value2") + PipelineOptions string `protobuf:"bytes,3,opt,name=pipeline_options,json=pipelineOptions,proto3" json:"pipeline_options,omitempty"` + Complexity Complexity `protobuf:"varint,4,opt,name=complexity,proto3,enum=api.v1.Complexity" json:"complexity,omitempty"` +} + +func (x *GetSnippetResponse) Reset() { + *x = GetSnippetResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetSnippetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSnippetResponse) ProtoMessage() {} + +func (x *GetSnippetResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[40] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSnippetResponse.ProtoReflect.Descriptor instead. +func (*GetSnippetResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{40} +} + +func (x *GetSnippetResponse) GetFiles() []*SnippetFile { + if x != nil { + return x.Files + } + return nil +} + +func (x *GetSnippetResponse) GetSdk() Sdk { + if x != nil { + return x.Sdk + } + return Sdk_SDK_UNSPECIFIED +} + +func (x *GetSnippetResponse) GetPipelineOptions() string { + if x != nil { + return x.PipelineOptions + } + return "" +} + +func (x *GetSnippetResponse) GetComplexity() Complexity { + if x != nil { + return x.Complexity + } + return Complexity_COMPLEXITY_UNSPECIFIED +} + +type Categories_Category struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CategoryName string `protobuf:"bytes,1,opt,name=category_name,json=categoryName,proto3" json:"category_name,omitempty"` + PrecompiledObjects []*PrecompiledObject `protobuf:"bytes,2,rep,name=precompiled_objects,json=precompiledObjects,proto3" json:"precompiled_objects,omitempty"` +} + +func (x *Categories_Category) Reset() { + *x = Categories_Category{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Categories_Category) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Categories_Category) ProtoMessage() {} + +func (x *Categories_Category) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[41] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Categories_Category.ProtoReflect.Descriptor instead. +func (*Categories_Category) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{21, 0} +} + +func (x *Categories_Category) GetCategoryName() string { + if x != nil { + return x.CategoryName + } + return "" +} + +func (x *Categories_Category) GetPrecompiledObjects() []*PrecompiledObject { + if x != nil { + return x.PrecompiledObjects + } + return nil +} + +var File_api_proto protoreflect.FileDescriptor + +var file_api_proto_rawDesc = []byte{ + 0x0a, 0x09, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x22, 0x6e, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x73, 0x64, 0x6b, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x64, 0x6b, 0x52, 0x03, 0x73, 0x64, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x69, 0x70, 0x65, + 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0f, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x4f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x22, 0x36, 0x0a, 0x0f, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, + 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, 0x39, 0x0a, 0x12, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x75, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, 0x3d, 0x0a, 0x13, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x41, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, + 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x69, 0x70, 0x65, + 0x6c, 0x69, 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, 0x35, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, + 0x42, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, + 0x0a, 0x0d, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x55, + 0x75, 0x69, 0x64, 0x22, 0x36, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x3e, 0x0a, 0x17, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, + 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, 0x32, 0x0a, 0x18, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, + 0x3a, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, + 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, 0x2e, 0x0a, 0x14, 0x47, + 0x65, 0x74, 0x52, 0x75, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x39, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x52, 0x75, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x75, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, + 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, 0x2d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x35, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, 0x70, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, 0x29, 0x0a, 0x0f, + 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x36, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x47, 0x72, + 0x61, 0x70, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, + 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, + 0x28, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x61, 0x70, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x61, 0x70, 0x68, 0x22, 0x34, 0x0a, 0x0d, 0x43, 0x61, 0x6e, + 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x69, + 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x55, 0x75, 0x69, 0x64, 0x22, + 0x10, 0x0a, 0x0e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0xab, 0x03, 0x0a, 0x11, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, + 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x6f, + 0x75, 0x64, 0x50, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x29, 0x0a, 0x10, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x69, 0x70, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, + 0x6e, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x1c, + 0x0a, 0x09, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x21, 0x0a, 0x0c, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x12, + 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x03, 0x73, 0x64, 0x6b, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x64, 0x6b, 0x52, 0x03, 0x73, 0x64, 0x6b, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x78, 0x69, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x74, 0x79, 0x52, + 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, + 0x61, 0x67, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x22, + 0xe5, 0x01, 0x0a, 0x0a, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x12, 0x1d, + 0x0a, 0x03, 0x73, 0x64, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x64, 0x6b, 0x52, 0x03, 0x73, 0x64, 0x6b, 0x12, 0x3b, 0x0a, + 0x0a, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, + 0x6f, 0x72, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x0a, + 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x1a, 0x7b, 0x0a, 0x08, 0x43, 0x61, + 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, + 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x13, 0x70, + 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x52, 0x12, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x22, 0x59, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x50, 0x72, + 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x03, 0x73, 0x64, 0x6b, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x64, + 0x6b, 0x52, 0x03, 0x73, 0x64, 0x6b, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x79, 0x22, 0x3c, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, + 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x50, 0x61, 0x74, 0x68, + 0x22, 0x40, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, + 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x5f, 0x70, 0x61, 0x74, + 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x50, 0x61, + 0x74, 0x68, 0x22, 0x42, 0x0a, 0x21, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, + 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x6f, + 0x75, 0x64, 0x50, 0x61, 0x74, 0x68, 0x22, 0x40, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, + 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, + 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x6f, + 0x75, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, + 0x6c, 0x6f, 0x75, 0x64, 0x50, 0x61, 0x74, 0x68, 0x22, 0x41, 0x0a, 0x20, 0x47, 0x65, 0x74, 0x50, + 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x50, 0x61, 0x74, 0x68, 0x22, 0x43, 0x0a, 0x22, 0x47, + 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, + 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x03, 0x73, 0x64, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x64, 0x6b, 0x52, 0x03, 0x73, 0x64, 0x6b, + 0x22, 0x5a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, + 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x39, 0x0a, 0x0e, 0x73, 0x64, 0x6b, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, + 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x52, 0x0d, 0x73, + 0x64, 0x6b, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x22, 0x68, 0x0a, 0x1c, + 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x12, + 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x6f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x2e, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x52, 0x11, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x36, 0x0a, 0x20, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, + 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x3c, + 0x0a, 0x22, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x3a, 0x0a, 0x20, + 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x39, 0x0a, 0x21, 0x47, 0x65, 0x74, 0x50, + 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x67, 0x72, 0x61, 0x70, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, + 0x61, 0x70, 0x68, 0x22, 0x6f, 0x0a, 0x23, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x12, 0x70, 0x72, + 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x52, 0x11, 0x70, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x22, 0x54, 0x0a, 0x0b, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x46, + 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4d, 0x61, 0x69, 0x6e, 0x22, 0xbd, 0x01, 0x0a, 0x12, 0x53, + 0x61, 0x76, 0x65, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x29, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x03, + 0x73, 0x64, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x76, 0x31, 0x2e, 0x53, 0x64, 0x6b, 0x52, 0x03, 0x73, 0x64, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x70, + 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x78, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x74, 0x79, 0x52, 0x0a, + 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x74, 0x79, 0x22, 0x25, 0x0a, 0x13, 0x53, 0x61, + 0x76, 0x65, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x22, 0x23, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xbd, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x53, 0x6e, + 0x69, 0x70, 0x70, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, + 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x46, 0x69, 0x6c, + 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x03, 0x73, 0x64, 0x6b, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x64, 0x6b, 0x52, 0x03, 0x73, 0x64, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x70, 0x69, 0x70, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0f, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x74, 0x79, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x74, 0x79, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x78, 0x69, 0x74, 0x79, 0x2a, 0x52, 0x0a, 0x03, 0x53, 0x64, 0x6b, 0x12, 0x13, 0x0a, + 0x0f, 0x53, 0x44, 0x4b, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x44, 0x4b, 0x5f, 0x4a, 0x41, 0x56, 0x41, 0x10, 0x01, + 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x44, 0x4b, 0x5f, 0x47, 0x4f, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, + 0x53, 0x44, 0x4b, 0x5f, 0x50, 0x59, 0x54, 0x48, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, + 0x53, 0x44, 0x4b, 0x5f, 0x53, 0x43, 0x49, 0x4f, 0x10, 0x04, 0x2a, 0xb8, 0x02, 0x0a, 0x06, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, + 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x49, + 0x4e, 0x47, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x56, + 0x41, 0x4c, 0x49, 0x44, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, + 0x02, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x52, 0x45, 0x50, + 0x41, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x50, 0x52, 0x45, 0x50, 0x41, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x43, 0x4f, 0x4d, 0x50, 0x49, 0x4c, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x18, 0x0a, 0x14, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x49, 0x4c, 0x45, 0x5f, 0x45, 0x52, + 0x52, 0x4f, 0x52, 0x10, 0x06, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x13, 0x0a, 0x0f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x49, 0x4e, 0x49, 0x53, 0x48, 0x45, 0x44, 0x10, 0x08, + 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x10, 0x09, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x0a, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x0b, + 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, + 0x4c, 0x45, 0x44, 0x10, 0x0c, 0x2a, 0xae, 0x01, 0x0a, 0x15, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, + 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x27, 0x0a, 0x23, 0x50, 0x52, 0x45, 0x43, 0x4f, 0x4d, 0x50, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x4f, + 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x52, 0x45, 0x43, + 0x4f, 0x4d, 0x50, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x45, 0x58, 0x41, 0x4d, 0x50, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x20, 0x0a, + 0x1c, 0x50, 0x52, 0x45, 0x43, 0x4f, 0x4d, 0x50, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x4f, 0x42, 0x4a, + 0x45, 0x43, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4b, 0x41, 0x54, 0x41, 0x10, 0x02, 0x12, + 0x25, 0x0a, 0x21, 0x50, 0x52, 0x45, 0x43, 0x4f, 0x4d, 0x50, 0x49, 0x4c, 0x45, 0x44, 0x5f, 0x4f, + 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x49, 0x54, 0x5f, + 0x54, 0x45, 0x53, 0x54, 0x10, 0x03, 0x2a, 0x6e, 0x0a, 0x0a, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x78, 0x69, 0x74, 0x79, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x58, 0x49, + 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, + 0x12, 0x14, 0x0a, 0x10, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x58, 0x49, 0x54, 0x59, 0x5f, 0x42, + 0x41, 0x53, 0x49, 0x43, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, + 0x58, 0x49, 0x54, 0x59, 0x5f, 0x4d, 0x45, 0x44, 0x49, 0x55, 0x4d, 0x10, 0x02, 0x12, 0x17, 0x0a, + 0x13, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x58, 0x49, 0x54, 0x59, 0x5f, 0x41, 0x44, 0x56, 0x41, + 0x4e, 0x43, 0x45, 0x44, 0x10, 0x03, 0x32, 0x8b, 0x0d, 0x0a, 0x11, 0x50, 0x6c, 0x61, 0x79, 0x67, + 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3a, 0x0a, 0x07, + 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x43, 0x6f, 0x64, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x49, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x4f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x07, 0x47, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x47, 0x72, + 0x61, 0x70, 0x68, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x75, 0x6e, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x52, 0x75, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x75, + 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, + 0x0a, 0x13, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, + 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x55, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x4f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x64, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, + 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, + 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x25, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, + 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x72, + 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, + 0x23, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, + 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x18, 0x47, 0x65, + 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x28, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, + 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x73, 0x0a, 0x1a, 0x47, 0x65, 0x74, + 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x29, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, + 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, + 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, + 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x27, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, + 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, + 0x19, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x28, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, + 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x76, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x72, 0x65, + 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2a, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x50, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x50, 0x72, + 0x65, 0x63, 0x6f, 0x6d, 0x70, 0x69, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, 0x53, 0x61, 0x76, 0x65, 0x53, + 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x61, 0x76, 0x65, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, + 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x12, 0x19, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x62, 0x65, 0x61, 0x6d, 0x2e, 0x61, 0x70, 0x61, + 0x63, 0x68, 0x65, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x70, 0x6c, 0x61, 0x79, 0x67, 0x72, 0x6f, 0x75, + 0x6e, 0x64, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x3b, 0x70, 0x6c, 0x61, 0x79, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_api_proto_rawDescOnce sync.Once + file_api_proto_rawDescData = file_api_proto_rawDesc +) + +func file_api_proto_rawDescGZIP() []byte { + file_api_proto_rawDescOnce.Do(func() { + file_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_rawDescData) + }) + return file_api_proto_rawDescData +} + +var file_api_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_api_proto_msgTypes = make([]protoimpl.MessageInfo, 42) +var file_api_proto_goTypes = []interface{}{ + (Sdk)(0), // 0: api.v1.Sdk + (Status)(0), // 1: api.v1.Status + (PrecompiledObjectType)(0), // 2: api.v1.PrecompiledObjectType + (Complexity)(0), // 3: api.v1.Complexity + (*RunCodeRequest)(nil), // 4: api.v1.RunCodeRequest + (*RunCodeResponse)(nil), // 5: api.v1.RunCodeResponse + (*CheckStatusRequest)(nil), // 6: api.v1.CheckStatusRequest + (*CheckStatusResponse)(nil), // 7: api.v1.CheckStatusResponse + (*GetValidationOutputRequest)(nil), // 8: api.v1.GetValidationOutputRequest + (*GetValidationOutputResponse)(nil), // 9: api.v1.GetValidationOutputResponse + (*GetPreparationOutputRequest)(nil), // 10: api.v1.GetPreparationOutputRequest + (*GetPreparationOutputResponse)(nil), // 11: api.v1.GetPreparationOutputResponse + (*GetCompileOutputRequest)(nil), // 12: api.v1.GetCompileOutputRequest + (*GetCompileOutputResponse)(nil), // 13: api.v1.GetCompileOutputResponse + (*GetRunOutputRequest)(nil), // 14: api.v1.GetRunOutputRequest + (*GetRunOutputResponse)(nil), // 15: api.v1.GetRunOutputResponse + (*GetRunErrorRequest)(nil), // 16: api.v1.GetRunErrorRequest + (*GetRunErrorResponse)(nil), // 17: api.v1.GetRunErrorResponse + (*GetLogsRequest)(nil), // 18: api.v1.GetLogsRequest + (*GetLogsResponse)(nil), // 19: api.v1.GetLogsResponse + (*GetGraphRequest)(nil), // 20: api.v1.GetGraphRequest + (*GetGraphResponse)(nil), // 21: api.v1.GetGraphResponse + (*CancelRequest)(nil), // 22: api.v1.CancelRequest + (*CancelResponse)(nil), // 23: api.v1.CancelResponse + (*PrecompiledObject)(nil), // 24: api.v1.PrecompiledObject + (*Categories)(nil), // 25: api.v1.Categories + (*GetPrecompiledObjectsRequest)(nil), // 26: api.v1.GetPrecompiledObjectsRequest + (*GetPrecompiledObjectRequest)(nil), // 27: api.v1.GetPrecompiledObjectRequest + (*GetPrecompiledObjectCodeRequest)(nil), // 28: api.v1.GetPrecompiledObjectCodeRequest + (*GetPrecompiledObjectOutputRequest)(nil), // 29: api.v1.GetPrecompiledObjectOutputRequest + (*GetPrecompiledObjectLogsRequest)(nil), // 30: api.v1.GetPrecompiledObjectLogsRequest + (*GetPrecompiledObjectGraphRequest)(nil), // 31: api.v1.GetPrecompiledObjectGraphRequest + (*GetDefaultPrecompiledObjectRequest)(nil), // 32: api.v1.GetDefaultPrecompiledObjectRequest + (*GetPrecompiledObjectsResponse)(nil), // 33: api.v1.GetPrecompiledObjectsResponse + (*GetPrecompiledObjectResponse)(nil), // 34: api.v1.GetPrecompiledObjectResponse + (*GetPrecompiledObjectCodeResponse)(nil), // 35: api.v1.GetPrecompiledObjectCodeResponse + (*GetPrecompiledObjectOutputResponse)(nil), // 36: api.v1.GetPrecompiledObjectOutputResponse + (*GetPrecompiledObjectLogsResponse)(nil), // 37: api.v1.GetPrecompiledObjectLogsResponse + (*GetPrecompiledObjectGraphResponse)(nil), // 38: api.v1.GetPrecompiledObjectGraphResponse + (*GetDefaultPrecompiledObjectResponse)(nil), // 39: api.v1.GetDefaultPrecompiledObjectResponse + (*SnippetFile)(nil), // 40: api.v1.SnippetFile + (*SaveSnippetRequest)(nil), // 41: api.v1.SaveSnippetRequest + (*SaveSnippetResponse)(nil), // 42: api.v1.SaveSnippetResponse + (*GetSnippetRequest)(nil), // 43: api.v1.GetSnippetRequest + (*GetSnippetResponse)(nil), // 44: api.v1.GetSnippetResponse + (*Categories_Category)(nil), // 45: api.v1.Categories.Category +} +var file_api_proto_depIdxs = []int32{ + 0, // 0: api.v1.RunCodeRequest.sdk:type_name -> api.v1.Sdk + 1, // 1: api.v1.CheckStatusResponse.status:type_name -> api.v1.Status + 2, // 2: api.v1.PrecompiledObject.type:type_name -> api.v1.PrecompiledObjectType + 0, // 3: api.v1.PrecompiledObject.sdk:type_name -> api.v1.Sdk + 3, // 4: api.v1.PrecompiledObject.complexity:type_name -> api.v1.Complexity + 0, // 5: api.v1.Categories.sdk:type_name -> api.v1.Sdk + 45, // 6: api.v1.Categories.categories:type_name -> api.v1.Categories.Category + 0, // 7: api.v1.GetPrecompiledObjectsRequest.sdk:type_name -> api.v1.Sdk + 0, // 8: api.v1.GetDefaultPrecompiledObjectRequest.sdk:type_name -> api.v1.Sdk + 25, // 9: api.v1.GetPrecompiledObjectsResponse.sdk_categories:type_name -> api.v1.Categories + 24, // 10: api.v1.GetPrecompiledObjectResponse.precompiled_object:type_name -> api.v1.PrecompiledObject + 24, // 11: api.v1.GetDefaultPrecompiledObjectResponse.precompiled_object:type_name -> api.v1.PrecompiledObject + 40, // 12: api.v1.SaveSnippetRequest.files:type_name -> api.v1.SnippetFile + 0, // 13: api.v1.SaveSnippetRequest.sdk:type_name -> api.v1.Sdk + 3, // 14: api.v1.SaveSnippetRequest.complexity:type_name -> api.v1.Complexity + 40, // 15: api.v1.GetSnippetResponse.files:type_name -> api.v1.SnippetFile + 0, // 16: api.v1.GetSnippetResponse.sdk:type_name -> api.v1.Sdk + 3, // 17: api.v1.GetSnippetResponse.complexity:type_name -> api.v1.Complexity + 24, // 18: api.v1.Categories.Category.precompiled_objects:type_name -> api.v1.PrecompiledObject + 4, // 19: api.v1.PlaygroundService.RunCode:input_type -> api.v1.RunCodeRequest + 6, // 20: api.v1.PlaygroundService.CheckStatus:input_type -> api.v1.CheckStatusRequest + 14, // 21: api.v1.PlaygroundService.GetRunOutput:input_type -> api.v1.GetRunOutputRequest + 18, // 22: api.v1.PlaygroundService.GetLogs:input_type -> api.v1.GetLogsRequest + 20, // 23: api.v1.PlaygroundService.GetGraph:input_type -> api.v1.GetGraphRequest + 16, // 24: api.v1.PlaygroundService.GetRunError:input_type -> api.v1.GetRunErrorRequest + 8, // 25: api.v1.PlaygroundService.GetValidationOutput:input_type -> api.v1.GetValidationOutputRequest + 10, // 26: api.v1.PlaygroundService.GetPreparationOutput:input_type -> api.v1.GetPreparationOutputRequest + 12, // 27: api.v1.PlaygroundService.GetCompileOutput:input_type -> api.v1.GetCompileOutputRequest + 22, // 28: api.v1.PlaygroundService.Cancel:input_type -> api.v1.CancelRequest + 26, // 29: api.v1.PlaygroundService.GetPrecompiledObjects:input_type -> api.v1.GetPrecompiledObjectsRequest + 27, // 30: api.v1.PlaygroundService.GetPrecompiledObject:input_type -> api.v1.GetPrecompiledObjectRequest + 28, // 31: api.v1.PlaygroundService.GetPrecompiledObjectCode:input_type -> api.v1.GetPrecompiledObjectCodeRequest + 29, // 32: api.v1.PlaygroundService.GetPrecompiledObjectOutput:input_type -> api.v1.GetPrecompiledObjectOutputRequest + 30, // 33: api.v1.PlaygroundService.GetPrecompiledObjectLogs:input_type -> api.v1.GetPrecompiledObjectLogsRequest + 31, // 34: api.v1.PlaygroundService.GetPrecompiledObjectGraph:input_type -> api.v1.GetPrecompiledObjectGraphRequest + 32, // 35: api.v1.PlaygroundService.GetDefaultPrecompiledObject:input_type -> api.v1.GetDefaultPrecompiledObjectRequest + 41, // 36: api.v1.PlaygroundService.SaveSnippet:input_type -> api.v1.SaveSnippetRequest + 43, // 37: api.v1.PlaygroundService.GetSnippet:input_type -> api.v1.GetSnippetRequest + 5, // 38: api.v1.PlaygroundService.RunCode:output_type -> api.v1.RunCodeResponse + 7, // 39: api.v1.PlaygroundService.CheckStatus:output_type -> api.v1.CheckStatusResponse + 15, // 40: api.v1.PlaygroundService.GetRunOutput:output_type -> api.v1.GetRunOutputResponse + 19, // 41: api.v1.PlaygroundService.GetLogs:output_type -> api.v1.GetLogsResponse + 21, // 42: api.v1.PlaygroundService.GetGraph:output_type -> api.v1.GetGraphResponse + 17, // 43: api.v1.PlaygroundService.GetRunError:output_type -> api.v1.GetRunErrorResponse + 9, // 44: api.v1.PlaygroundService.GetValidationOutput:output_type -> api.v1.GetValidationOutputResponse + 11, // 45: api.v1.PlaygroundService.GetPreparationOutput:output_type -> api.v1.GetPreparationOutputResponse + 13, // 46: api.v1.PlaygroundService.GetCompileOutput:output_type -> api.v1.GetCompileOutputResponse + 23, // 47: api.v1.PlaygroundService.Cancel:output_type -> api.v1.CancelResponse + 33, // 48: api.v1.PlaygroundService.GetPrecompiledObjects:output_type -> api.v1.GetPrecompiledObjectsResponse + 34, // 49: api.v1.PlaygroundService.GetPrecompiledObject:output_type -> api.v1.GetPrecompiledObjectResponse + 35, // 50: api.v1.PlaygroundService.GetPrecompiledObjectCode:output_type -> api.v1.GetPrecompiledObjectCodeResponse + 36, // 51: api.v1.PlaygroundService.GetPrecompiledObjectOutput:output_type -> api.v1.GetPrecompiledObjectOutputResponse + 37, // 52: api.v1.PlaygroundService.GetPrecompiledObjectLogs:output_type -> api.v1.GetPrecompiledObjectLogsResponse + 38, // 53: api.v1.PlaygroundService.GetPrecompiledObjectGraph:output_type -> api.v1.GetPrecompiledObjectGraphResponse + 39, // 54: api.v1.PlaygroundService.GetDefaultPrecompiledObject:output_type -> api.v1.GetDefaultPrecompiledObjectResponse + 42, // 55: api.v1.PlaygroundService.SaveSnippet:output_type -> api.v1.SaveSnippetResponse + 44, // 56: api.v1.PlaygroundService.GetSnippet:output_type -> api.v1.GetSnippetResponse + 38, // [38:57] is the sub-list for method output_type + 19, // [19:38] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name +} + +func init() { file_api_proto_init() } +func file_api_proto_init() { + if File_api_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RunCodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RunCodeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckStatusRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CheckStatusResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetValidationOutputRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetValidationOutputResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPreparationOutputRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPreparationOutputResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetCompileOutputRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetCompileOutputResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetRunOutputRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetRunOutputResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetRunErrorRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetRunErrorResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetLogsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetLogsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetGraphRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetGraphResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CancelRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CancelResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PrecompiledObject); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Categories); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectCodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectOutputRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectLogsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectGraphRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetDefaultPrecompiledObjectRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectCodeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectOutputResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectLogsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetPrecompiledObjectGraphResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetDefaultPrecompiledObjectResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SnippetFile); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SaveSnippetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SaveSnippetResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetSnippetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetSnippetResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Categories_Category); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_api_proto_rawDesc, + NumEnums: 4, + NumMessages: 42, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_proto_goTypes, + DependencyIndexes: file_api_proto_depIdxs, + EnumInfos: file_api_proto_enumTypes, + MessageInfos: file_api_proto_msgTypes, + }.Build() + File_api_proto = out.File + file_api_proto_rawDesc = nil + file_api_proto_goTypes = nil + file_api_proto_depIdxs = nil +} diff --git a/learning/tour-of-beam/backend/playground_api/api_grpc.pb.go b/learning/tour-of-beam/backend/playground_api/api_grpc.pb.go new file mode 100644 index 000000000000..e50d4961adee --- /dev/null +++ b/learning/tour-of-beam/backend/playground_api/api_grpc.pb.go @@ -0,0 +1,791 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.12.4 +// source: api.proto + +package playground + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// PlaygroundServiceClient is the client API for PlaygroundService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type PlaygroundServiceClient interface { + // Submit the job for an execution and get the pipeline uuid. + RunCode(ctx context.Context, in *RunCodeRequest, opts ...grpc.CallOption) (*RunCodeResponse, error) + // Get the status of pipeline execution. + CheckStatus(ctx context.Context, in *CheckStatusRequest, opts ...grpc.CallOption) (*CheckStatusResponse, error) + // Get the result of pipeline execution. + GetRunOutput(ctx context.Context, in *GetRunOutputRequest, opts ...grpc.CallOption) (*GetRunOutputResponse, error) + // Get the logs of pipeline execution. + GetLogs(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error) + // Get the string representation of the pipeline execution graph in DOT format. + GetGraph(ctx context.Context, in *GetGraphRequest, opts ...grpc.CallOption) (*GetGraphResponse, error) + // Get the error of pipeline execution. + GetRunError(ctx context.Context, in *GetRunErrorRequest, opts ...grpc.CallOption) (*GetRunErrorResponse, error) + // Get the result of pipeline validation. + GetValidationOutput(ctx context.Context, in *GetValidationOutputRequest, opts ...grpc.CallOption) (*GetValidationOutputResponse, error) + // Get the result of pipeline preparation. + GetPreparationOutput(ctx context.Context, in *GetPreparationOutputRequest, opts ...grpc.CallOption) (*GetPreparationOutputResponse, error) + // Get the result of pipeline compilation. + GetCompileOutput(ctx context.Context, in *GetCompileOutputRequest, opts ...grpc.CallOption) (*GetCompileOutputResponse, error) + // Cancel code processing + Cancel(ctx context.Context, in *CancelRequest, opts ...grpc.CallOption) (*CancelResponse, error) + // Get all precompiled objects from the cloud datastore. + GetPrecompiledObjects(ctx context.Context, in *GetPrecompiledObjectsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectsResponse, error) + // Get precompiled object from the cloud datastore. + GetPrecompiledObject(ctx context.Context, in *GetPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectResponse, error) + // Get the code of an PrecompiledObject. + GetPrecompiledObjectCode(ctx context.Context, in *GetPrecompiledObjectCodeRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectCodeResponse, error) + // Get the precompiled details of an PrecompiledObject. + GetPrecompiledObjectOutput(ctx context.Context, in *GetPrecompiledObjectOutputRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectOutputResponse, error) + // Get the logs of an PrecompiledObject. + GetPrecompiledObjectLogs(ctx context.Context, in *GetPrecompiledObjectLogsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectLogsResponse, error) + // Get the graph of an PrecompiledObject. + GetPrecompiledObjectGraph(ctx context.Context, in *GetPrecompiledObjectGraphRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectGraphResponse, error) + // Get the default precompile object for the sdk. + GetDefaultPrecompiledObject(ctx context.Context, in *GetDefaultPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetDefaultPrecompiledObjectResponse, error) + // Save the snippet required for the sharing. + SaveSnippet(ctx context.Context, in *SaveSnippetRequest, opts ...grpc.CallOption) (*SaveSnippetResponse, error) + // Get the snippet of playground. + GetSnippet(ctx context.Context, in *GetSnippetRequest, opts ...grpc.CallOption) (*GetSnippetResponse, error) +} + +type playgroundServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPlaygroundServiceClient(cc grpc.ClientConnInterface) PlaygroundServiceClient { + return &playgroundServiceClient{cc} +} + +func (c *playgroundServiceClient) RunCode(ctx context.Context, in *RunCodeRequest, opts ...grpc.CallOption) (*RunCodeResponse, error) { + out := new(RunCodeResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/RunCode", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) CheckStatus(ctx context.Context, in *CheckStatusRequest, opts ...grpc.CallOption) (*CheckStatusResponse, error) { + out := new(CheckStatusResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/CheckStatus", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetRunOutput(ctx context.Context, in *GetRunOutputRequest, opts ...grpc.CallOption) (*GetRunOutputResponse, error) { + out := new(GetRunOutputResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetRunOutput", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetLogs(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error) { + out := new(GetLogsResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetLogs", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetGraph(ctx context.Context, in *GetGraphRequest, opts ...grpc.CallOption) (*GetGraphResponse, error) { + out := new(GetGraphResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetGraph", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetRunError(ctx context.Context, in *GetRunErrorRequest, opts ...grpc.CallOption) (*GetRunErrorResponse, error) { + out := new(GetRunErrorResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetRunError", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetValidationOutput(ctx context.Context, in *GetValidationOutputRequest, opts ...grpc.CallOption) (*GetValidationOutputResponse, error) { + out := new(GetValidationOutputResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetValidationOutput", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetPreparationOutput(ctx context.Context, in *GetPreparationOutputRequest, opts ...grpc.CallOption) (*GetPreparationOutputResponse, error) { + out := new(GetPreparationOutputResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetPreparationOutput", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetCompileOutput(ctx context.Context, in *GetCompileOutputRequest, opts ...grpc.CallOption) (*GetCompileOutputResponse, error) { + out := new(GetCompileOutputResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetCompileOutput", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) Cancel(ctx context.Context, in *CancelRequest, opts ...grpc.CallOption) (*CancelResponse, error) { + out := new(CancelResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/Cancel", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetPrecompiledObjects(ctx context.Context, in *GetPrecompiledObjectsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectsResponse, error) { + out := new(GetPrecompiledObjectsResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetPrecompiledObjects", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetPrecompiledObject(ctx context.Context, in *GetPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectResponse, error) { + out := new(GetPrecompiledObjectResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetPrecompiledObject", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetPrecompiledObjectCode(ctx context.Context, in *GetPrecompiledObjectCodeRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectCodeResponse, error) { + out := new(GetPrecompiledObjectCodeResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetPrecompiledObjectCode", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetPrecompiledObjectOutput(ctx context.Context, in *GetPrecompiledObjectOutputRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectOutputResponse, error) { + out := new(GetPrecompiledObjectOutputResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetPrecompiledObjectOutput", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetPrecompiledObjectLogs(ctx context.Context, in *GetPrecompiledObjectLogsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectLogsResponse, error) { + out := new(GetPrecompiledObjectLogsResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetPrecompiledObjectLogs", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetPrecompiledObjectGraph(ctx context.Context, in *GetPrecompiledObjectGraphRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectGraphResponse, error) { + out := new(GetPrecompiledObjectGraphResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetPrecompiledObjectGraph", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetDefaultPrecompiledObject(ctx context.Context, in *GetDefaultPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetDefaultPrecompiledObjectResponse, error) { + out := new(GetDefaultPrecompiledObjectResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetDefaultPrecompiledObject", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) SaveSnippet(ctx context.Context, in *SaveSnippetRequest, opts ...grpc.CallOption) (*SaveSnippetResponse, error) { + out := new(SaveSnippetResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/SaveSnippet", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *playgroundServiceClient) GetSnippet(ctx context.Context, in *GetSnippetRequest, opts ...grpc.CallOption) (*GetSnippetResponse, error) { + out := new(GetSnippetResponse) + err := c.cc.Invoke(ctx, "/api.v1.PlaygroundService/GetSnippet", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PlaygroundServiceServer is the server API for PlaygroundService service. +// All implementations must embed UnimplementedPlaygroundServiceServer +// for forward compatibility +type PlaygroundServiceServer interface { + // Submit the job for an execution and get the pipeline uuid. + RunCode(context.Context, *RunCodeRequest) (*RunCodeResponse, error) + // Get the status of pipeline execution. + CheckStatus(context.Context, *CheckStatusRequest) (*CheckStatusResponse, error) + // Get the result of pipeline execution. + GetRunOutput(context.Context, *GetRunOutputRequest) (*GetRunOutputResponse, error) + // Get the logs of pipeline execution. + GetLogs(context.Context, *GetLogsRequest) (*GetLogsResponse, error) + // Get the string representation of the pipeline execution graph in DOT format. + GetGraph(context.Context, *GetGraphRequest) (*GetGraphResponse, error) + // Get the error of pipeline execution. + GetRunError(context.Context, *GetRunErrorRequest) (*GetRunErrorResponse, error) + // Get the result of pipeline validation. + GetValidationOutput(context.Context, *GetValidationOutputRequest) (*GetValidationOutputResponse, error) + // Get the result of pipeline preparation. + GetPreparationOutput(context.Context, *GetPreparationOutputRequest) (*GetPreparationOutputResponse, error) + // Get the result of pipeline compilation. + GetCompileOutput(context.Context, *GetCompileOutputRequest) (*GetCompileOutputResponse, error) + // Cancel code processing + Cancel(context.Context, *CancelRequest) (*CancelResponse, error) + // Get all precompiled objects from the cloud datastore. + GetPrecompiledObjects(context.Context, *GetPrecompiledObjectsRequest) (*GetPrecompiledObjectsResponse, error) + // Get precompiled object from the cloud datastore. + GetPrecompiledObject(context.Context, *GetPrecompiledObjectRequest) (*GetPrecompiledObjectResponse, error) + // Get the code of an PrecompiledObject. + GetPrecompiledObjectCode(context.Context, *GetPrecompiledObjectCodeRequest) (*GetPrecompiledObjectCodeResponse, error) + // Get the precompiled details of an PrecompiledObject. + GetPrecompiledObjectOutput(context.Context, *GetPrecompiledObjectOutputRequest) (*GetPrecompiledObjectOutputResponse, error) + // Get the logs of an PrecompiledObject. + GetPrecompiledObjectLogs(context.Context, *GetPrecompiledObjectLogsRequest) (*GetPrecompiledObjectLogsResponse, error) + // Get the graph of an PrecompiledObject. + GetPrecompiledObjectGraph(context.Context, *GetPrecompiledObjectGraphRequest) (*GetPrecompiledObjectGraphResponse, error) + // Get the default precompile object for the sdk. + GetDefaultPrecompiledObject(context.Context, *GetDefaultPrecompiledObjectRequest) (*GetDefaultPrecompiledObjectResponse, error) + // Save the snippet required for the sharing. + SaveSnippet(context.Context, *SaveSnippetRequest) (*SaveSnippetResponse, error) + // Get the snippet of playground. + GetSnippet(context.Context, *GetSnippetRequest) (*GetSnippetResponse, error) + mustEmbedUnimplementedPlaygroundServiceServer() +} + +// UnimplementedPlaygroundServiceServer must be embedded to have forward compatible implementations. +type UnimplementedPlaygroundServiceServer struct { +} + +func (UnimplementedPlaygroundServiceServer) RunCode(context.Context, *RunCodeRequest) (*RunCodeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RunCode not implemented") +} +func (UnimplementedPlaygroundServiceServer) CheckStatus(context.Context, *CheckStatusRequest) (*CheckStatusResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CheckStatus not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetRunOutput(context.Context, *GetRunOutputRequest) (*GetRunOutputResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetRunOutput not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetLogs(context.Context, *GetLogsRequest) (*GetLogsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetLogs not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetGraph(context.Context, *GetGraphRequest) (*GetGraphResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetGraph not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetRunError(context.Context, *GetRunErrorRequest) (*GetRunErrorResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetRunError not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetValidationOutput(context.Context, *GetValidationOutputRequest) (*GetValidationOutputResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetValidationOutput not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetPreparationOutput(context.Context, *GetPreparationOutputRequest) (*GetPreparationOutputResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPreparationOutput not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetCompileOutput(context.Context, *GetCompileOutputRequest) (*GetCompileOutputResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCompileOutput not implemented") +} +func (UnimplementedPlaygroundServiceServer) Cancel(context.Context, *CancelRequest) (*CancelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Cancel not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetPrecompiledObjects(context.Context, *GetPrecompiledObjectsRequest) (*GetPrecompiledObjectsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPrecompiledObjects not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetPrecompiledObject(context.Context, *GetPrecompiledObjectRequest) (*GetPrecompiledObjectResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPrecompiledObject not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetPrecompiledObjectCode(context.Context, *GetPrecompiledObjectCodeRequest) (*GetPrecompiledObjectCodeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPrecompiledObjectCode not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetPrecompiledObjectOutput(context.Context, *GetPrecompiledObjectOutputRequest) (*GetPrecompiledObjectOutputResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPrecompiledObjectOutput not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetPrecompiledObjectLogs(context.Context, *GetPrecompiledObjectLogsRequest) (*GetPrecompiledObjectLogsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPrecompiledObjectLogs not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetPrecompiledObjectGraph(context.Context, *GetPrecompiledObjectGraphRequest) (*GetPrecompiledObjectGraphResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetPrecompiledObjectGraph not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetDefaultPrecompiledObject(context.Context, *GetDefaultPrecompiledObjectRequest) (*GetDefaultPrecompiledObjectResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetDefaultPrecompiledObject not implemented") +} +func (UnimplementedPlaygroundServiceServer) SaveSnippet(context.Context, *SaveSnippetRequest) (*SaveSnippetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SaveSnippet not implemented") +} +func (UnimplementedPlaygroundServiceServer) GetSnippet(context.Context, *GetSnippetRequest) (*GetSnippetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSnippet not implemented") +} +func (UnimplementedPlaygroundServiceServer) mustEmbedUnimplementedPlaygroundServiceServer() {} + +// UnsafePlaygroundServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PlaygroundServiceServer will +// result in compilation errors. +type UnsafePlaygroundServiceServer interface { + mustEmbedUnimplementedPlaygroundServiceServer() +} + +func RegisterPlaygroundServiceServer(s grpc.ServiceRegistrar, srv PlaygroundServiceServer) { + s.RegisterService(&PlaygroundService_ServiceDesc, srv) +} + +func _PlaygroundService_RunCode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RunCodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).RunCode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/RunCode", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).RunCode(ctx, req.(*RunCodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_CheckStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).CheckStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/CheckStatus", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).CheckStatus(ctx, req.(*CheckStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetRunOutput_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRunOutputRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetRunOutput(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetRunOutput", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetRunOutput(ctx, req.(*GetRunOutputRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetLogsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetLogs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetLogs", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetLogs(ctx, req.(*GetLogsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetGraph_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetGraphRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetGraph(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetGraph", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetGraph(ctx, req.(*GetGraphRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetRunError_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRunErrorRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetRunError(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetRunError", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetRunError(ctx, req.(*GetRunErrorRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetValidationOutput_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetValidationOutputRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetValidationOutput(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetValidationOutput", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetValidationOutput(ctx, req.(*GetValidationOutputRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetPreparationOutput_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPreparationOutputRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetPreparationOutput(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetPreparationOutput", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetPreparationOutput(ctx, req.(*GetPreparationOutputRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetCompileOutput_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCompileOutputRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetCompileOutput(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetCompileOutput", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetCompileOutput(ctx, req.(*GetCompileOutputRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_Cancel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CancelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).Cancel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/Cancel", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).Cancel(ctx, req.(*CancelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetPrecompiledObjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPrecompiledObjectsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetPrecompiledObjects(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetPrecompiledObjects", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetPrecompiledObjects(ctx, req.(*GetPrecompiledObjectsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetPrecompiledObject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPrecompiledObjectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetPrecompiledObject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetPrecompiledObject", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetPrecompiledObject(ctx, req.(*GetPrecompiledObjectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetPrecompiledObjectCode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPrecompiledObjectCodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetPrecompiledObjectCode(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetPrecompiledObjectCode", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetPrecompiledObjectCode(ctx, req.(*GetPrecompiledObjectCodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetPrecompiledObjectOutput_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPrecompiledObjectOutputRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetPrecompiledObjectOutput(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetPrecompiledObjectOutput", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetPrecompiledObjectOutput(ctx, req.(*GetPrecompiledObjectOutputRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetPrecompiledObjectLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPrecompiledObjectLogsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetPrecompiledObjectLogs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetPrecompiledObjectLogs", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetPrecompiledObjectLogs(ctx, req.(*GetPrecompiledObjectLogsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetPrecompiledObjectGraph_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPrecompiledObjectGraphRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetPrecompiledObjectGraph(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetPrecompiledObjectGraph", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetPrecompiledObjectGraph(ctx, req.(*GetPrecompiledObjectGraphRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetDefaultPrecompiledObject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDefaultPrecompiledObjectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetDefaultPrecompiledObject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetDefaultPrecompiledObject", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetDefaultPrecompiledObject(ctx, req.(*GetDefaultPrecompiledObjectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_SaveSnippet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SaveSnippetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).SaveSnippet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/SaveSnippet", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).SaveSnippet(ctx, req.(*SaveSnippetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PlaygroundService_GetSnippet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSnippetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlaygroundServiceServer).GetSnippet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.v1.PlaygroundService/GetSnippet", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlaygroundServiceServer).GetSnippet(ctx, req.(*GetSnippetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// PlaygroundService_ServiceDesc is the grpc.ServiceDesc for PlaygroundService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PlaygroundService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "api.v1.PlaygroundService", + HandlerType: (*PlaygroundServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RunCode", + Handler: _PlaygroundService_RunCode_Handler, + }, + { + MethodName: "CheckStatus", + Handler: _PlaygroundService_CheckStatus_Handler, + }, + { + MethodName: "GetRunOutput", + Handler: _PlaygroundService_GetRunOutput_Handler, + }, + { + MethodName: "GetLogs", + Handler: _PlaygroundService_GetLogs_Handler, + }, + { + MethodName: "GetGraph", + Handler: _PlaygroundService_GetGraph_Handler, + }, + { + MethodName: "GetRunError", + Handler: _PlaygroundService_GetRunError_Handler, + }, + { + MethodName: "GetValidationOutput", + Handler: _PlaygroundService_GetValidationOutput_Handler, + }, + { + MethodName: "GetPreparationOutput", + Handler: _PlaygroundService_GetPreparationOutput_Handler, + }, + { + MethodName: "GetCompileOutput", + Handler: _PlaygroundService_GetCompileOutput_Handler, + }, + { + MethodName: "Cancel", + Handler: _PlaygroundService_Cancel_Handler, + }, + { + MethodName: "GetPrecompiledObjects", + Handler: _PlaygroundService_GetPrecompiledObjects_Handler, + }, + { + MethodName: "GetPrecompiledObject", + Handler: _PlaygroundService_GetPrecompiledObject_Handler, + }, + { + MethodName: "GetPrecompiledObjectCode", + Handler: _PlaygroundService_GetPrecompiledObjectCode_Handler, + }, + { + MethodName: "GetPrecompiledObjectOutput", + Handler: _PlaygroundService_GetPrecompiledObjectOutput_Handler, + }, + { + MethodName: "GetPrecompiledObjectLogs", + Handler: _PlaygroundService_GetPrecompiledObjectLogs_Handler, + }, + { + MethodName: "GetPrecompiledObjectGraph", + Handler: _PlaygroundService_GetPrecompiledObjectGraph_Handler, + }, + { + MethodName: "GetDefaultPrecompiledObject", + Handler: _PlaygroundService_GetDefaultPrecompiledObject_Handler, + }, + { + MethodName: "SaveSnippet", + Handler: _PlaygroundService_SaveSnippet_Handler, + }, + { + MethodName: "GetSnippet", + Handler: _PlaygroundService_GetSnippet_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api.proto", +} diff --git a/learning/tour-of-beam/backend/playground_api/helper.go b/learning/tour-of-beam/backend/playground_api/helper.go new file mode 100644 index 000000000000..896d3ecc5cf0 --- /dev/null +++ b/learning/tour-of-beam/backend/playground_api/helper.go @@ -0,0 +1,40 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package playground + +import ( + context "context" + + grpc "google.golang.org/grpc" +) + +func GetMockClient() PlaygroundServiceClient { + + return &PlaygroundServiceClientMock{ + SaveSnippetFunc: func(ctx context.Context, in *SaveSnippetRequest, opts ...grpc.CallOption) (*SaveSnippetResponse, error) { + return &SaveSnippetResponse{Id: "snippet_id_1"}, nil + }, + GetSnippetFunc: func(ctx context.Context, in *GetSnippetRequest, opts ...grpc.CallOption) (*GetSnippetResponse, error) { + return &GetSnippetResponse{ + Files: []*SnippetFile{ + {Name: "main.py", Content: "import sys; sys.exit(0)", IsMain: true}, + }, + Sdk: Sdk_SDK_PYTHON, + PipelineOptions: "some opts", + }, nil + }, + } +} diff --git a/learning/tour-of-beam/backend/playground_api/mock.go b/learning/tour-of-beam/backend/playground_api/mock.go new file mode 100644 index 000000000000..c4dc009c1ff3 --- /dev/null +++ b/learning/tour-of-beam/backend/playground_api/mock.go @@ -0,0 +1,1077 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package playground + +import ( + context "context" + grpc "google.golang.org/grpc" + sync "sync" +) + +// Ensure, that PlaygroundServiceClientMock does implement PlaygroundServiceClient. +// If this is not the case, regenerate this file with moq. +var _ PlaygroundServiceClient = &PlaygroundServiceClientMock{} + +// PlaygroundServiceClientMock is a mock implementation of PlaygroundServiceClient. +// +// func TestSomethingThatUsesPlaygroundServiceClient(t *testing.T) { +// +// // make and configure a mocked PlaygroundServiceClient +// mockedPlaygroundServiceClient := &PlaygroundServiceClientMock{ +// CancelFunc: func(ctx context.Context, in *CancelRequest, opts ...grpc.CallOption) (*CancelResponse, error) { +// panic("mock out the Cancel method") +// }, +// CheckStatusFunc: func(ctx context.Context, in *CheckStatusRequest, opts ...grpc.CallOption) (*CheckStatusResponse, error) { +// panic("mock out the CheckStatus method") +// }, +// GetCompileOutputFunc: func(ctx context.Context, in *GetCompileOutputRequest, opts ...grpc.CallOption) (*GetCompileOutputResponse, error) { +// panic("mock out the GetCompileOutput method") +// }, +// GetDefaultPrecompiledObjectFunc: func(ctx context.Context, in *GetDefaultPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetDefaultPrecompiledObjectResponse, error) { +// panic("mock out the GetDefaultPrecompiledObject method") +// }, +// GetGraphFunc: func(ctx context.Context, in *GetGraphRequest, opts ...grpc.CallOption) (*GetGraphResponse, error) { +// panic("mock out the GetGraph method") +// }, +// GetLogsFunc: func(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error) { +// panic("mock out the GetLogs method") +// }, +// GetPrecompiledObjectFunc: func(ctx context.Context, in *GetPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectResponse, error) { +// panic("mock out the GetPrecompiledObject method") +// }, +// GetPrecompiledObjectCodeFunc: func(ctx context.Context, in *GetPrecompiledObjectCodeRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectCodeResponse, error) { +// panic("mock out the GetPrecompiledObjectCode method") +// }, +// GetPrecompiledObjectGraphFunc: func(ctx context.Context, in *GetPrecompiledObjectGraphRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectGraphResponse, error) { +// panic("mock out the GetPrecompiledObjectGraph method") +// }, +// GetPrecompiledObjectLogsFunc: func(ctx context.Context, in *GetPrecompiledObjectLogsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectLogsResponse, error) { +// panic("mock out the GetPrecompiledObjectLogs method") +// }, +// GetPrecompiledObjectOutputFunc: func(ctx context.Context, in *GetPrecompiledObjectOutputRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectOutputResponse, error) { +// panic("mock out the GetPrecompiledObjectOutput method") +// }, +// GetPrecompiledObjectsFunc: func(ctx context.Context, in *GetPrecompiledObjectsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectsResponse, error) { +// panic("mock out the GetPrecompiledObjects method") +// }, +// GetPreparationOutputFunc: func(ctx context.Context, in *GetPreparationOutputRequest, opts ...grpc.CallOption) (*GetPreparationOutputResponse, error) { +// panic("mock out the GetPreparationOutput method") +// }, +// GetRunErrorFunc: func(ctx context.Context, in *GetRunErrorRequest, opts ...grpc.CallOption) (*GetRunErrorResponse, error) { +// panic("mock out the GetRunError method") +// }, +// GetRunOutputFunc: func(ctx context.Context, in *GetRunOutputRequest, opts ...grpc.CallOption) (*GetRunOutputResponse, error) { +// panic("mock out the GetRunOutput method") +// }, +// GetSnippetFunc: func(ctx context.Context, in *GetSnippetRequest, opts ...grpc.CallOption) (*GetSnippetResponse, error) { +// panic("mock out the GetSnippet method") +// }, +// GetValidationOutputFunc: func(ctx context.Context, in *GetValidationOutputRequest, opts ...grpc.CallOption) (*GetValidationOutputResponse, error) { +// panic("mock out the GetValidationOutput method") +// }, +// RunCodeFunc: func(ctx context.Context, in *RunCodeRequest, opts ...grpc.CallOption) (*RunCodeResponse, error) { +// panic("mock out the RunCode method") +// }, +// SaveSnippetFunc: func(ctx context.Context, in *SaveSnippetRequest, opts ...grpc.CallOption) (*SaveSnippetResponse, error) { +// panic("mock out the SaveSnippet method") +// }, +// } +// +// // use mockedPlaygroundServiceClient in code that requires PlaygroundServiceClient +// // and then make assertions. +// +// } +type PlaygroundServiceClientMock struct { + // CancelFunc mocks the Cancel method. + CancelFunc func(ctx context.Context, in *CancelRequest, opts ...grpc.CallOption) (*CancelResponse, error) + + // CheckStatusFunc mocks the CheckStatus method. + CheckStatusFunc func(ctx context.Context, in *CheckStatusRequest, opts ...grpc.CallOption) (*CheckStatusResponse, error) + + // GetCompileOutputFunc mocks the GetCompileOutput method. + GetCompileOutputFunc func(ctx context.Context, in *GetCompileOutputRequest, opts ...grpc.CallOption) (*GetCompileOutputResponse, error) + + // GetDefaultPrecompiledObjectFunc mocks the GetDefaultPrecompiledObject method. + GetDefaultPrecompiledObjectFunc func(ctx context.Context, in *GetDefaultPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetDefaultPrecompiledObjectResponse, error) + + // GetGraphFunc mocks the GetGraph method. + GetGraphFunc func(ctx context.Context, in *GetGraphRequest, opts ...grpc.CallOption) (*GetGraphResponse, error) + + // GetLogsFunc mocks the GetLogs method. + GetLogsFunc func(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error) + + // GetPrecompiledObjectFunc mocks the GetPrecompiledObject method. + GetPrecompiledObjectFunc func(ctx context.Context, in *GetPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectResponse, error) + + // GetPrecompiledObjectCodeFunc mocks the GetPrecompiledObjectCode method. + GetPrecompiledObjectCodeFunc func(ctx context.Context, in *GetPrecompiledObjectCodeRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectCodeResponse, error) + + // GetPrecompiledObjectGraphFunc mocks the GetPrecompiledObjectGraph method. + GetPrecompiledObjectGraphFunc func(ctx context.Context, in *GetPrecompiledObjectGraphRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectGraphResponse, error) + + // GetPrecompiledObjectLogsFunc mocks the GetPrecompiledObjectLogs method. + GetPrecompiledObjectLogsFunc func(ctx context.Context, in *GetPrecompiledObjectLogsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectLogsResponse, error) + + // GetPrecompiledObjectOutputFunc mocks the GetPrecompiledObjectOutput method. + GetPrecompiledObjectOutputFunc func(ctx context.Context, in *GetPrecompiledObjectOutputRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectOutputResponse, error) + + // GetPrecompiledObjectsFunc mocks the GetPrecompiledObjects method. + GetPrecompiledObjectsFunc func(ctx context.Context, in *GetPrecompiledObjectsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectsResponse, error) + + // GetPreparationOutputFunc mocks the GetPreparationOutput method. + GetPreparationOutputFunc func(ctx context.Context, in *GetPreparationOutputRequest, opts ...grpc.CallOption) (*GetPreparationOutputResponse, error) + + // GetRunErrorFunc mocks the GetRunError method. + GetRunErrorFunc func(ctx context.Context, in *GetRunErrorRequest, opts ...grpc.CallOption) (*GetRunErrorResponse, error) + + // GetRunOutputFunc mocks the GetRunOutput method. + GetRunOutputFunc func(ctx context.Context, in *GetRunOutputRequest, opts ...grpc.CallOption) (*GetRunOutputResponse, error) + + // GetSnippetFunc mocks the GetSnippet method. + GetSnippetFunc func(ctx context.Context, in *GetSnippetRequest, opts ...grpc.CallOption) (*GetSnippetResponse, error) + + // GetValidationOutputFunc mocks the GetValidationOutput method. + GetValidationOutputFunc func(ctx context.Context, in *GetValidationOutputRequest, opts ...grpc.CallOption) (*GetValidationOutputResponse, error) + + // RunCodeFunc mocks the RunCode method. + RunCodeFunc func(ctx context.Context, in *RunCodeRequest, opts ...grpc.CallOption) (*RunCodeResponse, error) + + // SaveSnippetFunc mocks the SaveSnippet method. + SaveSnippetFunc func(ctx context.Context, in *SaveSnippetRequest, opts ...grpc.CallOption) (*SaveSnippetResponse, error) + + // calls tracks calls to the methods. + calls struct { + // Cancel holds details about calls to the Cancel method. + Cancel []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *CancelRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // CheckStatus holds details about calls to the CheckStatus method. + CheckStatus []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *CheckStatusRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetCompileOutput holds details about calls to the GetCompileOutput method. + GetCompileOutput []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetCompileOutputRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetDefaultPrecompiledObject holds details about calls to the GetDefaultPrecompiledObject method. + GetDefaultPrecompiledObject []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetDefaultPrecompiledObjectRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetGraph holds details about calls to the GetGraph method. + GetGraph []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetGraphRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetLogs holds details about calls to the GetLogs method. + GetLogs []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetLogsRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetPrecompiledObject holds details about calls to the GetPrecompiledObject method. + GetPrecompiledObject []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetPrecompiledObjectRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetPrecompiledObjectCode holds details about calls to the GetPrecompiledObjectCode method. + GetPrecompiledObjectCode []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetPrecompiledObjectCodeRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetPrecompiledObjectGraph holds details about calls to the GetPrecompiledObjectGraph method. + GetPrecompiledObjectGraph []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetPrecompiledObjectGraphRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetPrecompiledObjectLogs holds details about calls to the GetPrecompiledObjectLogs method. + GetPrecompiledObjectLogs []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetPrecompiledObjectLogsRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetPrecompiledObjectOutput holds details about calls to the GetPrecompiledObjectOutput method. + GetPrecompiledObjectOutput []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetPrecompiledObjectOutputRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetPrecompiledObjects holds details about calls to the GetPrecompiledObjects method. + GetPrecompiledObjects []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetPrecompiledObjectsRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetPreparationOutput holds details about calls to the GetPreparationOutput method. + GetPreparationOutput []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetPreparationOutputRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetRunError holds details about calls to the GetRunError method. + GetRunError []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetRunErrorRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetRunOutput holds details about calls to the GetRunOutput method. + GetRunOutput []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetRunOutputRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetSnippet holds details about calls to the GetSnippet method. + GetSnippet []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetSnippetRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // GetValidationOutput holds details about calls to the GetValidationOutput method. + GetValidationOutput []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *GetValidationOutputRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // RunCode holds details about calls to the RunCode method. + RunCode []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *RunCodeRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // SaveSnippet holds details about calls to the SaveSnippet method. + SaveSnippet []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *SaveSnippetRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + } + lockCancel sync.RWMutex + lockCheckStatus sync.RWMutex + lockGetCompileOutput sync.RWMutex + lockGetDefaultPrecompiledObject sync.RWMutex + lockGetGraph sync.RWMutex + lockGetLogs sync.RWMutex + lockGetPrecompiledObject sync.RWMutex + lockGetPrecompiledObjectCode sync.RWMutex + lockGetPrecompiledObjectGraph sync.RWMutex + lockGetPrecompiledObjectLogs sync.RWMutex + lockGetPrecompiledObjectOutput sync.RWMutex + lockGetPrecompiledObjects sync.RWMutex + lockGetPreparationOutput sync.RWMutex + lockGetRunError sync.RWMutex + lockGetRunOutput sync.RWMutex + lockGetSnippet sync.RWMutex + lockGetValidationOutput sync.RWMutex + lockRunCode sync.RWMutex + lockSaveSnippet sync.RWMutex +} + +// Cancel calls CancelFunc. +func (mock *PlaygroundServiceClientMock) Cancel(ctx context.Context, in *CancelRequest, opts ...grpc.CallOption) (*CancelResponse, error) { + if mock.CancelFunc == nil { + panic("PlaygroundServiceClientMock.CancelFunc: method is nil but PlaygroundServiceClient.Cancel was just called") + } + callInfo := struct { + Ctx context.Context + In *CancelRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockCancel.Lock() + mock.calls.Cancel = append(mock.calls.Cancel, callInfo) + mock.lockCancel.Unlock() + return mock.CancelFunc(ctx, in, opts...) +} + +// CancelCalls gets all the calls that were made to Cancel. +// Check the length with: +// len(mockedPlaygroundServiceClient.CancelCalls()) +func (mock *PlaygroundServiceClientMock) CancelCalls() []struct { + Ctx context.Context + In *CancelRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *CancelRequest + Opts []grpc.CallOption + } + mock.lockCancel.RLock() + calls = mock.calls.Cancel + mock.lockCancel.RUnlock() + return calls +} + +// CheckStatus calls CheckStatusFunc. +func (mock *PlaygroundServiceClientMock) CheckStatus(ctx context.Context, in *CheckStatusRequest, opts ...grpc.CallOption) (*CheckStatusResponse, error) { + if mock.CheckStatusFunc == nil { + panic("PlaygroundServiceClientMock.CheckStatusFunc: method is nil but PlaygroundServiceClient.CheckStatus was just called") + } + callInfo := struct { + Ctx context.Context + In *CheckStatusRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockCheckStatus.Lock() + mock.calls.CheckStatus = append(mock.calls.CheckStatus, callInfo) + mock.lockCheckStatus.Unlock() + return mock.CheckStatusFunc(ctx, in, opts...) +} + +// CheckStatusCalls gets all the calls that were made to CheckStatus. +// Check the length with: +// len(mockedPlaygroundServiceClient.CheckStatusCalls()) +func (mock *PlaygroundServiceClientMock) CheckStatusCalls() []struct { + Ctx context.Context + In *CheckStatusRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *CheckStatusRequest + Opts []grpc.CallOption + } + mock.lockCheckStatus.RLock() + calls = mock.calls.CheckStatus + mock.lockCheckStatus.RUnlock() + return calls +} + +// GetCompileOutput calls GetCompileOutputFunc. +func (mock *PlaygroundServiceClientMock) GetCompileOutput(ctx context.Context, in *GetCompileOutputRequest, opts ...grpc.CallOption) (*GetCompileOutputResponse, error) { + if mock.GetCompileOutputFunc == nil { + panic("PlaygroundServiceClientMock.GetCompileOutputFunc: method is nil but PlaygroundServiceClient.GetCompileOutput was just called") + } + callInfo := struct { + Ctx context.Context + In *GetCompileOutputRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetCompileOutput.Lock() + mock.calls.GetCompileOutput = append(mock.calls.GetCompileOutput, callInfo) + mock.lockGetCompileOutput.Unlock() + return mock.GetCompileOutputFunc(ctx, in, opts...) +} + +// GetCompileOutputCalls gets all the calls that were made to GetCompileOutput. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetCompileOutputCalls()) +func (mock *PlaygroundServiceClientMock) GetCompileOutputCalls() []struct { + Ctx context.Context + In *GetCompileOutputRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetCompileOutputRequest + Opts []grpc.CallOption + } + mock.lockGetCompileOutput.RLock() + calls = mock.calls.GetCompileOutput + mock.lockGetCompileOutput.RUnlock() + return calls +} + +// GetDefaultPrecompiledObject calls GetDefaultPrecompiledObjectFunc. +func (mock *PlaygroundServiceClientMock) GetDefaultPrecompiledObject(ctx context.Context, in *GetDefaultPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetDefaultPrecompiledObjectResponse, error) { + if mock.GetDefaultPrecompiledObjectFunc == nil { + panic("PlaygroundServiceClientMock.GetDefaultPrecompiledObjectFunc: method is nil but PlaygroundServiceClient.GetDefaultPrecompiledObject was just called") + } + callInfo := struct { + Ctx context.Context + In *GetDefaultPrecompiledObjectRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetDefaultPrecompiledObject.Lock() + mock.calls.GetDefaultPrecompiledObject = append(mock.calls.GetDefaultPrecompiledObject, callInfo) + mock.lockGetDefaultPrecompiledObject.Unlock() + return mock.GetDefaultPrecompiledObjectFunc(ctx, in, opts...) +} + +// GetDefaultPrecompiledObjectCalls gets all the calls that were made to GetDefaultPrecompiledObject. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetDefaultPrecompiledObjectCalls()) +func (mock *PlaygroundServiceClientMock) GetDefaultPrecompiledObjectCalls() []struct { + Ctx context.Context + In *GetDefaultPrecompiledObjectRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetDefaultPrecompiledObjectRequest + Opts []grpc.CallOption + } + mock.lockGetDefaultPrecompiledObject.RLock() + calls = mock.calls.GetDefaultPrecompiledObject + mock.lockGetDefaultPrecompiledObject.RUnlock() + return calls +} + +// GetGraph calls GetGraphFunc. +func (mock *PlaygroundServiceClientMock) GetGraph(ctx context.Context, in *GetGraphRequest, opts ...grpc.CallOption) (*GetGraphResponse, error) { + if mock.GetGraphFunc == nil { + panic("PlaygroundServiceClientMock.GetGraphFunc: method is nil but PlaygroundServiceClient.GetGraph was just called") + } + callInfo := struct { + Ctx context.Context + In *GetGraphRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetGraph.Lock() + mock.calls.GetGraph = append(mock.calls.GetGraph, callInfo) + mock.lockGetGraph.Unlock() + return mock.GetGraphFunc(ctx, in, opts...) +} + +// GetGraphCalls gets all the calls that were made to GetGraph. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetGraphCalls()) +func (mock *PlaygroundServiceClientMock) GetGraphCalls() []struct { + Ctx context.Context + In *GetGraphRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetGraphRequest + Opts []grpc.CallOption + } + mock.lockGetGraph.RLock() + calls = mock.calls.GetGraph + mock.lockGetGraph.RUnlock() + return calls +} + +// GetLogs calls GetLogsFunc. +func (mock *PlaygroundServiceClientMock) GetLogs(ctx context.Context, in *GetLogsRequest, opts ...grpc.CallOption) (*GetLogsResponse, error) { + if mock.GetLogsFunc == nil { + panic("PlaygroundServiceClientMock.GetLogsFunc: method is nil but PlaygroundServiceClient.GetLogs was just called") + } + callInfo := struct { + Ctx context.Context + In *GetLogsRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetLogs.Lock() + mock.calls.GetLogs = append(mock.calls.GetLogs, callInfo) + mock.lockGetLogs.Unlock() + return mock.GetLogsFunc(ctx, in, opts...) +} + +// GetLogsCalls gets all the calls that were made to GetLogs. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetLogsCalls()) +func (mock *PlaygroundServiceClientMock) GetLogsCalls() []struct { + Ctx context.Context + In *GetLogsRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetLogsRequest + Opts []grpc.CallOption + } + mock.lockGetLogs.RLock() + calls = mock.calls.GetLogs + mock.lockGetLogs.RUnlock() + return calls +} + +// GetPrecompiledObject calls GetPrecompiledObjectFunc. +func (mock *PlaygroundServiceClientMock) GetPrecompiledObject(ctx context.Context, in *GetPrecompiledObjectRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectResponse, error) { + if mock.GetPrecompiledObjectFunc == nil { + panic("PlaygroundServiceClientMock.GetPrecompiledObjectFunc: method is nil but PlaygroundServiceClient.GetPrecompiledObject was just called") + } + callInfo := struct { + Ctx context.Context + In *GetPrecompiledObjectRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetPrecompiledObject.Lock() + mock.calls.GetPrecompiledObject = append(mock.calls.GetPrecompiledObject, callInfo) + mock.lockGetPrecompiledObject.Unlock() + return mock.GetPrecompiledObjectFunc(ctx, in, opts...) +} + +// GetPrecompiledObjectCalls gets all the calls that were made to GetPrecompiledObject. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetPrecompiledObjectCalls()) +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectCalls() []struct { + Ctx context.Context + In *GetPrecompiledObjectRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetPrecompiledObjectRequest + Opts []grpc.CallOption + } + mock.lockGetPrecompiledObject.RLock() + calls = mock.calls.GetPrecompiledObject + mock.lockGetPrecompiledObject.RUnlock() + return calls +} + +// GetPrecompiledObjectCode calls GetPrecompiledObjectCodeFunc. +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectCode(ctx context.Context, in *GetPrecompiledObjectCodeRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectCodeResponse, error) { + if mock.GetPrecompiledObjectCodeFunc == nil { + panic("PlaygroundServiceClientMock.GetPrecompiledObjectCodeFunc: method is nil but PlaygroundServiceClient.GetPrecompiledObjectCode was just called") + } + callInfo := struct { + Ctx context.Context + In *GetPrecompiledObjectCodeRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetPrecompiledObjectCode.Lock() + mock.calls.GetPrecompiledObjectCode = append(mock.calls.GetPrecompiledObjectCode, callInfo) + mock.lockGetPrecompiledObjectCode.Unlock() + return mock.GetPrecompiledObjectCodeFunc(ctx, in, opts...) +} + +// GetPrecompiledObjectCodeCalls gets all the calls that were made to GetPrecompiledObjectCode. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetPrecompiledObjectCodeCalls()) +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectCodeCalls() []struct { + Ctx context.Context + In *GetPrecompiledObjectCodeRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetPrecompiledObjectCodeRequest + Opts []grpc.CallOption + } + mock.lockGetPrecompiledObjectCode.RLock() + calls = mock.calls.GetPrecompiledObjectCode + mock.lockGetPrecompiledObjectCode.RUnlock() + return calls +} + +// GetPrecompiledObjectGraph calls GetPrecompiledObjectGraphFunc. +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectGraph(ctx context.Context, in *GetPrecompiledObjectGraphRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectGraphResponse, error) { + if mock.GetPrecompiledObjectGraphFunc == nil { + panic("PlaygroundServiceClientMock.GetPrecompiledObjectGraphFunc: method is nil but PlaygroundServiceClient.GetPrecompiledObjectGraph was just called") + } + callInfo := struct { + Ctx context.Context + In *GetPrecompiledObjectGraphRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetPrecompiledObjectGraph.Lock() + mock.calls.GetPrecompiledObjectGraph = append(mock.calls.GetPrecompiledObjectGraph, callInfo) + mock.lockGetPrecompiledObjectGraph.Unlock() + return mock.GetPrecompiledObjectGraphFunc(ctx, in, opts...) +} + +// GetPrecompiledObjectGraphCalls gets all the calls that were made to GetPrecompiledObjectGraph. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetPrecompiledObjectGraphCalls()) +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectGraphCalls() []struct { + Ctx context.Context + In *GetPrecompiledObjectGraphRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetPrecompiledObjectGraphRequest + Opts []grpc.CallOption + } + mock.lockGetPrecompiledObjectGraph.RLock() + calls = mock.calls.GetPrecompiledObjectGraph + mock.lockGetPrecompiledObjectGraph.RUnlock() + return calls +} + +// GetPrecompiledObjectLogs calls GetPrecompiledObjectLogsFunc. +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectLogs(ctx context.Context, in *GetPrecompiledObjectLogsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectLogsResponse, error) { + if mock.GetPrecompiledObjectLogsFunc == nil { + panic("PlaygroundServiceClientMock.GetPrecompiledObjectLogsFunc: method is nil but PlaygroundServiceClient.GetPrecompiledObjectLogs was just called") + } + callInfo := struct { + Ctx context.Context + In *GetPrecompiledObjectLogsRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetPrecompiledObjectLogs.Lock() + mock.calls.GetPrecompiledObjectLogs = append(mock.calls.GetPrecompiledObjectLogs, callInfo) + mock.lockGetPrecompiledObjectLogs.Unlock() + return mock.GetPrecompiledObjectLogsFunc(ctx, in, opts...) +} + +// GetPrecompiledObjectLogsCalls gets all the calls that were made to GetPrecompiledObjectLogs. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetPrecompiledObjectLogsCalls()) +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectLogsCalls() []struct { + Ctx context.Context + In *GetPrecompiledObjectLogsRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetPrecompiledObjectLogsRequest + Opts []grpc.CallOption + } + mock.lockGetPrecompiledObjectLogs.RLock() + calls = mock.calls.GetPrecompiledObjectLogs + mock.lockGetPrecompiledObjectLogs.RUnlock() + return calls +} + +// GetPrecompiledObjectOutput calls GetPrecompiledObjectOutputFunc. +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectOutput(ctx context.Context, in *GetPrecompiledObjectOutputRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectOutputResponse, error) { + if mock.GetPrecompiledObjectOutputFunc == nil { + panic("PlaygroundServiceClientMock.GetPrecompiledObjectOutputFunc: method is nil but PlaygroundServiceClient.GetPrecompiledObjectOutput was just called") + } + callInfo := struct { + Ctx context.Context + In *GetPrecompiledObjectOutputRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetPrecompiledObjectOutput.Lock() + mock.calls.GetPrecompiledObjectOutput = append(mock.calls.GetPrecompiledObjectOutput, callInfo) + mock.lockGetPrecompiledObjectOutput.Unlock() + return mock.GetPrecompiledObjectOutputFunc(ctx, in, opts...) +} + +// GetPrecompiledObjectOutputCalls gets all the calls that were made to GetPrecompiledObjectOutput. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetPrecompiledObjectOutputCalls()) +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectOutputCalls() []struct { + Ctx context.Context + In *GetPrecompiledObjectOutputRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetPrecompiledObjectOutputRequest + Opts []grpc.CallOption + } + mock.lockGetPrecompiledObjectOutput.RLock() + calls = mock.calls.GetPrecompiledObjectOutput + mock.lockGetPrecompiledObjectOutput.RUnlock() + return calls +} + +// GetPrecompiledObjects calls GetPrecompiledObjectsFunc. +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjects(ctx context.Context, in *GetPrecompiledObjectsRequest, opts ...grpc.CallOption) (*GetPrecompiledObjectsResponse, error) { + if mock.GetPrecompiledObjectsFunc == nil { + panic("PlaygroundServiceClientMock.GetPrecompiledObjectsFunc: method is nil but PlaygroundServiceClient.GetPrecompiledObjects was just called") + } + callInfo := struct { + Ctx context.Context + In *GetPrecompiledObjectsRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetPrecompiledObjects.Lock() + mock.calls.GetPrecompiledObjects = append(mock.calls.GetPrecompiledObjects, callInfo) + mock.lockGetPrecompiledObjects.Unlock() + return mock.GetPrecompiledObjectsFunc(ctx, in, opts...) +} + +// GetPrecompiledObjectsCalls gets all the calls that were made to GetPrecompiledObjects. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetPrecompiledObjectsCalls()) +func (mock *PlaygroundServiceClientMock) GetPrecompiledObjectsCalls() []struct { + Ctx context.Context + In *GetPrecompiledObjectsRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetPrecompiledObjectsRequest + Opts []grpc.CallOption + } + mock.lockGetPrecompiledObjects.RLock() + calls = mock.calls.GetPrecompiledObjects + mock.lockGetPrecompiledObjects.RUnlock() + return calls +} + +// GetPreparationOutput calls GetPreparationOutputFunc. +func (mock *PlaygroundServiceClientMock) GetPreparationOutput(ctx context.Context, in *GetPreparationOutputRequest, opts ...grpc.CallOption) (*GetPreparationOutputResponse, error) { + if mock.GetPreparationOutputFunc == nil { + panic("PlaygroundServiceClientMock.GetPreparationOutputFunc: method is nil but PlaygroundServiceClient.GetPreparationOutput was just called") + } + callInfo := struct { + Ctx context.Context + In *GetPreparationOutputRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetPreparationOutput.Lock() + mock.calls.GetPreparationOutput = append(mock.calls.GetPreparationOutput, callInfo) + mock.lockGetPreparationOutput.Unlock() + return mock.GetPreparationOutputFunc(ctx, in, opts...) +} + +// GetPreparationOutputCalls gets all the calls that were made to GetPreparationOutput. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetPreparationOutputCalls()) +func (mock *PlaygroundServiceClientMock) GetPreparationOutputCalls() []struct { + Ctx context.Context + In *GetPreparationOutputRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetPreparationOutputRequest + Opts []grpc.CallOption + } + mock.lockGetPreparationOutput.RLock() + calls = mock.calls.GetPreparationOutput + mock.lockGetPreparationOutput.RUnlock() + return calls +} + +// GetRunError calls GetRunErrorFunc. +func (mock *PlaygroundServiceClientMock) GetRunError(ctx context.Context, in *GetRunErrorRequest, opts ...grpc.CallOption) (*GetRunErrorResponse, error) { + if mock.GetRunErrorFunc == nil { + panic("PlaygroundServiceClientMock.GetRunErrorFunc: method is nil but PlaygroundServiceClient.GetRunError was just called") + } + callInfo := struct { + Ctx context.Context + In *GetRunErrorRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetRunError.Lock() + mock.calls.GetRunError = append(mock.calls.GetRunError, callInfo) + mock.lockGetRunError.Unlock() + return mock.GetRunErrorFunc(ctx, in, opts...) +} + +// GetRunErrorCalls gets all the calls that were made to GetRunError. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetRunErrorCalls()) +func (mock *PlaygroundServiceClientMock) GetRunErrorCalls() []struct { + Ctx context.Context + In *GetRunErrorRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetRunErrorRequest + Opts []grpc.CallOption + } + mock.lockGetRunError.RLock() + calls = mock.calls.GetRunError + mock.lockGetRunError.RUnlock() + return calls +} + +// GetRunOutput calls GetRunOutputFunc. +func (mock *PlaygroundServiceClientMock) GetRunOutput(ctx context.Context, in *GetRunOutputRequest, opts ...grpc.CallOption) (*GetRunOutputResponse, error) { + if mock.GetRunOutputFunc == nil { + panic("PlaygroundServiceClientMock.GetRunOutputFunc: method is nil but PlaygroundServiceClient.GetRunOutput was just called") + } + callInfo := struct { + Ctx context.Context + In *GetRunOutputRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetRunOutput.Lock() + mock.calls.GetRunOutput = append(mock.calls.GetRunOutput, callInfo) + mock.lockGetRunOutput.Unlock() + return mock.GetRunOutputFunc(ctx, in, opts...) +} + +// GetRunOutputCalls gets all the calls that were made to GetRunOutput. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetRunOutputCalls()) +func (mock *PlaygroundServiceClientMock) GetRunOutputCalls() []struct { + Ctx context.Context + In *GetRunOutputRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetRunOutputRequest + Opts []grpc.CallOption + } + mock.lockGetRunOutput.RLock() + calls = mock.calls.GetRunOutput + mock.lockGetRunOutput.RUnlock() + return calls +} + +// GetSnippet calls GetSnippetFunc. +func (mock *PlaygroundServiceClientMock) GetSnippet(ctx context.Context, in *GetSnippetRequest, opts ...grpc.CallOption) (*GetSnippetResponse, error) { + if mock.GetSnippetFunc == nil { + panic("PlaygroundServiceClientMock.GetSnippetFunc: method is nil but PlaygroundServiceClient.GetSnippet was just called") + } + callInfo := struct { + Ctx context.Context + In *GetSnippetRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetSnippet.Lock() + mock.calls.GetSnippet = append(mock.calls.GetSnippet, callInfo) + mock.lockGetSnippet.Unlock() + return mock.GetSnippetFunc(ctx, in, opts...) +} + +// GetSnippetCalls gets all the calls that were made to GetSnippet. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetSnippetCalls()) +func (mock *PlaygroundServiceClientMock) GetSnippetCalls() []struct { + Ctx context.Context + In *GetSnippetRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetSnippetRequest + Opts []grpc.CallOption + } + mock.lockGetSnippet.RLock() + calls = mock.calls.GetSnippet + mock.lockGetSnippet.RUnlock() + return calls +} + +// GetValidationOutput calls GetValidationOutputFunc. +func (mock *PlaygroundServiceClientMock) GetValidationOutput(ctx context.Context, in *GetValidationOutputRequest, opts ...grpc.CallOption) (*GetValidationOutputResponse, error) { + if mock.GetValidationOutputFunc == nil { + panic("PlaygroundServiceClientMock.GetValidationOutputFunc: method is nil but PlaygroundServiceClient.GetValidationOutput was just called") + } + callInfo := struct { + Ctx context.Context + In *GetValidationOutputRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockGetValidationOutput.Lock() + mock.calls.GetValidationOutput = append(mock.calls.GetValidationOutput, callInfo) + mock.lockGetValidationOutput.Unlock() + return mock.GetValidationOutputFunc(ctx, in, opts...) +} + +// GetValidationOutputCalls gets all the calls that were made to GetValidationOutput. +// Check the length with: +// len(mockedPlaygroundServiceClient.GetValidationOutputCalls()) +func (mock *PlaygroundServiceClientMock) GetValidationOutputCalls() []struct { + Ctx context.Context + In *GetValidationOutputRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *GetValidationOutputRequest + Opts []grpc.CallOption + } + mock.lockGetValidationOutput.RLock() + calls = mock.calls.GetValidationOutput + mock.lockGetValidationOutput.RUnlock() + return calls +} + +// RunCode calls RunCodeFunc. +func (mock *PlaygroundServiceClientMock) RunCode(ctx context.Context, in *RunCodeRequest, opts ...grpc.CallOption) (*RunCodeResponse, error) { + if mock.RunCodeFunc == nil { + panic("PlaygroundServiceClientMock.RunCodeFunc: method is nil but PlaygroundServiceClient.RunCode was just called") + } + callInfo := struct { + Ctx context.Context + In *RunCodeRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockRunCode.Lock() + mock.calls.RunCode = append(mock.calls.RunCode, callInfo) + mock.lockRunCode.Unlock() + return mock.RunCodeFunc(ctx, in, opts...) +} + +// RunCodeCalls gets all the calls that were made to RunCode. +// Check the length with: +// len(mockedPlaygroundServiceClient.RunCodeCalls()) +func (mock *PlaygroundServiceClientMock) RunCodeCalls() []struct { + Ctx context.Context + In *RunCodeRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *RunCodeRequest + Opts []grpc.CallOption + } + mock.lockRunCode.RLock() + calls = mock.calls.RunCode + mock.lockRunCode.RUnlock() + return calls +} + +// SaveSnippet calls SaveSnippetFunc. +func (mock *PlaygroundServiceClientMock) SaveSnippet(ctx context.Context, in *SaveSnippetRequest, opts ...grpc.CallOption) (*SaveSnippetResponse, error) { + if mock.SaveSnippetFunc == nil { + panic("PlaygroundServiceClientMock.SaveSnippetFunc: method is nil but PlaygroundServiceClient.SaveSnippet was just called") + } + callInfo := struct { + Ctx context.Context + In *SaveSnippetRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockSaveSnippet.Lock() + mock.calls.SaveSnippet = append(mock.calls.SaveSnippet, callInfo) + mock.lockSaveSnippet.Unlock() + return mock.SaveSnippetFunc(ctx, in, opts...) +} + +// SaveSnippetCalls gets all the calls that were made to SaveSnippet. +// Check the length with: +// len(mockedPlaygroundServiceClient.SaveSnippetCalls()) +func (mock *PlaygroundServiceClientMock) SaveSnippetCalls() []struct { + Ctx context.Context + In *SaveSnippetRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *SaveSnippetRequest + Opts []grpc.CallOption + } + mock.lockSaveSnippet.RLock() + calls = mock.calls.SaveSnippet + mock.lockSaveSnippet.RUnlock() + return calls +} diff --git a/learning/tour-of-beam/backend/samples/api/get_user_progress.json b/learning/tour-of-beam/backend/samples/api/get_user_progress.json new file mode 100644 index 000000000000..fc8027c9eb9f --- /dev/null +++ b/learning/tour-of-beam/backend/samples/api/get_user_progress.json @@ -0,0 +1,12 @@ +{ + "units": [ + { + "id": "unit_id_1", + "isCompleted": true + }, + { + "id": "unit_id_2", + "userSnippetId": "QY2dskTNu_w" + } + ] +} \ No newline at end of file diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index f3d88fa9e0f3..9983b9bb5304 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -50,6 +50,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.9.0" + autotrie: + dependency: transitive + description: + name: autotrie + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" boolean_selector: dependency: transitive description: @@ -120,6 +127,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -141,15 +155,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.0" - code_text_field: - dependency: "direct main" - description: - path: "." - ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437" - resolved-ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437" - url: "https://github.com/BertrandBev/code_field.git" - source: git - version: "1.0.3" collection: dependency: transitive description: @@ -267,6 +272,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_code_editor: + dependency: transitive + description: + name: flutter_code_editor + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" flutter_driver: dependency: transitive description: flutter @@ -390,6 +402,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.0" + hive: + dependency: transitive + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" http: dependency: "direct main" description: @@ -668,6 +687,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.27.5" + scrollable_positioned_list: + dependency: transitive + description: + name: scrollable_positioned_list + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" shared_preferences: dependency: "direct main" description: @@ -834,6 +860,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.17.4" + tuple: + dependency: transitive + description: + name: tuple + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" typed_data: dependency: transitive description: diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml index da6c4c74ffa4..a6e829542e0c 100644 --- a/learning/tour-of-beam/frontend/pubspec.yaml +++ b/learning/tour-of-beam/frontend/pubspec.yaml @@ -28,10 +28,6 @@ environment: dependencies: app_state: ^0.8.1 - code_text_field: - git: - url: https://github.com/BertrandBev/code_field.git - ref: 9e2c9fe52a69481f038f4b6609e8a0a776429437 easy_localization: ^3.0.1 easy_localization_ext: ^0.1.0 easy_localization_loader: ^1.0.0 diff --git a/model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml b/model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml index ca914b6e6f8d..8ce9d2751545 100644 --- a/model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml +++ b/model/fn-execution/src/main/resources/org/apache/beam/model/fnexecution/v1/standard_coders.yaml @@ -487,6 +487,17 @@ examples: --- +coder: + urn: "beam:coder:row:v1" + # f_char: logical(fixed_char(5)), f_varchar: logical(var_char(5)), f_bytes: logical(fixed_bytes(5)), f_varbytes: logical(var_bytes(5)) + payload: "\n=\n\x06f_char\x1a3\x08\x01:/\n\x1fbeam:logical_type:fixed_char:v1\x1a\x02\x10\x07\"\x02\x10\x03*\x04\n\x02\x18\x05\nB\n\tf_varchar\x1a1\x08\x01:-\n\x1dbeam:logical_type:var_char:v1\x1a\x02\x10\x07\"\x02\x10\x03*\x04\n\x02\x18\n \x01(\x01\nC\n\x07f_bytes\x1a4\x08\x01:0\n beam:logical_type:fixed_bytes:v1\x1a\x02\x10\t\"\x02\x10\x03*\x04\n\x02\x18\x05 \x02(\x02\nD\n\nf_varbytes\x1a2\x08\x01:.\n\x1ebeam:logical_type:var_bytes:v1\x1a\x02\x10\t\"\x02\x10\x03*\x04\n\x02\x18\n \x03(\x03\x12$f0ffb3a4-f46f-41ca-a942-85e3e939452a" +examples: + "\x04\x00\x05ABCDE\x05ABCDE\x05ABCDE\x05ABCDE": {f_char: "ABCDE", f_varchar: "ABCDE", f_bytes: "ABCDE", f_varbytes: "ABCDE"} + "\x04\x00\x05A\n \x02A\n\x05A\n\x00\x00\x00\x02A\n": {f_char: "A\n ", f_varchar: "A\n", f_bytes: "A\n\x00\x00\x00", f_varbytes: "A\n"} + "\x04\x01\x06\x05null?\x04null": {f_char: "null?", f_varchar: null, f_bytes: null, f_varbytes: "null"} + +--- + coder: urn: "beam:coder:sharded_key:v1" components: [{urn: "beam:coder:string_utf8:v1"}] diff --git a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto index fa626bc747f0..6e05aada21f9 100644 --- a/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto +++ b/model/pipeline/src/main/proto/org/apache/beam/model/pipeline/v1/schema.proto @@ -147,6 +147,34 @@ message LogicalTypes { // two's complement encoded big integer. DECIMAL = 3 [(org.apache.beam.model.pipeline.v1.beam_urn) = "beam:logical_type:decimal:v1"]; + + // A URN for FixedLengthBytes type + // - Representation type: BYTES + // - Argument type: INT32. + // A fixed-length bytes with its length as the argument. + FIXED_BYTES = 4 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:logical_type:fixed_bytes:v1"]; + + // A URN for VariableLengthBytes type + // - Representation type: BYTES + // - Argument type: INT32. + // A variable-length bytes with its maximum length as the argument. + VAR_BYTES = 5 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:logical_type:var_bytes:v1"]; + + // A URN for FixedLengthString type + // - Representation type: STRING + // - Argument type: INT32. + // A fixed-length string with its length as the argument. + FIXED_CHAR = 6 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:logical_type:fixed_char:v1"]; + + // A URN for VariableLengthString type + // - Representation type: STRING + // - Argument type: INT32. + // A variable-length string with its maximum length as the argument. + VAR_CHAR = 7 [(org.apache.beam.model.pipeline.v1.beam_urn) = + "beam:logical_type:var_char:v1"]; } } diff --git a/playground/backend/README.md b/playground/backend/README.md index 8d847d2569e2..1b3ad66f2b42 100644 --- a/playground/backend/README.md +++ b/playground/backend/README.md @@ -94,6 +94,7 @@ default value and there is no need to set them up to launch locally: - `SDK_CONFIG` - is the sdk configuration file path, e.g. default example for corresponding sdk. It will be saved to cloud datastore during application startup (default value = `../sdks.yaml`) - `DATASTORE_EMULATOR_HOST` - is the datastore emulator address. If it is given in the environment, the application will connect to the datastore emulator. - `PROPERTY_PATH` - is the application properties path (default value = `.`) +- `CACHE_REQUEST_TIMEOUT` - is the timeout to request data from cache (default value = `5 sec`) ### Application properties diff --git a/playground/backend/cmd/server/controller.go b/playground/backend/cmd/server/controller.go index 43cf57fd3e62..d2a5f087132c 100644 --- a/playground/backend/cmd/server/controller.go +++ b/playground/backend/cmd/server/controller.go @@ -284,7 +284,7 @@ func (controller *playgroundController) Cancel(ctx context.Context, info *pb.Can // - If there is no catalog in the cache, gets the catalog from the Datastore and saves it to the cache // - If SDK or category is specified in the request, gets the catalog from the cache and filters it by SDK and category func (controller *playgroundController) GetPrecompiledObjects(ctx context.Context, info *pb.GetPrecompiledObjectsRequest) (*pb.GetPrecompiledObjectsResponse, error) { - catalog, err := controller.cacheComponent.GetCatalogFromCacheOrDatastore(ctx) + catalog, err := controller.cacheComponent.GetCatalogFromCacheOrDatastore(ctx, controller.env.ApplicationEnvs.CacheRequestTimeout()) if err != nil { return nil, errors.InternalError(errorTitleGetCatalog, userCloudConnectionErrMsg) } @@ -299,7 +299,7 @@ func (controller *playgroundController) GetPrecompiledObject(ctx context.Context if err != nil { return nil, errors.InvalidArgumentError(errorTitleGetExample, userBadCloudPathErrMsg) } - sdks, err := controller.cacheComponent.GetSdkCatalogFromCacheOrDatastore(ctx) + sdks, err := controller.cacheComponent.GetSdkCatalogFromCacheOrDatastore(ctx, controller.env.ApplicationEnvs.CacheRequestTimeout()) if err != nil { return nil, errors.InternalError(errorTitleGetExample, err.Error()) } @@ -398,7 +398,7 @@ func (controller *playgroundController) GetDefaultPrecompiledObject(ctx context. logger.Errorf("GetDefaultPrecompiledObject(): unimplemented sdk: %s\n", info.Sdk) return nil, errors.InvalidArgumentError("Error during preparing", "Sdk is not implemented yet: %s", info.Sdk.String()) } - precompiledObject, err := controller.cacheComponent.GetDefaultPrecompiledObjectFromCacheOrDatastore(ctx, info.Sdk) + precompiledObject, err := controller.cacheComponent.GetDefaultPrecompiledObjectFromCacheOrDatastore(ctx, info.Sdk, controller.env.ApplicationEnvs.CacheRequestTimeout()) if err != nil { logger.Errorf("GetDefaultPrecompiledObject(): error during getting catalog: %s", err.Error()) return nil, errors.InternalError("Error during getting Precompiled Objects", "Error with cloud connection") diff --git a/playground/backend/containers/python/Dockerfile b/playground/backend/containers/python/Dockerfile index 2a9182a129a5..5c6fba0fd7c3 100644 --- a/playground/backend/containers/python/Dockerfile +++ b/playground/backend/containers/python/Dockerfile @@ -49,7 +49,6 @@ COPY --from=build /go/bin/server_python_backend /opt/playground/backend/ COPY --from=build /go/src/playground/backend/configs /opt/playground/backend/configs/ # Install Python Katas Utils -COPY katas/log_elements.py /go/src/katas/ ENV PYTHONPATH="$PYTHONPATH:/go/src/katas" # Install mitmpoxy diff --git a/playground/backend/internal/components/cache_component.go b/playground/backend/internal/components/cache_component.go index 9626e9a3030c..72d6c6213dce 100644 --- a/playground/backend/internal/components/cache_component.go +++ b/playground/backend/internal/components/cache_component.go @@ -18,6 +18,7 @@ package components import ( "context" "fmt" + "time" pb "beam.apache.org/playground/backend/internal/api/v1" "beam.apache.org/playground/backend/internal/cache" @@ -37,73 +38,102 @@ func NewService(cache cache.Cache, db db.Database) *CacheComponent { // GetSdkCatalogFromCacheOrDatastore returns the sdk catalog from the cache // - If there is no sdk catalog in the cache, gets the sdk catalog from the Cloud Datastore and saves it to the cache -func (cp *CacheComponent) GetSdkCatalogFromCacheOrDatastore(ctx context.Context) ([]*entity.SDKEntity, error) { - sdks, err := cp.cache.GetSdkCatalog(ctx) +func (cp *CacheComponent) GetSdkCatalogFromCacheOrDatastore(ctx context.Context, cacheRequestTimeout time.Duration) ([]*entity.SDKEntity, error) { + cctx, cancel := context.WithTimeout(ctx, cacheRequestTimeout) + defer cancel() + sdks, err := cp.cache.GetSdkCatalog(cctx) if err != nil { logger.Errorf("error during getting the sdk catalog from the cache, err: %s", err.Error()) - sdks, err = cp.db.GetSDKs(ctx) - if err != nil { - logger.Errorf("error during getting the sdk catalog from the cloud datastore, err: %s", err.Error()) - return nil, err - } - if err = cp.cache.SetSdkCatalog(ctx, sdks); err != nil { - logger.Errorf("error during setting the sdk catalog to the cache, err: %s", err.Error()) - return nil, err - } + return cp.getSdks(ctx, cacheRequestTimeout) + } else { + return sdks, nil + } +} + +func (cp *CacheComponent) getSdks(ctx context.Context, cacheRequestTimeout time.Duration) ([]*entity.SDKEntity, error) { + sdks, err := cp.db.GetSDKs(ctx) + if err != nil { + logger.Errorf("error during getting the sdk catalog from the cloud datastore, err: %s", err.Error()) + return nil, err + } + cctx, cancel := context.WithTimeout(ctx, cacheRequestTimeout) + defer cancel() + if err = cp.cache.SetSdkCatalog(cctx, sdks); err != nil { + logger.Errorf("error during setting the sdk catalog to the cache, err: %s", err.Error()) } return sdks, nil } // GetCatalogFromCacheOrDatastore returns the example catalog from cache // - If there is no catalog in the cache, gets the catalog from the Cloud Datastore and saves it to the cache -func (cp *CacheComponent) GetCatalogFromCacheOrDatastore(ctx context.Context) ([]*pb.Categories, error) { - catalog, err := cp.cache.GetCatalog(ctx) +func (cp *CacheComponent) GetCatalogFromCacheOrDatastore(ctx context.Context, cacheRequestTimeout time.Duration) ([]*pb.Categories, error) { + cctx, cancel := context.WithTimeout(ctx, cacheRequestTimeout) + defer cancel() + catalog, err := cp.cache.GetCatalog(cctx) if err != nil { logger.Errorf("error during getting the catalog from the cache, err: %s", err.Error()) - sdkCatalog, err := cp.GetSdkCatalogFromCacheOrDatastore(ctx) - if err != nil { - logger.Errorf("error during getting the sdk catalog from the cache or datastore, err: %s", err.Error()) - return nil, err - } - catalog, err = cp.db.GetCatalog(ctx, sdkCatalog) - if err != nil { - return nil, err - } - if len(catalog) == 0 { - logger.Warn("example catalog is empty") - return catalog, nil - } - if err = cp.cache.SetCatalog(ctx, catalog); err != nil { - logger.Errorf("SetCatalog(): cache error: %s", err.Error()) - return nil, err - } + return cp.getCatalog(ctx, cacheRequestTimeout) + } else { + return catalog, nil + } +} + +func (cp *CacheComponent) getCatalog(ctx context.Context, cacheRequestTimeout time.Duration) ([]*pb.Categories, error) { + sdkCatalog, err := cp.GetSdkCatalogFromCacheOrDatastore(ctx, cacheRequestTimeout) + if err != nil { + return nil, err + } + catalog, err := cp.db.GetCatalog(ctx, sdkCatalog) + if err != nil { + return nil, err + } + if len(catalog) == 0 { + logger.Warn("example catalog is empty") + return catalog, nil + } + cctx, cancel := context.WithTimeout(ctx, cacheRequestTimeout) + defer cancel() + if err = cp.cache.SetCatalog(cctx, catalog); err != nil { + logger.Errorf("SetCatalog(): cache error: %s", err.Error()) } return catalog, nil } // GetDefaultPrecompiledObjectFromCacheOrDatastore returns the default example from cache by sdk // - If there is no a default example in the cache, gets the default example from the Cloud Datastore and saves it to the cache -func (cp *CacheComponent) GetDefaultPrecompiledObjectFromCacheOrDatastore(ctx context.Context, sdk pb.Sdk) (*pb.PrecompiledObject, error) { - defaultExample, err := cp.cache.GetDefaultPrecompiledObject(ctx, sdk) +func (cp *CacheComponent) GetDefaultPrecompiledObjectFromCacheOrDatastore(ctx context.Context, sdk pb.Sdk, cacheRequestTimeout time.Duration) (*pb.PrecompiledObject, error) { + cctx, cancel := context.WithTimeout(ctx, cacheRequestTimeout) + defer cancel() + defaultExample, err := cp.cache.GetDefaultPrecompiledObject(cctx, sdk) if err != nil { - logger.Errorf("error during getting a default precompiled object, err: %s", err.Error()) - sdks, err := cp.GetSdkCatalogFromCacheOrDatastore(ctx) - if err != nil { - logger.Errorf("error during getting sdk catalog from the cache or the cloud datastore, err: %s", err.Error()) - return nil, err - } - defaultExamples, err := cp.db.GetDefaultExamples(ctx, sdks) - for sdk, defaultExample := range defaultExamples { - if err := cp.cache.SetDefaultPrecompiledObject(ctx, sdk, defaultExample); err != nil { - logger.Errorf("error during setting a default example to the cache: %s", err.Error()) - return nil, err - } - } - defaultExample, ok := defaultExamples[sdk] - if !ok { - return nil, fmt.Errorf("no default example found for this sdk: %s", sdk) - } + logger.Errorf("error during getting the default precompiled object from the cache, err: %s", err.Error()) + return cp.getDefaultExample(ctx, sdk, cacheRequestTimeout) + } else { return defaultExample, nil } +} + +func (cp *CacheComponent) getDefaultExample(ctx context.Context, sdk pb.Sdk, cacheRequestTimeout time.Duration) (*pb.PrecompiledObject, error) { + sdks, err := cp.GetSdkCatalogFromCacheOrDatastore(ctx, cacheRequestTimeout) + if err != nil { + logger.Errorf("error during getting sdk catalog from the cache or the cloud datastore, err: %s", err.Error()) + return nil, err + } + defaultExamples, err := cp.db.GetDefaultExamples(ctx, sdks) + if err != nil { + logger.Errorf("error during getting default examples from the cloud datastore, err: %s", err.Error()) + return nil, err + } + cctx, cancel := context.WithTimeout(ctx, cacheRequestTimeout) + defer cancel() + for sdk, defaultExample := range defaultExamples { + if err := cp.cache.SetDefaultPrecompiledObject(cctx, sdk, defaultExample); err != nil { + logger.Errorf("error during setting a default example to the cache: %s", err.Error()) + } + } + defaultExample, ok := defaultExamples[sdk] + if !ok { + return nil, fmt.Errorf("no default example found for this sdk: %s", sdk) + } return defaultExample, nil } diff --git a/playground/backend/internal/components/cache_component_test.go b/playground/backend/internal/components/cache_component_test.go index 2651b4d2ac8c..9aaad6ea7b1b 100644 --- a/playground/backend/internal/components/cache_component_test.go +++ b/playground/backend/internal/components/cache_component_test.go @@ -20,6 +20,7 @@ import ( "os" "reflect" "testing" + "time" pb "beam.apache.org/playground/backend/internal/api/v1" "beam.apache.org/playground/backend/internal/cache" @@ -36,6 +37,7 @@ var datastoreDb *db.Datastore var ctx context.Context var cacheComponent *CacheComponent var cacheService cache.Cache +var defaultCacheRequestTimeout = 10 * time.Second func TestMain(m *testing.M) { setup() @@ -89,7 +91,7 @@ func TestCacheComponent_GetSdkCatalogFromCacheOrDatastore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.prepare() - result, err := cacheComponent.GetSdkCatalogFromCacheOrDatastore(ctx) + result, err := cacheComponent.GetSdkCatalogFromCacheOrDatastore(ctx, defaultCacheRequestTimeout) if (err != nil) != tt.wantErr { t.Error("GetSdkCatalogFromCacheOrDatastore() unexpected error") return @@ -145,7 +147,7 @@ func TestCacheComponent_GetCatalogFromCacheOrDatastore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.prepare() - result, err := cacheComponent.GetCatalogFromCacheOrDatastore(ctx) + result, err := cacheComponent.GetCatalogFromCacheOrDatastore(ctx, defaultCacheRequestTimeout) if (err != nil) != tt.wantErr { t.Error("GetCatalogFromCacheOrDatastore() unexpected error") return @@ -202,7 +204,7 @@ func TestCacheComponent_GetDefaultPrecompiledObjectFromCacheOrDatastore(t *testi for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.prepare() - result, err := cacheComponent.GetDefaultPrecompiledObjectFromCacheOrDatastore(ctx, pb.Sdk_SDK_JAVA) + result, err := cacheComponent.GetDefaultPrecompiledObjectFromCacheOrDatastore(ctx, pb.Sdk_SDK_JAVA, defaultCacheRequestTimeout) if (err != nil) != tt.wantErr { t.Error("GetDefaultPrecompiledObjectFromCacheOrDatastore() unexpected error") return diff --git a/playground/backend/internal/db/mapper/datastore_mapper_test.go b/playground/backend/internal/db/mapper/datastore_mapper_test.go index f032d939d961..8cd8d19b2028 100644 --- a/playground/backend/internal/db/mapper/datastore_mapper_test.go +++ b/playground/backend/internal/db/mapper/datastore_mapper_test.go @@ -31,7 +31,7 @@ var testable *DatastoreMapper var datastoreMapperCtx = context.Background() func TestMain(m *testing.M) { - appEnv := environment.NewApplicationEnvs("/app", "", "", "", "", "../../../.", nil, 0) + appEnv := environment.NewApplicationEnvs("/app", "", "", "", "", "../../../.", nil, 0, 0) appEnv.SetSchemaVersion("MOCK_SCHEMA") props, _ := environment.NewProperties(appEnv.PropertyPath()) testable = NewDatastoreMapper(datastoreMapperCtx, appEnv, props) diff --git a/playground/backend/internal/db/schema/migration/migrations_test.go b/playground/backend/internal/db/schema/migration/migrations_test.go index c22d07e3fdfd..f2ca13f2c54e 100644 --- a/playground/backend/internal/db/schema/migration/migrations_test.go +++ b/playground/backend/internal/db/schema/migration/migrations_test.go @@ -53,7 +53,7 @@ func setup() { if err != nil { panic(err) } - appEnvs = environment.NewApplicationEnvs("/app", "", "", "", "../../../../../sdks-emulator.yaml", "../../../../.", nil, 0) + appEnvs = environment.NewApplicationEnvs("/app", "", "", "", "../../../../../sdks-emulator.yaml", "../../../../.", nil, 0, 0) props, err = environment.NewProperties(appEnvs.PropertyPath()) if err != nil { panic(err) diff --git a/playground/backend/internal/environment/application.go b/playground/backend/internal/environment/application.go index b32a41a8e69f..05c28a7293d9 100644 --- a/playground/backend/internal/environment/application.go +++ b/playground/backend/internal/environment/application.go @@ -108,13 +108,16 @@ type ApplicationEnvs struct { // propertyPath is the application properties path propertyPath string + + // cacheRequestTimeout is timeout to request data from cache + cacheRequestTimeout time.Duration } // NewApplicationEnvs constructor for ApplicationEnvs func NewApplicationEnvs( workingDir, launchSite, projectId, pipelinesFolder, sdkConfigPath, propertyPath string, cacheEnvs *CacheEnvs, - pipelineExecuteTimeout time.Duration, + pipelineExecuteTimeout, cacheRequestTimeout time.Duration, ) *ApplicationEnvs { return &ApplicationEnvs{ workingDir: workingDir, @@ -125,6 +128,7 @@ func NewApplicationEnvs( pipelinesFolder: pipelinesFolder, sdkConfigPath: sdkConfigPath, propertyPath: propertyPath, + cacheRequestTimeout: cacheRequestTimeout, } } @@ -177,3 +181,8 @@ func (ae *ApplicationEnvs) PropertyPath() string { func (ae *ApplicationEnvs) SetSchemaVersion(schemaVersion string) { ae.schemaVersion = schemaVersion } + +// CacheRequestTimeout returns timeout to request data from cache +func (ae *ApplicationEnvs) CacheRequestTimeout() time.Duration { + return ae.cacheRequestTimeout +} diff --git a/playground/backend/internal/environment/environment_service.go b/playground/backend/internal/environment/environment_service.go index 0cbf8149d5e5..a7ddf0984c03 100644 --- a/playground/backend/internal/environment/environment_service.go +++ b/playground/backend/internal/environment/environment_service.go @@ -65,6 +65,8 @@ const ( defaultSDKConfigPath = "../sdks.yaml" propertyPathKey = "PROPERTY_PATH" defaultPropertyPath = "." + cacheRequestTimeoutKey = "CACHE_REQUEST_TIMEOUT" + defaultCacheRequestTimeout = time.Second * 5 ) // Environment operates with environment structures: NetworkEnvs, BeamEnvs, ApplicationEnvs @@ -99,8 +101,8 @@ func NewEnvironment(networkEnvs NetworkEnvs, beamEnvs BeamEnvs, appEnvs Applicat // - cache address: localhost:6379 // If os environment variables don't contain a value for app working dir - returns error. func GetApplicationEnvsFromOsEnvs() (*ApplicationEnvs, error) { - pipelineExecuteTimeout := defaultPipelineExecuteTimeout - cacheExpirationTime := defaultCacheKeyExpirationTime + pipelineExecuteTimeout := getEnvAsDuration(pipelineExecuteTimeoutKey, defaultPipelineExecuteTimeout, "couldn't convert provided pipeline execute timeout. Using default %s\n") + cacheExpirationTime := getEnvAsDuration(cacheKeyExpirationTimeKey, defaultCacheKeyExpirationTime, "couldn't convert provided cache expiration time. Using default %s\n") cacheType := getEnv(cacheTypeKey, defaultCacheType) cacheAddress := getEnv(cacheAddressKey, defaultCacheAddress) launchSite := getEnv(launchSiteKey, defaultLaunchSite) @@ -108,24 +110,10 @@ func GetApplicationEnvsFromOsEnvs() (*ApplicationEnvs, error) { pipelinesFolder := getEnv(pipelinesFolderKey, defaultPipelinesFolder) sdkConfigPath := getEnv(SDKConfigPathKey, defaultSDKConfigPath) propertyPath := getEnv(propertyPathKey, defaultPropertyPath) - - if value, present := os.LookupEnv(cacheKeyExpirationTimeKey); present { - if converted, err := time.ParseDuration(value); err == nil { - cacheExpirationTime = converted - } else { - log.Printf("couldn't convert provided cache expiration time. Using default %s\n", defaultCacheKeyExpirationTime) - } - } - if value, present := os.LookupEnv(pipelineExecuteTimeoutKey); present { - if converted, err := time.ParseDuration(value); err == nil { - pipelineExecuteTimeout = converted - } else { - log.Printf("couldn't convert provided pipeline execute timeout. Using default %s\n", defaultPipelineExecuteTimeout) - } - } + cacheRequestTimeout := getEnvAsDuration(cacheRequestTimeoutKey, defaultCacheRequestTimeout, "couldn't convert provided cache request timeout. Using default %s\n") if value, present := os.LookupEnv(workingDirKey); present { - return NewApplicationEnvs(value, launchSite, projectId, pipelinesFolder, sdkConfigPath, propertyPath, NewCacheEnvs(cacheType, cacheAddress, cacheExpirationTime), pipelineExecuteTimeout), nil + return NewApplicationEnvs(value, launchSite, projectId, pipelinesFolder, sdkConfigPath, propertyPath, NewCacheEnvs(cacheType, cacheAddress, cacheExpirationTime), pipelineExecuteTimeout, cacheRequestTimeout), nil } return nil, errors.New("APP_WORK_DIR env should be provided with os.env") } @@ -260,3 +248,15 @@ func getEnvAsInt(key string, defaultValue int) int { } return defaultValue } + +// getEnvAsDuration returns an environment variable or default value as duration +func getEnvAsDuration(key string, defaultValue time.Duration, errMsg string) time.Duration { + if value, present := os.LookupEnv(key); present { + if converted, err := time.ParseDuration(value); err == nil { + return converted + } else { + log.Printf(errMsg, defaultValue) + } + } + return defaultValue +} diff --git a/playground/backend/internal/environment/environment_service_test.go b/playground/backend/internal/environment/environment_service_test.go index 04eb13d4b980..0df904f14e9c 100644 --- a/playground/backend/internal/environment/environment_service_test.go +++ b/playground/backend/internal/environment/environment_service_test.go @@ -105,7 +105,7 @@ func TestNewEnvironment(t *testing.T) { {name: "Create env service with default envs", want: &Environment{ NetworkEnvs: *NewNetworkEnvs(defaultIp, defaultPort, defaultProtocol), BeamSdkEnvs: *NewBeamEnvs(defaultSdk, executorConfig, preparedModDir, 0), - ApplicationEnvs: *NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout), + ApplicationEnvs: *NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout, defaultCacheRequestTimeout), }}, } for _, tt := range tests { @@ -113,7 +113,7 @@ func TestNewEnvironment(t *testing.T) { if got := NewEnvironment( *NewNetworkEnvs(defaultIp, defaultPort, defaultProtocol), *NewBeamEnvs(defaultSdk, executorConfig, preparedModDir, 0), - *NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout)); !reflect.DeepEqual(got, tt.want) { + *NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout, defaultCacheRequestTimeout)); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewEnvironment() = %v, want %v", got, tt.want) } }) @@ -224,7 +224,7 @@ func Test_getApplicationEnvsFromOsEnvs(t *testing.T) { }{ { name: "Working dir is provided", - want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout), + want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout, defaultCacheRequestTimeout), wantErr: false, envsToSet: map[string]string{workingDirKey: "/app", launchSiteKey: defaultLaunchSite, projectIdKey: defaultProjectId}, }, @@ -235,25 +235,25 @@ func Test_getApplicationEnvsFromOsEnvs(t *testing.T) { }, { name: "CacheKeyExpirationTimeKey is set with the correct value", - want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, convertedTime}, defaultPipelineExecuteTimeout), + want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, convertedTime}, defaultPipelineExecuteTimeout, defaultCacheRequestTimeout), wantErr: false, envsToSet: map[string]string{workingDirKey: "/app", cacheKeyExpirationTimeKey: hour}, }, { name: "CacheKeyExpirationTimeKey is set with the incorrect value", - want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout), + want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout, defaultCacheRequestTimeout), wantErr: false, envsToSet: map[string]string{workingDirKey: "/app", cacheKeyExpirationTimeKey: "1"}, }, { name: "CacheKeyExpirationTimeKey is set with the correct value", - want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, convertedTime), + want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, convertedTime, defaultCacheRequestTimeout), wantErr: false, envsToSet: map[string]string{workingDirKey: "/app", pipelineExecuteTimeoutKey: hour}, }, { name: "PipelineExecuteTimeoutKey is set with the incorrect value", - want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout), + want: NewApplicationEnvs("/app", defaultLaunchSite, defaultProjectId, defaultPipelinesFolder, defaultSDKConfigPath, defaultPropertyPath, &CacheEnvs{defaultCacheType, defaultCacheAddress, defaultCacheKeyExpirationTime}, defaultPipelineExecuteTimeout, defaultCacheRequestTimeout), wantErr: false, envsToSet: map[string]string{workingDirKey: "/app", pipelineExecuteTimeoutKey: "1"}, }, diff --git a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart index 794449110772..f3f1f52ba098 100644 --- a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart +++ b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart @@ -115,7 +115,7 @@ class PlaygroundController with ChangeNotifier { return controller; } - String? get source => snippetEditingController?.codeController.text; + String? get source => snippetEditingController?.codeController.fullText; bool get isCodeRunning => !(result?.isFinished ?? true); @@ -177,7 +177,7 @@ class PlaygroundController with ChangeNotifier { void setSource(String source) { final controller = requireSnippetEditingController(); - controller.codeController.text = source; + controller.setSource(source); } void setSelectedOutputFilterType(OutputType type) { @@ -234,7 +234,7 @@ class PlaygroundController with ChangeNotifier { _showPrecompiledResult(controller); } else { final request = RunCodeRequest( - code: controller.codeController.text, + code: controller.codeController.fullText, sdk: controller.sdk, pipelineOptions: parsedPipelineOptions, ); @@ -351,7 +351,9 @@ class PlaygroundController with ChangeNotifier { final controller = requireSnippetEditingController(); return exampleCache.getSnippetId( - files: [SharedFile(code: controller.codeController.text, isMain: true)], + files: [ + SharedFile(code: controller.codeController.fullText, isMain: true), + ], sdk: controller.sdk, pipelineOptions: controller.pipelineOptions, ); @@ -370,27 +372,27 @@ class PlaygroundController with ChangeNotifier { } late BeamShortcut runShortcut = BeamShortcut( - shortcuts: LogicalKeySet( - LogicalKeyboardKey.meta, - LogicalKeyboardKey.enter, - ), - actionIntent: const RunIntent(), - createAction: (BuildContext context) => CallbackAction( - onInvoke: (_) => runCode(), - ), - ); + shortcuts: LogicalKeySet( + LogicalKeyboardKey.meta, + LogicalKeyboardKey.enter, + ), + actionIntent: const RunIntent(), + createAction: (BuildContext context) => CallbackAction( + onInvoke: (_) => runCode(), + ), + ); late BeamShortcut resetShortcut = BeamShortcut( - shortcuts: LogicalKeySet( - LogicalKeyboardKey.meta, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyE, - ), - actionIntent: const ResetIntent(), - createAction: (BuildContext context) => CallbackAction( - onInvoke: (_) => reset(), - ), - ); + shortcuts: LogicalKeySet( + LogicalKeyboardKey.meta, + LogicalKeyboardKey.shift, + LogicalKeyboardKey.keyE, + ), + actionIntent: const ResetIntent(), + createAction: (BuildContext context) => CallbackAction( + onInvoke: (_) => reset(), + ), + ); List get shortcuts => [ runShortcut, diff --git a/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart b/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart index 9e36eed809bf..8bb285eff421 100644 --- a/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart +++ b/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart @@ -16,8 +16,8 @@ * limitations under the License. */ -import 'package:code_text_field/code_text_field.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; import '../enums/complexity.dart'; import '../models/example.dart'; @@ -44,7 +44,7 @@ class SnippetEditingController extends ChangeNotifier { set selectedExample(Example? value) { _selectedExample = value; - codeController.text = _selectedExample?.source ?? ''; + setSource(_selectedExample?.source ?? ''); _pipelineOptions = _selectedExample?.pipelineOptions ?? ''; notifyListeners(); } @@ -63,7 +63,7 @@ class SnippetEditingController extends ChangeNotifier { } bool _isCodeChanged() { - return _selectedExample?.source != codeController.text; + return _selectedExample?.source != codeController.fullText; } bool _arePipelineOptionsChanged() { @@ -82,10 +82,15 @@ class SnippetEditingController extends ChangeNotifier { // user-shared examples, and an empty editor, // https://github.com/apache/beam/issues/23252 return ContentExampleLoadingDescriptor( - content: codeController.text, + content: codeController.fullText, name: _selectedExample?.name, complexity: _selectedExample?.complexity ?? Complexity.unspecified, sdk: sdk, ); } + + void setSource(String source) { + codeController.text = source; + codeController.historyController.deleteHistory(); + } } diff --git a/playground/frontend/playground_components/lib/src/theme/theme.dart b/playground/frontend/playground_components/lib/src/theme/theme.dart index fed70dee36b2..14c811abe931 100644 --- a/playground/frontend/playground_components/lib/src/theme/theme.dart +++ b/playground/frontend/playground_components/lib/src/theme/theme.dart @@ -16,8 +16,8 @@ * limitations under the License. */ -import 'package:code_text_field/code_text_field.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -138,7 +138,6 @@ final kLightTheme = ThemeData( secondaryBackgroundColor: BeamLightThemeColors.secondaryBackground, codeBackgroundColor: BeamLightThemeColors.codeBackground, codeRootStyle: GoogleFonts.sourceCodePro( - backgroundColor: BeamLightThemeColors.primaryBackground, color: BeamLightThemeColors.text, fontSize: codeFontSize, ), @@ -212,7 +211,6 @@ final kDarkTheme = ThemeData( secondaryBackgroundColor: BeamDarkThemeColors.secondaryBackground, codeBackgroundColor: BeamDarkThemeColors.codeBackground, codeRootStyle: GoogleFonts.sourceCodePro( - backgroundColor: BeamDarkThemeColors.primaryBackground, color: BeamDarkThemeColors.text, fontSize: codeFontSize, ), diff --git a/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart b/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart index 05231d0ee8b7..9714177ec945 100644 --- a/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart +++ b/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart @@ -18,8 +18,8 @@ // TODO(alexeyinkin): Refactor this, merge into snippet_editor.dart -import 'package:code_text_field/code_text_field.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_code_editor/flutter_code_editor.dart'; import '../models/example.dart'; import '../models/sdk.dart'; @@ -58,7 +58,7 @@ class EditorTextArea extends StatefulWidget { class _EditorTextAreaState extends State { var focusNode = FocusNode(); - final GlobalKey codeFieldKey = LabeledGlobalKey('CodeFieldKey'); + final GlobalKey _sizeKey = LabeledGlobalKey('CodeFieldKey'); @override void dispose() { @@ -82,16 +82,21 @@ class _EditorTextAreaState extends State { readOnly: widget.enabled, label: 'widgets.codeEditor.label', child: FocusScope( + key: _sizeKey, node: FocusScopeNode(canRequestFocus: widget.isEditable), child: CodeTheme( data: ext.codeTheme, - child: CodeField( - key: codeFieldKey, - focusNode: focusNode, - enabled: widget.enabled, - controller: widget.codeController, - textStyle: ext.codeRootStyle, - expands: true, + child: Container( + color: ext.codeTheme.styles['root']?.backgroundColor, + child: SingleChildScrollView( + child: CodeField( + key: ValueKey(widget.codeController), + focusNode: focusNode, + enabled: widget.enabled, + controller: widget.codeController, + textStyle: ext.codeRootStyle, + ), + ), ), ), ), @@ -137,9 +142,8 @@ class _EditorTextAreaState extends State { } int _getQntOfStringsOnScreen() { - RenderBox rBox = - codeFieldKey.currentContext?.findRenderObject() as RenderBox; - double height = rBox.size.height * .75; + final renderBox = _sizeKey.currentContext!.findRenderObject()! as RenderBox; + final height = renderBox.size.height * .75; return height ~/ codeFontSize; } diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index 1a4c91197a0c..7922c2bbec35 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -26,16 +26,13 @@ environment: dependencies: aligned_dialog: ^0.0.6 - code_text_field: - git: - url: https://github.com/BertrandBev/code_field.git - ref: 9e2c9fe52a69481f038f4b6609e8a0a776429437 collection: ^1.16.0 easy_localization: ^3.0.1 easy_localization_ext: ^0.1.1 easy_localization_loader: ^1.0.0 equatable: ^2.0.5 flutter: { sdk: flutter } + flutter_code_editor: ^0.1.3 flutter_markdown: ^0.6.12 flutter_svg: ^1.0.3 google_fonts: ^3.0.1 diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock index 7b3186dbd6bd..6e7da5f916a1 100644 --- a/playground/frontend/pubspec.lock +++ b/playground/frontend/pubspec.lock @@ -50,6 +50,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.9.0" + autotrie: + dependency: transitive + description: + name: autotrie + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" boolean_selector: dependency: transitive description: @@ -148,15 +155,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.1.0" - code_text_field: - dependency: transitive - description: - path: "." - ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437" - resolved-ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437" - url: "https://github.com/BertrandBev/code_field.git" - source: git - version: "1.0.3" collection: dependency: "direct main" description: @@ -274,6 +272,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_code_editor: + dependency: transitive + description: + name: flutter_code_editor + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" flutter_highlight: dependency: transitive description: @@ -293,6 +298,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: transitive + description: + name: flutter_markdown + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.12" flutter_svg: dependency: "direct main" description: @@ -359,6 +371,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.0" + hive: + dependency: transitive + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" html: dependency: transitive description: @@ -421,7 +440,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.5.0" + version: "4.7.0" linked_scroll_controller: dependency: transitive description: @@ -443,6 +462,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" matcher: dependency: transitive description: @@ -639,6 +665,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + scrollable_positioned_list: + dependency: transitive + description: + name: scrollable_positioned_list + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" shared_preferences: dependency: "direct main" description: @@ -777,6 +810,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + tuple: + dependency: transitive + description: + name: tuple + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" typed_data: dependency: transitive description: diff --git a/release/build.gradle.kts b/release/build.gradle.kts index 7de4ab3af61a..ce895af80f8b 100644 --- a/release/build.gradle.kts +++ b/release/build.gradle.kts @@ -38,7 +38,7 @@ task("runJavaExamplesValidationTask") { description = "Run the Beam quickstart across all Java runners" dependsOn(":runners:direct-java:runQuickstartJavaDirect") dependsOn(":runners:google-cloud-dataflow-java:runQuickstartJavaDataflow") - dependsOn(":runners:spark:2:runQuickstartJavaSpark") + dependsOn(":runners:spark:3:runQuickstartJavaSpark") dependsOn(":runners:flink:1.13:runQuickstartJavaFlinkLocal") dependsOn(":runners:direct-java:runMobileGamingJavaDirect") dependsOn(":runners:google-cloud-dataflow-java:runMobileGamingJavaDataflow") diff --git a/release/src/main/scripts/mass_comment.py b/release/src/main/scripts/mass_comment.py index dde2fc7e8e04..cb60bf6d49d0 100644 --- a/release/src/main/scripts/mass_comment.py +++ b/release/src/main/scripts/mass_comment.py @@ -61,7 +61,6 @@ "Run Java Examples_Flink", "Run Java Examples_Spark", "Run Java Flink PortableValidatesRunner Streaming", - "Run Java Portability examples on Dataflow with Java 11", "Run Java PostCommit", "Run Java PreCommit", "Run Java Samza PortableValidatesRunner", diff --git a/runners/google-cloud-dataflow-java/build.gradle b/runners/google-cloud-dataflow-java/build.gradle index 8429bb40816a..b8f292df9f9d 100644 --- a/runners/google-cloud-dataflow-java/build.gradle +++ b/runners/google-cloud-dataflow-java/build.gradle @@ -55,7 +55,7 @@ processResources { 'dataflow.legacy_environment_major_version' : '8', 'dataflow.fnapi_environment_major_version' : '8', 'dataflow.legacy_container_version' : 'beam-master-20220816', - 'dataflow.fnapi_container_version' : 'beam-master-20220923', + 'dataflow.fnapi_container_version' : 'beam-master-20221022', 'dataflow.container_base_repository' : 'gcr.io/cloud-dataflow/v1beta3', ] } diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunner.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunner.java index 7120696aa4f1..76dfe5b720d8 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunner.java +++ b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunner.java @@ -18,7 +18,10 @@ package org.apache.beam.runners.samza.runtime; import java.util.Collection; +import java.util.Collections; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import org.apache.beam.runners.core.DoFnRunner; @@ -27,6 +30,8 @@ import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.util.WindowedValue; +import org.apache.beam.sdk.values.KV; +import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,30 +44,46 @@ public class AsyncDoFnRunner implements DoFnRunner { private static final Logger LOG = LoggerFactory.getLogger(AsyncDoFnRunner.class); + // A dummy key to represent null keys + private static final Object NULL_KEY = new Object(); + private final DoFnRunner underlying; private final ExecutorService executor; private final OpEmitter emitter; private final FutureCollector futureCollector; + private final boolean isStateful; + + /** + * This map keeps track of the last outputFutures for a certain key. When the next element of the + * key comes in, its outputFutures will be chained from the last outputFutures in the map. When + * all futures of a key have been complete, the key entry will be removed. The map is bounded by + * (bundle size * 2). + */ + private final Map>>> keyedOutputFutures; public static AsyncDoFnRunner create( DoFnRunner runner, OpEmitter emitter, FutureCollector futureCollector, + boolean isStateful, SamzaPipelineOptions options) { LOG.info("Run DoFn with " + AsyncDoFnRunner.class.getName()); - return new AsyncDoFnRunner<>(runner, emitter, futureCollector, options); + return new AsyncDoFnRunner<>(runner, emitter, futureCollector, isStateful, options); } private AsyncDoFnRunner( DoFnRunner runner, OpEmitter emitter, FutureCollector futureCollector, + boolean isStateful, SamzaPipelineOptions options) { this.underlying = runner; this.executor = options.getExecutorServiceForProcessElement(); this.emitter = emitter; this.futureCollector = futureCollector; + this.isStateful = isStateful; + this.keyedOutputFutures = new ConcurrentHashMap<>(); } @Override @@ -72,23 +93,59 @@ public void startBundle() { @Override public void processElement(WindowedValue elem) { - final CompletableFuture future = - CompletableFuture.runAsync( - () -> { - underlying.processElement(elem); - }, - executor); - final CompletableFuture>> outputFutures = - future.thenApply( - x -> - emitter.collectOutput().stream() - .map(OpMessage::getElement) - .collect(Collectors.toList())); + isStateful ? processStateful(elem) : processElement(elem, null); futureCollector.addAll(outputFutures); } + private CompletableFuture>> processElement( + WindowedValue elem, + @Nullable CompletableFuture>> prevOutputFuture) { + + final CompletableFuture>> prevFuture = + prevOutputFuture == null + ? CompletableFuture.completedFuture(Collections.emptyList()) + : prevOutputFuture; + + // For ordering by key, we chain the processing of the elem to the completion of + // the previous output of the same key + return prevFuture.thenApplyAsync( + x -> { + underlying.processElement(elem); + + return emitter.collectOutput().stream() + .map(OpMessage::getElement) + .collect(Collectors.toList()); + }, + executor); + } + + private CompletableFuture>> processStateful( + WindowedValue elem) { + final Object key = getKey(elem); + + final CompletableFuture>> outputFutures = + processElement(elem, keyedOutputFutures.get(key)); + + // Update the latest outputFuture for key + keyedOutputFutures.put(key, outputFutures); + + // Remove the outputFuture from the map once it's complete. + // This ensures the map will be cleaned up immediately. + return outputFutures.thenApply( + output -> { + // Under the condition that the outputFutures has not been updated + keyedOutputFutures.remove(key, outputFutures); + return output; + }); + } + + /** Package private for testing. */ + boolean hasOutputFuturesForKey(Object key) { + return keyedOutputFutures.containsKey(key); + } + @Override public void onTimer( String timerId, @@ -115,4 +172,14 @@ public void onWindowExpiration(BoundedWindow window, Instant timestamp, K public DoFn getFn() { return underlying.getFn(); } + + private Object getKey(WindowedValue elem) { + KV kv = (KV) elem.getValue(); + if (kv == null) { + return NULL_KEY; + } else { + Object key = kv.getKey(); + return key == null ? NULL_KEY : key; + } + } } diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpAdapter.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpAdapter.java index eabcd87f5f36..c5353b0e2352 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpAdapter.java +++ b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/OpAdapter.java @@ -159,12 +159,12 @@ public void close() { op.close(); } - private static class OpEmitterImpl implements OpEmitter { + static class OpEmitterImpl implements OpEmitter { private final Queue> outputQueue; private CompletionStage>> outputFuture; private Instant outputWatermark; - private OpEmitterImpl() { + OpEmitterImpl() { outputQueue = new ConcurrentLinkedQueue<>(); } diff --git a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnRunners.java b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnRunners.java index 12872b82d8f7..ec1a9f365090 100644 --- a/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnRunners.java +++ b/runners/samza/src/main/java/org/apache/beam/runners/samza/runtime/SamzaDoFnRunners.java @@ -153,7 +153,8 @@ public static DoFnRunner create( } return pipelineOptions.getNumThreadsForProcessElement() > 1 - ? AsyncDoFnRunner.create(doFnRunnerWithStates, emitter, futureCollector, pipelineOptions) + ? AsyncDoFnRunner.create( + doFnRunnerWithStates, emitter, futureCollector, keyedInternals != null, pipelineOptions) : doFnRunnerWithStates; } diff --git a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunnerTest.java b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunnerTest.java index d62a28b374f1..6d4ffc70d5a4 100644 --- a/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunnerTest.java +++ b/runners/samza/src/test/java/org/apache/beam/runners/samza/runtime/AsyncDoFnRunnerTest.java @@ -17,12 +17,22 @@ */ package org.apache.beam.runners.samza.runtime; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +import org.apache.beam.runners.core.DoFnRunner; +import org.apache.beam.runners.samza.SamzaPipelineOptions; import org.apache.beam.sdk.coders.VarIntCoder; import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.state.CombiningState; @@ -36,6 +46,7 @@ import org.apache.beam.sdk.transforms.MapElements; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Sum; +import org.apache.beam.sdk.util.WindowedValue; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.sdk.values.TypeDescriptors; @@ -116,11 +127,7 @@ public void processElement( return; } - // Need explicit synchronization here - synchronized (this) { - countState.add(1); - } - + countState.add(1); String key = c.element().getKey(); int n = countState.read(); if (n >= expectedCount.get(key)) { @@ -152,7 +159,7 @@ public void testPipelineWithAggregation() { KV.of("banana", 5L))); // TODO: remove after SAMZA-2761 fix - for (int i = 0; i < 20; i++) { + for (int i = 0; i < 50; i++) { input.add(KV.of("*", 0L)); } @@ -168,4 +175,61 @@ public void testPipelineWithAggregation() { pipeline.run(); } + + @Test + public void testKeyedOutputFutures() { + // We test the scenario that two elements of the same key needs to be processed in order. + final DoFnRunner, Void> doFnRunner = mock(DoFnRunner.class); + final AtomicInteger prev = new AtomicInteger(0); + final CountDownLatch latch = new CountDownLatch(1); + doAnswer( + invocation -> { + latch.await(); + WindowedValue> wv = invocation.getArgument(0); + Integer val = wv.getValue().getValue(); + + // Verify the previous element has been fully processed by checking the prev value + assertEquals(val - 1, prev.get()); + + prev.set(val); + return null; + }) + .when(doFnRunner) + .processElement(any()); + + SamzaPipelineOptions options = PipelineOptionsFactory.as(SamzaPipelineOptions.class); + options.setNumThreadsForProcessElement(4); + + final OpEmitter opEmitter = new OpAdapter.OpEmitterImpl<>(); + final FutureCollector futureCollector = new DoFnOp.FutureCollectorImpl<>(); + futureCollector.prepare(); + + final AsyncDoFnRunner, Void> asyncDoFnRunner = + AsyncDoFnRunner.create(doFnRunner, opEmitter, futureCollector, true, options); + + final String appleKey = "apple"; + + final WindowedValue> input1 = + WindowedValue.valueInGlobalWindow(KV.of(appleKey, 1)); + + final WindowedValue> input2 = + WindowedValue.valueInGlobalWindow(KV.of(appleKey, 2)); + + asyncDoFnRunner.processElement(input1); + asyncDoFnRunner.processElement(input2); + // Resume input1 process afterwards + latch.countDown(); + + // Waiting for the futures to be resolved + try { + futureCollector.finish().toCompletableFuture().get(); + } catch (Exception e) { + // ignore interruption here. + } + + // The final val should be the last element value + assertEquals(2, prev.get()); + // The appleKey in keyedOutputFutures map should be removed + assertFalse(asyncDoFnRunner.hasOutputFuturesForKey(appleKey)); + } } diff --git a/runners/spark/3/build.gradle b/runners/spark/3/build.gradle index 3d59bd525c4b..494d367131b4 100644 --- a/runners/spark/3/build.gradle +++ b/runners/spark/3/build.gradle @@ -29,6 +29,9 @@ project.ext { // Load the main build script which contains all build logic. apply from: "$basePath/spark_runner.gradle" +// Generates runQuickstartJavaSpark task (can only support 1 version of Spark) +createJavaExamplesArchetypeValidationTask(type: 'Quickstart', runner: 'Spark') + // Additional supported Spark versions (used in compatibility tests) def sparkVersions = [ "330": "3.3.0", diff --git a/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/TransformTranslator.java b/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/TransformTranslator.java index d991a0d9148d..468fefe3fca3 100644 --- a/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/TransformTranslator.java +++ b/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/TransformTranslator.java @@ -69,6 +69,17 @@ public final void translate( translate(transform, new Context(appliedTransform, cxt)); } + /** + * Checks if a composite / primitive transform can be translated. Composites that cannot be + * translated as is, will be exploded further for translation of their parts. + * + *

This should be overridden where necessary. If a transform is know to be unsupported, this + * should throw a runtime exception to give early feedback before any part of the pipeline is run. + */ + public boolean canTranslate(TransformT transform) { + return true; + } + protected class Context { private final AppliedPTransform> transform; private final TranslationContext cxt; diff --git a/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombineGroupedValuesTranslatorBatch.java b/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombineGroupedValuesTranslatorBatch.java new file mode 100644 index 000000000000..58fcf9b737d2 --- /dev/null +++ b/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombineGroupedValuesTranslatorBatch.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.batch; + +import java.io.IOException; +import org.apache.beam.runners.spark.structuredstreaming.translation.TransformTranslator; +import org.apache.beam.runners.spark.structuredstreaming.translation.utils.ScalaInterop.Fun1; +import org.apache.beam.sdk.transforms.Combine; +import org.apache.beam.sdk.transforms.Combine.CombineFn; +import org.apache.beam.sdk.transforms.CombineWithContext; +import org.apache.beam.sdk.util.WindowedValue; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Encoder; +import org.apache.spark.sql.expressions.Aggregator; + +/** + * Translator for {@link Combine.GroupedValues} if the {@link CombineFn} doesn't require context / + * side-inputs. + * + *

This doesn't require a Spark {@link Aggregator}. Instead it can directly use the respective + * {@link CombineFn} to reduce each iterable of values into an aggregated output value. + */ +public class CombineGroupedValuesTranslatorBatch + extends TransformTranslator< + PCollection>>, + PCollection>, + Combine.GroupedValues> { + + @Override + protected void translate(Combine.GroupedValues transform, Context cxt) + throws IOException { + CombineFn combineFn = (CombineFn) transform.getFn(); + + Encoder>> enc = cxt.windowedEncoder(cxt.getOutput().getCoder()); + Dataset>>> inputDs = (Dataset) cxt.getDataset(cxt.getInput()); + + cxt.putDataset(cxt.getOutput(), inputDs.map(reduce(combineFn), enc)); + } + + @Override + public boolean canTranslate(Combine.GroupedValues transform) { + return !(transform.getFn() instanceof CombineWithContext); + } + + private static + Fun1>>, WindowedValue>> reduce( + CombineFn fn) { + return wv -> { + KV> kv = wv.getValue(); + AccT acc = null; + for (InT in : kv.getValue()) { + acc = fn.addInput(acc != null ? acc : fn.createAccumulator(), in); + } + OutT res = acc != null ? fn.extractOutput(acc) : fn.defaultValue(); + return wv.withValue(KV.of(kv.getKey(), res)); + }; + } +} diff --git a/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/PipelineTranslatorBatch.java b/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/PipelineTranslatorBatch.java index 455cf4cce01a..1a635f24a798 100644 --- a/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/PipelineTranslatorBatch.java +++ b/runners/spark/3/src/main/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/PipelineTranslatorBatch.java @@ -65,6 +65,8 @@ public class PipelineTranslatorBatch extends PipelineTranslator { TRANSFORM_TRANSLATORS.put(Impulse.class, new ImpulseTranslatorBatch()); TRANSFORM_TRANSLATORS.put(Combine.PerKey.class, new CombinePerKeyTranslatorBatch<>()); TRANSFORM_TRANSLATORS.put(Combine.Globally.class, new CombineGloballyTranslatorBatch<>()); + TRANSFORM_TRANSLATORS.put( + Combine.GroupedValues.class, new CombineGroupedValuesTranslatorBatch<>()); TRANSFORM_TRANSLATORS.put(GroupByKey.class, new GroupByKeyTranslatorBatch<>()); TRANSFORM_TRANSLATORS.put(Reshuffle.class, new ReshuffleTranslatorBatch<>()); @@ -98,6 +100,8 @@ TransformTranslator getTransformTranslator( if (transform == null) { return null; } - return TRANSFORM_TRANSLATORS.get(transform.getClass()); + TransformTranslator translator = + TRANSFORM_TRANSLATORS.get(transform.getClass()); + return translator != null && translator.canTranslate(transform) ? translator : null; } } diff --git a/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombineGroupedValuesTest.java b/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombineGroupedValuesTest.java new file mode 100644 index 000000000000..cce3199d2c37 --- /dev/null +++ b/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/translation/batch/CombineGroupedValuesTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.runners.spark.structuredstreaming.translation.batch; + +import java.io.Serializable; +import org.apache.beam.runners.spark.structuredstreaming.SparkStructuredStreamingPipelineOptions; +import org.apache.beam.runners.spark.structuredstreaming.SparkStructuredStreamingRunner; +import org.apache.beam.sdk.coders.IterableCoder; +import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.coders.VarIntCoder; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Combine; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.Sum; +import org.apache.beam.sdk.values.KV; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableList; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test class for beam to spark {@link Combine#groupedValues} translation. */ +@RunWith(JUnit4.class) +public class CombineGroupedValuesTest implements Serializable { + @Rule public transient TestPipeline pipeline = TestPipeline.fromOptions(testOptions()); + + private static PipelineOptions testOptions() { + SparkStructuredStreamingPipelineOptions options = + PipelineOptionsFactory.create().as(SparkStructuredStreamingPipelineOptions.class); + options.setRunner(SparkStructuredStreamingRunner.class); + options.setTestMode(true); + return options; + } + + @Test + public void testCombineGroupedValues() { + PCollection> input = + pipeline + .apply( + Create.>>of( + KV.of("a", ImmutableList.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), + KV.of("b", ImmutableList.of())) + .withCoder( + KvCoder.of(StringUtf8Coder.of(), IterableCoder.of(VarIntCoder.of())))) + .apply(Combine.groupedValues(Sum.ofIntegers())); + + PAssert.that(input).containsInAnyOrder(KV.of("a", 55), KV.of("b", 0)); + pipeline.run(); + } +} diff --git a/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/utils/SerializationDebugger.java b/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/utils/SerializationDebugger.java deleted file mode 100644 index b384b9b9d35d..000000000000 --- a/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/utils/SerializationDebugger.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.beam.runners.spark.structuredstreaming.utils; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.io.OutputStream; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; - -/** A {@code SerializationDebugger} for Spark Runner. */ -public class SerializationDebugger { - - public static void testSerialization(Object object, File to) throws IOException { - DebuggingObjectOutputStream out = new DebuggingObjectOutputStream(new FileOutputStream(to)); - try { - out.writeObject(object); - } catch (Exception e) { - throw new RuntimeException("Serialization error. Path to bad object: " + out.getStack(), e); - } - } - - private static class DebuggingObjectOutputStream extends ObjectOutputStream { - - private static final Field DEPTH_FIELD; - - static { - try { - DEPTH_FIELD = ObjectOutputStream.class.getDeclaredField("depth"); - DEPTH_FIELD.setAccessible(true); - } catch (NoSuchFieldException e) { - throw new AssertionError(e); - } - } - - final List stack = new ArrayList<>(); - - /** - * Indicates whether or not OOS has tried to write an IOException (presumably as the result of a - * serialization error) to the stream. - */ - boolean broken = false; - - DebuggingObjectOutputStream(OutputStream out) throws IOException { - super(out); - enableReplaceObject(true); - } - - /** Abuse {@code replaceObject()} as a hook to maintain our stack. */ - @Override - protected Object replaceObject(Object o) { - // ObjectOutputStream writes serialization - // exceptions to the stream. Ignore - // everything after that so we don't lose - // the path to a non-serializable object. So - // long as the user doesn't write an - // IOException as the root object, we're OK. - int currentDepth = currentDepth(); - if (o instanceof IOException && currentDepth == 0) { - broken = true; - } - if (!broken) { - truncate(currentDepth); - stack.add(o); - } - return o; - } - - private void truncate(int depth) { - while (stack.size() > depth) { - pop(); - } - } - - private Object pop() { - return stack.remove(stack.size() - 1); - } - - /** Returns a 0-based depth within the object graph of the current object being serialized. */ - private int currentDepth() { - try { - Integer oneBased = ((Integer) DEPTH_FIELD.get(this)); - return oneBased - 1; - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } - } - - /** - * Returns the path to the last object serialized. If an exception occurred, this should be the - * path to the non-serializable object. - */ - List getStack() { - return stack; - } - } -} diff --git a/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/utils/package-info.java b/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/utils/package-info.java deleted file mode 100644 index 3d7da111a9c4..000000000000 --- a/runners/spark/3/src/test/java/org/apache/beam/runners/spark/structuredstreaming/utils/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Testing utils for spark structured streaming runner. */ -package org.apache.beam.runners.spark.structuredstreaming.utils; diff --git a/runners/spark/spark_runner.gradle b/runners/spark/spark_runner.gradle index 14a433162fb6..f8c0a061b0d7 100644 --- a/runners/spark/spark_runner.gradle +++ b/runners/spark/spark_runner.gradle @@ -152,15 +152,16 @@ dependencies { implementation project(":sdks:java:fn-execution") implementation library.java.vendored_grpc_1_48_1 implementation library.java.vendored_guava_26_0_jre - implementation "io.dropwizard.metrics:metrics-core:3.1.5" // version used by Spark 2.4 spark.components.each { component -> provided "$component:$spark_version" } permitUnusedDeclared "org.apache.spark:spark-network-common_$spark_scala_version:$spark_version" if (project.property("spark_scala_version").equals("2.11")) { + implementation "io.dropwizard.metrics:metrics-core:3.1.5" // version used by Spark 2.4 compileOnly "org.scala-lang:scala-library:2.11.12" runtimeOnly library.java.jackson_module_scala_2_11 } else { + implementation "io.dropwizard.metrics:metrics-core:4.1.1" // version used by Spark 3.1 compileOnly "org.scala-lang:scala-library:2.12.15" runtimeOnly library.java.jackson_module_scala_2_12 } @@ -385,9 +386,6 @@ tasks.register("validatesRunner") { //dependsOn validatesStructuredStreamingRunnerBatch } -// Generates :runners:spark:*:runQuickstartJavaSpark task -createJavaExamplesArchetypeValidationTask(type: 'Quickstart', runner: 'Spark') - tasks.register("hadoopVersionsTest") { group = "Verification" dependsOn hadoopVersions.collect{k,v -> "hadoopVersion${k}Test"} diff --git a/scripts/ci/ci_check_git_branch.sh b/scripts/ci/ci_check_git_branch.sh old mode 100644 new mode 100755 index e7bff3e77998..8040664c4885 --- a/scripts/ci/ci_check_git_branch.sh +++ b/scripts/ci/ci_check_git_branch.sh @@ -16,18 +16,22 @@ # specific language governing permissions and limitations # under the License. -function is_branch() { - #if nothing matches show-ref will return an error code of 1 - if git show-ref --quiet --verify -- "refs/heads/$1" ; then - return 1 +function is_in_remote() { + local branch=${1} + local existed_in_remote=$(git ls-remote --heads origin ${branch}) + + if [[ -z ${existed_in_remote} ]]; then + return 1 else - return 0 + return 0 fi } -if is_branch "$1"; then +if ! is_in_remote "$1"; then echo "Branch [$1] doesn't exist." + exit 0 else echo "Branch [$1] already exists!" - echo >&2 "Please make sure your branch doesn't exist." + echo "Please make sure your branch doesn't exist." + exit 1 fi diff --git a/sdks/go.mod b/sdks/go.mod index 438ffe1b5b6f..27670370ecf2 100644 --- a/sdks/go.mod +++ b/sdks/go.mod @@ -23,10 +23,10 @@ module github.com/apache/beam/sdks/v2 go 1.18 require ( - cloud.google.com/go/bigquery v1.42.0 + cloud.google.com/go/bigquery v1.43.0 cloud.google.com/go/datastore v1.8.0 cloud.google.com/go/profiler v0.3.0 - cloud.google.com/go/pubsub v1.25.1 + cloud.google.com/go/pubsub v1.26.0 cloud.google.com/go/storage v1.27.0 github.com/docker/go-connections v0.4.0 github.com/go-sql-driver/mysql v1.6.0 @@ -37,17 +37,17 @@ require ( github.com/linkedin/goavro v2.1.0+incompatible github.com/nightlyone/lockfile v1.0.0 github.com/proullon/ramsql v0.0.0-20211120092837-c8d0a408b939 - github.com/spf13/cobra v1.6.0 + github.com/spf13/cobra v1.6.1 github.com/testcontainers/testcontainers-go v0.14.0 github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20220315005136-aec0fe3e777c - golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 - golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 - golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 + golang.org/x/net v0.0.0-20221014081412-f15817d10f9b + golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 + golang.org/x/sync v0.1.0 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 golang.org/x/text v0.4.0 - google.golang.org/api v0.99.0 - google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e + google.golang.org/api v0.101.0 + google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 google.golang.org/grpc v1.50.1 google.golang.org/protobuf v1.28.1 gopkg.in/retry.v1 v1.0.3 @@ -57,7 +57,7 @@ require ( require ( cloud.google.com/go v0.104.0 // indirect cloud.google.com/go/compute v1.10.0 // indirect - cloud.google.com/go/iam v0.3.0 // indirect + cloud.google.com/go/iam v0.5.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/hcsshim v0.9.4 // indirect @@ -74,7 +74,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/pprof v0.0.0-20220412212628-83db2b799d1f // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect - github.com/googleapis/gax-go/v2 v2.5.1 // indirect + github.com/googleapis/gax-go/v2 v2.6.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/klauspost/compress v1.13.1 // indirect github.com/magiconair/properties v1.8.6 // indirect diff --git a/sdks/go.sum b/sdks/go.sum index a129517f4dfe..55d19289d8e6 100644 --- a/sdks/go.sum +++ b/sdks/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0 h1:JuTk8po4bCKRwObdT0zLb1K0BGkGHJdtgs2GK3j2Gws= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0 h1:u0fvz5ysJBe1jwUPI4LuPwAX+o+6fCUwf3ECeg6eDUQ= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= @@ -53,8 +53,9 @@ cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1 cloud.google.com/go/datastore v1.8.0 h1:2qo2G7hABSeqswa+5Ga3+QB8/ZwKOJmDsCISM9scmsU= cloud.google.com/go/datastore v1.8.0/go.mod h1:q1CpHVByTlXppdqTcu4LIhCsTn3fhtZ5R7+TajciO+M= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0 h1:fz9X5zyTWBmamZsqvqZqD7khbifcZF/q+Z1J8pfhIUg= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo= cloud.google.com/go/profiler v0.3.0 h1:R6y/xAeifaUXxd2x6w+jIwKxoKl8Cv5HJvcvASTPWJo= cloud.google.com/go/profiler v0.3.0/go.mod h1:9wYk9eY4iZHsev8TQb61kh3wiOiSyz/xOYixWPzweCU= @@ -62,8 +63,8 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.25.1 h1:l0wCNZKuEp2Q54wAy8283EV9O57+7biWOXnnU2/Tq/A= -cloud.google.com/go/pubsub v1.25.1/go.mod h1:bY6l7rF8kCcwz6V3RaQ6kK4p5g7qc7PqjRoE9wDOqOU= +cloud.google.com/go/pubsub v1.26.0 h1:Y/HcMxVXgkUV2pYeLMUkclMg0ue6U0jVyI5xEARQ4zA= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -587,8 +588,8 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1 h1:kBRZU0PSuI7PspsSb/ChWoVResUcwNVIdpB049pKTiw= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= @@ -928,8 +929,8 @@ github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKv github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= -github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -1176,8 +1177,8 @@ golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458 h1:MgJ6t2zo8v0tbmLCueaCbF1RM+TtB0rs3Lv8DGtOIpY= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1198,8 +1199,8 @@ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 h1:3VPzK7eqH25j7GYw5w6g/GzNRc0/fYtrxz27z1gD4W0= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1212,8 +1213,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1361,7 +1362,7 @@ golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1476,8 +1477,8 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.99.0 h1:tsBtOIklCE2OFxhmcYSVqGwSAN/Y897srxmcvAQnwK8= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.101.0 h1:lJPPeEBIRxGpGLwnBTam1NPEM8Z2BmmXEd3z812pjwM= +google.golang.org/api v0.101.0/go.mod h1:CjxAAWWt3A3VrUE2IGDY2bgK5qhoG/OkyWVlYcP05MY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1570,8 +1571,8 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e h1:halCgTFuLWDRD61piiNSxPsARANGD3Xl16hPrLgLiIg= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55 h1:U1u4KB2kx6KR/aJDjQ97hZ15wQs8ZPvDcGcRynBhkvg= +google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55/go.mod h1:45EK0dUbEZ2NHjCeAd2LXmyjAgGUGrpGROgjhC3ADck= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= diff --git a/sdks/go/pkg/beam/core/metrics/dumper_test.go b/sdks/go/pkg/beam/core/metrics/dumper_test.go new file mode 100644 index 000000000000..e618354eda8c --- /dev/null +++ b/sdks/go/pkg/beam/core/metrics/dumper_test.go @@ -0,0 +1,49 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestDumperExtractor(t *testing.T) { + var got []string + printer := func(format string, args ...interface{}) { + got = append(got, fmt.Sprintf(format, args...)) + } + + store := newStore() + now := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC) + store.storeMetric("pid", newName("ns", "counter"), &counter{value: 1}) + store.storeMetric("pid", newName("ns", "distribution"), &distribution{count: 1, sum: 2, min: 3, max: 4}) + store.storeMetric("pid", newName("ns", "gauge"), &gauge{v: 1, t: now}) + + expected := []string{ + "PTransformID: \"pid\"", + " ns.counter - value: 1", + " ns.distribution - count: 1 sum: 2 min: 3 max: 4", + " ns.gauge - Gauge time: 2019-01-01 00:00:00 +0000 UTC value: 1", + } + + dumperExtractor(store, printer) + if diff := cmp.Diff(expected, got); diff != "" { + t.Errorf("dumperExtractor() got diff (-want +got): %v", diff) + } +} diff --git a/sdks/go/pkg/beam/core/metrics/metrics_test.go b/sdks/go/pkg/beam/core/metrics/metrics_test.go index ff3141d748aa..75b483184ab2 100644 --- a/sdks/go/pkg/beam/core/metrics/metrics_test.go +++ b/sdks/go/pkg/beam/core/metrics/metrics_test.go @@ -400,6 +400,53 @@ func TestMergeDistributions(t *testing.T) { } } +func TestMergePCols(t *testing.T) { + realKey := StepKey{Name: "real"} + pColA := PColValue{ElementCount: 1, SampledByteSize: DistributionValue{Count: 2, Sum: 3, Min: 4, Max: 5}} + pColB := PColValue{ElementCount: 5, SampledByteSize: DistributionValue{Count: 4, Sum: 3, Min: 2, Max: 1}} + tests := []struct { + name string + attempted, committed map[StepKey]PColValue + want []PColResult + }{ + { + name: "merge", + attempted: map[StepKey]PColValue{ + realKey: pColA, + }, + committed: map[StepKey]PColValue{ + realKey: pColB, + }, + want: []PColResult{{Attempted: pColA, Committed: pColB, Key: realKey}}, + }, { + name: "attempted only", + attempted: map[StepKey]PColValue{ + realKey: pColA, + }, + committed: map[StepKey]PColValue{}, + want: []PColResult{{Attempted: pColA, Key: realKey}}, + }, { + name: "committed only", + attempted: map[StepKey]PColValue{}, + committed: map[StepKey]PColValue{ + realKey: pColB, + }, + want: []PColResult{{Committed: pColB, Key: realKey}}, + }, + } + less := func(a, b DistributionResult) bool { + return a.Key.Name < b.Key.Name + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := MergePCols(test.attempted, test.committed) + if d := cmp.Diff(test.want, got, cmpopts.SortSlices(less)); d != "" { + t.Errorf("MergePCols(%+v, %+v) = %+v, want %+v\ndiff:\n%v", test.attempted, test.committed, got, test.want, d) + } + }) + } +} + func TestMergeGauges(t *testing.T) { realKey := StepKey{Name: "real"} now := time.Now() @@ -449,6 +496,53 @@ func TestMergeGauges(t *testing.T) { } } +func TestMergeMsecs(t *testing.T) { + realKey := StepKey{Name: "real"} + msecA := MsecValue{Start: time.Second, Process: 2 * time.Second, Finish: time.Second, Total: 4 * time.Second} + msecB := MsecValue{Start: 2 * time.Second, Process: time.Second, Finish: 2 * time.Second, Total: 5 * time.Second} + tests := []struct { + name string + attempted, committed map[StepKey]MsecValue + want []MsecResult + }{ + { + name: "merge", + attempted: map[StepKey]MsecValue{ + realKey: msecA, + }, + committed: map[StepKey]MsecValue{ + realKey: msecB, + }, + want: []MsecResult{{Attempted: msecA, Committed: msecB, Key: realKey}}, + }, { + name: "attempted only", + attempted: map[StepKey]MsecValue{ + realKey: msecA, + }, + committed: map[StepKey]MsecValue{}, + want: []MsecResult{{Attempted: msecA, Key: realKey}}, + }, { + name: "committed only", + attempted: map[StepKey]MsecValue{}, + committed: map[StepKey]MsecValue{ + realKey: msecB, + }, + want: []MsecResult{{Committed: msecB, Key: realKey}}, + }, + } + less := func(a, b DistributionResult) bool { + return a.Key.Name < b.Key.Name + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := MergeMsecs(test.attempted, test.committed) + if d := cmp.Diff(test.want, got, cmpopts.SortSlices(less)); d != "" { + t.Errorf("MergeMsecs(%+v, %+v) = %+v, want %+v\ndiff:\n%v", test.attempted, test.committed, got, test.want, d) + } + }) + } +} + func TestMsecQueryResult(t *testing.T) { realKey := StepKey{Step: "sumFn"} msecA := MsecValue{Start: 0, Process: 0, Finish: 0, Total: 0} diff --git a/sdks/go/pkg/beam/core/metrics/store_test.go b/sdks/go/pkg/beam/core/metrics/store_test.go new file mode 100644 index 000000000000..dcdadad74292 --- /dev/null +++ b/sdks/go/pkg/beam/core/metrics/store_test.go @@ -0,0 +1,62 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package metrics + +import ( + "reflect" + "testing" + "time" +) + +func TestStore(t *testing.T) { + store := newStore() + + m := make(map[Labels]interface{}) + e := &Extractor{ + SumInt64: func(l Labels, v int64) { + m[l] = &counter{value: v} + }, + DistributionInt64: func(l Labels, count, sum, min, max int64) { + m[l] = &distribution{count: count, sum: sum, min: min, max: max} + }, + GaugeInt64: func(l Labels, v int64, t time.Time) { + m[l] = &gauge{v: v, t: t} + }, + MsecsInt64: func(labels string, e *[4]ExecutionState) {}, + } + + now := time.Now() + + store.storeMetric("pid", newName("ns", "counter"), &counter{value: 1}) + store.storeMetric("pid", newName("ns", "distribution"), &distribution{count: 1, sum: 2, min: 3, max: 4}) + store.storeMetric("pid", newName("ns", "gauge"), &gauge{v: 1, t: now}) + + // storing the same metric twice doesn't change anything + store.storeMetric("pid", newName("ns", "counter"), &counter{value: 2}) + + err := e.ExtractFrom(store) + if err != nil { + t.Fatalf("e.ExtractFrom(store) = %q, want nil", err) + } + + expected := map[Labels]interface{}{ + {transform: "pid", namespace: "ns", name: "counter"}: &counter{value: 1}, + {transform: "pid", namespace: "ns", name: "distribution"}: &distribution{count: 1, sum: 2, min: 3, max: 4}, + {transform: "pid", namespace: "ns", name: "gauge"}: &gauge{v: 1, t: now}, + } + if !reflect.DeepEqual(m, expected) { + t.Errorf("e.ExtractFrom(store) = %v, want %v", m, expected) + } +} diff --git a/sdks/go/pkg/beam/core/runtime/graphx/schema/schema.go b/sdks/go/pkg/beam/core/runtime/graphx/schema/schema.go index 7ca516b56944..c5bf708fa76f 100644 --- a/sdks/go/pkg/beam/core/runtime/graphx/schema/schema.go +++ b/sdks/go/pkg/beam/core/runtime/graphx/schema/schema.go @@ -92,6 +92,7 @@ func getUUID(ut reflect.Type) string { // Registered returns whether the given type has been registered with // the schema package. func (r *Registry) Registered(ut reflect.Type) bool { + r.reconcileRegistrations() _, ok := r.syntheticToUser[ut] return ok } @@ -118,7 +119,10 @@ func (r *Registry) reconcileRegistrations() (deferedErr error) { check := func(ut reflect.Type) bool { return coder.LookupCustomCoder(ut) != nil } - if check(ut) || check(reflect.PtrTo(ut)) { + // We could have either a pointer or non pointer here, + // so we strip pointerness and then check both. + vT := reflectx.SkipPtr(ut) + if check(vT) && check(reflect.PtrTo(vT)) { continue } if err := r.registerType(ut, map[reflect.Type]struct{}{}); err != nil { diff --git a/sdks/go/pkg/beam/core/runtime/graphx/schema/schema_test.go b/sdks/go/pkg/beam/core/runtime/graphx/schema/schema_test.go index 5ef2f707280a..37b3e79f8f50 100644 --- a/sdks/go/pkg/beam/core/runtime/graphx/schema/schema_test.go +++ b/sdks/go/pkg/beam/core/runtime/graphx/schema/schema_test.go @@ -26,6 +26,7 @@ import ( pipepb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/pipeline_v1" "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/testing/protocmp" ) @@ -792,6 +793,7 @@ func TestSchemaConversion(t *testing.T) { // real embedded type. if !hasEmbeddedField(test.rt) && !test.rt.AssignableTo(got) { t.Errorf("%v not assignable to %v", test.rt, got) + t.Errorf("%v for schema %v", test.rt, prototext.Format(test.st)) if d := cmp.Diff(reflect.New(test.rt).Elem().Interface(), reflect.New(got).Elem().Interface()); d != "" { t.Errorf("diff (-want, +got): %v", d) } diff --git a/sdks/go/pkg/beam/core/runtime/xlangx/expansionx/process.go b/sdks/go/pkg/beam/core/runtime/xlangx/expansionx/process.go index 28dc3294f44f..590c9392a991 100644 --- a/sdks/go/pkg/beam/core/runtime/xlangx/expansionx/process.go +++ b/sdks/go/pkg/beam/core/runtime/xlangx/expansionx/process.go @@ -58,7 +58,7 @@ func NewExpansionServiceRunner(jarPath, servicePort string) (*ExpansionServiceRu return &ExpansionServiceRunner{execPath: jarPath, servicePort: servicePort, serviceCommand: serviceCommand}, nil } -// NewExpansionServiceRunner builds an ExpansionServiceRunner struct for a given python module and +// NewPyExpansionServiceRunner builds an ExpansionServiceRunner struct for a given python module and // Beam version and returns a pointer to it. Passing an empty string as servicePort will request an // open port to be assigned to the service. func NewPyExpansionServiceRunner(pythonExec, module, servicePort string) (*ExpansionServiceRunner, error) { diff --git a/sdks/go/pkg/beam/register/emitter.go b/sdks/go/pkg/beam/register/emitter.go index 6c88d28d9fce..3b9cb9910d25 100644 --- a/sdks/go/pkg/beam/register/emitter.go +++ b/sdks/go/pkg/beam/register/emitter.go @@ -129,7 +129,9 @@ func Emitter1[T1 any]() { registerFunc := func(n exec.ElementProcessor) exec.ReusableEmitter { return &emit1[T1]{n: n} } - exec.RegisterEmitter(reflect.TypeOf(e).Elem(), registerFunc) + eT := reflect.TypeOf(e).Elem() + registerType(eT.In(0)) + exec.RegisterEmitter(eT, registerFunc) } // Emitter2 registers parameters from your DoFn with a @@ -147,18 +149,25 @@ func Emitter2[T1, T2 any]() { return &emit1WithTimestamp[T2]{n: n} } } - exec.RegisterEmitter(reflect.TypeOf(e).Elem(), registerFunc) + eT := reflect.TypeOf(e).Elem() + registerType(eT.In(0)) + registerType(eT.In(1)) + exec.RegisterEmitter(eT, registerFunc) } // Emitter3 registers parameters from your DoFn with a -// signature func(T1, T2, T3) and optimizes their execution. +// signature func(beam.EventTime, T2, T3) and optimizes their execution. // This must be done by passing in type parameters of all inputs as constraints, // aka: register.Emitter3[beam.EventTime, T1, T2](), where T1 is the type of // your key and T2 is the type of your value. -func Emitter3[T1 typex.EventTime, T2, T3 any]() { - e := (*func(T1, T2, T3))(nil) +func Emitter3[ET typex.EventTime, T1, T2 any]() { + e := (*func(ET, T1, T2))(nil) registerFunc := func(n exec.ElementProcessor) exec.ReusableEmitter { - return &emit2WithTimestamp[T2, T3]{n: n} + return &emit2WithTimestamp[T1, T2]{n: n} } - exec.RegisterEmitter(reflect.TypeOf(e).Elem(), registerFunc) + eT := reflect.TypeOf(e).Elem() + // No need to register event time. + registerType(eT.In(1)) + registerType(eT.In(2)) + exec.RegisterEmitter(eT, registerFunc) } diff --git a/sdks/go/pkg/beam/register/emitter_test.go b/sdks/go/pkg/beam/register/emitter_test.go new file mode 100644 index 000000000000..32a45f5da9e4 --- /dev/null +++ b/sdks/go/pkg/beam/register/emitter_test.go @@ -0,0 +1,166 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package register + +import ( + "context" + "reflect" + "testing" + + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/mtime" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/exec" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/typex" +) + +type myTestTypeEmitter1 struct { + Int int +} + +func TestEmitter1(t *testing.T) { + Emitter1[int]() + e1T := reflect.TypeOf((*func(int))(nil)).Elem() + if !exec.IsEmitterRegistered(e1T) { + t.Fatalf("exec.IsEmitterRegistered(%v) = false, want true", e1T) + } + + Emitter1[myTestTypeEmitter1]() + rt := reflect.TypeOf((*myTestTypeEmitter1)(nil)).Elem() + checkRegisterations(t, rt) +} + +type myTestTypeEmitter2A struct { + Int int +} + +type myTestTypeEmitter2B struct { + String string +} + +func TestEmitter2(t *testing.T) { + Emitter2[int, string]() + e2isT := reflect.TypeOf((*func(int, string))(nil)).Elem() + if !exec.IsEmitterRegistered(e2isT) { + t.Fatalf("exec.IsEmitterRegistered(%v) = false, want true", e2isT) + } + + Emitter2[*myTestTypeEmitter2A, myTestTypeEmitter2B]() + e2ABT := reflect.TypeOf((*func(*myTestTypeEmitter2A, myTestTypeEmitter2B))(nil)).Elem() + if !exec.IsEmitterRegistered(e2ABT) { + t.Fatalf("exec.IsEmitterRegistered(%v) = false, want true", e2ABT) + } + + tA := reflect.TypeOf((*myTestTypeEmitter2A)(nil)).Elem() + checkRegisterations(t, tA) + tB := reflect.TypeOf((*myTestTypeEmitter2B)(nil)).Elem() + checkRegisterations(t, tB) +} + +func TestEmitter2_WithTimestamp(t *testing.T) { + Emitter2[typex.EventTime, string]() + e2tssT := reflect.TypeOf((*func(typex.EventTime, string))(nil)).Elem() + if !exec.IsEmitterRegistered(e2tssT) { + t.Fatalf("exec.IsEmitterRegistered(%v) = false, want true", e2tssT) + } +} + +type myTestTypeEmitter3A struct { + Int int +} + +type myTestTypeEmitter3B struct { + String string +} + +func TestEmitter3(t *testing.T) { + Emitter3[typex.EventTime, int, string]() + if !exec.IsEmitterRegistered(reflect.TypeOf((*func(typex.EventTime, int, string))(nil)).Elem()) { + t.Fatalf("exec.IsEmitterRegistered(reflect.TypeOf((*func(typex.EventTime, int, string))(nil)).Elem()) = false, want true") + } + + Emitter3[typex.EventTime, myTestTypeEmitter3A, *myTestTypeEmitter3B]() + e3tsABT := reflect.TypeOf((*func(typex.EventTime, myTestTypeEmitter3A, *myTestTypeEmitter3B))(nil)).Elem() + if !exec.IsEmitterRegistered(e3tsABT) { + t.Fatalf("exec.IsEmitterRegistered(%v) = false, want true", e3tsABT) + } + tA := reflect.TypeOf((*myTestTypeEmitter3A)(nil)).Elem() + checkRegisterations(t, tA) + tB := reflect.TypeOf((*myTestTypeEmitter3B)(nil)).Elem() + checkRegisterations(t, tB) +} + +func TestEmit1(t *testing.T) { + e := &emit1[int]{n: &elementProcessor{}} + e.Init(context.Background(), []typex.Window{}, mtime.ZeroTimestamp) + fn := e.Value().(func(int)) + fn(3) + if got, want := e.n.(*elementProcessor).inFV.Elm, 3; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Elm=%v, want %v", got, want) + } + if got := e.n.(*elementProcessor).inFV.Elm2; got != nil { + t.Errorf("e.Value.(func(int))(3).n.inFV.Elm2=%v, want nil", got) + } + if got, want := e.n.(*elementProcessor).inFV.Timestamp, mtime.ZeroTimestamp; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Timestamp=%v, want %v", got, want) + } +} + +func TestEmit2(t *testing.T) { + e := &emit2[int, string]{n: &elementProcessor{}} + e.Init(context.Background(), []typex.Window{}, mtime.ZeroTimestamp) + fn := e.Value().(func(int, string)) + fn(3, "hello") + if got, want := e.n.(*elementProcessor).inFV.Elm, 3; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Elm=%v, want %v", got, want) + } + if got, want := e.n.(*elementProcessor).inFV.Elm2, "hello"; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Elm2=%v, want %v", got, want) + } + if got, want := e.n.(*elementProcessor).inFV.Timestamp, mtime.ZeroTimestamp; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Timestamp=%v, want %v", got, want) + } +} + +func TestEmit1WithTimestamp(t *testing.T) { + e := &emit1WithTimestamp[int]{n: &elementProcessor{}} + e.Init(context.Background(), []typex.Window{}, mtime.ZeroTimestamp) + fn := e.Value().(func(typex.EventTime, int)) + fn(mtime.MaxTimestamp, 3) + if got, want := e.n.(*elementProcessor).inFV.Elm, 3; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Elm=%v, want %v", got, want) + } + if got := e.n.(*elementProcessor).inFV.Elm2; got != nil { + t.Errorf("e.Value.(func(int))(3).n.inFV.Elm2=%v, want nil", got) + } + if got, want := e.n.(*elementProcessor).inFV.Timestamp, mtime.MaxTimestamp; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Timestamp=%v, want %v", got, want) + } +} + +func TestEmit2WithTimestamp(t *testing.T) { + e := &emit2WithTimestamp[int, string]{n: &elementProcessor{}} + e.Init(context.Background(), []typex.Window{}, mtime.ZeroTimestamp) + fn := e.Value().(func(typex.EventTime, int, string)) + fn(mtime.MaxTimestamp, 3, "hello") + if got, want := e.n.(*elementProcessor).inFV.Elm, 3; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Elm=%v, want %v", got, want) + } + if got, want := e.n.(*elementProcessor).inFV.Elm2, "hello"; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Elm2=%v, want %v", got, want) + } + if got, want := e.n.(*elementProcessor).inFV.Timestamp, mtime.MaxTimestamp; got != want { + t.Errorf("e.Value.(func(int))(3).n.inFV.Timestamp=%v, want %v", got, want) + } +} diff --git a/sdks/go/pkg/beam/register/iter.go b/sdks/go/pkg/beam/register/iter.go index 71d3f3df723f..2244a68b88af 100644 --- a/sdks/go/pkg/beam/register/iter.go +++ b/sdks/go/pkg/beam/register/iter.go @@ -20,7 +20,10 @@ import ( "io" "reflect" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/exec" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/graphx/schema" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/reflectx" ) type iter1[T any] struct { @@ -104,6 +107,16 @@ func (v *iter2[T1, T2]) invoke(key *T1, value *T2) bool { return true } +func registerType(t reflect.Type) { + // strip the pointer if present. + t = reflectx.SkipPtr(t) + if _, ok := runtime.TypeKey(t); !ok { + return + } + runtime.RegisterType(t) + schema.RegisterType(t) +} + // Iter1 registers parameters from your DoFn with a // signature func(*T) bool and optimizes their execution. // This must be done by passing in type parameters of all inputs as constraints, @@ -113,7 +126,9 @@ func Iter1[T any]() { registerFunc := func(s exec.ReStream) exec.ReusableInput { return &iter1[T]{s: s} } - exec.RegisterInput(reflect.TypeOf(i).Elem(), registerFunc) + itT := reflect.TypeOf(i).Elem() + registerType(itT.In(0).Elem()) + exec.RegisterInput(itT, registerFunc) } // Iter1 registers parameters from your DoFn with a @@ -125,5 +140,8 @@ func Iter2[T1, T2 any]() { registerFunc := func(s exec.ReStream) exec.ReusableInput { return &iter2[T1, T2]{s: s} } - exec.RegisterInput(reflect.TypeOf(i).Elem(), registerFunc) + itT := reflect.TypeOf(i).Elem() + registerType(itT.In(0).Elem()) + registerType(itT.In(1).Elem()) + exec.RegisterInput(itT, registerFunc) } diff --git a/sdks/go/pkg/beam/register/iter_test.go b/sdks/go/pkg/beam/register/iter_test.go new file mode 100644 index 000000000000..8994fac13799 --- /dev/null +++ b/sdks/go/pkg/beam/register/iter_test.go @@ -0,0 +1,200 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package register + +import ( + "reflect" + "testing" + + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/mtime" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/window" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/exec" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/graphx/schema" + "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/reflectx" +) + +type myTestTypeIter1 struct { + Int int +} + +func checkRegisterations(t *testing.T, ort reflect.Type) { + t.Helper() + // Strip pointers for the original type since type key doesn't support them. + // Pointer handling is done elsewhere. + rt := reflectx.SkipPtr(ort) + key, ok := runtime.TypeKey(rt) + if !ok { + t.Fatalf("runtime.TypeKey(%v): no typekey for type", rt) + } + if _, ok := runtime.LookupType(key); !ok { + t.Errorf("want type %v to be available with key %q", rt, key) + } + if !schema.Registered(ort) { + t.Errorf("want type %v to be registered with schemas", ort) + } +} + +func TestIter1(t *testing.T) { + Iter1[int]() + itiT := reflect.TypeOf((*func(*int) bool)(nil)).Elem() + if !exec.IsInputRegistered(itiT) { + t.Fatalf("exec.IsInputRegistered(%v) = false, want true", itiT) + } + + Iter1[myTestTypeIter1]() + it1T := reflect.TypeOf((*func(*int) bool)(nil)).Elem() + if !exec.IsInputRegistered(it1T) { + t.Fatalf("exec.IsInputRegistered(%v) = false, want true", it1T) + } + + ttrt := reflect.TypeOf((*myTestTypeIter1)(nil)).Elem() + checkRegisterations(t, ttrt) +} + +type myTestTypeIter2A struct { + Int int +} + +type myTestTypeIter2B struct { + Int int +} + +func TestIter2(t *testing.T) { + Iter2[int, string]() + it2isT := reflect.TypeOf((*func(*int, *string) bool)(nil)).Elem() + if !exec.IsInputRegistered(it2isT) { + t.Fatalf("exec.IsInputRegistered(%v) = false, want true", it2isT) + } + + Iter2[myTestTypeIter2A, *myTestTypeIter2B]() + it2ABT := reflect.TypeOf((*func(*myTestTypeIter2A, **myTestTypeIter2B) bool)(nil)).Elem() + if !exec.IsInputRegistered(it2ABT) { + t.Fatalf("exec.IsInputRegistered(%v) = false, want true", it2ABT) + } + + ttArt := reflect.TypeOf((*myTestTypeIter2A)(nil)).Elem() + checkRegisterations(t, ttArt) + ttBrt := reflect.TypeOf((*myTestTypeIter2B)(nil)) + checkRegisterations(t, ttBrt) +} + +func TestIter1_Struct(t *testing.T) { + values := []exec.FullValue{{ + Windows: window.SingleGlobalWindow, + Timestamp: mtime.ZeroTimestamp, + Elm: "one", + }, { + Windows: window.SingleGlobalWindow, + Timestamp: mtime.ZeroTimestamp, + Elm: "two", + }, { + Windows: window.SingleGlobalWindow, + Timestamp: mtime.ZeroTimestamp, + Elm: "three", + }} + + i := iter1[string]{s: &exec.FixedReStream{Buf: values}} + + i.Init() + fn := i.Value().(func(value *string) bool) + + var s string + if ok := fn(&s); !ok { + t.Fatalf("First i.Value()(&s)=false, want true") + } + if got, want := s, "one"; got != want { + t.Fatalf("First iter value = %v, want %v", got, want) + } + if ok := fn(&s); !ok { + t.Fatalf("Second i.Value()(&s)=false, want true") + } + if got, want := s, "two"; got != want { + t.Fatalf("Second iter value = %v, want %v", got, want) + } + if ok := fn(&s); !ok { + t.Fatalf("Third i.Value()(&s)=false, want true") + } + if got, want := s, "three"; got != want { + t.Fatalf("Third iter value = %v, want %v", got, want) + } + if ok := fn(&s); ok { + t.Fatalf("Fourth i.Value()(&s)=true, want false") + } + if err := i.Reset(); err != nil { + t.Fatalf("i.Reset()=%v, want nil", err) + } +} + +func TestIter2_Struct(t *testing.T) { + values := []exec.FullValue{{ + Windows: window.SingleGlobalWindow, + Timestamp: mtime.ZeroTimestamp, + Elm: 1, + Elm2: "one", + }, { + Windows: window.SingleGlobalWindow, + Timestamp: mtime.ZeroTimestamp, + Elm: 2, + Elm2: "two", + }, { + Windows: window.SingleGlobalWindow, + Timestamp: mtime.ZeroTimestamp, + Elm: 3, + Elm2: "three", + }} + + i := iter2[int, string]{s: &exec.FixedReStream{Buf: values}} + + i.Init() + fn := i.Value().(func(key *int, value *string) bool) + + var s string + var key int + if ok := fn(&key, &s); !ok { + t.Fatalf("First i.Value()(&s)=false, want true") + } + if got, want := key, 1; got != want { + t.Fatalf("First iter key = %v, want %v", got, want) + } + if got, want := s, "one"; got != want { + t.Fatalf("First iter value = %v, want %v", got, want) + } + if ok := fn(&key, &s); !ok { + t.Fatalf("Second i.Value()(&s)=false, want true") + } + if got, want := key, 2; got != want { + t.Fatalf("Second iter key = %v, want %v", got, want) + } + if got, want := s, "two"; got != want { + t.Fatalf("Second iter value = %v, want %v", got, want) + } + if ok := fn(&key, &s); !ok { + t.Fatalf("Third i.Value()(&s)=false, want true") + } + if got, want := key, 3; got != want { + t.Fatalf("Third iter key = %v, want %v", got, want) + } + if got, want := s, "three"; got != want { + t.Fatalf("Third iter value = %v, want %v", got, want) + } + if ok := fn(&key, &s); ok { + t.Fatalf("Fourth i.Value()(&s)=true, want false") + } + if err := i.Reset(); err != nil { + t.Fatalf("i.Reset()=%v, want nil", err) + } +} diff --git a/sdks/go/pkg/beam/register/register_test.go b/sdks/go/pkg/beam/register/register_test.go index 39962ab3c421..8cab02122e98 100644 --- a/sdks/go/pkg/beam/register/register_test.go +++ b/sdks/go/pkg/beam/register/register_test.go @@ -21,13 +21,10 @@ import ( "testing" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph" - "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/mtime" - "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/window" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/exec" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/graphx" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/graphx/schema" - "github.com/apache/beam/sdks/v2/go/pkg/beam/core/typex" "github.com/apache/beam/sdks/v2/go/pkg/beam/core/util/reflectx" ) @@ -324,219 +321,6 @@ func TestCombiner_PartialCombiner2(t *testing.T) { } } -func TestEmitter1(t *testing.T) { - Emitter1[int]() - if !exec.IsEmitterRegistered(reflect.TypeOf((*func(int))(nil)).Elem()) { - t.Fatalf("exec.IsEmitterRegistered(reflect.TypeOf((*func(int))(nil)).Elem()) = false, want true") - } -} - -func TestEmitter2(t *testing.T) { - Emitter2[int, string]() - if !exec.IsEmitterRegistered(reflect.TypeOf((*func(int, string))(nil)).Elem()) { - t.Fatalf("exec.IsEmitterRegistered(reflect.TypeOf((*func(int, string))(nil)).Elem()) = false, want true") - } -} - -func TestEmitter2_WithTimestamp(t *testing.T) { - Emitter2[typex.EventTime, string]() - if !exec.IsEmitterRegistered(reflect.TypeOf((*func(typex.EventTime, string))(nil)).Elem()) { - t.Fatalf("exec.IsEmitterRegistered(reflect.TypeOf((*func(typex.EventTime, string))(nil)).Elem()) = false, want true") - } -} - -func TestEmitter3(t *testing.T) { - Emitter3[typex.EventTime, int, string]() - if !exec.IsEmitterRegistered(reflect.TypeOf((*func(typex.EventTime, int, string))(nil)).Elem()) { - t.Fatalf("exec.IsEmitterRegistered(reflect.TypeOf((*func(typex.EventTime, int, string))(nil)).Elem()) = false, want true") - } -} - -func TestEmit1(t *testing.T) { - e := &emit1[int]{n: &elementProcessor{}} - e.Init(context.Background(), []typex.Window{}, mtime.ZeroTimestamp) - fn := e.Value().(func(int)) - fn(3) - if got, want := e.n.(*elementProcessor).inFV.Elm, 3; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Elm=%v, want %v", got, want) - } - if got := e.n.(*elementProcessor).inFV.Elm2; got != nil { - t.Errorf("e.Value.(func(int))(3).n.inFV.Elm2=%v, want nil", got) - } - if got, want := e.n.(*elementProcessor).inFV.Timestamp, mtime.ZeroTimestamp; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Timestamp=%v, want %v", got, want) - } -} - -func TestEmit2(t *testing.T) { - e := &emit2[int, string]{n: &elementProcessor{}} - e.Init(context.Background(), []typex.Window{}, mtime.ZeroTimestamp) - fn := e.Value().(func(int, string)) - fn(3, "hello") - if got, want := e.n.(*elementProcessor).inFV.Elm, 3; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Elm=%v, want %v", got, want) - } - if got, want := e.n.(*elementProcessor).inFV.Elm2, "hello"; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Elm2=%v, want %v", got, want) - } - if got, want := e.n.(*elementProcessor).inFV.Timestamp, mtime.ZeroTimestamp; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Timestamp=%v, want %v", got, want) - } -} - -func TestEmit1WithTimestamp(t *testing.T) { - e := &emit1WithTimestamp[int]{n: &elementProcessor{}} - e.Init(context.Background(), []typex.Window{}, mtime.ZeroTimestamp) - fn := e.Value().(func(typex.EventTime, int)) - fn(mtime.MaxTimestamp, 3) - if got, want := e.n.(*elementProcessor).inFV.Elm, 3; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Elm=%v, want %v", got, want) - } - if got := e.n.(*elementProcessor).inFV.Elm2; got != nil { - t.Errorf("e.Value.(func(int))(3).n.inFV.Elm2=%v, want nil", got) - } - if got, want := e.n.(*elementProcessor).inFV.Timestamp, mtime.MaxTimestamp; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Timestamp=%v, want %v", got, want) - } -} - -func TestEmit2WithTimestamp(t *testing.T) { - e := &emit2WithTimestamp[int, string]{n: &elementProcessor{}} - e.Init(context.Background(), []typex.Window{}, mtime.ZeroTimestamp) - fn := e.Value().(func(typex.EventTime, int, string)) - fn(mtime.MaxTimestamp, 3, "hello") - if got, want := e.n.(*elementProcessor).inFV.Elm, 3; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Elm=%v, want %v", got, want) - } - if got, want := e.n.(*elementProcessor).inFV.Elm2, "hello"; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Elm2=%v, want %v", got, want) - } - if got, want := e.n.(*elementProcessor).inFV.Timestamp, mtime.MaxTimestamp; got != want { - t.Errorf("e.Value.(func(int))(3).n.inFV.Timestamp=%v, want %v", got, want) - } -} - -func TestIter1(t *testing.T) { - Iter1[int]() - if !exec.IsInputRegistered(reflect.TypeOf((*func(*int) bool)(nil)).Elem()) { - t.Fatalf("exec.IsInputRegistered(reflect.TypeOf(((*func(*int) bool)(nil)).Elem()) = false, want true") - } -} - -func TestIter2(t *testing.T) { - Iter2[int, string]() - if !exec.IsInputRegistered(reflect.TypeOf((*func(*int, *string) bool)(nil)).Elem()) { - t.Fatalf("exec.IsInputRegistered(reflect.TypeOf((*func(*int, *string) bool)(nil)).Elem()) = false, want true") - } -} - -func TestIter1_Struct(t *testing.T) { - values := []exec.FullValue{exec.FullValue{ - Windows: window.SingleGlobalWindow, - Timestamp: mtime.ZeroTimestamp, - Elm: "one", - }, exec.FullValue{ - Windows: window.SingleGlobalWindow, - Timestamp: mtime.ZeroTimestamp, - Elm: "two", - }, exec.FullValue{ - Windows: window.SingleGlobalWindow, - Timestamp: mtime.ZeroTimestamp, - Elm: "three", - }} - - i := iter1[string]{s: &exec.FixedReStream{Buf: values}} - - i.Init() - fn := i.Value().(func(value *string) bool) - - var s string - if ok := fn(&s); !ok { - t.Fatalf("First i.Value()(&s)=false, want true") - } - if got, want := s, "one"; got != want { - t.Fatalf("First iter value = %v, want %v", got, want) - } - if ok := fn(&s); !ok { - t.Fatalf("Second i.Value()(&s)=false, want true") - } - if got, want := s, "two"; got != want { - t.Fatalf("Second iter value = %v, want %v", got, want) - } - if ok := fn(&s); !ok { - t.Fatalf("Third i.Value()(&s)=false, want true") - } - if got, want := s, "three"; got != want { - t.Fatalf("Third iter value = %v, want %v", got, want) - } - if ok := fn(&s); ok { - t.Fatalf("Fourth i.Value()(&s)=true, want false") - } - if err := i.Reset(); err != nil { - t.Fatalf("i.Reset()=%v, want nil", err) - } -} - -func TestIter2_Struct(t *testing.T) { - values := []exec.FullValue{exec.FullValue{ - Windows: window.SingleGlobalWindow, - Timestamp: mtime.ZeroTimestamp, - Elm: 1, - Elm2: "one", - }, exec.FullValue{ - Windows: window.SingleGlobalWindow, - Timestamp: mtime.ZeroTimestamp, - Elm: 2, - Elm2: "two", - }, exec.FullValue{ - Windows: window.SingleGlobalWindow, - Timestamp: mtime.ZeroTimestamp, - Elm: 3, - Elm2: "three", - }} - - i := iter2[int, string]{s: &exec.FixedReStream{Buf: values}} - - i.Init() - fn := i.Value().(func(key *int, value *string) bool) - - var s string - var key int - if ok := fn(&key, &s); !ok { - t.Fatalf("First i.Value()(&s)=false, want true") - } - if got, want := key, 1; got != want { - t.Fatalf("First iter key = %v, want %v", got, want) - } - if got, want := s, "one"; got != want { - t.Fatalf("First iter value = %v, want %v", got, want) - } - if ok := fn(&key, &s); !ok { - t.Fatalf("Second i.Value()(&s)=false, want true") - } - if got, want := key, 2; got != want { - t.Fatalf("Second iter key = %v, want %v", got, want) - } - if got, want := s, "two"; got != want { - t.Fatalf("Second iter value = %v, want %v", got, want) - } - if ok := fn(&key, &s); !ok { - t.Fatalf("Third i.Value()(&s)=false, want true") - } - if got, want := key, 3; got != want { - t.Fatalf("Third iter key = %v, want %v", got, want) - } - if got, want := s, "three"; got != want { - t.Fatalf("Third iter value = %v, want %v", got, want) - } - if ok := fn(&key, &s); ok { - t.Fatalf("Fourth i.Value()(&s)=true, want false") - } - if err := i.Reset(); err != nil { - t.Fatalf("i.Reset()=%v, want nil", err) - } -} - type CustomFunctionParameter struct { key string val int diff --git a/sdks/go/pkg/beam/transforms/xlang/python/external.go b/sdks/go/pkg/beam/transforms/xlang/python/external.go index 629ede0f9527..3fd6edd37e32 100644 --- a/sdks/go/pkg/beam/transforms/xlang/python/external.go +++ b/sdks/go/pkg/beam/transforms/xlang/python/external.go @@ -27,7 +27,8 @@ import ( ) const ( - pythonCallableUrn = "beam:logical_type:python_callable:v1" + pythonCallableUrn = "beam:logical_type:python_callable:v1" + // ExpansionServiceModule is the module containing the python expansion service for python external transforms. ExpansionServiceModule = "apache_beam.runners.portability.expansion_service_main" ) diff --git a/sdks/go/test/build.gradle b/sdks/go/test/build.gradle index 76acadb5db17..5d34f9c72c8a 100644 --- a/sdks/go/test/build.gradle +++ b/sdks/go/test/build.gradle @@ -104,7 +104,7 @@ task sparkValidatesRunner { dependsOn ":sdks:go:test:goBuild" dependsOn ":sdks:java:container:java8:docker" - dependsOn ":runners:spark:2:job-server:shadowJar" + dependsOn ":runners:spark:3:job-server:shadowJar" dependsOn ":sdks:java:testing:expansion-service:buildTestExpansionServiceJar" doLast { def pipelineOptions = [ // Pipeline options piped directly to Go SDK flags. @@ -112,7 +112,7 @@ task sparkValidatesRunner { ] def options = [ "--runner spark", - "--spark_job_server_jar ${project(":runners:spark:2:job-server").shadowJar.archivePath}", + "--spark_job_server_jar ${project(":runners:spark:3:job-server").shadowJar.archivePath}", "--pipeline_opts \"${pipelineOptions.join(' ')}\"", ] exec { diff --git a/sdks/go/test/regression/coders/fromyaml/fromyaml.go b/sdks/go/test/regression/coders/fromyaml/fromyaml.go index 86d7969c2a08..5fddc6226dd5 100644 --- a/sdks/go/test/regression/coders/fromyaml/fromyaml.go +++ b/sdks/go/test/regression/coders/fromyaml/fromyaml.go @@ -53,6 +53,7 @@ var filteredCases = []struct{ filter, reason string }{ {"30ea5a25-dcd8-4cdb-abeb-5332d15ab4b9", "https://github.com/apache/beam/issues/21206: Support encoding position."}, {"80be749a-5700-4ede-89d8-dd9a4433a3f8", "https://github.com/apache/beam/issues/19817: Support millis_instant."}, {"800c44ae-a1b7-4def-bbf6-6217cca89ec4", "https://github.com/apache/beam/issues/19817: Support decimal."}, + {"f0ffb3a4-f46f-41ca-a942-85e3e939452a", "https://github.com/apache/beam/issues/23526: Support char/varchar, binary/varbinary."}, } // Coder is a representation a serialized beam coder. diff --git a/sdks/java/container/agent/build.gradle b/sdks/java/container/agent/build.gradle index 9d86fd430a6a..df3780e45446 100644 --- a/sdks/java/container/agent/build.gradle +++ b/sdks/java/container/agent/build.gradle @@ -19,6 +19,13 @@ plugins { id 'org.apache.beam.module' } + +if (project.hasProperty('java11Home')) { + javaVersion = "1.11" +} else if (project.hasProperty('java17Home')) { + javaVersion = "1.17" +} + applyJavaNature( exportJavadoc: false, publish: false @@ -35,9 +42,7 @@ jar { } } - if (project.hasProperty('java11Home')) { - javaVersion = "1.11" def java11Home = project.findProperty('java11Home') project.tasks.withType(JavaCompile) { options.fork = true @@ -45,7 +50,6 @@ if (project.hasProperty('java11Home')) { options.compilerArgs += ['-Xlint:-path'] } } else if (project.hasProperty('java17Home')) { - javaVersion = "1.17" project.tasks.withType(JavaCompile) { setJava17Options(options) diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/GenerateSequence.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/GenerateSequence.java index 78ac1777e95f..742a75960749 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/io/GenerateSequence.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/io/GenerateSequence.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.io; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; import com.google.auto.service.AutoService; @@ -33,6 +34,7 @@ import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableMap; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; import org.joda.time.Duration; import org.joda.time.Instant; @@ -70,22 +72,26 @@ * will be present in the resulting {@link PCollection}. */ @AutoValue -@SuppressWarnings({ - "nullness" // TODO(https://github.com/apache/beam/issues/20497) -}) public abstract class GenerateSequence extends PTransform> { + @Pure abstract long getFrom(); + @Pure abstract long getTo(); + @Pure abstract @Nullable SerializableFunction getTimestampFn(); + @Pure abstract long getElementsPerPeriod(); + @Pure abstract @Nullable Duration getPeriod(); + @Pure abstract @Nullable Duration getMaxReadTime(); + @Pure abstract Builder toBuilder(); @AutoValue.Builder @@ -97,14 +103,15 @@ abstract static class Builder abstract Builder setTo(long to); - abstract Builder setTimestampFn(SerializableFunction timestampFn); + abstract Builder setTimestampFn(@Nullable SerializableFunction timestampFn); abstract Builder setElementsPerPeriod(long elementsPerPeriod); - abstract Builder setPeriod(Duration period); + abstract Builder setPeriod(@Nullable Duration period); - abstract Builder setMaxReadTime(Duration maxReadTime); + abstract Builder setMaxReadTime(@Nullable Duration maxReadTime); + @Pure abstract GenerateSequence build(); @Override @@ -144,7 +151,7 @@ public static class External implements ExternalTransformRegistrar { /** Parameters class to expose the transform to an external SDK. */ @Experimental public static class ExternalConfiguration { - private Long start; + private Long start = 0L; private @Nullable Long stop; private @Nullable Long period; private @Nullable Long maxReadTime; @@ -223,8 +230,13 @@ public PCollection expand(PBegin input) { if (getTimestampFn() != null) { source = source.withTimestampFn(getTimestampFn()); } - if (getElementsPerPeriod() > 0) { - source = source.withRate(getElementsPerPeriod(), getPeriod()); + if (getPeriod() != null || getElementsPerPeriod() > 0) { + Duration period = + checkArgumentNotNull( + getPeriod(), "elements per period specified, but no period specified"); + checkArgument( + getElementsPerPeriod() > 0, "elements per period not specified, but period specified"); + source = source.withRate(getElementsPerPeriod(), period); } Read.Unbounded readUnbounded = Read.from(source); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaTranslation.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaTranslation.java index f38e50aea6f4..f79db31bf7ec 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaTranslation.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/SchemaTranslation.java @@ -44,11 +44,15 @@ import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.Schema.LogicalType; import org.apache.beam.sdk.schemas.Schema.TypeName; +import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; import org.apache.beam.sdk.schemas.logicaltypes.FixedPrecisionNumeric; +import org.apache.beam.sdk.schemas.logicaltypes.FixedString; import org.apache.beam.sdk.schemas.logicaltypes.MicrosInstant; import org.apache.beam.sdk.schemas.logicaltypes.PythonCallable; import org.apache.beam.sdk.schemas.logicaltypes.SchemaLogicalType; import org.apache.beam.sdk.schemas.logicaltypes.UnknownLogicalType; +import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; +import org.apache.beam.sdk.schemas.logicaltypes.VariableString; import org.apache.beam.sdk.util.SerializableUtils; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.grpc.v1p48p1.com.google.protobuf.ByteString; @@ -57,6 +61,7 @@ import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Maps; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.io.ByteStreams; +import org.apache.commons.lang3.ClassUtils; import org.checkerframework.checker.nullness.qual.Nullable; /** Utility methods for translating schemas. */ @@ -85,6 +90,10 @@ public class SchemaTranslation { .put(MicrosInstant.IDENTIFIER, MicrosInstant.class) .put(SchemaLogicalType.IDENTIFIER, SchemaLogicalType.class) .put(PythonCallable.IDENTIFIER, PythonCallable.class) + .put(FixedBytes.IDENTIFIER, FixedBytes.class) + .put(VariableBytes.IDENTIFIER, VariableBytes.class) + .put(FixedString.IDENTIFIER, FixedString.class) + .put(VariableString.IDENTIFIER, VariableString.class) .build(); public static SchemaApi.Schema schemaToProto(Schema schema, boolean serializeLogicalType) { @@ -350,7 +359,10 @@ private static FieldType fieldTypeFromProtoWithoutNullable(SchemaApi.FieldType p Object fieldValue = Objects.requireNonNull(fieldValueFromProto(fieldType, logicalType.getArgument())); Class clazz = fieldValue.getClass(); - if (fieldValue instanceof List) { + if (ClassUtils.isPrimitiveWrapper(clazz)) { + // argument is a primitive wrapper type (e.g. Integer) + clazz = ClassUtils.wrapperToPrimitive(clazz); + } else if (fieldValue instanceof List) { // argument is ArrayValue or iterableValue clazz = List.class; } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/FixedBytes.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/FixedBytes.java index 4022c634acdf..886f0851c494 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/FixedBytes.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/FixedBytes.java @@ -20,67 +20,68 @@ import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; import java.util.Arrays; -import org.apache.beam.sdk.annotations.Experimental; -import org.apache.beam.sdk.annotations.Experimental.Kind; +import org.apache.beam.model.pipeline.v1.RunnerApi; +import org.apache.beam.model.pipeline.v1.SchemaApi; import org.apache.beam.sdk.schemas.Schema.FieldType; -import org.apache.beam.sdk.schemas.Schema.LogicalType; +import org.checkerframework.checker.nullness.qual.Nullable; -/** A LogicalType representing a fixed-size byte array. */ -@Experimental(Kind.SCHEMAS) -public class FixedBytes implements LogicalType { - public static final String IDENTIFIER = "FixedBytes"; - private final int byteArraySize; +/** A LogicalType representing a fixed-length byte array. */ +public class FixedBytes extends PassThroughLogicalType { + public static final String IDENTIFIER = + SchemaApi.LogicalTypes.Enum.FIXED_BYTES + .getValueDescriptor() + .getOptions() + .getExtension(RunnerApi.beamUrn); - private FixedBytes(int byteArraySize) { - this.byteArraySize = byteArraySize; - } + private final @Nullable String name; + private final int byteArrayLength; - public static FixedBytes of(int byteArraySize) { - return new FixedBytes(byteArraySize); + /** + * Return an instance of FixedBytes with specified byte array length. + * + *

The name, if set, refers to the TYPE name in the underlying database, for example, BINARY. + */ + public static FixedBytes of(@Nullable String name, int byteArrayLength) { + return new FixedBytes(name, byteArrayLength); } - public int getLength() { - return byteArraySize; + /** Return an instance of FixedBytes with specified byte array length. */ + public static FixedBytes of(int byteArrayLength) { + return of(null, byteArrayLength); } - @Override - public String getIdentifier() { - return IDENTIFIER; + private FixedBytes(@Nullable String name, int byteArrayLength) { + super(IDENTIFIER, FieldType.INT32, byteArrayLength, FieldType.BYTES); + this.name = name; + this.byteArrayLength = byteArrayLength; } - @Override - public FieldType getArgumentType() { - return FieldType.INT32; - } - - @Override - public Integer getArgument() { - return byteArraySize; + public int getLength() { + return byteArrayLength; } - @Override - public FieldType getBaseType() { - return FieldType.BYTES; + public @Nullable String getName() { + return name; } @Override public byte[] toBaseType(byte[] input) { - checkArgument(input.length == byteArraySize); + checkArgument(input.length == byteArrayLength); return input; } @Override public byte[] toInputType(byte[] base) { - checkArgument(base.length <= byteArraySize); - if (base.length == byteArraySize) { + checkArgument(base.length <= byteArrayLength); + if (base.length == byteArrayLength) { return base; } else { - return Arrays.copyOf(base, byteArraySize); + return Arrays.copyOf(base, byteArrayLength); } } @Override public String toString() { - return "FixedBytes: " + byteArraySize; + return "FixedBytes: " + byteArrayLength; } } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/FixedString.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/FixedString.java new file mode 100644 index 000000000000..72dd97fae837 --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/FixedString.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.schemas.logicaltypes; + +import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; + +import org.apache.beam.model.pipeline.v1.RunnerApi; +import org.apache.beam.model.pipeline.v1.SchemaApi; +import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A LogicalType representing a fixed-length string. */ +public class FixedString extends PassThroughLogicalType { + public static final String IDENTIFIER = + SchemaApi.LogicalTypes.Enum.FIXED_CHAR + .getValueDescriptor() + .getOptions() + .getExtension(RunnerApi.beamUrn); + private final @Nullable String name; + private final int stringLength; + + /** + * Return an instance of FixedString with specified string length. + * + *

The name, if set, refers to the TYPE name in the underlying database, for example, CHAR. + */ + public static FixedString of(@Nullable String name, int stringLength) { + return new FixedString(name, stringLength); + } + + /** Return an instance of FixedString with specified string length. */ + public static FixedString of(int stringLength) { + return new FixedString(null, stringLength); + } + + private FixedString(@Nullable String name, int stringLength) { + super(IDENTIFIER, FieldType.INT32, stringLength, FieldType.STRING); + this.name = name; + this.stringLength = stringLength; + } + + public int getLength() { + return stringLength; + } + + public @Nullable String getName() { + return name; + } + + @Override + public String toInputType(String base) { + checkArgument(base.length() <= stringLength); + + return StringUtils.rightPad(base, stringLength); + } + + @Override + public String toString() { + return "FixedString: " + stringLength; + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/VariableBytes.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/VariableBytes.java new file mode 100644 index 000000000000..4c1cb87f8f9c --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/VariableBytes.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.schemas.logicaltypes; + +import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; + +import org.apache.beam.model.pipeline.v1.RunnerApi; +import org.apache.beam.model.pipeline.v1.SchemaApi; +import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A LogicalType representing a variable-length byte array with specified maximum length. */ +public class VariableBytes extends PassThroughLogicalType { + public static final String IDENTIFIER = + SchemaApi.LogicalTypes.Enum.VAR_BYTES + .getValueDescriptor() + .getOptions() + .getExtension(RunnerApi.beamUrn); + private final @Nullable String name; + private final int maxByteArrayLength; + + /** + * Return an instance of VariableBytes with specified max byte array length. + * + *

The name, if set, refers to the TYPE name in the underlying database, for example, VARBINARY + * and LONGVARBINARY. + */ + public static VariableBytes of(@Nullable String name, int maxByteArrayLength) { + return new VariableBytes(name, maxByteArrayLength); + } + + /** Return an instance of VariableBytes with specified max byte array length. */ + public static VariableBytes of(int maxByteArrayLength) { + return of(null, maxByteArrayLength); + } + + private VariableBytes(@Nullable String name, int maxByteArrayLength) { + super(IDENTIFIER, FieldType.INT32, maxByteArrayLength, FieldType.BYTES); + this.name = name; + this.maxByteArrayLength = maxByteArrayLength; + } + + public int getMaxLength() { + return maxByteArrayLength; + } + + public @Nullable String getName() { + return name; + } + + @Override + public byte[] toInputType(byte[] base) { + checkArgument(base.length <= maxByteArrayLength); + return base; + } + + @Override + public String toString() { + return "VariableBytes: " + maxByteArrayLength; + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/VariableString.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/VariableString.java new file mode 100644 index 000000000000..c635e70a625a --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/logicaltypes/VariableString.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.schemas.logicaltypes; + +import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; + +import org.apache.beam.model.pipeline.v1.RunnerApi; +import org.apache.beam.model.pipeline.v1.SchemaApi; +import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** A LogicalType representing a variable-length string with specified maximum length. */ +public class VariableString extends PassThroughLogicalType { + public static final String IDENTIFIER = + SchemaApi.LogicalTypes.Enum.VAR_CHAR + .getValueDescriptor() + .getOptions() + .getExtension(RunnerApi.beamUrn); + private final @Nullable String name; + private final int maxStringLength; + + /** + * Return an instance of VariableString with specified max string length. + * + *

The name, if set, refers to the TYPE name in the underlying database, for example, VARCHAR + * and LONGVARCHAR. + */ + public static VariableString of(@Nullable String name, int maxStringLength) { + return new VariableString(name, maxStringLength); + } + + /** Return an instance of VariableString with specified max string length. */ + public static VariableString of(int maxStringLength) { + return of(null, maxStringLength); + } + + private VariableString(@Nullable String name, int maxStringLength) { + super(IDENTIFIER, FieldType.INT32, maxStringLength, FieldType.STRING); + this.name = name; + this.maxStringLength = maxStringLength; + } + + public int getMaxLength() { + return maxStringLength; + } + + public @Nullable String getName() { + return name; + } + + @Override + public String toInputType(String base) { + checkArgument(base.length() <= maxStringLength); + return base; + } + + @Override + public String toString() { + return "VariableString: " + maxStringLength; + } +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AvroUtils.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AvroUtils.java index 8636e31300ce..8d01ed0406a0 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AvroUtils.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/schemas/utils/AvroUtils.java @@ -74,7 +74,10 @@ import org.apache.beam.sdk.schemas.SchemaUserTypeCreator; import org.apache.beam.sdk.schemas.logicaltypes.EnumerationType; import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; +import org.apache.beam.sdk.schemas.logicaltypes.FixedString; import org.apache.beam.sdk.schemas.logicaltypes.OneOfType; +import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; +import org.apache.beam.sdk.schemas.logicaltypes.VariableString; import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.ConvertType; import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.ConvertValueForGetter; import org.apache.beam.sdk.schemas.utils.ByteBuddyUtils.ConvertValueForSetter; @@ -899,48 +902,44 @@ private static org.apache.avro.Schema getFieldSchema( break; case LOGICAL_TYPE: - switch (fieldType.getLogicalType().getIdentifier()) { - case FixedBytes.IDENTIFIER: - FixedBytesField fixedBytesField = - checkNotNull(FixedBytesField.fromBeamFieldType(fieldType)); - baseType = fixedBytesField.toAvroType("fixed", namespace + "." + fieldName); - break; - case EnumerationType.IDENTIFIER: - EnumerationType enumerationType = fieldType.getLogicalType(EnumerationType.class); - baseType = - org.apache.avro.Schema.createEnum(fieldName, "", "", enumerationType.getValues()); - break; - case OneOfType.IDENTIFIER: - OneOfType oneOfType = fieldType.getLogicalType(OneOfType.class); - baseType = - org.apache.avro.Schema.createUnion( - oneOfType.getOneOfSchema().getFields().stream() - .map(x -> getFieldSchema(x.getType(), x.getName(), namespace)) - .collect(Collectors.toList())); - break; - case "CHAR": - case "NCHAR": - baseType = - buildHiveLogicalTypeSchema("char", (int) fieldType.getLogicalType().getArgument()); - break; - case "NVARCHAR": - case "VARCHAR": - case "LONGNVARCHAR": - case "LONGVARCHAR": - baseType = - buildHiveLogicalTypeSchema( - "varchar", (int) fieldType.getLogicalType().getArgument()); - break; - case "DATE": - baseType = LogicalTypes.date().addToSchema(org.apache.avro.Schema.create(Type.INT)); - break; - case "TIME": - baseType = - LogicalTypes.timeMillis().addToSchema(org.apache.avro.Schema.create(Type.INT)); - break; - default: - throw new RuntimeException( - "Unhandled logical type " + fieldType.getLogicalType().getIdentifier()); + String identifier = fieldType.getLogicalType().getIdentifier(); + if (FixedBytes.IDENTIFIER.equals(identifier)) { + FixedBytesField fixedBytesField = + checkNotNull(FixedBytesField.fromBeamFieldType(fieldType)); + baseType = fixedBytesField.toAvroType("fixed", namespace + "." + fieldName); + } else if (VariableBytes.IDENTIFIER.equals(identifier)) { + // treat VARBINARY as bytes as that is what avro supports + baseType = org.apache.avro.Schema.create(Type.BYTES); + } else if (FixedString.IDENTIFIER.equals(identifier) + || "CHAR".equals(identifier) + || "NCHAR".equals(identifier)) { + baseType = + buildHiveLogicalTypeSchema("char", (int) fieldType.getLogicalType().getArgument()); + } else if (VariableString.IDENTIFIER.equals(identifier) + || "NVARCHAR".equals(identifier) + || "VARCHAR".equals(identifier) + || "LONGNVARCHAR".equals(identifier) + || "LONGVARCHAR".equals(identifier)) { + baseType = + buildHiveLogicalTypeSchema("varchar", (int) fieldType.getLogicalType().getArgument()); + } else if (EnumerationType.IDENTIFIER.equals(identifier)) { + EnumerationType enumerationType = fieldType.getLogicalType(EnumerationType.class); + baseType = + org.apache.avro.Schema.createEnum(fieldName, "", "", enumerationType.getValues()); + } else if (OneOfType.IDENTIFIER.equals(identifier)) { + OneOfType oneOfType = fieldType.getLogicalType(OneOfType.class); + baseType = + org.apache.avro.Schema.createUnion( + oneOfType.getOneOfSchema().getFields().stream() + .map(x -> getFieldSchema(x.getType(), x.getName(), namespace)) + .collect(Collectors.toList())); + } else if ("DATE".equals(identifier)) { + baseType = LogicalTypes.date().addToSchema(org.apache.avro.Schema.create(Type.INT)); + } else if ("TIME".equals(identifier)) { + baseType = LogicalTypes.timeMillis().addToSchema(org.apache.avro.Schema.create(Type.INT)); + } else { + throw new RuntimeException( + "Unhandled logical type " + fieldType.getLogicalType().getIdentifier()); } break; @@ -1022,45 +1021,51 @@ private static org.apache.avro.Schema getFieldSchema( return ByteBuffer.wrap((byte[]) value); case LOGICAL_TYPE: - switch (fieldType.getLogicalType().getIdentifier()) { - case FixedBytes.IDENTIFIER: - FixedBytesField fixedBytesField = - checkNotNull(FixedBytesField.fromBeamFieldType(fieldType)); - byte[] byteArray = (byte[]) value; - if (byteArray.length != fixedBytesField.getSize()) { - throw new IllegalArgumentException("Incorrectly sized byte array."); - } - return GenericData.get().createFixed(null, (byte[]) value, typeWithNullability.type); - case EnumerationType.IDENTIFIER: - EnumerationType enumerationType = fieldType.getLogicalType(EnumerationType.class); - return GenericData.get() - .createEnum( - enumerationType.toString((EnumerationType.Value) value), - typeWithNullability.type); - case OneOfType.IDENTIFIER: - OneOfType oneOfType = fieldType.getLogicalType(OneOfType.class); - OneOfType.Value oneOfValue = (OneOfType.Value) value; - FieldType innerFieldType = oneOfType.getFieldType(oneOfValue); - if (typeWithNullability.nullable && oneOfValue.getValue() == null) { - return null; - } else { - return genericFromBeamField( - innerFieldType.withNullable(false), - typeWithNullability.type.getTypes().get(oneOfValue.getCaseType().getValue()), - oneOfValue.getValue()); - } - case "NVARCHAR": - case "VARCHAR": - case "LONGNVARCHAR": - case "LONGVARCHAR": - return new Utf8((String) value); - case "DATE": - return Days.daysBetween(Instant.EPOCH, (Instant) value).getDays(); - case "TIME": - return (int) ((Instant) value).getMillis(); - default: - throw new RuntimeException( - "Unhandled logical type " + fieldType.getLogicalType().getIdentifier()); + String identifier = fieldType.getLogicalType().getIdentifier(); + if (FixedBytes.IDENTIFIER.equals(identifier)) { + FixedBytesField fixedBytesField = + checkNotNull(FixedBytesField.fromBeamFieldType(fieldType)); + byte[] byteArray = (byte[]) value; + if (byteArray.length != fixedBytesField.getSize()) { + throw new IllegalArgumentException("Incorrectly sized byte array."); + } + return GenericData.get().createFixed(null, (byte[]) value, typeWithNullability.type); + } else if (VariableBytes.IDENTIFIER.equals(identifier)) { + return GenericData.get().createFixed(null, (byte[]) value, typeWithNullability.type); + } else if (FixedString.IDENTIFIER.equals(identifier) + || "CHAR".equals(identifier) + || "NCHAR".equals(identifier)) { + return new Utf8((String) value); + } else if (VariableString.IDENTIFIER.equals(identifier) + || "NVARCHAR".equals(identifier) + || "VARCHAR".equals(identifier) + || "LONGNVARCHAR".equals(identifier) + || "LONGVARCHAR".equals(identifier)) { + return new Utf8((String) value); + } else if (EnumerationType.IDENTIFIER.equals(identifier)) { + EnumerationType enumerationType = fieldType.getLogicalType(EnumerationType.class); + return GenericData.get() + .createEnum( + enumerationType.toString((EnumerationType.Value) value), + typeWithNullability.type); + } else if (OneOfType.IDENTIFIER.equals(identifier)) { + OneOfType oneOfType = fieldType.getLogicalType(OneOfType.class); + OneOfType.Value oneOfValue = (OneOfType.Value) value; + FieldType innerFieldType = oneOfType.getFieldType(oneOfValue); + if (typeWithNullability.nullable && oneOfValue.getValue() == null) { + return null; + } else { + return genericFromBeamField( + innerFieldType.withNullable(false), + typeWithNullability.type.getTypes().get(oneOfValue.getCaseType().getValue()), + oneOfValue.getValue()); + } + } else if ("DATE".equals(identifier)) { + return Days.daysBetween(Instant.EPOCH, (Instant) value).getDays(); + } else if ("TIME".equals(identifier)) { + return (int) ((Instant) value).getMillis(); + } else { + throw new RuntimeException("Unhandled logical type " + identifier); } case ARRAY: diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineOptions.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineOptions.java index 3327ae8fc747..6ff5ded5318d 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineOptions.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineOptions.java @@ -48,7 +48,7 @@ public interface TestPipelineOptions extends PipelineOptions { void setOnSuccessMatcher(SerializableMatcher value); - @Default.Long(10 * 60) + @Default.Long(15 * 60) @Nullable Long getTestTimeoutSeconds(); diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/MoreFutures.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/MoreFutures.java index 6f053752d3f6..57074a7d6db4 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/util/MoreFutures.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/util/MoreFutures.java @@ -19,6 +19,7 @@ import com.google.auto.value.AutoValue; import edu.umd.cs.findbugs.annotations.SuppressWarnings; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -161,17 +162,13 @@ public static CompletionStage runAsync(ThrowingRunnable runnable) { /** Like {@link CompletableFuture#allOf} but returning the result of constituent futures. */ public static CompletionStage> allAsList( Collection> futures) { - // CompletableFuture.allOf completes exceptionally if any of the futures do. // We have to gather the results separately. - CompletionStage blockAndDiscard = - CompletableFuture.allOf(futuresToCompletableFutures(futures)); + CompletableFuture[] f = futuresToCompletableFutures(futures); + CompletionStage blockAndDiscard = CompletableFuture.allOf(f); return blockAndDiscard.thenApply( - nothing -> - futures.stream() - .map(future -> future.toCompletableFuture().join()) - .collect(Collectors.toList())); + nothing -> Arrays.stream(f).map(CompletableFuture::join).collect(Collectors.toList())); } /** @@ -207,25 +204,25 @@ public static ExceptionOrResult result(T result) { } } - /** Like {@link #allAsList} but return a list . */ + /** + * Like {@link #allAsList} but return a list of {@link ExceptionOrResult} of constituent futures. + */ public static CompletionStage>> allAsListWithExceptions( Collection> futures) { - // CompletableFuture.allOf completes exceptionally if any of the futures do. // We have to gather the results separately. - CompletionStage blockAndDiscard = - CompletableFuture.allOf(futuresToCompletableFutures(futures)) - .whenComplete((ignoredValues, arbitraryException) -> {}); + CompletableFuture[] f = futuresToCompletableFutures(futures); + CompletionStage blockAndDiscard = CompletableFuture.allOf(f); return blockAndDiscard.thenApply( nothing -> - futures.stream() + Arrays.stream(f) .map( future -> { // The limited scope of the exceptions wrapped allows CancellationException // to still be thrown. try { - return ExceptionOrResult.result(future.toCompletableFuture().join()); + return ExceptionOrResult.result(future.join()); } catch (CompletionException exc) { return ExceptionOrResult.exception(exc); } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/logicaltypes/LogicalTypesTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/logicaltypes/LogicalTypesTest.java index 2de9096d2b46..23ebcf1616b0 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/logicaltypes/LogicalTypesTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/schemas/logicaltypes/LogicalTypesTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.schemas.logicaltypes; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; @@ -178,4 +179,56 @@ public void testFixedPrecisionNumeric() { row = Row.withSchema(schema).addValues(decimal).build(); assertEquals(decimal, row.getLogicalTypeValue(0, BigDecimal.class)); } + + @Test + public void testFixedBytes() { + FixedBytes fixedBytes = FixedBytes.of(5); + + // check argument valid case, with padding + byte[] resultBytes = fixedBytes.toInputType(new byte[] {0x1, 0x2, 0x3}); + assertArrayEquals(new byte[] {0x1, 0x2, 0x3, 0x0, 0x0}, resultBytes); + + // check argument invalid case + assertThrows( + IllegalArgumentException.class, + () -> fixedBytes.toInputType(new byte[] {0x1, 0x2, 0x3, 0x4, 0x5, 0x6})); + } + + @Test + public void testVariableBytes() { + VariableBytes variableBytes = VariableBytes.of(5); + + // check argument valid case, no padding + byte[] resultBytes = variableBytes.toInputType(new byte[] {0x1, 0x2, 0x3}); + assertArrayEquals(new byte[] {0x1, 0x2, 0x3}, resultBytes); + + // check argument invalid case + assertThrows( + IllegalArgumentException.class, + () -> variableBytes.toInputType(new byte[] {0x1, 0x2, 0x3, 0x4, 0x5, 0x6})); + } + + @Test + public void testFixedString() { + FixedString fixedString = FixedString.of(5); + + // check argument valid case, with padding + String resultString = fixedString.toInputType("123"); + assertEquals("123 ", resultString); + + // check argument invalid case + assertThrows(IllegalArgumentException.class, () -> fixedString.toInputType("123456")); + } + + @Test + public void testVariableString() { + VariableString varibaleString = VariableString.of(5); + + // check argument valid case, no padding + String resultString = varibaleString.toInputType("123"); + assertEquals("123", resultString); + + // check argument invalid case + assertThrows(IllegalArgumentException.class, () -> varibaleString.toInputType("123456")); + } } diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/MoreFuturesTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/MoreFuturesTest.java index 4b6790d22c30..b8a107935016 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/util/MoreFuturesTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/util/MoreFuturesTest.java @@ -20,10 +20,16 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.isA; +import static org.junit.Assert.assertEquals; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; +import org.apache.beam.sdk.util.MoreFutures.ExceptionOrResult; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -84,4 +90,72 @@ public void runAsyncFailure() throws Exception { thrown.expectMessage(testMessage); MoreFutures.get(sideEffectFuture); } + + @Test + public void testAllAsListRespectsOriginalList() throws Exception { + CountDownLatch waitTillThreadRunning = new CountDownLatch(1); + CountDownLatch waitTillClearHasHappened = new CountDownLatch(1); + List> stages = new ArrayList<>(); + stages.add(MoreFutures.runAsync(waitTillThreadRunning::countDown)); + stages.add(MoreFutures.runAsync(waitTillClearHasHappened::await)); + + CompletionStage> results = MoreFutures.allAsList(stages); + waitTillThreadRunning.await(); + stages.clear(); + waitTillClearHasHappened.countDown(); + assertEquals(MoreFutures.get(results), Arrays.asList(null, null)); + } + + @Test + public void testAllAsListNoExceptionDueToMutation() throws Exception { + // This loop runs many times trying to exercise a race condition that existed where mutation + // of the passed in completion stages lead to various exceptions (such as a + // ConcurrentModificationException). See https://github.com/apache/beam/issues/23809 + for (int i = 0; i < 10000; ++i) { + CountDownLatch waitTillThreadRunning = new CountDownLatch(1); + List> stages = new ArrayList<>(); + stages.add(MoreFutures.runAsync(waitTillThreadRunning::countDown)); + + CompletionStage> results = MoreFutures.allAsList(stages); + waitTillThreadRunning.await(); + stages.clear(); + MoreFutures.get(results); + } + } + + @Test + public void testAllAsListWithExceptionsRespectsOriginalList() throws Exception { + CountDownLatch waitTillThreadRunning = new CountDownLatch(1); + CountDownLatch waitTillClearHasHappened = new CountDownLatch(1); + List> stages = new ArrayList<>(); + stages.add(MoreFutures.runAsync(waitTillThreadRunning::countDown)); + stages.add(MoreFutures.runAsync(waitTillClearHasHappened::await)); + + CompletionStage>> results = + MoreFutures.allAsListWithExceptions(stages); + waitTillThreadRunning.await(); + stages.clear(); + waitTillClearHasHappened.countDown(); + assertEquals( + MoreFutures.get(results), + Arrays.asList(ExceptionOrResult.result(null), ExceptionOrResult.result(null))); + } + + @Test + public void testAllAsListWithExceptionsNoExceptionDueToMutation() throws Exception { + // This loop runs many times trying to exercise a race condition that existed where mutation + // of the passed in completion stages lead to various exceptions (such as a + // ConcurrentModificationException). See https://github.com/apache/beam/issues/23809 + for (int i = 0; i < 10000; ++i) { + CountDownLatch waitTillThreadRunning = new CountDownLatch(1); + List> stages = new ArrayList<>(); + stages.add(MoreFutures.runAsync(waitTillThreadRunning::countDown)); + + CompletionStage>> results = + MoreFutures.allAsListWithExceptions(stages); + waitTillThreadRunning.await(); + stages.clear(); + MoreFutures.get(results); + } + } } diff --git a/sdks/java/extensions/sorter/src/main/java/org/apache/beam/sdk/extensions/sorter/SortValues.java b/sdks/java/extensions/sorter/src/main/java/org/apache/beam/sdk/extensions/sorter/SortValues.java index 5c489af6e6b4..bc9fb2f89554 100644 --- a/sdks/java/extensions/sorter/src/main/java/org/apache/beam/sdk/extensions/sorter/SortValues.java +++ b/sdks/java/extensions/sorter/src/main/java/org/apache/beam/sdk/extensions/sorter/SortValues.java @@ -76,13 +76,20 @@ SortValues create( @Override public PCollection>>> expand( PCollection>>> input) { + + Coder secondaryKeyCoder = getSecondaryKeyCoder(input.getCoder()); + try { + secondaryKeyCoder.verifyDeterministic(); + } catch (Coder.NonDeterministicException e) { + throw new IllegalStateException( + "the secondary key coder of SortValues must be deterministic", e); + } + return input .apply( ParDo.of( new SortValuesDoFn<>( - sorterOptions, - getSecondaryKeyCoder(input.getCoder()), - getValueCoder(input.getCoder())))) + sorterOptions, secondaryKeyCoder, getValueCoder(input.getCoder())))) .setCoder(input.getCoder()); } diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRel.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRel.java index 804715f4362a..368bf8dc4645 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRel.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCalcRel.java @@ -53,7 +53,11 @@ import org.apache.beam.sdk.extensions.sql.impl.utils.CalciteUtils.TimeWithLocalTzType; import org.apache.beam.sdk.schemas.FieldAccessDescriptor; import org.apache.beam.sdk.schemas.Schema; +import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; +import org.apache.beam.sdk.schemas.logicaltypes.FixedString; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; +import org.apache.beam.sdk.schemas.logicaltypes.VariableString; import org.apache.beam.sdk.schemas.utils.SelectHelpers; import org.apache.beam.sdk.transforms.DoFn; import org.apache.beam.sdk.transforms.PTransform; @@ -404,8 +408,13 @@ static Object toBeamObject(Object value, FieldType fieldType, boolean verifyValu return toBeamRow((List) value, fieldType.getRowSchema(), verifyValues); case LOGICAL_TYPE: String identifier = fieldType.getLogicalType().getIdentifier(); - if (CharType.IDENTIFIER.equals(identifier)) { + if (CharType.IDENTIFIER.equals(identifier) + || FixedString.IDENTIFIER.equals(identifier) + || VariableString.IDENTIFIER.equals(identifier)) { return (String) value; + } else if (FixedBytes.IDENTIFIER.equals(identifier) + || VariableBytes.IDENTIFIER.equals(identifier)) { + return (byte[]) value; } else if (TimeWithLocalTzType.IDENTIFIER.equals(identifier)) { return Instant.ofEpochMilli(((Number) value).longValue()); } else if (SqlTypes.DATE.getIdentifier().equals(identifier)) { @@ -552,8 +561,13 @@ private static Expression getBeamField( break; case LOGICAL_TYPE: String identifier = fieldType.getLogicalType().getIdentifier(); - if (CharType.IDENTIFIER.equals(identifier)) { + if (CharType.IDENTIFIER.equals(identifier) + || FixedString.IDENTIFIER.equals(identifier) + || VariableString.IDENTIFIER.equals(identifier)) { value = Expressions.call(expression, "getString", fieldName); + } else if (FixedBytes.IDENTIFIER.equals(identifier) + || VariableBytes.IDENTIFIER.equals(identifier)) { + value = Expressions.call(expression, "getBytes", fieldName); } else if (TimeWithLocalTzType.IDENTIFIER.equals(identifier)) { value = Expressions.call(expression, "getDateTime", fieldName); } else if (SqlTypes.DATE.getIdentifier().equals(identifier)) { @@ -629,8 +643,13 @@ private static Expression toCalciteValue(Expression value, FieldType fieldType) return nullOr(value, toCalciteRow(value, fieldType.getRowSchema())); case LOGICAL_TYPE: String identifier = fieldType.getLogicalType().getIdentifier(); - if (CharType.IDENTIFIER.equals(identifier)) { + if (CharType.IDENTIFIER.equals(identifier) + || FixedString.IDENTIFIER.equals(identifier) + || VariableString.IDENTIFIER.equals(identifier)) { return Expressions.convert_(value, String.class); + } else if (FixedBytes.IDENTIFIER.equals(identifier) + || VariableBytes.IDENTIFIER.equals(identifier)) { + return Expressions.convert_(value, byte[].class); } else if (TimeWithLocalTzType.IDENTIFIER.equals(identifier)) { return nullOr( value, Expressions.call(Expressions.convert_(value, DateTime.class), "getMillis")); diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java index 50163b03172e..4f8d57a4fbc5 100644 --- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java +++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtils.java @@ -194,6 +194,13 @@ public static SqlTypeName toSqlTypeName(FieldType type) { typeName = BEAM_TO_CALCITE_DEFAULT_MAPPING.get(type); } if (typeName == null) { + if (type.getLogicalType() != null) { + Schema.LogicalType logicalType = type.getLogicalType(); + if (logicalType instanceof PassThroughLogicalType) { + // for pass through logical type, just return its base type + return toSqlTypeName(logicalType.getBaseType()); + } + } throw new IllegalArgumentException( String.format("Cannot find a matching Calcite SqlTypeName for Beam type: %s", type)); } else { diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamComplexTypeTest.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamComplexTypeTest.java index 9d8bb8c68280..6d2905db0b8f 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamComplexTypeTest.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamComplexTypeTest.java @@ -17,6 +17,7 @@ */ package org.apache.beam.sdk.extensions.sql; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -25,11 +26,16 @@ import java.util.Map; import org.apache.beam.sdk.extensions.sql.impl.BeamSqlEnv; import org.apache.beam.sdk.extensions.sql.impl.rel.BeamSqlRelUtils; +import org.apache.beam.sdk.extensions.sql.meta.BeamSqlTable; import org.apache.beam.sdk.extensions.sql.meta.provider.ReadOnlyTableProvider; import org.apache.beam.sdk.extensions.sql.meta.provider.test.TestBoundedTable; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; +import org.apache.beam.sdk.schemas.logicaltypes.FixedString; import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; +import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; +import org.apache.beam.sdk.schemas.logicaltypes.VariableString; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.Create; @@ -82,36 +88,66 @@ public class BeamComplexTypeTest { .addArrayField("field3", FieldType.INT64) .build(); + private static final Schema rowWithLogicalTypeSchema = + Schema.builder() + .addLogicalTypeField("field1", FixedString.of(10)) + .addLogicalTypeField("field2", VariableString.of(10)) + .addLogicalTypeField("field3", FixedBytes.of(10)) + .addLogicalTypeField("field4", VariableBytes.of(10)) + .build(); + private static final ReadOnlyTableProvider readOnlyTableProvider = new ReadOnlyTableProvider( "test_provider", - ImmutableMap.of( - "arrayWithRowTestTable", - TestBoundedTable.of(FieldType.array(FieldType.row(innerRowSchema)), "col") - .addRows( - Arrays.asList(Row.withSchema(innerRowSchema).addValues("str", 1L).build())), - "nestedArrayTestTable", - TestBoundedTable.of(FieldType.array(FieldType.array(FieldType.INT64)), "col") - .addRows(Arrays.asList(Arrays.asList(1L, 2L, 3L), Arrays.asList(4L, 5L))), - "nestedRowTestTable", - TestBoundedTable.of(Schema.FieldType.row(nestedRowSchema), "col") - .addRows( - Row.withSchema(nestedRowSchema) - .addValues( - "str", - Row.withSchema(innerRowSchema).addValues("inner_str_one", 1L).build(), - 2L, - Row.withSchema(innerRowSchema).addValues("inner_str_two", 3L).build()) - .build()), - "basicRowTestTable", - TestBoundedTable.of(Schema.FieldType.row(innerRowSchema), "col") - .addRows(Row.withSchema(innerRowSchema).addValues("innerStr", 1L).build()), - "rowWithArrayTestTable", - TestBoundedTable.of(Schema.FieldType.row(rowWithArraySchema), "col") - .addRows( - Row.withSchema(rowWithArraySchema) - .addValues("str", 4L, Arrays.asList(5L, 6L)) - .build()))); + ImmutableMap.builder() + .put( + "arrayWithRowTestTable", + TestBoundedTable.of(FieldType.array(FieldType.row(innerRowSchema)), "col") + .addRows( + Arrays.asList( + Row.withSchema(innerRowSchema).addValues("str", 1L).build()))) + .put( + "nestedArrayTestTable", + TestBoundedTable.of(FieldType.array(FieldType.array(FieldType.INT64)), "col") + .addRows(Arrays.asList(Arrays.asList(1L, 2L, 3L), Arrays.asList(4L, 5L)))) + .put( + "nestedRowTestTable", + TestBoundedTable.of(FieldType.row(nestedRowSchema), "col") + .addRows( + Row.withSchema(nestedRowSchema) + .addValues( + "str", + Row.withSchema(innerRowSchema) + .addValues("inner_str_one", 1L) + .build(), + 2L, + Row.withSchema(innerRowSchema) + .addValues("inner_str_two", 3L) + .build()) + .build())) + .put( + "basicRowTestTable", + TestBoundedTable.of(FieldType.row(innerRowSchema), "col") + .addRows(Row.withSchema(innerRowSchema).addValues("innerStr", 1L).build())) + .put( + "rowWithArrayTestTable", + TestBoundedTable.of(FieldType.row(rowWithArraySchema), "col") + .addRows( + Row.withSchema(rowWithArraySchema) + .addValues("str", 4L, Arrays.asList(5L, 6L)) + .build())) + .put( + "rowWithLogicalTypeSchema", + TestBoundedTable.of(FieldType.row(rowWithLogicalTypeSchema), "col") + .addRows( + Row.withSchema(rowWithLogicalTypeSchema) + .addValues( + "1234567890", + "1", + "1234567890".getBytes(StandardCharsets.UTF_8), + "1".getBytes(StandardCharsets.UTF_8)) + .build())) + .build()); @Rule public transient TestPipeline pipeline = TestPipeline.create(); @@ -211,6 +247,23 @@ public void testRowWithArray() { pipeline.run().waitUntilFinish(Duration.standardMinutes(2)); } + @Test + public void testRowWithLogicalTypeSchema() { + BeamSqlEnv sqlEnv = BeamSqlEnv.inMemory(readOnlyTableProvider); + PCollection stream = + BeamSqlRelUtils.toPCollection( + pipeline, + sqlEnv.parseQuery( + "SELECT rowWithLogicalTypeSchema.col.field1, rowWithLogicalTypeSchema.col.field4 FROM rowWithLogicalTypeSchema")); + PAssert.that(stream) + .containsInAnyOrder( + Row.withSchema( + Schema.builder().addStringField("field1").addByteArrayField("field2").build()) + .addValues("1234567890", "1".getBytes(StandardCharsets.UTF_8)) + .build()); + pipeline.run().waitUntilFinish(Duration.standardMinutes(2)); + } + @Test public void testFieldAccessToNestedRow() { BeamSqlEnv sqlEnv = BeamSqlEnv.inMemory(readOnlyTableProvider); diff --git a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/pubsub/PubsubTableProviderIT.java b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/pubsub/PubsubTableProviderIT.java index f8d8ff3098a7..7bd872e7c510 100644 --- a/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/pubsub/PubsubTableProviderIT.java +++ b/sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/meta/provider/pubsub/PubsubTableProviderIT.java @@ -78,7 +78,6 @@ import org.hamcrest.Matcher; import org.joda.time.Duration; import org.joda.time.Instant; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -196,7 +195,6 @@ public void testSQLSelectsPayloadContent() throws Exception { resultSignal.waitForSuccess(timeout); } - @Ignore("https://github.com/apache/beam/issues/20937") @Test public void testSQLSelectsArrayAttributes() throws Exception { diff --git a/sdks/java/io/amazon-web-services2/build.gradle b/sdks/java/io/amazon-web-services2/build.gradle index 1c5d3dc82683..5b25cde8f0e0 100644 --- a/sdks/java/io/amazon-web-services2/build.gradle +++ b/sdks/java/io/amazon-web-services2/build.gradle @@ -48,6 +48,7 @@ dependencies { implementation library.java.aws_java_sdk2_auth, excludeNetty implementation library.java.aws_java_sdk2_regions, excludeNetty implementation library.java.aws_java_sdk2_utils, excludeNetty + implementation library.java.aws_java_sdk2_profiles, excludeNetty implementation library.java.aws_java_sdk2_http_client_spi, excludeNetty implementation library.java.aws_java_sdk2_apache_client, excludeNetty implementation library.java.aws_java_sdk2_netty_client, excludeNetty diff --git a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/options/AwsModule.java b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/options/AwsModule.java index 0f8b138d0b95..d814b395950a 100644 --- a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/options/AwsModule.java +++ b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/options/AwsModule.java @@ -49,6 +49,7 @@ import org.apache.beam.sdk.annotations.Experimental.Kind; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableSet; import org.checkerframework.checker.nullness.qual.NonNull; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; @@ -60,6 +61,7 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider; import software.amazon.awssdk.http.apache.ProxyConfiguration; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; @@ -75,6 +77,7 @@ public class AwsModule extends SimpleModule { private static final String ACCESS_KEY_ID = "accessKeyId"; private static final String SECRET_ACCESS_KEY = "secretAccessKey"; private static final String SESSION_TOKEN = "sessionToken"; + private static final String PROFILE_NAME = "profileName"; public AwsModule() { super("AwsModule"); @@ -160,7 +163,9 @@ public AwsCredentialsProvider deserializeWithType( } else if (hasName(SystemPropertyCredentialsProvider.class, typeName)) { return SystemPropertyCredentialsProvider.create(); } else if (hasName(ProfileCredentialsProvider.class, typeName)) { - return ProfileCredentialsProvider.create(); + return json.has(PROFILE_NAME) + ? ProfileCredentialsProvider.create(getNotNull(json, PROFILE_NAME, typeName)) + : ProfileCredentialsProvider.create(); } else if (hasName(ContainerCredentialsProvider.class, typeName)) { return ContainerCredentialsProvider.builder().build(); } else if (typeName.equals(StsAssumeRoleCredentialsProvider.class.getSimpleName())) { @@ -195,7 +200,6 @@ private static class AWSCredentialsProviderSerializer DefaultCredentialsProvider.class, EnvironmentVariableCredentialsProvider.class, SystemPropertyCredentialsProvider.class, - ProfileCredentialsProvider.class, ContainerCredentialsProvider.class); @Override @@ -228,6 +232,23 @@ public void serializeWithType( jsonGenerator.writeStringField(ACCESS_KEY_ID, credentials.accessKeyId()); jsonGenerator.writeStringField(SECRET_ACCESS_KEY, credentials.secretAccessKey()); } + } else if (providerClass.equals(ProfileCredentialsProvider.class)) { + String profileName = (String) readField(credentialsProvider, PROFILE_NAME); + String envProfileName = ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); + if (profileName != null && !profileName.equals(envProfileName)) { + jsonGenerator.writeStringField(PROFILE_NAME, profileName); + } + try { + Exception exception = (Exception) readField(credentialsProvider, "loadException"); + if (exception != null) { + LoggerFactory.getLogger(AwsModule.class) + .warn("Serialized ProfileCredentialsProvider in faulty state.", exception); + } + } catch (RuntimeException e) { + LoggerFactory.getLogger(AwsModule.class) + .warn("Failed to check ProfileCredentialsProvider for loadException.", e); + } + } else if (providerClass.equals(StsAssumeRoleCredentialsProvider.class)) { Supplier reqSupplier = (Supplier) diff --git a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/options/AwsOptions.java b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/options/AwsOptions.java index d2e02217d15a..ae86c27b78e2 100644 --- a/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/options/AwsOptions.java +++ b/sdks/java/io/amazon-web-services2/src/main/java/org/apache/beam/sdk/io/aws2/options/AwsOptions.java @@ -78,21 +78,20 @@ public Region create(PipelineOptions options) { *

The class name of the provider must be set in the {@code @type} field. Note: Not all * available providers are supported and some configuration options might be ignored. * - *

Most providers rely on system's environment to follow AWS conventions, there's no further - * configuration: + *

Most providers must use the system environment following AWS conventions. Programmatic + * configuration for these providers is NOT supported: *

  • {@link DefaultCredentialsProvider} *
  • {@link EnvironmentVariableCredentialsProvider} *
  • {@link SystemPropertyCredentialsProvider} - *
  • {@link ProfileCredentialsProvider} *
  • {@link ContainerCredentialsProvider} * *

    Example: * - *

    {@code --awsCredentialsProvider={"@type": "ProfileCredentialsProvider"}}
    + *
    {@code --awsCredentialsProvider={"@type": "EnvironmentVariableCredentialsProvider"}}
    +   *     
    * - *

    Some other providers require additional configuration: + *

    Some other providers support additional configuration: *

  • {@link StaticCredentialsProvider} - *
  • {@link StsAssumeRoleCredentialsProvider} * *

    Examples: * @@ -107,9 +106,27 @@ public Region create(PipelineOptions options) { * "awsAccessKeyId": "key_id_value", * "awsSecretKey": "secret_value", * "sessionToken": "token_value" + * }} + * + *

  • {@link ProfileCredentialsProvider} + * + *

    {@code profileName} is optional, if not set the environment default is used. Be careful + * if using this provider programmatically, it can behave unexpectedly. + * + *

    Examples: + * + *

    {@code --awsCredentialsProvider={
    +   *   "@type": "ProfileCredentialsProvider"
        * }
        *
        * --awsCredentialsProvider={
    +   *   "@type": "ProfileCredentialsProvider",
    +   *   "profileName": "my_profile"
    +   * }}
    + * + *
  • {@link StsAssumeRoleCredentialsProvider} + * + *
    {@code --awsCredentialsProvider={
        *   "@type": "StsAssumeRoleCredentialsProvider",
        *   "roleArn": "role_arn_Value",
        *   "roleSessionName": "session_name_value",
    diff --git a/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/options/AwsModuleTest.java b/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/options/AwsModuleTest.java
    index e5962812e64b..17e6f528f969 100644
    --- a/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/options/AwsModuleTest.java
    +++ b/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/options/AwsModuleTest.java
    @@ -18,6 +18,7 @@
     package org.apache.beam.sdk.io.aws2.options;
     
     import static org.apache.beam.repackaged.core.org.apache.commons.lang3.reflect.FieldUtils.readField;
    +import static org.apache.beam.sdk.io.aws2.options.SerializationTestUtil.serialize;
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.hamcrest.Matchers.hasItem;
     import static org.hamcrest.Matchers.instanceOf;
    @@ -25,21 +26,32 @@
     import static software.amazon.awssdk.core.SdkSystemSetting.AWS_ACCESS_KEY_ID;
     import static software.amazon.awssdk.core.SdkSystemSetting.AWS_REGION;
     import static software.amazon.awssdk.core.SdkSystemSetting.AWS_SECRET_ACCESS_KEY;
    +import static software.amazon.awssdk.profiles.ProfileFileSystemSetting.AWS_CONFIG_FILE;
    +import static software.amazon.awssdk.profiles.ProfileFileSystemSetting.AWS_PROFILE;
     
     import com.amazonaws.regions.Regions;
     import com.fasterxml.jackson.databind.Module;
     import com.fasterxml.jackson.databind.ObjectMapper;
    +import java.io.IOException;
     import java.net.URI;
    +import java.nio.file.Files;
    +import java.nio.file.Path;
    +import java.util.Arrays;
     import java.util.List;
     import java.util.Properties;
     import java.util.function.Supplier;
    +import org.apache.beam.sdk.testing.ExpectedLogs;
     import org.apache.beam.sdk.util.ThrowingSupplier;
     import org.apache.beam.sdk.util.common.ReflectHelpers;
     import org.hamcrest.MatcherAssert;
    +import org.junit.ClassRule;
    +import org.junit.Rule;
     import org.junit.Test;
    +import org.junit.rules.ExternalResource;
     import org.junit.runner.RunWith;
     import org.junit.runners.JUnit4;
     import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
    +import software.amazon.awssdk.auth.credentials.AwsCredentials;
     import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
     import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
     import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider;
    @@ -57,6 +69,24 @@
     @RunWith(JUnit4.class)
     public class AwsModuleTest {
     
    +  @ClassRule
    +  public static final ProfileFile PROFILE =
    +      new ProfileFile(
    +          "[default]",
    +          "aws_access_key_id=defaultkey",
    +          "aws_secret_access_key=123",
    +          "[profile other]",
    +          "aws_access_key_id=otherkey",
    +          "aws_secret_access_key=abc");
    +
    +  private static final AwsCredentials DEFAULT_CREDENTIALS =
    +      AwsBasicCredentials.create("defaultkey", "123");
    +
    +  private static final AwsCredentials OTHER_CREDENTIALS =
    +      AwsBasicCredentials.create("otherkey", "abc");
    +
    +  @Rule public final ExpectedLogs logs = ExpectedLogs.none(AwsModule.class);
    +
       @Test
       public void testObjectMapperIsAbleToFindModule() {
         List modules = ObjectMapper.findModules(ReflectHelpers.findClassLoader());
    @@ -68,7 +98,7 @@ private  T serializeAndDeserialize(T obj) {
       }
     
       @Test
    -  public void testStaticCredentialsProviderSerializationDeserialization() {
    +  public void testStaticCredentialsProviderSerDe() {
         AwsCredentialsProvider provider =
             StaticCredentialsProvider.create(AwsBasicCredentials.create("key", "secret"));
     
    @@ -84,7 +114,7 @@ public void testStaticCredentialsProviderSerializationDeserialization() {
       }
     
       @Test
    -  public void testAwsCredentialsProviderSerializationDeserialization() {
    +  public void testAwsCredentialsProviderSerDe() {
         AwsCredentialsProvider provider = DefaultCredentialsProvider.create();
         AwsCredentialsProvider deserializedProvider = serializeAndDeserialize(provider);
         assertEquals(provider.getClass(), deserializedProvider.getClass());
    @@ -97,17 +127,90 @@ public void testAwsCredentialsProviderSerializationDeserialization() {
         deserializedProvider = serializeAndDeserialize(provider);
         assertEquals(provider.getClass(), deserializedProvider.getClass());
     
    -    provider = ProfileCredentialsProvider.create();
    -    deserializedProvider = serializeAndDeserialize(provider);
    -    assertEquals(provider.getClass(), deserializedProvider.getClass());
    -
         provider = ContainerCredentialsProvider.builder().build();
         deserializedProvider = serializeAndDeserialize(provider);
         assertEquals(provider.getClass(), deserializedProvider.getClass());
       }
     
       @Test
    -  public void testStsAssumeRoleCredentialsProviderSerializationDeserialization() throws Exception {
    +  public void testProfileCredentialsProviderSerDeWithDefaultProfile() throws Exception {
    +    withSystemProperties(
    +        PROFILE.properties("default"),
    +        () -> {
    +          AwsCredentialsProvider provider = ProfileCredentialsProvider.create();
    +          String serializedProvider = serialize(provider);
    +
    +          assertThat(serializedProvider).isEqualTo("{\"@type\":\"ProfileCredentialsProvider\"}");
    +
    +          AwsCredentialsProvider actual = deserialize(serializedProvider);
    +          assertThat(actual.resolveCredentials())
    +              .isEqualToComparingFieldByField(DEFAULT_CREDENTIALS);
    +          return assertThat(actual)
    +              .isExactlyInstanceOf(ProfileCredentialsProvider.class)
    +              .isEqualToComparingFieldByFieldRecursively(provider);
    +        });
    +  }
    +
    +  @Test
    +  public void testProfileCredentialsProviderSerDeWithCustomProfile() throws Exception {
    +    withSystemProperties(
    +        PROFILE.properties("default"),
    +        () -> {
    +          AwsCredentialsProvider provider = ProfileCredentialsProvider.create("other");
    +          String serializedProvider = serialize(provider);
    +
    +          assertThat(serializedProvider)
    +              .isEqualTo("{\"@type\":\"ProfileCredentialsProvider\",\"profileName\":\"other\"}");
    +
    +          AwsCredentialsProvider actual = deserialize(serializedProvider);
    +          assertThat(actual.resolveCredentials()).isEqualToComparingFieldByField(OTHER_CREDENTIALS);
    +          return assertThat(actual)
    +              .isExactlyInstanceOf(ProfileCredentialsProvider.class)
    +              .isEqualToComparingFieldByFieldRecursively(provider);
    +        });
    +  }
    +
    +  @Test
    +  public void testProfileCredentialsProviderSerDeWithCustomDefaultProfile() throws Exception {
    +    withSystemProperties(
    +        PROFILE.properties("other"),
    +        () -> {
    +          AwsCredentialsProvider provider = ProfileCredentialsProvider.create("other");
    +          String serializedProvider = serialize(provider);
    +
    +          assertThat(serializedProvider).isEqualTo("{\"@type\":\"ProfileCredentialsProvider\"}");
    +
    +          AwsCredentialsProvider actual = deserialize(serializedProvider);
    +          assertThat(actual.resolveCredentials())
    +              .isEqualToComparingFieldByFieldRecursively(OTHER_CREDENTIALS);
    +          return assertThat(actual)
    +              .isExactlyInstanceOf(ProfileCredentialsProvider.class)
    +              .isEqualToComparingFieldByFieldRecursively(provider);
    +        });
    +  }
    +
    +  @Test
    +  public void testProfileCredentialsProviderSerDeWithUnknownProfile() throws Exception {
    +    withSystemProperties(
    +        PROFILE.properties("default"),
    +        () -> {
    +          AwsCredentialsProvider provider = ProfileCredentialsProvider.create("unknown");
    +          String serializedProvider = serialize(provider);
    +
    +          // ProfileCredentialsProvider SILENTLY drops unknown profiles
    +          assertThat(serializedProvider).isEqualTo("{\"@type\":\"ProfileCredentialsProvider\"}");
    +
    +          AwsCredentialsProvider actual = deserialize(serializedProvider);
    +          // NOTE: This documents the unexpected behavior in case a faulty provider is serialized
    +          return assertThat(actual.resolveCredentials())
    +              .isEqualToComparingFieldByField(DEFAULT_CREDENTIALS);
    +        });
    +
    +    logs.verifyWarn("Serialized ProfileCredentialsProvider in faulty state.");
    +  }
    +
    +  @Test
    +  public void testStsAssumeRoleCredentialsProviderSerDe() throws Exception {
         AssumeRoleRequest req = AssumeRoleRequest.builder().roleArn("roleArn").policy("policy").build();
         Supplier provider =
             () ->
    @@ -123,7 +226,7 @@ public void testStsAssumeRoleCredentialsProviderSerializationDeserialization() t
     
         // Region and credentials for STS client are resolved using default providers
         AwsCredentialsProvider deserializedProvider =
    -        withSystemPropertyOverrides(overrides, () -> serializeAndDeserialize(provider.get()));
    +        withSystemProperties(overrides, () -> serializeAndDeserialize(provider.get()));
     
         Supplier requestSupplier =
             (Supplier)
    @@ -132,7 +235,7 @@ public void testStsAssumeRoleCredentialsProviderSerializationDeserialization() t
       }
     
       @Test
    -  public void testProxyConfigurationSerializationDeserialization() {
    +  public void testProxyConfigurationSerDe() {
         ProxyConfiguration proxyConfiguration =
             ProxyConfiguration.builder()
                 .endpoint(URI.create("http://localhost:8080"))
    @@ -147,7 +250,7 @@ public void testProxyConfigurationSerializationDeserialization() {
         assertEquals("password", deserializedProxyConfiguration.password());
       }
     
    -  private  T withSystemPropertyOverrides(Properties overrides, ThrowingSupplier fun)
    +  private  T withSystemProperties(Properties overrides, ThrowingSupplier fun)
           throws Exception {
         Properties systemProps = System.getProperties();
     
    @@ -164,4 +267,39 @@ private  T withSystemPropertyOverrides(Properties overrides, ThrowingSupplier
           previousProps.forEach(systemProps::put);
         }
       }
    +
    +  private static AwsCredentialsProvider deserialize(String provider) {
    +    return SerializationTestUtil.deserialize(provider, AwsCredentialsProvider.class);
    +  }
    +
    +  static class ProfileFile extends ExternalResource {
    +    private String[] lines;
    +    private Path path;
    +
    +    public ProfileFile(String... lines) {
    +      this.lines = lines;
    +    }
    +
    +    public Properties properties(String defaultProfile) {
    +      Properties props = new Properties();
    +      props.setProperty(AWS_CONFIG_FILE.property(), path.toString());
    +      props.setProperty(AWS_PROFILE.property(), defaultProfile);
    +      return props;
    +    }
    +
    +    @Override
    +    protected void before() throws Throwable {
    +      path = Files.createTempFile("profile", ".conf");
    +      Files.write(path, Arrays.asList(lines));
    +    }
    +
    +    @Override
    +    protected void after() {
    +      try {
    +        Files.delete(path);
    +      } catch (IOException e) {
    +        // ignore
    +      }
    +    }
    +  }
     }
    diff --git a/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/options/SerializationTestUtil.java b/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/options/SerializationTestUtil.java
    index 0f5daf0bc92c..6cf79c958090 100644
    --- a/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/options/SerializationTestUtil.java
    +++ b/sdks/java/io/amazon-web-services2/src/test/java/org/apache/beam/sdk/io/aws2/options/SerializationTestUtil.java
    @@ -28,11 +28,22 @@ public class SerializationTestUtil {
               .registerModules(ObjectMapper.findModules(ReflectHelpers.findClassLoader()));
     
       public static  T serializeDeserialize(Class clazz, T obj) {
    +    return deserialize(serialize(obj), clazz);
    +  }
    +
    +  public static  String serialize(T obj) {
    +    try {
    +      return MAPPER.writeValueAsString(obj);
    +    } catch (JsonProcessingException e) {
    +      throw new RuntimeException("Failed to serialize " + obj.getClass().getSimpleName(), e);
    +    }
    +  }
    +
    +  public static  T deserialize(String jsonString, Class clazz) {
         try {
    -      String jsonString = MAPPER.writeValueAsString(obj);
           return MAPPER.readValue(jsonString, clazz);
         } catch (JsonProcessingException e) {
    -      throw new RuntimeException("Failed to serialize/deserialize " + clazz.getSimpleName(), e);
    +      throw new RuntimeException("Failed to deserialize " + clazz.getSimpleName(), e);
         }
       }
     }
    diff --git a/sdks/java/io/bigquery-io-perf-tests/src/test/java/org/apache/beam/sdk/bigqueryioperftests/BigQueryIOIT.java b/sdks/java/io/bigquery-io-perf-tests/src/test/java/org/apache/beam/sdk/bigqueryioperftests/BigQueryIOIT.java
    index c67dc936705e..0dfc7addc6f7 100644
    --- a/sdks/java/io/bigquery-io-perf-tests/src/test/java/org/apache/beam/sdk/bigqueryioperftests/BigQueryIOIT.java
    +++ b/sdks/java/io/bigquery-io-perf-tests/src/test/java/org/apache/beam/sdk/bigqueryioperftests/BigQueryIOIT.java
    @@ -17,6 +17,7 @@
      */
     package org.apache.beam.sdk.bigqueryioperftests;
     
    +import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.assertNotEquals;
     
     import com.google.api.services.bigquery.model.TableFieldSchema;
    @@ -28,6 +29,7 @@
     import com.google.cloud.bigquery.TableId;
     import java.io.IOException;
     import java.nio.ByteBuffer;
    +import java.util.Base64;
     import java.util.Collections;
     import java.util.List;
     import java.util.UUID;
    @@ -43,7 +45,10 @@
     import org.apache.beam.sdk.io.synthetic.SyntheticBoundedSource;
     import org.apache.beam.sdk.io.synthetic.SyntheticOptions;
     import org.apache.beam.sdk.io.synthetic.SyntheticSourceOptions;
    +import org.apache.beam.sdk.metrics.Counter;
    +import org.apache.beam.sdk.metrics.Metrics;
     import org.apache.beam.sdk.options.Description;
    +import org.apache.beam.sdk.options.StreamingOptions;
     import org.apache.beam.sdk.options.Validation;
     import org.apache.beam.sdk.options.ValueProvider;
     import org.apache.beam.sdk.testutils.NamedTestResult;
    @@ -54,6 +59,7 @@
     import org.apache.beam.sdk.transforms.DoFn;
     import org.apache.beam.sdk.transforms.ParDo;
     import org.apache.beam.sdk.values.KV;
    +import org.joda.time.Duration;
     import org.junit.AfterClass;
     import org.junit.BeforeClass;
     import org.junit.Test;
    @@ -87,6 +93,7 @@ public class BigQueryIOIT {
       private static final String READ_TIME_METRIC_NAME = "read_time";
       private static final String WRITE_TIME_METRIC_NAME = "write_time";
       private static final String AVRO_WRITE_TIME_METRIC_NAME = "avro_write_time";
    +  private static final String READ_ELEMENT_METRIC_NAME = "read_count";
       private static String testBigQueryDataset;
       private static String testBigQueryTable;
       private static SyntheticSourceOptions sourceOptions;
    @@ -141,10 +148,11 @@ public void testWriteThenRead() {
       private void testJsonWrite() {
         BigQueryIO.Write writeIO =
             BigQueryIO.write()
    +            .withSuccessfulInsertsPropagation(false)
                 .withFormatFunction(
                     input -> {
                       TableRow tableRow = new TableRow();
    -                  tableRow.set("data", input);
    +                  tableRow.set("data", Base64.getEncoder().encodeToString(input));
                       return tableRow;
                     });
         testWrite(writeIO, WRITE_TIME_METRIC_NAME);
    @@ -165,9 +173,13 @@ private void testAvroWrite() {
       }
     
       private void testWrite(BigQueryIO.Write writeIO, String metricName) {
    -    Pipeline pipeline = Pipeline.create(options);
    -
         BigQueryIO.Write.Method method = BigQueryIO.Write.Method.valueOf(options.getWriteMethod());
    +    if (method == BigQueryIO.Write.Method.STREAMING_INSERTS) {
    +      // set streaming for STREAMING_INSERTS write
    +      options.as(StreamingOptions.class).setStreaming(true);
    +    }
    +
    +    Pipeline pipeline = Pipeline.create(options);
         pipeline
             .apply("Read from source", Read.from(new SyntheticBoundedSource(sourceOptions)))
             .apply("Gather time", ParDo.of(new TimeMonitor<>(NAMESPACE, metricName)))
    @@ -185,19 +197,30 @@ private void testWrite(BigQueryIO.Write writeIO, String metricName) {
                                     new TableFieldSchema().setName("data").setType("BYTES")))));
     
         PipelineResult pipelineResult = pipeline.run();
    -    PipelineResult.State pipelineState = pipelineResult.waitUntilFinish();
    +    PipelineResult.State pipelineState =
    +        options.getPipelineTimeout() == null
    +            ? pipelineResult.waitUntilFinish()
    +            : pipelineResult.waitUntilFinish(
    +                Duration.standardSeconds(options.getPipelineTimeout()));
         extractAndPublishTime(pipelineResult, metricName);
         // Fail the test if pipeline failed.
         assertNotEquals(pipelineState, PipelineResult.State.FAILED);
    +
    +    // set back streaming
    +    options.as(StreamingOptions.class).setStreaming(false);
       }
     
       private void testRead() {
         Pipeline pipeline = Pipeline.create(options);
         pipeline
             .apply("Read from BQ", BigQueryIO.readTableRows().from(tableQualifier))
    -        .apply("Gather time", ParDo.of(new TimeMonitor<>(NAMESPACE, READ_TIME_METRIC_NAME)));
    +        .apply("Gather time", ParDo.of(new TimeMonitor<>(NAMESPACE, READ_TIME_METRIC_NAME)))
    +        .apply("Counting element", ParDo.of(new CountingFn<>(NAMESPACE, READ_ELEMENT_METRIC_NAME)));
         PipelineResult result = pipeline.run();
         PipelineResult.State pipelineState = result.waitUntilFinish();
    +
    +    assertEquals(
    +        sourceOptions.numRecords, readElementMetric(result, NAMESPACE, READ_ELEMENT_METRIC_NAME));
         extractAndPublishTime(result, READ_TIME_METRIC_NAME);
         // Fail the test if pipeline failed.
         assertNotEquals(pipelineState, PipelineResult.State.FAILED);
    @@ -219,6 +242,11 @@ private static Function getMetricSupplier(String
         };
       }
     
    +  private long readElementMetric(PipelineResult result, String namespace, String name) {
    +    MetricsReader metricsReader = new MetricsReader(result, namespace);
    +    return metricsReader.getCounterMetric(name);
    +  }
    +
       /** Options for this io performance test. */
       public interface BigQueryPerfTestOptions extends IOTestPipelineOptions {
         @Description("Synthetic source options")
    @@ -256,6 +284,11 @@ public interface BigQueryPerfTestOptions extends IOTestPipelineOptions {
     
         @Description("Write Avro or JSON to BQ")
         void setWriteFormat(String value);
    +
    +    Integer getPipelineTimeout();
    +
    +    @Description("Time to wait for the events to be processed by the pipeline (in seconds)")
    +    void setPipelineTimeout(Integer writeTimeout);
       }
     
       private static class MapKVToV extends DoFn, byte[]> {
    @@ -265,6 +298,20 @@ public void process(ProcessContext context) {
         }
       }
     
    +  private static class CountingFn extends DoFn {
    +
    +    private final Counter elementCounter;
    +
    +    CountingFn(String namespace, String name) {
    +      elementCounter = Metrics.counter(namespace, name);
    +    }
    +
    +    @ProcessElement
    +    public void processElement() {
    +      elementCounter.inc(1L);
    +    }
    +  }
    +
       private enum WriteFormat {
         AVRO,
         JSON
    diff --git a/sdks/java/io/cdap/build.gradle b/sdks/java/io/cdap/build.gradle
    index 1bcc0ece146b..3cfc01f79f7a 100644
    --- a/sdks/java/io/cdap/build.gradle
    +++ b/sdks/java/io/cdap/build.gradle
    @@ -52,14 +52,17 @@ dependencies {
         implementation library.java.cdap_plugin_zendesk
         implementation library.java.commons_lang3
         implementation library.java.guava
    +    implementation library.java.google_code_gson
         implementation library.java.hadoop_common
         implementation library.java.hadoop_mapreduce_client_core
         implementation library.java.jackson_core
         implementation library.java.jackson_databind
         implementation library.java.slf4j_api
    +    implementation library.java.spark_streaming
         implementation library.java.tephra
         implementation library.java.vendored_guava_26_0_jre
         implementation project(path: ":sdks:java:core", configuration: "shadow")
    +    implementation project(":sdks:java:io:sparkreceiver")
         implementation project(":sdks:java:io:hadoop-format")
         testImplementation library.java.cdap_plugin_service_now
         testImplementation library.java.cdap_etl_api
    diff --git a/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/CdapIO.java b/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/CdapIO.java
    index f2655507cf56..5590bb061654 100644
    --- a/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/CdapIO.java
    +++ b/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/CdapIO.java
    @@ -17,27 +17,163 @@
      */
     package org.apache.beam.sdk.io.cdap;
     
    -import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull;
    +import static org.apache.beam.sdk.io.cdap.MappingUtils.getOffsetFnForPluginClass;
    +import static org.apache.beam.sdk.io.cdap.MappingUtils.getPluginByClass;
    +import static org.apache.beam.sdk.io.cdap.MappingUtils.getReceiverBuilderByPluginClass;
    +import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull;
     import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument;
     
     import com.google.auto.value.AutoValue;
     import io.cdap.cdap.api.plugin.PluginConfig;
    +import java.util.Map;
     import org.apache.beam.sdk.annotations.Experimental;
     import org.apache.beam.sdk.annotations.Experimental.Kind;
    +import org.apache.beam.sdk.coders.CannotProvideCoderException;
    +import org.apache.beam.sdk.coders.Coder;
     import org.apache.beam.sdk.io.hadoop.format.HDFSSynchronization;
     import org.apache.beam.sdk.io.hadoop.format.HadoopFormatIO;
    +import org.apache.beam.sdk.io.sparkreceiver.SparkReceiverIO;
    +import org.apache.beam.sdk.transforms.MapElements;
     import org.apache.beam.sdk.transforms.PTransform;
    +import org.apache.beam.sdk.transforms.SerializableFunction;
     import org.apache.beam.sdk.values.KV;
     import org.apache.beam.sdk.values.PBegin;
     import org.apache.beam.sdk.values.PCollection;
     import org.apache.beam.sdk.values.PDone;
    +import org.apache.beam.sdk.values.TypeDescriptor;
     import org.apache.commons.lang3.NotImplementedException;
     import org.apache.hadoop.conf.Configuration;
    +import org.apache.hadoop.mapreduce.InputFormat;
    +import org.apache.hadoop.mapreduce.OutputFormat;
     import org.checkerframework.checker.nullness.qual.Nullable;
     
     /**
    - * An unbounded/bounded sources and sinks from CDAP plugins.
    + * A {@link CdapIO} is a Transform for reading data from source or writing data to sink of a Cdap
    + * Plugin. It uses {@link HadoopFormatIO} for Batch and SparkReceiverIO for Streaming.
    + *
    + * 

    Read from Cdap Plugin Bounded Source

    + * + *

    To configure {@link CdapIO} source, you must specify Cdap {@link Plugin}, Cdap {@link + * PluginConfig}, key and value classes. + * + *

    {@link Plugin} is the Wrapper class for the Cdap Plugin. It contains main information about + * the Plugin. The object of the {@link Plugin} class can be created with the {@link + * Plugin#createBatch(Class, Class, Class)} method. Method requires the following parameters: + * + *

      + *
    • {@link io.cdap.cdap.etl.api.batch.BatchSource} class + *
    • {@link InputFormat} class + *
    • {@link io.cdap.cdap.api.data.batch.InputFormatProvider} class + *
    + * + *

    For more information about the InputFormat and InputFormatProvider, see {@link + * HadoopFormatIO}. + * + *

    Every Cdap Plugin has its {@link PluginConfig} class with necessary fields to configure the + * Plugin. You can set the {@link Map} of your parameters with the {@link + * ConfigWrapper#withParams(Map)} method where the key is the field name. + * + *

    For example, to create a basic {@link CdapIO#read()} transform: + * + *

    {@code
    + * Pipeline p = ...; // Create pipeline.
    + *
    + * // Create PluginConfig for specific plugin
    + * EmployeeConfig pluginConfig =
    + *         new ConfigWrapper<>(EmployeeConfig.class).withParams(TEST_EMPLOYEE_PARAMS_MAP).build();
    + *
    + * // Read using CDAP batch plugin
    + * p.apply("ReadBatch",
    + * CdapIO.read()
    + *             .withCdapPlugin(
    + *                 Plugin.createBatch(
    + *                     EmployeeBatchSource.class,
    + *                     EmployeeInputFormat.class,
    + *                     EmployeeInputFormatProvider.class))
    + *             .withPluginConfig(pluginConfig)
    + *             .withKeyClass(String.class)
    + *             .withValueClass(String.class));
    + * }
    + * + *

    Write to Cdap Plugin Bounded Sink

    + * + *

    To configure {@link CdapIO} sink, just as {@link CdapIO#read()} Cdap {@link Plugin}, Cdap + * {@link PluginConfig}, key, value classes must be specified. In addition, it's necessary to + * determine locks directory path {@link CdapIO.Write#withLocksDirPath(String)}. It's used for + * {@link HDFSSynchronization} configuration for {@link HadoopFormatIO}. More info can be found in + * {@link HadoopFormatIO} documentation. + * + *

    To create the object of the {@link Plugin} class with the {@link Plugin#createBatch(Class, + * Class, Class)} method, need to specify the following parameters: + * + *

      + *
    • {@link io.cdap.cdap.etl.api.batch.BatchSink} class + *
    • {@link OutputFormat} class + *
    • {@link io.cdap.cdap.api.data.batch.OutputFormatProvider} class + *
    + * + *

    For more information about the OutputFormat and OutputFormatProvider, see {@link + * HadoopFormatIO}. + * + *

    Example of {@link CdapIO#write()} usage: + * + *

    {@code
    + * Pipeline p = ...; // Create pipeline.
    + *
    + * // Get or create data to write
    + * PCollection> input = p.apply(Create.of(data));
    + *
    + * // Create PluginConfig for specific plugin
    + * EmployeeConfig pluginConfig =
    + *         new ConfigWrapper<>(EmployeeConfig.class).withParams(TEST_EMPLOYEE_PARAMS_MAP).build();
    + *
    + * // Write using CDAP batch plugin
    + * input.apply(
    + *         "WriteBatch",
    + *         CdapIO.write()
    + *             .withCdapPlugin(
    + *                 Plugin.createBatch(
    + *                     EmployeeBatchSink.class,
    + *                     EmployeeOutputFormat.class,
    + *                     EmployeeOutputFormatProvider.class))
    + *             .withPluginConfig(pluginConfig)
    + *             .withKeyClass(String.class)
    + *             .withValueClass(String.class)
    + *             .withLocksDirPath(tmpFolder.getRoot().getAbsolutePath()));
    + *     p.run();
    + * }
    + * + *

    Read from Cdap Plugin Streaming Source

    + * + *

    To configure {@link CdapIO} source, you must specify Cdap {@link Plugin}, Cdap {@link + * PluginConfig}, key and value classes. + * + *

    {@link Plugin} is the Wrapper class for the Cdap Plugin. It contains main information about + * the Plugin. The object of the {@link Plugin} class can be created with the {@link + * Plugin#createStreaming(Class)} method. Method requires {@link + * io.cdap.cdap.etl.api.streaming.StreamingSource} class parameter. + * + *

    Every Cdap Plugin has its {@link PluginConfig} class with necessary fields to configure the + * Plugin. You can set the {@link Map} of your parameters with the {@link + * ConfigWrapper#withParams(Map)} method where the key is the field name. + * + *

    For example, to create a basic {@link CdapIO#read()} transform: + * + *

    {@code
    + * Pipeline p = ...; // Create pipeline.
    + *
    + * // Create PluginConfig for specific plugin
    + * EmployeeConfig pluginConfig =
    + *         new ConfigWrapper<>(EmployeeConfig.class).withParams(TEST_EMPLOYEE_PARAMS_MAP).build();
    + *
    + * // Read using CDAP streaming plugin
    + * p.apply("ReadStreaming",
    + * CdapIO.read()
    + *             .withCdapPlugin(Plugin.createStreaming(EmployeeStreamingSource.class))
    + *             .withPluginConfig(pluginConfig)
    + *             .withKeyClass(String.class)
    + *             .withValueClass(String.class));
    + * }
    */ @Experimental(Kind.SOURCE_SINK) public class CdapIO { @@ -54,12 +190,25 @@ public static Write write() { @AutoValue @AutoValue.CopyAnnotations public abstract static class Read extends PTransform>> { + abstract @Nullable PluginConfig getPluginConfig(); abstract @Nullable Plugin getCdapPlugin(); + /** + * Depending on selected {@link HadoopFormatIO} type ({@link InputFormat} or {@link + * OutputFormat}), appropriate key class ("key.class") in Hadoop {@link Configuration} must be + * provided. If you set different Format key class than Format's actual key class then, it may + * result in an error. More info can be found in {@link HadoopFormatIO} documentation. + */ abstract @Nullable Class getKeyClass(); + /** + * Depending on selected {@link HadoopFormatIO} type ({@link InputFormat} or {@link + * OutputFormat}), appropriate value class ("value.class") in Hadoop {@link Configuration} must + * be provided. If you set different Format value class than Format's actual value class then, + * it may result in an error. More info can be found in {@link HadoopFormatIO} documentation. + */ abstract @Nullable Class getValueClass(); abstract Builder toBuilder(); @@ -79,27 +228,32 @@ abstract static class Builder { abstract Read build(); } + /** Sets a CDAP {@link Plugin}. */ public Read withCdapPlugin(Plugin plugin) { checkArgument(plugin != null, "Cdap plugin can not be null"); return toBuilder().setCdapPlugin(plugin).build(); } + /** Sets a CDAP Plugin class. */ public Read withCdapPluginClass(Class cdapPluginClass) { checkArgument(cdapPluginClass != null, "Cdap plugin class can not be null"); Plugin plugin = MappingUtils.getPluginByClass(cdapPluginClass); return toBuilder().setCdapPlugin(plugin).build(); } + /** Sets a {@link PluginConfig}. */ public Read withPluginConfig(PluginConfig pluginConfig) { checkArgument(pluginConfig != null, "Plugin config can not be null"); return toBuilder().setPluginConfig(pluginConfig).build(); } + /** Sets a key class. */ public Read withKeyClass(Class keyClass) { checkArgument(keyClass != null, "Key class can not be null"); return toBuilder().setKeyClass(keyClass).build(); } + /** Sets a value class. */ public Read withValueClass(Class valueClass) { checkArgument(valueClass != null, "Value class can not be null"); return toBuilder().setValueClass(valueClass).build(); @@ -107,19 +261,38 @@ public Read withValueClass(Class valueClass) { @Override public PCollection> expand(PBegin input) { - Plugin plugin = checkArgumentNotNull(getCdapPlugin(), "withCdapPluginClass() is required"); - PluginConfig pluginConfig = - checkArgumentNotNull(getPluginConfig(), "withPluginConfig() is required"); - Class keyClass = checkArgumentNotNull(getKeyClass(), "withKeyClass() is required"); - Class valueClass = checkArgumentNotNull(getValueClass(), "withValueClass() is required"); - - plugin.withConfig(pluginConfig).withHadoopConfiguration(keyClass, valueClass).prepareRun(); - - if (plugin.isUnbounded()) { - // TODO: implement SparkReceiverIO.<~>read() - throw new NotImplementedException("Support for unbounded plugins is not implemented!"); + Plugin cdapPlugin = getCdapPlugin(); + checkStateNotNull(cdapPlugin, "withCdapPluginClass() is required"); + + PluginConfig pluginConfig = getPluginConfig(); + checkStateNotNull(pluginConfig, "withPluginConfig() is required"); + + Class valueClass = getValueClass(); + checkStateNotNull(valueClass, "withValueClass() is required"); + + Class keyClass = getKeyClass(); + checkStateNotNull(keyClass, "withKeyClass() is required"); + + cdapPlugin.withConfig(pluginConfig); + + if (cdapPlugin.isUnbounded()) { + SparkReceiverIO.Read reader = + SparkReceiverIO.read() + .withGetOffsetFn(getOffsetFnForPluginClass(cdapPlugin.getPluginClass(), valueClass)) + .withSparkReceiverBuilder( + getReceiverBuilderByPluginClass( + cdapPlugin.getPluginClass(), pluginConfig, valueClass)); + try { + Coder coder = input.getPipeline().getCoderRegistry().getCoder(valueClass); + PCollection values = input.apply(reader).setCoder(coder); + SerializableFunction> fn = input1 -> KV.of(null, input1); + return values.apply(MapElements.into(new TypeDescriptor>() {}).via(fn)); + } catch (CannotProvideCoderException e) { + throw new IllegalStateException("Could not get value Coder", e); + } } else { - Configuration hConf = plugin.getHadoopConfiguration(); + cdapPlugin.withHadoopConfiguration(keyClass, valueClass).prepareRun(); + Configuration hConf = cdapPlugin.getHadoopConfiguration(); HadoopFormatIO.Read readFromHadoop = HadoopFormatIO.read().withConfiguration(hConf); return input.apply(readFromHadoop); @@ -127,7 +300,7 @@ public PCollection> expand(PBegin input) { } } - /** A {@link PTransform} to read from CDAP source. */ + /** A {@link PTransform} to write to CDAP sink. */ @AutoValue @AutoValue.CopyAnnotations public abstract static class Write extends PTransform>, PDone> { @@ -136,10 +309,28 @@ public abstract static class Write extends PTransform abstract @Nullable Plugin getCdapPlugin(); + /** + * Depending on selected {@link HadoopFormatIO} type ({@link InputFormat} or {@link + * OutputFormat}), appropriate key class ("key.class") in Hadoop {@link Configuration} must be + * provided. If you set different Format key class than Format's actual key class then, it may + * result in an error. More info can be found in {@link HadoopFormatIO} documentation. + */ abstract @Nullable Class getKeyClass(); + /** + * Depending on selected {@link HadoopFormatIO} type ({@link InputFormat} or {@link + * OutputFormat}), appropriate value class ("value.class") in Hadoop {@link Configuration} must + * be provided. If you set different Format value class than Format's actual value class then, + * it may result in an error. More info can be found in {@link HadoopFormatIO} documentation. + */ abstract @Nullable Class getValueClass(); + /** + * Directory where locks will be stored. This directory MUST be different that directory which + * is possibly stored under FileOutputFormat.outputDir key. Used for {@link HDFSSynchronization} + * configuration for {@link HadoopFormatIO}. More info can be found in {@link HadoopFormatIO} + * documentation. + */ abstract @Nullable String getLocksDirPath(); abstract Builder toBuilder(); @@ -161,32 +352,38 @@ abstract static class Builder { abstract Write build(); } + /** Sets a CDAP {@link Plugin}. */ public Write withCdapPlugin(Plugin plugin) { checkArgument(plugin != null, "Cdap plugin can not be null"); return toBuilder().setCdapPlugin(plugin).build(); } + /** Sets a CDAP Plugin class. */ public Write withCdapPluginClass(Class cdapPluginClass) { checkArgument(cdapPluginClass != null, "Cdap plugin class can not be null"); - Plugin plugin = MappingUtils.getPluginByClass(cdapPluginClass); + Plugin plugin = getPluginByClass(cdapPluginClass); return toBuilder().setCdapPlugin(plugin).build(); } + /** Sets a {@link PluginConfig}. */ public Write withPluginConfig(PluginConfig pluginConfig) { checkArgument(pluginConfig != null, "Plugin config can not be null"); return toBuilder().setPluginConfig(pluginConfig).build(); } + /** Sets a key class. */ public Write withKeyClass(Class keyClass) { checkArgument(keyClass != null, "Key class can not be null"); return toBuilder().setKeyClass(keyClass).build(); } + /** Sets path to directory where locks will be stored. */ public Write withLocksDirPath(String locksDirPath) { checkArgument(locksDirPath != null, "Locks dir path can not be null"); return toBuilder().setLocksDirPath(locksDirPath).build(); } + /** Sets a value class. */ public Write withValueClass(Class valueClass) { checkArgument(valueClass != null, "Value class can not be null"); return toBuilder().setValueClass(valueClass).build(); @@ -194,21 +391,30 @@ public Write withValueClass(Class valueClass) { @Override public PDone expand(PCollection> input) { - Plugin plugin = checkArgumentNotNull(getCdapPlugin(), "withKeyClass() is required"); - PluginConfig pluginConfig = - checkArgumentNotNull(getPluginConfig(), "withKeyClass() is required"); - Class keyClass = checkArgumentNotNull(getKeyClass(), "withKeyClass() is required"); - Class valueClass = checkArgumentNotNull(getValueClass(), "withValueClass() is required"); - String locksDirPath = - checkArgumentNotNull(getLocksDirPath(), "withLocksDirPath() is required"); + Plugin cdapPlugin = getCdapPlugin(); + checkStateNotNull(cdapPlugin, "withCdapPluginClass() is required"); + + PluginConfig pluginConfig = getPluginConfig(); + checkStateNotNull(pluginConfig, "withPluginConfig() is required"); + + Class keyClass = getKeyClass(); + checkStateNotNull(keyClass, "withKeyClass() is required"); + Class valueClass = getValueClass(); + checkStateNotNull(valueClass, "withValueClass() is required"); + + String locksDirPath = getLocksDirPath(); + checkStateNotNull(locksDirPath, "withLocksDirPath() is required"); - plugin.withConfig(pluginConfig).withHadoopConfiguration(keyClass, valueClass).prepareRun(); + cdapPlugin + .withConfig(pluginConfig) + .withHadoopConfiguration(keyClass, valueClass) + .prepareRun(); - if (plugin.isUnbounded()) { + if (cdapPlugin.isUnbounded()) { // TODO: implement SparkReceiverIO.<~>write() throw new NotImplementedException("Support for unbounded plugins is not implemented!"); } else { - Configuration hConf = plugin.getHadoopConfiguration(); + Configuration hConf = cdapPlugin.getHadoopConfiguration(); HadoopFormatIO.Write writeHadoop = HadoopFormatIO.write() .withConfiguration(hConf) diff --git a/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/ConfigWrapper.java b/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/ConfigWrapper.java index 9a2124e21b46..b073e275be38 100644 --- a/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/ConfigWrapper.java +++ b/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/ConfigWrapper.java @@ -41,6 +41,7 @@ public ConfigWrapper(Class configClass) { this.configClass = configClass; } + /** Gets {@link ConfigWrapper} by JSON string. */ public ConfigWrapper fromJsonString(String jsonString) throws IOException { TypeReference> typeRef = new TypeReference>() {}; @@ -53,6 +54,7 @@ public ConfigWrapper fromJsonString(String jsonString) throws IOException { return this; } + /** Gets {@link ConfigWrapper} by JSON file. */ public ConfigWrapper fromJsonFile(File jsonFile) throws IOException { TypeReference> typeRef = new TypeReference>() {}; @@ -65,11 +67,13 @@ public ConfigWrapper fromJsonFile(File jsonFile) throws IOException { return this; } + /** Sets a {@link Plugin} parameters {@link Map}. */ public ConfigWrapper withParams(Map paramsMap) { this.paramsMap = new HashMap<>(paramsMap); return this; } + /** Sets a {@link Plugin} single parameter. */ public ConfigWrapper setParam(String paramName, Object param) { getParamsMap().put(paramName, param); return this; diff --git a/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/MappingUtils.java b/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/MappingUtils.java index f8c7ce5d7550..463cc501a982 100644 --- a/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/MappingUtils.java +++ b/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/MappingUtils.java @@ -17,14 +17,19 @@ */ package org.apache.beam.sdk.io.cdap; +import static org.apache.beam.sdk.util.Preconditions.checkArgumentNotNull; import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; +import com.google.gson.Gson; +import io.cdap.cdap.api.plugin.PluginConfig; import io.cdap.plugin.common.SourceInputFormatProvider; import io.cdap.plugin.hubspot.sink.batch.HubspotBatchSink; import io.cdap.plugin.hubspot.sink.batch.HubspotOutputFormat; import io.cdap.plugin.hubspot.source.batch.HubspotBatchSource; import io.cdap.plugin.hubspot.source.batch.HubspotInputFormat; import io.cdap.plugin.hubspot.source.batch.HubspotInputFormatProvider; +import io.cdap.plugin.hubspot.source.streaming.HubspotReceiver; +import io.cdap.plugin.hubspot.source.streaming.HubspotStreamingSource; import io.cdap.plugin.salesforce.plugin.source.batch.SalesforceBatchSource; import io.cdap.plugin.salesforce.plugin.source.batch.SalesforceInputFormat; import io.cdap.plugin.salesforce.plugin.source.batch.SalesforceInputFormatProvider; @@ -33,23 +38,118 @@ import io.cdap.plugin.zendesk.source.batch.ZendeskBatchSource; import io.cdap.plugin.zendesk.source.batch.ZendeskInputFormat; import io.cdap.plugin.zendesk.source.batch.ZendeskInputFormatProvider; +import java.util.HashMap; +import java.util.Map; +import org.apache.beam.sdk.io.sparkreceiver.ReceiverBuilder; +import org.apache.beam.sdk.transforms.SerializableFunction; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.reflect.TypeToken; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.spark.streaming.receiver.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +/** Util class for mapping plugins. */ public class MappingUtils { - public static Plugin getPluginByClass(Class pluginClass) { + private static final Logger LOG = LoggerFactory.getLogger(MappingUtils.class); + private static final String HUBSPOT_ID_FIELD = "vid"; + private static final Gson GSON = new Gson(); + + private static final Map< + Class, Pair, ReceiverBuilder>>> + REGISTERED_PLUGINS; + + static { + REGISTERED_PLUGINS = new HashMap<>(); + } + + /** Gets a {@link Plugin} by its class. */ + static Plugin getPluginByClass(Class pluginClass) { checkArgument(pluginClass != null, "Plugin class can not be null!"); if (pluginClass.equals(SalesforceBatchSource.class)) { - return Plugin.create( + return Plugin.createBatch( pluginClass, SalesforceInputFormat.class, SalesforceInputFormatProvider.class); } else if (pluginClass.equals(HubspotBatchSource.class)) { - return Plugin.create(pluginClass, HubspotInputFormat.class, HubspotInputFormatProvider.class); + return Plugin.createBatch( + pluginClass, HubspotInputFormat.class, HubspotInputFormatProvider.class); } else if (pluginClass.equals(ZendeskBatchSource.class)) { - return Plugin.create(pluginClass, ZendeskInputFormat.class, ZendeskInputFormatProvider.class); + return Plugin.createBatch( + pluginClass, ZendeskInputFormat.class, ZendeskInputFormatProvider.class); } else if (pluginClass.equals(HubspotBatchSink.class)) { - return Plugin.create(pluginClass, HubspotOutputFormat.class, SourceInputFormatProvider.class); + return Plugin.createBatch( + pluginClass, HubspotOutputFormat.class, SourceInputFormatProvider.class); } else if (pluginClass.equals(ServiceNowSource.class)) { - return Plugin.create( + return Plugin.createBatch( pluginClass, ServiceNowInputFormat.class, SourceInputFormatProvider.class); + } else if (pluginClass.equals(HubspotStreamingSource.class)) { + return Plugin.createStreaming(pluginClass); + } + throw new UnsupportedOperationException( + String.format("Given plugin class '%s' is not supported!", pluginClass.getName())); + } + + /** Gets a {@link ReceiverBuilder} by CDAP {@link Plugin} class. */ + @SuppressWarnings("unchecked") + static ReceiverBuilder> getReceiverBuilderByPluginClass( + Class pluginClass, PluginConfig pluginConfig, Class valueClass) { + checkArgument(pluginClass != null, "Plugin class can not be null!"); + checkArgument(pluginConfig != null, "Plugin config can not be null!"); + checkArgument(valueClass != null, "Value class can not be null!"); + if (pluginClass.equals(HubspotStreamingSource.class) && String.class.equals(valueClass)) { + ReceiverBuilder> receiverBuilder = + new ReceiverBuilder<>(HubspotReceiver.class).withConstructorArgs(pluginConfig); + return (ReceiverBuilder>) receiverBuilder; + } + if (REGISTERED_PLUGINS.containsKey(pluginClass)) { + return (ReceiverBuilder>) + REGISTERED_PLUGINS.get(pluginClass).getRight(); + } + throw new UnsupportedOperationException( + String.format("Given plugin class '%s' is not supported!", pluginClass.getName())); + } + + /** + * Register new CDAP Streaming {@link Plugin} class providing corresponding {@param getOffsetFn} + * and {@param receiverBuilder} params. + */ + public static void registerStreamingPlugin( + Class pluginClass, + SerializableFunction getOffsetFn, + ReceiverBuilder> receiverBuilder) { + REGISTERED_PLUGINS.put(pluginClass, new ImmutablePair<>(getOffsetFn, receiverBuilder)); + } + + private static SerializableFunction getOffsetFnForHubspot() { + return input -> { + if (input != null) { + try { + HashMap json = + GSON.fromJson(input, new TypeToken>() {}.getType()); + checkArgumentNotNull(json, "Can not get JSON from Hubspot input string"); + Object id = json.get(HUBSPOT_ID_FIELD); + checkArgumentNotNull(id, "Can not get ID from Hubspot input string"); + return ((Integer) id).longValue(); + } catch (Exception e) { + LOG.error("Can not get offset from json", e); + } + } + return 0L; + }; + } + + /** + * Gets a {@link SerializableFunction} that defines how to get record offset for CDAP {@link + * Plugin} class. + */ + @SuppressWarnings("unchecked") + static SerializableFunction getOffsetFnForPluginClass( + Class pluginClass, Class valueClass) { + if (pluginClass.equals(HubspotStreamingSource.class) && String.class.equals(valueClass)) { + return (SerializableFunction) getOffsetFnForHubspot(); + } + if (REGISTERED_PLUGINS.containsKey(pluginClass)) { + return (SerializableFunction) REGISTERED_PLUGINS.get(pluginClass).getLeft(); } throw new UnsupportedOperationException( String.format("Given plugin class '%s' is not supported!", pluginClass.getName())); diff --git a/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/Plugin.java b/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/Plugin.java index 31deb9d258db..6da476b56f3e 100644 --- a/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/Plugin.java +++ b/sdks/java/io/cdap/src/main/java/org/apache/beam/sdk/io/cdap/Plugin.java @@ -26,6 +26,7 @@ import io.cdap.cdap.etl.api.batch.BatchSinkContext; import io.cdap.cdap.etl.api.batch.BatchSource; import io.cdap.cdap.etl.api.batch.BatchSourceContext; +import io.cdap.cdap.etl.api.streaming.StreamingSource; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -37,6 +38,7 @@ import org.apache.beam.sdk.io.cdap.context.BatchContextImpl; import org.apache.beam.sdk.io.cdap.context.BatchSinkContextImpl; import org.apache.beam.sdk.io.cdap.context.BatchSourceContextImpl; +import org.apache.beam.sdk.io.cdap.context.StreamingSourceContextImpl; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.mapreduce.MRJobConfig; import org.slf4j.Logger; @@ -49,6 +51,7 @@ public abstract class Plugin { private static final Logger LOG = LoggerFactory.getLogger(Plugin.class); private static final String PREPARE_RUN_METHOD_NAME = "prepareRun"; + private static final String GET_STREAM_METHOD_NAME = "getStream"; protected @Nullable PluginConfig pluginConfig; protected @Nullable Configuration hadoopConfiguration; @@ -61,10 +64,10 @@ public abstract class Plugin { public abstract Class getPluginClass(); /** Gets InputFormat or OutputFormat class for a plugin. */ - public abstract Class getFormatClass(); + public @Nullable abstract Class getFormatClass(); /** Gets InputFormatProvider or OutputFormatProvider class for a plugin. */ - public abstract Class getFormatProviderClass(); + public @Nullable abstract Class getFormatProviderClass(); /** Sets a plugin config. */ public Plugin withConfig(PluginConfig pluginConfig) { @@ -83,46 +86,57 @@ public Plugin withConfig(PluginConfig pluginConfig) { * validating connection to the CDAP sink/source and performing initial tuning. */ public void prepareRun() { - PluginConfig pluginConfig = getPluginConfig(); - checkStateNotNull(pluginConfig, "PluginConfig should be not null!"); + if (isUnbounded()) { + // Not needed for unbounded plugins + return; + } if (cdapPluginObj == null) { - try { - Constructor constructor = - getPluginClass().getDeclaredConstructor(pluginConfig.getClass()); - constructor.setAccessible(true); - cdapPluginObj = (SubmitterLifecycle) constructor.newInstance(pluginConfig); - } catch (Exception e) { - LOG.error("Can not instantiate CDAP plugin class", e); - throw new IllegalStateException("Can not call prepareRun"); - } + instantiateCdapPluginObj(); } + checkStateNotNull(cdapPluginObj, "Cdap Plugin object can't be null!"); try { cdapPluginObj.prepareRun(getContext()); - if (getPluginType().equals(PluginConstants.PluginType.SOURCE)) { - for (Map.Entry entry : - getContext().getInputFormatProvider().getInputFormatConfiguration().entrySet()) { - getHadoopConfiguration().set(entry.getKey(), entry.getValue()); - } - } else { - for (Map.Entry entry : - getContext().getOutputFormatProvider().getOutputFormatConfiguration().entrySet()) { - getHadoopConfiguration().set(entry.getKey(), entry.getValue()); - } - getHadoopConfiguration().set(MRJobConfig.ID, String.valueOf(1)); - } } catch (Exception e) { LOG.error("Error while prepareRun", e); throw new IllegalStateException("Error while prepareRun"); } + if (getPluginType().equals(PluginConstants.PluginType.SOURCE)) { + for (Map.Entry entry : + getContext().getInputFormatProvider().getInputFormatConfiguration().entrySet()) { + getHadoopConfiguration().set(entry.getKey(), entry.getValue()); + } + } else { + for (Map.Entry entry : + getContext().getOutputFormatProvider().getOutputFormatConfiguration().entrySet()) { + getHadoopConfiguration().set(entry.getKey(), entry.getValue()); + } + getHadoopConfiguration().set(MRJobConfig.ID, String.valueOf(1)); + } + } + + /** Creates an instance of {@link #cdapPluginObj} using {@link #pluginConfig}. */ + private void instantiateCdapPluginObj() { + PluginConfig pluginConfig = getPluginConfig(); + checkStateNotNull(pluginConfig, "PluginConfig should be not null!"); + try { + Constructor constructor = getPluginClass().getDeclaredConstructor(pluginConfig.getClass()); + constructor.setAccessible(true); + cdapPluginObj = (SubmitterLifecycle) constructor.newInstance(pluginConfig); + } catch (Exception e) { + LOG.error("Can not instantiate CDAP plugin class", e); + throw new IllegalStateException("Can not call prepareRun"); + } } /** Sets a plugin Hadoop configuration. */ public Plugin withHadoopConfiguration(Class formatKeyClass, Class formatValueClass) { + Class formatClass = getFormatClass(); + checkStateNotNull(formatClass, "Format class can't be null!"); PluginConstants.Format formatType = getFormatType(); PluginConstants.Hadoop hadoopType = getHadoopType(); getHadoopConfiguration() - .setClass(hadoopType.getFormatClass(), getFormatClass(), formatType.getFormatClass()); + .setClass(hadoopType.getFormatClass(), formatClass, formatType.getFormatClass()); getHadoopConfiguration().setClass(hadoopType.getKeyClass(), formatKeyClass, Object.class); getHadoopConfiguration().setClass(hadoopType.getValueClass(), formatValueClass, Object.class); @@ -163,7 +177,8 @@ private PluginConstants.Hadoop getHadoopType() { /** Gets value of a plugin type. */ public static PluginConstants.PluginType initPluginType(Class pluginClass) throws IllegalArgumentException { - if (BatchSource.class.isAssignableFrom(pluginClass)) { + if (StreamingSource.class.isAssignableFrom(pluginClass) + || BatchSource.class.isAssignableFrom(pluginClass)) { return PluginConstants.PluginType.SOURCE; } else if (BatchSink.class.isAssignableFrom(pluginClass)) { return PluginConstants.PluginType.SINK; @@ -188,6 +203,8 @@ public static BatchContextImpl initContext(Class cdapPluginClass) { } else if (contextClass.equals(BatchSinkContext.class)) { return new BatchSinkContextImpl(); } + } else if (method.getName().equals(GET_STREAM_METHOD_NAME)) { + return new StreamingSourceContextImpl(); } } throw new IllegalStateException("Cannot determine context class"); @@ -209,8 +226,8 @@ public Boolean isUnbounded() { return isUnbounded; } - /** Creates a plugin instance. */ - public static Plugin create( + /** Creates a batch plugin instance. */ + public static Plugin createBatch( Class newPluginClass, Class newFormatClass, Class newFormatProviderClass) { return builder() .setPluginClass(newPluginClass) @@ -221,6 +238,15 @@ public static Plugin create( .build(); } + /** Creates a streaming plugin instance. */ + public static Plugin createStreaming(Class newPluginClass) { + return builder() + .setPluginClass(newPluginClass) + .setPluginType(Plugin.initPluginType(newPluginClass)) + .setContext(Plugin.initContext(newPluginClass)) + .build(); + } + /** Creates a plugin builder instance. */ public static Builder builder() { return new AutoValue_Plugin.Builder(); diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/CdapIOIT.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/CdapIOIT.java index bb5f205fc517..8f2a987a5cda 100644 --- a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/CdapIOIT.java +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/CdapIOIT.java @@ -162,7 +162,8 @@ private CdapIO.Write writeToDB(Mapwrite() .withCdapPlugin( - Plugin.create(DBBatchSink.class, DBOutputFormat.class, DBOutputFormatProvider.class)) + Plugin.createBatch( + DBBatchSink.class, DBOutputFormat.class, DBOutputFormatProvider.class)) .withPluginConfig(pluginConfig) .withKeyClass(TestRowDBWritable.class) .withValueClass(NullWritable.class) @@ -174,7 +175,8 @@ private CdapIO.Read readFromDB(Mapread() .withCdapPlugin( - Plugin.create(DBBatchSource.class, DBInputFormat.class, DBInputFormatProvider.class)) + Plugin.createBatch( + DBBatchSource.class, DBInputFormat.class, DBInputFormatProvider.class)) .withPluginConfig(pluginConfig) .withKeyClass(LongWritable.class) .withValueClass(TestRowDBWritable.class); diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/CdapIOTest.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/CdapIOTest.java index e978f5b8fcad..e18126e69acf 100644 --- a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/CdapIOTest.java +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/CdapIOTest.java @@ -27,18 +27,34 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.apache.beam.runners.direct.DirectOptions; +import org.apache.beam.runners.direct.DirectRunner; +import org.apache.beam.sdk.Pipeline; import org.apache.beam.sdk.coders.KvCoder; +import org.apache.beam.sdk.coders.NullableCoder; import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.io.cdap.batch.EmployeeBatchSink; +import org.apache.beam.sdk.io.cdap.batch.EmployeeBatchSource; +import org.apache.beam.sdk.io.cdap.batch.EmployeeInputFormat; +import org.apache.beam.sdk.io.cdap.batch.EmployeeInputFormatProvider; +import org.apache.beam.sdk.io.cdap.batch.EmployeeOutputFormat; +import org.apache.beam.sdk.io.cdap.batch.EmployeeOutputFormatProvider; import org.apache.beam.sdk.io.cdap.context.BatchSinkContextImpl; import org.apache.beam.sdk.io.cdap.context.BatchSourceContextImpl; +import org.apache.beam.sdk.io.cdap.streaming.EmployeeReceiver; +import org.apache.beam.sdk.io.cdap.streaming.EmployeeStreamingSource; +import org.apache.beam.sdk.io.sparkreceiver.ReceiverBuilder; +import org.apache.beam.sdk.options.PipelineOptionsFactory; import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.Values; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PBegin; import org.apache.beam.sdk.values.PCollection; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableMap; import org.apache.hadoop.mapreduce.OutputCommitter; +import org.joda.time.Duration; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -74,7 +90,7 @@ public void testReadBuildsCorrectly() { CdapIO.Read read = CdapIO.read() .withCdapPlugin( - Plugin.create( + Plugin.createBatch( EmployeeBatchSource.class, EmployeeInputFormat.class, EmployeeInputFormatProvider.class)) @@ -125,7 +141,7 @@ public void testReadObjectCreationFailsIfValueClassIsNull() { public void testReadExpandingFailsMissingCdapPluginClass() { PBegin testPBegin = PBegin.in(TestPipeline.create()); CdapIO.Read read = CdapIO.read(); - assertThrows(IllegalArgumentException.class, () -> read.expand(testPBegin)); + assertThrows(IllegalStateException.class, () -> read.expand(testPBegin)); } @Test @@ -136,13 +152,13 @@ public void testReadObjectCreationFailsIfCdapPluginClassIsNotSupported() { } @Test - public void testReadingData() { + public void testReadFromCdapBatchPlugin() { EmployeeConfig pluginConfig = new ConfigWrapper<>(EmployeeConfig.class).withParams(TEST_EMPLOYEE_PARAMS_MAP).build(); CdapIO.Read read = CdapIO.read() .withCdapPlugin( - Plugin.create( + Plugin.createBatch( EmployeeBatchSource.class, EmployeeInputFormat.class, EmployeeInputFormatProvider.class)) @@ -154,11 +170,43 @@ public void testReadingData() { for (int i = 1; i < EmployeeInputFormat.NUM_OF_TEST_EMPLOYEE_RECORDS; i++) { expected.add(KV.of(String.valueOf(i), EmployeeInputFormat.EMPLOYEE_NAME_PREFIX + i)); } - PCollection> actual = p.apply("ReadTest", read); + PCollection> actual = p.apply("ReadBatchTest", read); PAssert.that(actual).containsInAnyOrder(expected); p.run(); } + @Test + public void testReadFromCdapStreamingPlugin() { + DirectOptions options = PipelineOptionsFactory.as(DirectOptions.class); + options.setBlockOnRun(false); + options.setRunner(DirectRunner.class); + Pipeline p = Pipeline.create(options); + + EmployeeConfig pluginConfig = + new ConfigWrapper<>(EmployeeConfig.class).withParams(TEST_EMPLOYEE_PARAMS_MAP).build(); + MappingUtils.registerStreamingPlugin( + EmployeeStreamingSource.class, + Long::valueOf, + new ReceiverBuilder<>(EmployeeReceiver.class).withConstructorArgs(pluginConfig)); + + CdapIO.Read read = + CdapIO.read() + .withCdapPlugin(Plugin.createStreaming(EmployeeStreamingSource.class)) + .withPluginConfig(pluginConfig) + .withKeyClass(String.class) + .withValueClass(String.class); + + List storedRecords = EmployeeReceiver.getStoredRecords(); + + PCollection actual = + p.apply("ReadStreamingTest", read) + .setCoder(KvCoder.of(NullableCoder.of(StringUtf8Coder.of()), StringUtf8Coder.of())) + .apply(Values.create()); + + PAssert.that(actual).containsInAnyOrder(storedRecords); + p.run().waitUntilFinish(Duration.standardSeconds(15)); + } + @Test public void testWriteBuildsCorrectly() { EmployeeConfig pluginConfig = @@ -167,7 +215,7 @@ public void testWriteBuildsCorrectly() { CdapIO.Write write = CdapIO.write() .withCdapPlugin( - Plugin.create( + Plugin.createBatch( EmployeeBatchSink.class, EmployeeOutputFormat.class, EmployeeOutputFormatProvider.class)) @@ -230,7 +278,7 @@ public void testWriteExpandingFailsMissingCdapPluginClass() { PCollection> testPCollection = Create.empty(KvCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of())).expand(testPBegin); CdapIO.Write write = CdapIO.write(); - assertThrows(IllegalArgumentException.class, () -> write.expand(testPCollection)); + assertThrows(IllegalStateException.class, () -> write.expand(testPCollection)); } @Test @@ -241,7 +289,7 @@ public void testWriteObjectCreationFailsIfCdapPluginClassIsNotSupported() { } @Test - public void testWritingData() throws IOException { + public void testWriteWithCdapBatchSinkPlugin() throws IOException { List> data = new ArrayList<>(); for (int i = 0; i < EmployeeInputFormat.NUM_OF_TEST_EMPLOYEE_RECORDS; i++) { data.add(KV.of(String.valueOf(i), EmployeeInputFormat.EMPLOYEE_NAME_PREFIX + i)); @@ -254,10 +302,10 @@ public void testWritingData() throws IOException { "Write", CdapIO.write() .withCdapPlugin( - Plugin.create( + Plugin.createBatch( EmployeeBatchSink.class, EmployeeOutputFormat.class, - EmployeeInputFormatProvider.class)) + EmployeeOutputFormatProvider.class)) .withPluginConfig(pluginConfig) .withKeyClass(String.class) .withValueClass(String.class) diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeConfig.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeConfig.java index d02f4548cd3a..547af887fc5d 100644 --- a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeConfig.java +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeConfig.java @@ -25,10 +25,13 @@ import io.cdap.plugin.common.ReferencePluginConfig; import java.util.HashSet; import java.util.Set; +import org.apache.beam.sdk.io.cdap.batch.EmployeeBatchSink; +import org.apache.beam.sdk.io.cdap.batch.EmployeeBatchSource; /** * {@link io.cdap.cdap.api.plugin.PluginConfig} for {@link EmployeeBatchSource} and {@link - * EmployeeBatchSink} CDAP plugins. Used to test {@link CdapIO#read()} and {@link CdapIO#write()}. + * EmployeeBatchSink} CDAP plugins. Used to test {@link org.apache.beam.sdk.io.cdap.CdapIO#read()} + * and {@link org.apache.beam.sdk.io.cdap.CdapIO#write()}. */ public class EmployeeConfig extends ReferencePluginConfig { diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/PluginTest.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/PluginTest.java index 501c91b6cdaf..2fcfe6f36c0b 100644 --- a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/PluginTest.java +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/PluginTest.java @@ -65,7 +65,7 @@ public class PluginTest { public void testBuildingSourcePluginWithCDAPClasses() { try { Plugin serviceNowSourcePlugin = - Plugin.create( + Plugin.createBatch( ServiceNowSource.class, ServiceNowInputFormat.class, SourceInputFormatProvider.class) @@ -93,7 +93,7 @@ public void testBuildingSourcePluginWithCDAPClasses() { @Test public void testSettingPluginType() { Plugin serviceNowSourcePlugin = - Plugin.create( + Plugin.createBatch( ServiceNowSource.class, ServiceNowInputFormat.class, SourceInputFormatProvider.class) @@ -108,7 +108,7 @@ public void testSettingPluginType() { public void testSettingPluginTypeFailed() { try { Plugin serviceNowSourcePlugin = - Plugin.create(Object.class, Object.class, Object.class) + Plugin.createBatch(Object.class, Object.class, Object.class) .withConfig(serviceNowSourceConfig) .withHadoopConfiguration(Schema.class, MapWritable.class); fail("This should have thrown an exception"); diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeBatchSink.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeBatchSink.java similarity index 95% rename from sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeBatchSink.java rename to sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeBatchSink.java index 1e0b835fac77..052d9ab0f6a8 100644 --- a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeBatchSink.java +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeBatchSink.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.sdk.io.cdap; +package org.apache.beam.sdk.io.cdap.batch; import io.cdap.cdap.api.annotation.Description; import io.cdap.cdap.api.annotation.Name; @@ -28,6 +28,8 @@ import io.cdap.cdap.etl.api.PipelineConfigurer; import io.cdap.cdap.etl.api.batch.BatchSink; import io.cdap.cdap.etl.api.batch.BatchSinkContext; +import org.apache.beam.sdk.io.cdap.CdapIO; +import org.apache.beam.sdk.io.cdap.EmployeeConfig; /** Imitation of CDAP {@link BatchSink} plugin. Used to test {@link CdapIO#write()}. */ @Plugin(type = BatchSink.PLUGIN_TYPE) diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeBatchSource.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeBatchSource.java similarity index 94% rename from sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeBatchSource.java rename to sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeBatchSource.java index 27494c8ce9c8..3daf2fb69b98 100644 --- a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeBatchSource.java +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeBatchSource.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.sdk.io.cdap; +package org.apache.beam.sdk.io.cdap.batch; import io.cdap.cdap.api.annotation.Description; import io.cdap.cdap.api.annotation.Name; @@ -32,6 +32,8 @@ import io.cdap.plugin.common.IdUtils; import io.cdap.plugin.common.LineageRecorder; import java.util.stream.Collectors; +import org.apache.beam.sdk.io.cdap.CdapIO; +import org.apache.beam.sdk.io.cdap.EmployeeConfig; /** Imitation of CDAP {@link BatchSource} plugin. Used to test {@link CdapIO#read()}. */ @Plugin(type = BatchSource.PLUGIN_TYPE) @@ -41,7 +43,7 @@ public class EmployeeBatchSource extends BatchSource()); } - static List> getWrittenOutput() { + public static List> getWrittenOutput() { return output; } - static OutputCommitter getOutputCommitter() { + public static OutputCommitter getOutputCommitter() { return outputCommitter; } } diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeOutputFormatProvider.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeOutputFormatProvider.java similarity index 93% rename from sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeOutputFormatProvider.java rename to sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeOutputFormatProvider.java index 826b3177d302..a42c0c89aca1 100644 --- a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/EmployeeOutputFormatProvider.java +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/batch/EmployeeOutputFormatProvider.java @@ -15,12 +15,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.beam.sdk.io.cdap; +package org.apache.beam.sdk.io.cdap.batch; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.cdap.cdap.api.data.batch.OutputFormatProvider; import java.util.Map; +import org.apache.beam.sdk.io.cdap.CdapIO; +import org.apache.beam.sdk.io.cdap.EmployeeConfig; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableMap; /** diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/streaming/EmployeeReceiver.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/streaming/EmployeeReceiver.java new file mode 100644 index 000000000000..fcd0fa7b8d76 --- /dev/null +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/streaming/EmployeeReceiver.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.cdap.streaming; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.apache.beam.sdk.io.cdap.EmployeeConfig; +import org.apache.beam.sdk.io.sparkreceiver.HasOffset; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.spark.storage.StorageLevel; +import org.apache.spark.streaming.receiver.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Imitation of Spark {@link Receiver} for {@link EmployeeStreamingSource} CDAP plugin. Used to test + * {@link org.apache.beam.sdk.io.cdap.CdapIO#read()}. + */ +public class EmployeeReceiver extends Receiver implements HasOffset { + + public static final int RECORDS_COUNT = 20; + + private static final Logger LOG = LoggerFactory.getLogger(EmployeeReceiver.class); + private static final int TIMEOUT_MS = 500; + private static final List STORED_RECORDS = new ArrayList<>(); + private final EmployeeConfig config; + private Long startOffset; + + EmployeeReceiver(EmployeeConfig config) { + super(StorageLevel.MEMORY_AND_DISK_2()); + this.config = config; + LOG.info("Created EmployeeReceiver with objectType = {}", this.config.objectType); + } + + @Override + public void setStartOffset(Long startOffset) { + if (startOffset != null) { + this.startOffset = startOffset; + } + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void onStart() { + Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().build()).submit(this::receive); + } + + @Override + public void onStop() {} + + @Override + public Long getEndOffset() { + return Long.MAX_VALUE; + } + + private void receive() { + Long currentOffset = startOffset; + while (!isStopped()) { + if (currentOffset <= RECORDS_COUNT) { + STORED_RECORDS.add(currentOffset.toString()); + store((currentOffset++).toString()); + } + try { + TimeUnit.MILLISECONDS.sleep(TIMEOUT_MS); + } catch (InterruptedException e) { + LOG.error("Interrupted", e); + } + } + } + + public static List getStoredRecords() { + return STORED_RECORDS; + } +} diff --git a/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/streaming/EmployeeStreamingSource.java b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/streaming/EmployeeStreamingSource.java new file mode 100644 index 000000000000..e73688e97725 --- /dev/null +++ b/sdks/java/io/cdap/src/test/java/org/apache/beam/sdk/io/cdap/streaming/EmployeeStreamingSource.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.cdap.streaming; + +import io.cdap.cdap.api.annotation.Description; +import io.cdap.cdap.api.annotation.Name; +import io.cdap.cdap.api.annotation.Plugin; +import io.cdap.cdap.api.data.format.StructuredRecord; +import io.cdap.cdap.etl.api.FailureCollector; +import io.cdap.cdap.etl.api.PipelineConfigurer; +import io.cdap.cdap.etl.api.streaming.StreamingContext; +import io.cdap.cdap.etl.api.streaming.StreamingSource; +import java.io.IOException; +import org.apache.beam.sdk.io.cdap.CdapIO; +import org.apache.beam.sdk.io.cdap.EmployeeConfig; +import org.apache.spark.streaming.api.java.JavaDStream; +import org.apache.spark.streaming.api.java.JavaStreamingContext; + +/** Imitation of CDAP {@link StreamingSource} plugin. Used to test {@link CdapIO#read()}. */ +@Plugin(type = StreamingSource.PLUGIN_TYPE) +@Name(EmployeeStreamingSource.NAME) +@Description("Plugin reads Employee in streaming") +public class EmployeeStreamingSource extends StreamingSource { + + public static final String NAME = "EmployeeStreamingSource"; + + private final EmployeeConfig config; + + public EmployeeStreamingSource(EmployeeConfig config) { + this.config = config; + } + + @Override + public void configurePipeline(PipelineConfigurer pipelineConfigurer) { + FailureCollector collector = pipelineConfigurer.getStageConfigurer().getFailureCollector(); + config.validate(collector); // validate when macros are not substituted + collector.getOrThrowException(); + + pipelineConfigurer.getStageConfigurer().setOutputSchema(config.getSchema()); + } + + @Override + public JavaDStream getStream(StreamingContext streamingContext) + throws IOException { + FailureCollector collector = streamingContext.getFailureCollector(); + config.validate(collector); // validate when macros are substituted + collector.getOrThrowException(); + + JavaStreamingContext jssc = streamingContext.getSparkStreamingContext(); + + return jssc.receiverStream(new EmployeeReceiver(config)) + .map(jsonString -> transform(jsonString, config)); + } + + public static StructuredRecord transform(String value, EmployeeConfig config) { + StructuredRecord.Builder builder = StructuredRecord.builder(config.getSchema()); + builder.set("id", value); + builder.set("name", "Employee " + value); + return builder.build(); + } +} diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java index 953d1237d9c9..53cb27136412 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryOptions.java @@ -150,4 +150,10 @@ public interface BigQueryOptions Integer getStorageApiAppendThresholdRecordCount(); void setStorageApiAppendThresholdRecordCount(Integer value); + + @Description("Maximum request size allowed by the storage write API. ") + @Default.Long(10 * 1000 * 1000) + Long getStorageWriteApiMaxRequestSize(); + + void setStorageWriteApiMaxRequestSize(Long value); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java index 9df75a5be943..5ec5549ed4ca 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryServicesImpl.java @@ -29,7 +29,6 @@ import com.google.api.client.util.ExponentialBackOff; import com.google.api.client.util.Sleeper; import com.google.api.core.ApiFuture; -import com.google.api.gax.core.ExecutorProvider; import com.google.api.gax.core.FixedCredentialsProvider; import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.FixedHeaderProvider; @@ -106,7 +105,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -122,7 +120,6 @@ import org.apache.beam.sdk.extensions.gcp.util.Transport; import org.apache.beam.sdk.metrics.Counter; import org.apache.beam.sdk.metrics.Metrics; -import org.apache.beam.sdk.options.ExecutorOptions; import org.apache.beam.sdk.options.PipelineOptions; import org.apache.beam.sdk.transforms.SerializableFunction; import org.apache.beam.sdk.util.FluentBackoff; @@ -1209,8 +1206,14 @@ long insertAll( } rowsToPublish = retryRows; idsToPublish = retryIds; + // print first 5 failures + int numErrorToLog = Math.min(allErrors.size(), 5); + LOG.info( + "Retrying {} failed inserts to BigQuery. First {} fails: {}", + rowsToPublish.size(), + numErrorToLog, + allErrors.subList(0, numErrorToLog)); allErrors.clear(); - LOG.info("Retrying {} failed inserts to BigQuery", rowsToPublish.size()); } if (successfulRows != null) { for (int i = 0; i < rowsToPublish.size(); i++) { @@ -1488,36 +1491,12 @@ private static BigQueryWriteClient newBigQueryWriteClient(BigQueryOptions option return BigQueryWriteClient.create( BigQueryWriteSettings.newBuilder() .setCredentialsProvider(() -> options.as(GcpOptions.class).getGcpCredential()) - .setBackgroundExecutorProvider(new OptionsExecutionProvider(options)) .build()); } catch (Exception e) { throw new RuntimeException(e); } } - /** - * OptionsExecutionProvider is a utility class used to wrap the Pipeline-wide {@link - * ScheduledExecutorService} into a supplier for the {@link BigQueryWriteClient}. - */ - private static class OptionsExecutionProvider implements ExecutorProvider { - - private final BigQueryOptions options; - - public OptionsExecutionProvider(BigQueryOptions options) { - this.options = options; - } - - @Override - public boolean shouldAutoClose() { - return false; - } - - @Override - public ScheduledExecutorService getExecutor() { - return options.as(ExecutorOptions.class).getScheduledExecutorService(); - } - } - public static CustomHttpErrors createBigQueryClientCustomErrors() { CustomHttpErrors.Builder builder = new CustomHttpErrors.Builder(); // 403 errors, to list tables, matching this URL: diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryStorageTableSource.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryStorageTableSource.java index 017fcd6c7e7d..26a9bed20c72 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryStorageTableSource.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryStorageTableSource.java @@ -185,7 +185,7 @@ protected Table getTargetTable(BigQueryOptions options) throws Exception { : options.getBigQueryProject()); } try (DatasetService datasetService = bqServices.getDatasetService(options)) { - Table table = bqServices.getDatasetService(options).getTable(tableReference); + Table table = datasetService.getTable(tableReference); if (table == null) { throw new IllegalArgumentException("Table not found" + table); } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiLoads.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiLoads.java index e48b9a196902..20ab251c9c0c 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiLoads.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiLoads.java @@ -24,6 +24,7 @@ import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO.Write.CreateDisposition; import org.apache.beam.sdk.schemas.NoSuchSchemaException; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.Flatten; import org.apache.beam.sdk.transforms.GroupIntoBatches; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; @@ -32,6 +33,7 @@ import org.apache.beam.sdk.util.ShardedKey; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionList; import org.apache.beam.sdk.values.PCollectionTuple; import org.apache.beam.sdk.values.TupleTag; import org.joda.time.Duration; @@ -101,7 +103,7 @@ public WriteResult expandInconsistent( PCollection> inputInGlobalWindow = input.apply("rewindowIntoGlobal", Window.into(new GlobalWindows())); - PCollectionTuple convertedRecords = + PCollectionTuple convertMessagesResult = inputInGlobalWindow .apply( "CreateTables", @@ -116,20 +118,23 @@ public WriteResult expandInconsistent( successfulRowsTag, BigQueryStorageApiInsertErrorCoder.of(), successCoder)); - convertedRecords - .get(successfulRowsTag) - .apply( - "StorageApiWriteInconsistent", - new StorageApiWriteRecordsInconsistent<>(dynamicDestinations, bqServices)); + PCollectionTuple writeRecordsResult = + convertMessagesResult + .get(successfulRowsTag) + .apply( + "StorageApiWriteInconsistent", + new StorageApiWriteRecordsInconsistent<>( + dynamicDestinations, + bqServices, + failedRowsTag, + BigQueryStorageApiInsertErrorCoder.of())); + + PCollection insertErrors = + PCollectionList.of(convertMessagesResult.get(failedRowsTag)) + .and(writeRecordsResult.get(failedRowsTag)) + .apply("flattenErrors", Flatten.pCollections()); return WriteResult.in( - input.getPipeline(), - null, - null, - null, - null, - null, - failedRowsTag, - convertedRecords.get(failedRowsTag)); + input.getPipeline(), null, null, null, null, null, failedRowsTag, insertErrors); } public WriteResult expandTriggered( @@ -139,7 +144,7 @@ public WriteResult expandTriggered( // Handle triggered, low-latency loads into BigQuery. PCollection> inputInGlobalWindow = input.apply("rewindowIntoGlobal", Window.into(new GlobalWindows())); - PCollectionTuple result = + PCollectionTuple convertMessagesResult = inputInGlobalWindow .apply( "CreateTables", @@ -159,7 +164,7 @@ public WriteResult expandTriggered( if (this.allowAutosharding) { groupedRecords = - result + convertMessagesResult .get(successfulRowsTag) .apply( "GroupIntoBatches", @@ -171,7 +176,7 @@ public WriteResult expandTriggered( } else { PCollection, StorageApiWritePayload>> shardedRecords = - createShardedKeyValuePairs(result) + createShardedKeyValuePairs(convertMessagesResult) .setCoder(KvCoder.of(ShardedKey.Coder.of(destinationCoder), payloadCoder)); groupedRecords = shardedRecords.apply( @@ -181,20 +186,25 @@ public WriteResult expandTriggered( (StorageApiWritePayload e) -> (long) e.getPayload().length) .withMaxBufferingDuration(triggeringFrequency)); } - groupedRecords.apply( - "StorageApiWriteSharded", - new StorageApiWritesShardedRecords<>( - dynamicDestinations, createDisposition, kmsKey, bqServices, destinationCoder)); + PCollectionTuple writeRecordsResult = + groupedRecords.apply( + "StorageApiWriteSharded", + new StorageApiWritesShardedRecords<>( + dynamicDestinations, + createDisposition, + kmsKey, + bqServices, + destinationCoder, + BigQueryStorageApiInsertErrorCoder.of(), + failedRowsTag)); + + PCollection insertErrors = + PCollectionList.of(convertMessagesResult.get(failedRowsTag)) + .and(writeRecordsResult.get(failedRowsTag)) + .apply("flattenErrors", Flatten.pCollections()); return WriteResult.in( - input.getPipeline(), - null, - null, - null, - null, - null, - failedRowsTag, - result.get(failedRowsTag)); + input.getPipeline(), null, null, null, null, null, failedRowsTag, insertErrors); } private PCollection, StorageApiWritePayload>> @@ -232,7 +242,7 @@ public WriteResult expandUntriggered( PCollection> inputInGlobalWindow = input.apply( "rewindowIntoGlobal", Window.>into(new GlobalWindows())); - PCollectionTuple convertedRecords = + PCollectionTuple convertMessagesResult = inputInGlobalWindow .apply( "CreateTables", @@ -247,20 +257,24 @@ public WriteResult expandUntriggered( successfulRowsTag, BigQueryStorageApiInsertErrorCoder.of(), successCoder)); - convertedRecords - .get(successfulRowsTag) - .apply( - "StorageApiWriteUnsharded", - new StorageApiWriteUnshardedRecords<>(dynamicDestinations, bqServices)); + + PCollectionTuple writeRecordsResult = + convertMessagesResult + .get(successfulRowsTag) + .apply( + "StorageApiWriteUnsharded", + new StorageApiWriteUnshardedRecords<>( + dynamicDestinations, + bqServices, + failedRowsTag, + BigQueryStorageApiInsertErrorCoder.of())); + + PCollection insertErrors = + PCollectionList.of(convertMessagesResult.get(failedRowsTag)) + .and(writeRecordsResult.get(failedRowsTag)) + .apply("flattenErrors", Flatten.pCollections()); return WriteResult.in( - input.getPipeline(), - null, - null, - null, - null, - null, - failedRowsTag, - convertedRecords.get(failedRowsTag)); + input.getPipeline(), null, null, null, null, null, failedRowsTag, insertErrors); } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteRecordsInconsistent.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteRecordsInconsistent.java index 35b3ddfd080a..190525925aec 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteRecordsInconsistent.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteRecordsInconsistent.java @@ -17,12 +17,14 @@ */ package org.apache.beam.sdk.io.gcp.bigquery; -import org.apache.beam.sdk.coders.VoidCoder; -import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; /** * A transform to write sharded records to BigQuery using the Storage API. This transform uses the @@ -32,34 +34,46 @@ */ @SuppressWarnings("FutureReturnValueIgnored") public class StorageApiWriteRecordsInconsistent - extends PTransform>, PCollection> { + extends PTransform>, PCollectionTuple> { private final StorageApiDynamicDestinations dynamicDestinations; private final BigQueryServices bqServices; + private final TupleTag failedRowsTag; + private final TupleTag> finalizeTag = new TupleTag<>("finalizeTag"); + private final Coder failedRowsCoder; public StorageApiWriteRecordsInconsistent( StorageApiDynamicDestinations dynamicDestinations, - BigQueryServices bqServices) { + BigQueryServices bqServices, + TupleTag failedRowsTag, + Coder failedRowsCoder) { this.dynamicDestinations = dynamicDestinations; this.bqServices = bqServices; + this.failedRowsTag = failedRowsTag; + this.failedRowsCoder = failedRowsCoder; } @Override - public PCollection expand(PCollection> input) { + public PCollectionTuple expand(PCollection> input) { String operationName = input.getName() + "/" + getName(); BigQueryOptions bigQueryOptions = input.getPipeline().getOptions().as(BigQueryOptions.class); // Append records to the Storage API streams. - input.apply( - "Write Records", - ParDo.of( - new StorageApiWriteUnshardedRecords.WriteRecordsDoFn<>( - operationName, - dynamicDestinations, - bqServices, - true, - bigQueryOptions.getStorageApiAppendThresholdBytes(), - bigQueryOptions.getStorageApiAppendThresholdRecordCount(), - bigQueryOptions.getNumStorageWriteApiStreamAppendClients())) - .withSideInputs(dynamicDestinations.getSideInputs())); - return input.getPipeline().apply("voids", Create.empty(VoidCoder.of())); + PCollectionTuple result = + input.apply( + "Write Records", + ParDo.of( + new StorageApiWriteUnshardedRecords.WriteRecordsDoFn<>( + operationName, + dynamicDestinations, + bqServices, + true, + bigQueryOptions.getStorageApiAppendThresholdBytes(), + bigQueryOptions.getStorageApiAppendThresholdRecordCount(), + bigQueryOptions.getNumStorageWriteApiStreamAppendClients(), + finalizeTag, + failedRowsTag)) + .withOutputTags(finalizeTag, TupleTagList.of(failedRowsTag)) + .withSideInputs(dynamicDestinations.getSideInputs())); + result.get(failedRowsTag).setCoder(failedRowsCoder); + return result; } } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java index 871fc73698af..0f86b8871f0e 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWriteUnshardedRecords.java @@ -20,26 +20,31 @@ import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.services.bigquery.model.TableRow; import com.google.cloud.bigquery.storage.v1.AppendRowsResponse; +import com.google.cloud.bigquery.storage.v1.Exceptions; import com.google.cloud.bigquery.storage.v1.ProtoRows; import com.google.cloud.bigquery.storage.v1.WriteStream.Type; import com.google.protobuf.ByteString; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; import java.io.IOException; import java.time.Instant; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; +import org.apache.beam.sdk.coders.Coder; import org.apache.beam.sdk.coders.KvCoder; import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.io.gcp.bigquery.BigQueryServices.DatasetService; import org.apache.beam.sdk.io.gcp.bigquery.BigQueryServices.StreamAppendClient; -import org.apache.beam.sdk.io.gcp.bigquery.RetryManager.Operation.Context; import org.apache.beam.sdk.io.gcp.bigquery.RetryManager.RetryType; import org.apache.beam.sdk.io.gcp.bigquery.StorageApiDynamicDestinations.DescriptorWrapper; import org.apache.beam.sdk.io.gcp.bigquery.StorageApiDynamicDestinations.MessageConverter; @@ -51,14 +56,18 @@ import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.Reshuffle; -import org.apache.beam.sdk.transforms.windowing.BoundedWindow; import org.apache.beam.sdk.transforms.windowing.GlobalWindow; import org.apache.beam.sdk.util.Preconditions; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Strings; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.cache.Cache; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.cache.CacheBuilder; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.cache.RemovalNotification; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Iterables; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Lists; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Maps; import org.checkerframework.checker.nullness.qual.NonNull; @@ -75,11 +84,14 @@ */ @SuppressWarnings({"FutureReturnValueIgnored"}) public class StorageApiWriteUnshardedRecords - extends PTransform>, PCollection> { + extends PTransform>, PCollectionTuple> { private static final Logger LOG = LoggerFactory.getLogger(StorageApiWriteUnshardedRecords.class); private final StorageApiDynamicDestinations dynamicDestinations; private final BigQueryServices bqServices; + private final TupleTag failedRowsTag; + private final TupleTag> finalizeTag = new TupleTag<>("finalizeTag"); + private final Coder failedRowsCoder; private static final ExecutorService closeWriterExecutor = Executors.newCachedThreadPool(); /** @@ -87,6 +99,8 @@ public class StorageApiWriteUnshardedRecords * StreamAppendClient after looking up the cache, and we must ensure that the cache is not * accessed in between the lookup and the pin (any access of the cache could trigger element * expiration). Therefore most used of APPEND_CLIENTS should synchronize. + * + *

    TODO(reuvenlax); Once all uses of StreamWriter are using */ private static final Cache APPEND_CLIENTS = CacheBuilder.newBuilder() @@ -122,20 +136,24 @@ private static void runAsyncIgnoreFailure(ExecutorService executor, ThrowingRunn public StorageApiWriteUnshardedRecords( StorageApiDynamicDestinations dynamicDestinations, - BigQueryServices bqServices) { + BigQueryServices bqServices, + TupleTag failedRowsTag, + Coder failedRowsCoder) { this.dynamicDestinations = dynamicDestinations; this.bqServices = bqServices; + this.failedRowsTag = failedRowsTag; + this.failedRowsCoder = failedRowsCoder; } @Override - public PCollection expand(PCollection> input) { + public PCollectionTuple expand(PCollection> input) { String operationName = input.getName() + "/" + getName(); BigQueryOptions options = input.getPipeline().getOptions().as(BigQueryOptions.class); org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument( !options.getUseStorageApiConnectionPool(), "useStorageApiConnectionPool only supported " + "when using STORAGE_API_AT_LEAST_ONCE"); - return input - .apply( + PCollectionTuple writeResults = + input.apply( "Write Records", ParDo.of( new WriteRecordsDoFn<>( @@ -145,19 +163,39 @@ public PCollection expand(PCollection extends DoFn, KV> { private final Counter forcedFlushes = Metrics.counter(WriteRecordsDoFn.class, "forcedFlushes"); + private final TupleTag> finalizeTag; + private final TupleTag failedRowsTag; + + static class AppendRowsContext extends RetryManager.Operation.Context { + long offset; + ProtoRows protoRows; + + public AppendRowsContext(long offset, ProtoRows protoRows) { + this.offset = offset; + this.protoRows = protoRows; + } + } class DestinationState { private final String tableUrn; @@ -175,11 +213,17 @@ class DestinationState { Metrics.counter(WriteRecordsDoFn.class, "schemaMismatches"); private final Distribution inflightWaitSecondsDistribution = Metrics.distribution(WriteRecordsDoFn.class, "streamWriterWaitSeconds"); + private final Counter rowsSentToFailedRowsCollection = + Metrics.counter( + StorageApiWritesShardedRecords.WriteRecordsDoFn.class, + "rowsSentToFailedRowsCollection"); + private final boolean useDefaultStream; private DescriptorWrapper descriptorWrapper; private Instant nextCacheTickle = Instant.MAX; private final int clientNumber; private final boolean usingMultiplexing; + private final long maxRequestSize; public DestinationState( String tableUrn, @@ -187,7 +231,8 @@ public DestinationState( DatasetService datasetService, boolean useDefaultStream, int streamAppendClientCount, - BigQueryOptions bigQueryOptions) { + boolean usingMultiplexing, + long maxRequestSize) { this.tableUrn = tableUrn; this.messageConverter = messageConverter; this.pendingMessages = Lists.newArrayList(); @@ -195,7 +240,8 @@ public DestinationState( this.useDefaultStream = useDefaultStream; this.descriptorWrapper = messageConverter.getSchemaDescriptor(); this.clientNumber = new Random().nextInt(streamAppendClientCount); - this.usingMultiplexing = bigQueryOptions.getUseStorageApiConnectionPool(); + this.usingMultiplexing = usingMultiplexing; + this.maxRequestSize = maxRequestSize; } void teardown() { @@ -217,7 +263,7 @@ String getStreamAppendClientCacheEntryKey() { return this.streamName; } - String createStreamIfNeeded() { + String getOrCreateStreamName() { try { if (!useDefaultStream) { this.streamName = @@ -242,7 +288,7 @@ StreamAppendClient generateClient() throws Exception { StreamAppendClient getStreamAppendClient(boolean lookupCache) { try { if (this.streamAppendClient == null) { - createStreamIfNeeded(); + getOrCreateStreamName(); final StreamAppendClient newStreamAppendClient; synchronized (APPEND_CLIENTS) { if (lookupCache) { @@ -313,7 +359,8 @@ void addMessage(StorageApiWritePayload payload) throws Exception { invalidateWriteStream(); if (useDefaultStream) { // Since the default stream client is shared across many bundles and threads, we can't - // simply look it upfrom the cache, as another thread may have recreated it with the old + // simply look it up from the cache, as another thread may have recreated it with the + // old // schema. getStreamAppendClient(false); } @@ -328,29 +375,62 @@ void addMessage(StorageApiWritePayload payload) throws Exception { pendingMessages.add(ByteString.copyFrom(payload.getPayload())); } - void flush(RetryManager> retryManager) + long flush( + RetryManager retryManager, + OutputReceiver failedRowsReceiver) throws Exception { if (pendingMessages.isEmpty()) { - return; + return 0; } - final ProtoRows.Builder inserts = ProtoRows.newBuilder(); - inserts.addAllSerializedRows(pendingMessages); - ProtoRows protoRows = inserts.build(); + final ProtoRows.Builder insertsBuilder = ProtoRows.newBuilder(); + insertsBuilder.addAllSerializedRows(pendingMessages); + final ProtoRows inserts = insertsBuilder.build(); pendingMessages.clear(); + // Handle the case where the request is too large. + if (inserts.getSerializedSize() >= maxRequestSize) { + if (inserts.getSerializedRowsCount() > 1) { + // TODO(reuvenlax): Is it worth trying to handle this case by splitting the protoRows? + // Given that we split + // the ProtoRows iterable at 2MB and the max request size is 10MB, this scenario seems + // nearly impossible. + LOG.error( + "A request containing more than one row is over the request size limit of " + + maxRequestSize + + ". This is unexpected. All rows in the request will be sent to the failed-rows PCollection."); + } + for (ByteString rowBytes : inserts.getSerializedRowsList()) { + TableRow failedRow = + TableRowToStorageApiProto.tableRowFromMessage( + DynamicMessage.parseFrom(descriptorWrapper.descriptor, rowBytes)); + failedRowsReceiver.output( + new BigQueryStorageApiInsertError( + failedRow, "Row payload too large. Maximum size " + maxRequestSize)); + } + return 0; + } + + long offset = -1; + if (!this.useDefaultStream) { + offset = this.currentOffset; + this.currentOffset += inserts.getSerializedRowsCount(); + } + AppendRowsContext appendRowsContext = new AppendRowsContext(offset, inserts); + retryManager.addOperation( c -> { + if (c.protoRows.getSerializedRowsCount() == 0) { + // This might happen if all rows in a batch failed and were sent to the failed-rows + // PCollection. + return ApiFutures.immediateFuture(AppendRowsResponse.newBuilder().build()); + } try { StreamAppendClient writeStream = getStreamAppendClient(true); - long offset = -1; - if (!this.useDefaultStream) { - offset = this.currentOffset; - this.currentOffset += inserts.getSerializedRowsCount(); - } - ApiFuture response = writeStream.appendRows(offset, protoRows); + ApiFuture response = + writeStream.appendRows(c.offset, c.protoRows); + inflightWaitSecondsDistribution.update(writeStream.getInflightWaitSeconds()); if (!usingMultiplexing) { - inflightWaitSecondsDistribution.update(writeStream.getInflightWaitSeconds()); if (writeStream.getInflightWaitSeconds() > 5) { LOG.warn( "Storage Api write delay more than {} seconds.", @@ -363,33 +443,78 @@ void flush(RetryManager> retryMa } }, contexts -> { + AppendRowsContext failedContext = + Preconditions.checkStateNotNull(Iterables.getFirst(contexts, null)); + if (failedContext.getError() != null + && failedContext.getError() instanceof Exceptions.AppendSerializtionError) { + Exceptions.AppendSerializtionError error = + Preconditions.checkStateNotNull( + (Exceptions.AppendSerializtionError) failedContext.getError()); + Set failedRowIndices = error.getRowIndexToErrorMessage().keySet(); + for (int failedIndex : failedRowIndices) { + // Convert the message to a TableRow and send it to the failedRows collection. + ByteString protoBytes = failedContext.protoRows.getSerializedRows(failedIndex); + try { + TableRow failedRow = + TableRowToStorageApiProto.tableRowFromMessage( + DynamicMessage.parseFrom(descriptorWrapper.descriptor, protoBytes)); + new BigQueryStorageApiInsertError( + failedRow, error.getRowIndexToErrorMessage().get(failedIndex)); + failedRowsReceiver.output( + new BigQueryStorageApiInsertError( + failedRow, error.getRowIndexToErrorMessage().get(failedIndex))); + } catch (InvalidProtocolBufferException e) { + LOG.error("Failed to insert row and could not parse the result!"); + } + } + rowsSentToFailedRowsCollection.inc(failedRowIndices.size()); + + // Remove the failed row from the payload, so we retry the batch without the failed + // rows. + ProtoRows.Builder retryRows = ProtoRows.newBuilder(); + for (int i = 0; i < failedContext.protoRows.getSerializedRowsCount(); ++i) { + if (!failedRowIndices.contains(i)) { + ByteString rowBytes = failedContext.protoRows.getSerializedRows(i); + retryRows.addSerializedRows(rowBytes); + } + } + failedContext.protoRows = retryRows.build(); + + // Since we removed rows, we need to update the insert offsets for all remaining + // rows. + long newOffset = failedContext.offset; + for (AppendRowsContext context : contexts) { + context.offset = newOffset; + newOffset += context.protoRows.getSerializedRowsCount(); + } + this.currentOffset = newOffset; + return RetryType.RETRY_ALL_OPERATIONS; + } + LOG.warn( "Append to stream {} by client #{} failed with error, operations will be retried. Details: {}", streamName, clientNumber, - retrieveErrorDetails(contexts)); + retrieveErrorDetails(failedContext)); invalidateWriteStream(); appendFailures.inc(); return RetryType.RETRY_ALL_OPERATIONS; }, - response -> { - recordsAppended.inc(protoRows.getSerializedRowsCount()); + c -> { + recordsAppended.inc(c.protoRows.getSerializedRowsCount()); }, - new Context<>()); + appendRowsContext); maybeTickleCache(); + return inserts.getSerializedRowsCount(); } - String retrieveErrorDetails(Iterable> contexts) { - return StreamSupport.stream(contexts.spliterator(), false) - .<@Nullable Throwable>map(ctx -> ctx.getError()) - .map( - err -> - (err == null) - ? "no error" - : Lists.newArrayList(err.getStackTrace()).stream() - .map(se -> se.toString()) - .collect(Collectors.joining("\n"))) - .collect(Collectors.joining(",")); + String retrieveErrorDetails(AppendRowsContext failedContext) { + return (failedContext.getError() != null) + ? Arrays.stream( + Preconditions.checkStateNotNull(failedContext.getError()).getStackTrace()) + .map(StackTraceElement::toString) + .collect(Collectors.joining("\n")) + : "no execption"; } } @@ -412,7 +537,9 @@ String retrieveErrorDetails(Iterable> contexts) { boolean useDefaultStream, int flushThresholdBytes, int flushThresholdCount, - int streamAppendClientCount) { + int streamAppendClientCount, + TupleTag> finalizeTag, + TupleTag failedRowsTag) { this.messageConverters = new TwoLevelMessageConverterCache<>(operationName); this.dynamicDestinations = dynamicDestinations; this.bqServices = bqServices; @@ -420,31 +547,47 @@ String retrieveErrorDetails(Iterable> contexts) { this.flushThresholdBytes = flushThresholdBytes; this.flushThresholdCount = flushThresholdCount; this.streamAppendClientCount = streamAppendClientCount; + this.finalizeTag = finalizeTag; + this.failedRowsTag = failedRowsTag; } boolean shouldFlush() { return numPendingRecords > flushThresholdCount || numPendingRecordBytes > flushThresholdBytes; } - void flushIfNecessary() throws Exception { + void flushIfNecessary(OutputReceiver failedRowsReceiver) + throws Exception { if (shouldFlush()) { forcedFlushes.inc(); // Too much memory being used. Flush the state and wait for it to drain out. // TODO(reuvenlax): Consider waiting for memory usage to drop instead of waiting for all the // appends to finish. - flushAll(); + flushAll(failedRowsReceiver); } } - void flushAll() throws Exception { - RetryManager> - retryManager = - new RetryManager<>(Duration.standardSeconds(1), Duration.standardSeconds(10), 1000); - Preconditions.checkStateNotNull(destinations); - for (DestinationState destinationState : destinations.values()) { - destinationState.flush(retryManager); + void flushAll(OutputReceiver failedRowsReceiver) + throws Exception { + List> retryManagers = + Lists.newArrayListWithCapacity(Preconditions.checkStateNotNull(destinations).size()); + long numRowsWritten = 0; + for (DestinationState destinationState : + Preconditions.checkStateNotNull(destinations).values()) { + RetryManager retryManager = + new RetryManager<>(Duration.standardSeconds(1), Duration.standardSeconds(10), 1000); + retryManagers.add(retryManager); + numRowsWritten += destinationState.flush(retryManager, failedRowsReceiver); + retryManager.run(false); + } + if (numRowsWritten > 0) { + // TODO(reuvenlax): Can we await in parallel instead? Failure retries aren't triggered until + // await is called, so + // this approach means that if one call fais, it has to wait for all prior calls to complete + // before a retry happens. + for (RetryManager retryManager : retryManagers) { + retryManager.await(); + } } - retryManager.run(true); numPendingRecords = 0; numPendingRecordBytes = 0; } @@ -488,14 +631,16 @@ DestinationState createDestinationState( datasetService, useDefaultStream, streamAppendClientCount, - bigQueryOptions); + bigQueryOptions.getUseStorageApiConnectionPool(), + bigQueryOptions.getStorageWriteApiMaxRequestSize()); } @ProcessElement public void process( ProcessContext c, PipelineOptions pipelineOptions, - @Element KV element) + @Element KV element, + MultiOutputReceiver o) throws Exception { DatasetService initializedDatasetService = initializeDatasetService(pipelineOptions); dynamicDestinations.setSideInputAccessorFromProcessContext(c); @@ -506,7 +651,7 @@ public void process( k -> createDestinationState( c, k, initializedDatasetService, pipelineOptions.as(BigQueryOptions.class))); - flushIfNecessary(); + flushIfNecessary(o.get(failedRowsTag)); state.addMessage(element.getValue()); ++numPendingRecords; numPendingRecordBytes += element.getValue().getPayload().length; @@ -514,14 +659,28 @@ public void process( @FinishBundle public void finishBundle(FinishBundleContext context) throws Exception { - flushAll(); + flushAll( + new OutputReceiver() { + @Override + public void output(BigQueryStorageApiInsertError output) { + outputWithTimestamp(output, GlobalWindow.INSTANCE.maxTimestamp()); + } + + @Override + public void outputWithTimestamp( + BigQueryStorageApiInsertError output, org.joda.time.Instant timestamp) { + context.output(failedRowsTag, output, timestamp, GlobalWindow.INSTANCE); + } + }); + final Map destinations = Preconditions.checkStateNotNull(this.destinations); for (DestinationState state : destinations.values()) { - if (!useDefaultStream) { + if (!useDefaultStream && !Strings.isNullOrEmpty(state.streamName)) { context.output( + finalizeTag, KV.of(state.tableUrn, state.streamName), - BoundedWindow.TIMESTAMP_MAX_VALUE.minus(Duration.millis(1)), + GlobalWindow.INSTANCE.maxTimestamp(), GlobalWindow.INSTANCE); } state.teardown(); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java index c8bb805b6e8f..af0ae5169bc9 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiWritesShardedRecords.java @@ -20,16 +20,23 @@ import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.services.bigquery.model.TableRow; import com.google.cloud.bigquery.storage.v1.AppendRowsResponse; +import com.google.cloud.bigquery.storage.v1.Exceptions; import com.google.cloud.bigquery.storage.v1.Exceptions.StreamFinalizedException; import com.google.cloud.bigquery.storage.v1.ProtoRows; import com.google.cloud.bigquery.storage.v1.WriteStream.Type; +import com.google.protobuf.ByteString; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; import io.grpc.Status; import io.grpc.Status.Code; import java.io.IOException; import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -74,6 +81,9 @@ import org.apache.beam.sdk.util.ShardedKey; import org.apache.beam.sdk.values.KV; import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.PCollectionTuple; +import org.apache.beam.sdk.values.TupleTag; +import org.apache.beam.sdk.values.TupleTagList; import org.apache.beam.sdk.values.TypeDescriptor; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.MoreObjects; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Strings; @@ -99,7 +109,7 @@ public class StorageApiWritesShardedRecords extends PTransform< PCollection, Iterable>>, - PCollection> { + PCollectionTuple> { private static final Logger LOG = LoggerFactory.getLogger(StorageApiWritesShardedRecords.class); private static final Duration DEFAULT_STREAM_IDLE_TIME = Duration.standardHours(1); @@ -108,7 +118,10 @@ public class StorageApiWritesShardedRecords destinationCoder; + private final Coder failedRowsCoder; private final Duration streamIdleTime = DEFAULT_STREAM_IDLE_TIME; + private final TupleTag failedRowsTag; + private final TupleTag> flushTag = new TupleTag<>("flushTag"); private static final ExecutorService closeWriterExecutor = Executors.newCachedThreadPool(); private static final Cache APPEND_CLIENTS = @@ -147,24 +160,29 @@ public StorageApiWritesShardedRecords( CreateDisposition createDisposition, String kmsKey, BigQueryServices bqServices, - Coder destinationCoder) { + Coder destinationCoder, + Coder failedRowsCoder, + TupleTag failedRowsTag) { this.dynamicDestinations = dynamicDestinations; this.createDisposition = createDisposition; this.kmsKey = kmsKey; this.bqServices = bqServices; this.destinationCoder = destinationCoder; + this.failedRowsCoder = failedRowsCoder; + this.failedRowsTag = failedRowsTag; } @Override - public PCollection expand( + public PCollectionTuple expand( PCollection, Iterable>> input) { String operationName = input.getName() + "/" + getName(); // Append records to the Storage API streams. - PCollection> written = + PCollectionTuple writeRecordsResult = input.apply( "Write Records", ParDo.of(new WriteRecordsDoFn(operationName, streamIdleTime)) - .withSideInputs(dynamicDestinations.getSideInputs())); + .withSideInputs(dynamicDestinations.getSideInputs()) + .withOutputTags(flushTag, TupleTagList.of(failedRowsTag))); SchemaCoder operationCoder; try { @@ -180,7 +198,8 @@ public PCollection expand( } // Send all successful writes to be flushed. - return written + writeRecordsResult + .get(flushTag) .setCoder(KvCoder.of(StringUtf8Coder.of(), operationCoder)) .apply( Window.>configure() @@ -192,6 +211,8 @@ public PCollection expand( .apply("maxFlushPosition", Combine.perKey(Max.naturalOrder(new Operation(-1, false)))) .apply( "Flush and finalize writes", ParDo.of(new StorageApiFlushAndFinalizeDoFn(bqServices))); + writeRecordsResult.get(failedRowsTag).setCoder(failedRowsCoder); + return writeRecordsResult; } class WriteRecordsDoFn @@ -215,6 +236,8 @@ class WriteRecordsDoFn Metrics.distribution(WriteRecordsDoFn.class, "appendSizeDistribution"); private final Distribution appendSplitDistribution = Metrics.distribution(WriteRecordsDoFn.class, "appendSplitDistribution"); + private final Counter rowsSentToFailedRowsCollection = + Metrics.counter(WriteRecordsDoFn.class, "rowsSentToFailedRowsCollection"); private TwoLevelMessageConverterCache messageConverters; @@ -297,8 +320,10 @@ public void process( final @AlwaysFetched @StateId("streamName") ValueState streamName, final @AlwaysFetched @StateId("streamOffset") ValueState streamOffset, @TimerId("idleTimer") Timer idleTimer, - final OutputReceiver> o) + final MultiOutputReceiver o) throws Exception { + BigQueryOptions bigQueryOptions = pipelineOptions.as(BigQueryOptions.class); + dynamicDestinations.setSideInputAccessorFromProcessContext(c); TableDestination tableDestination = destinations.computeIfAbsent( @@ -323,7 +348,7 @@ public void process( // Each ProtoRows object contains at most 1MB of rows. // TODO: Push messageFromTableRow up to top level. That we we cans skip TableRow entirely if // already proto or already schema. - final long oneMb = 1024 * 1024; + final long splitSize = bigQueryOptions.getStorageApiAppendThresholdBytes(); // Called if the schema does not match. Function updateSchemaHash = (Long expectedHash) -> { @@ -343,7 +368,7 @@ public void process( } }; Iterable messages = - new SplittingIterable(element.getValue(), oneMb, descriptor.get(), updateSchemaHash); + new SplittingIterable(element.getValue(), splitSize, descriptor.get(), updateSchemaHash); class AppendRowsContext extends RetryManager.Operation.Context { final ShardedKey key; @@ -352,9 +377,11 @@ class AppendRowsContext extends RetryManager.Operation.Context key) { + AppendRowsContext(ShardedKey key, ProtoRows protoRows) { this.key = key; + this.protoRows = protoRows; } @Override @@ -396,7 +423,7 @@ public String toString() { context.client = appendClient; context.offset = streamOffset.read(); ++context.tryIteration; - streamOffset.write(context.offset + context.numRows); + streamOffset.write(context.offset + context.protoRows.getSerializedRowsCount()); } } catch (Exception e) { throw new RuntimeException(e); @@ -415,114 +442,200 @@ public String toString() { } }; - Instant now = Instant.now(); - List contexts = Lists.newArrayList(); - RetryManager retryManager = - new RetryManager<>(Duration.standardSeconds(1), Duration.standardSeconds(10), 1000); - int numSplits = 0; - for (ProtoRows protoRows : messages) { - ++numSplits; - Function> run = - context -> { - try { - StreamAppendClient appendClient = - APPEND_CLIENTS.get( - context.streamName, - () -> - datasetService.getStreamAppendClient( - context.streamName, descriptor.get().descriptor, false)); - return appendClient.appendRows(context.offset, protoRows); - } catch (Exception e) { - throw new RuntimeException(e); + Function> runOperation = + context -> { + if (context.protoRows.getSerializedRowsCount() == 0) { + // This might happen if all rows in a batch failed and were sent to the failed-rows + // PCollection. + return ApiFutures.immediateFuture(AppendRowsResponse.newBuilder().build()); + } + try { + StreamAppendClient appendClient = + APPEND_CLIENTS.get( + context.streamName, + () -> + datasetService.getStreamAppendClient( + context.streamName, descriptor.get().descriptor, false)); + return appendClient.appendRows(context.offset, context.protoRows); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + + Function, RetryType> onError = + failedContexts -> { + // The first context is always the one that fails. + AppendRowsContext failedContext = + Preconditions.checkStateNotNull(Iterables.getFirst(failedContexts, null)); + + // AppendSerializationError means that BigQuery detected errors on individual rows, e.g. + // a row not conforming + // to bigQuery invariants. These errors are persistent, so we redirect those rows to the + // failedInserts + // PCollection, and retry with the remaining rows. + if (failedContext.getError() != null + && failedContext.getError() instanceof Exceptions.AppendSerializtionError) { + Exceptions.AppendSerializtionError error = + Preconditions.checkArgumentNotNull( + (Exceptions.AppendSerializtionError) failedContext.getError()); + Set failedRowIndices = error.getRowIndexToErrorMessage().keySet(); + for (int failedIndex : failedRowIndices) { + // Convert the message to a TableRow and send it to the failedRows collection. + ByteString protoBytes = failedContext.protoRows.getSerializedRows(failedIndex); + try { + TableRow failedRow = + TableRowToStorageApiProto.tableRowFromMessage( + DynamicMessage.parseFrom(descriptor.get().descriptor, protoBytes)); + new BigQueryStorageApiInsertError( + failedRow, error.getRowIndexToErrorMessage().get(failedIndex)); + o.get(failedRowsTag) + .output( + new BigQueryStorageApiInsertError( + failedRow, error.getRowIndexToErrorMessage().get(failedIndex))); + } catch (InvalidProtocolBufferException e) { + LOG.error("Failed to insert row and could not parse the result!"); + } } - }; - - // RetryManager - Function, RetryType> onError = - failedContexts -> { - // The first context is always the one that fails. - AppendRowsContext failedContext = - Preconditions.checkStateNotNull(Iterables.getFirst(failedContexts, null)); - // Invalidate the StreamWriter and force a new one to be created. - LOG.error( - "Got error " + failedContext.getError() + " closing " + failedContext.streamName); - clearClients.accept(contexts); - appendFailures.inc(); - - boolean explicitStreamFinalized = - failedContext.getError() instanceof StreamFinalizedException; - Throwable error = Preconditions.checkStateNotNull(failedContext.getError()); - Status.Code statusCode = Status.fromThrowable(error).getCode(); - // This means that the offset we have stored does not match the current end of - // the stream in the Storage API. Usually this happens because a crash or a bundle - // failure - // happened after an append but before the worker could checkpoint it's - // state. The records that were appended in a failed bundle will be retried, - // meaning that the unflushed tail of the stream must be discarded to prevent - // duplicates. - boolean offsetMismatch = - statusCode.equals(Code.OUT_OF_RANGE) || statusCode.equals(Code.ALREADY_EXISTS); - // This implies that the stream doesn't exist or has already been finalized. In this - // case we have no choice but to create a new stream. - boolean streamDoesNotExist = - explicitStreamFinalized - || statusCode.equals(Code.INVALID_ARGUMENT) - || statusCode.equals(Code.NOT_FOUND) - || statusCode.equals(Code.FAILED_PRECONDITION); - if (offsetMismatch || streamDoesNotExist) { - appendOffsetFailures.inc(); - LOG.warn( - "Append to " - + failedContext - + " failed with " - + failedContext.getError() - + " Will retry with a new stream"); - // Finalize the stream and clear streamName so a new stream will be created. - o.output( - KV.of(failedContext.streamName, new Operation(failedContext.offset - 1, true))); - // Reinitialize all contexts with the new stream and new offsets. - initializeContexts.accept(failedContexts, true); - - // Offset failures imply that all subsequent parallel appends will also fail. - // Retry them all. - return RetryType.RETRY_ALL_OPERATIONS; + rowsSentToFailedRowsCollection.inc(failedRowIndices.size()); + + // Remove the failed row from the payload, so we retry the batch without the failed + // rows. + ProtoRows.Builder retryRows = ProtoRows.newBuilder(); + for (int i = 0; i < failedContext.protoRows.getSerializedRowsCount(); ++i) { + if (!failedRowIndices.contains(i)) { + ByteString rowBytes = failedContext.protoRows.getSerializedRows(i); + retryRows.addSerializedRows(rowBytes); + } } + failedContext.protoRows = retryRows.build(); + // Since we removed rows, we need to update the insert offsets for all remaining rows. + long offset = failedContext.offset; + for (AppendRowsContext context : failedContexts) { + context.offset = offset; + offset += context.protoRows.getSerializedRowsCount(); + } + streamOffset.write(offset); return RetryType.RETRY_ALL_OPERATIONS; - }; + } - Consumer onSuccess = - context -> { - o.output( - KV.of( - context.streamName, - new Operation(context.offset + context.numRows - 1, false))); - flushesScheduled.inc(protoRows.getSerializedRowsCount()); - }; - - AppendRowsContext context = new AppendRowsContext(element.getKey()); - context.numRows = protoRows.getSerializedRowsCount(); - contexts.add(context); - retryManager.addOperation(run, onError, onSuccess, context); - recordsAppended.inc(protoRows.getSerializedRowsCount()); - appendSizeDistribution.update(context.numRows); - } - initializeContexts.accept(contexts, false); + // Invalidate the StreamWriter and force a new one to be created. + LOG.error( + "Got error " + failedContext.getError() + " closing " + failedContext.streamName); + clearClients.accept(failedContexts); + appendFailures.inc(); + + boolean explicitStreamFinalized = + failedContext.getError() instanceof StreamFinalizedException; + Throwable error = Preconditions.checkStateNotNull(failedContext.getError()); + Status.Code statusCode = Status.fromThrowable(error).getCode(); + // This means that the offset we have stored does not match the current end of + // the stream in the Storage API. Usually this happens because a crash or a bundle + // failure + // happened after an append but before the worker could checkpoint it's + // state. The records that were appended in a failed bundle will be retried, + // meaning that the unflushed tail of the stream must be discarded to prevent + // duplicates. + boolean offsetMismatch = + statusCode.equals(Code.OUT_OF_RANGE) || statusCode.equals(Code.ALREADY_EXISTS); + // This implies that the stream doesn't exist or has already been finalized. In this + // case we have no choice but to create a new stream. + boolean streamDoesNotExist = + explicitStreamFinalized + || statusCode.equals(Code.INVALID_ARGUMENT) + || statusCode.equals(Code.NOT_FOUND) + || statusCode.equals(Code.FAILED_PRECONDITION); + if (offsetMismatch || streamDoesNotExist) { + appendOffsetFailures.inc(); + LOG.warn( + "Append to " + + failedContext + + " failed with " + + failedContext.getError() + + " Will retry with a new stream"); + // Finalize the stream and clear streamName so a new stream will be created. + o.get(flushTag) + .output( + KV.of( + failedContext.streamName, new Operation(failedContext.offset - 1, true))); + // Reinitialize all contexts with the new stream and new offsets. + initializeContexts.accept(failedContexts, true); + + // Offset failures imply that all subsequent parallel appends will also fail. + // Retry them all. + return RetryType.RETRY_ALL_OPERATIONS; + } - try { - retryManager.run(true); - } finally { - // Make sure that all pins are removed. - for (AppendRowsContext context : contexts) { - if (context.client != null) { - runAsyncIgnoreFailure(closeWriterExecutor, context.client::unpin); + return RetryType.RETRY_ALL_OPERATIONS; + }; + + Consumer onSuccess = + context -> { + o.get(flushTag) + .output( + KV.of( + context.streamName, + new Operation( + context.offset + context.protoRows.getSerializedRowsCount() - 1, + false))); + flushesScheduled.inc(context.protoRows.getSerializedRowsCount()); + }; + long maxRequestSize = bigQueryOptions.getStorageWriteApiMaxRequestSize(); + Instant now = Instant.now(); + List contexts = Lists.newArrayList(); + RetryManager retryManager = + new RetryManager<>(Duration.standardSeconds(1), Duration.standardSeconds(10), 1000); + int numAppends = 0; + for (ProtoRows protoRows : messages) { + // Handle the case of a row that is too large. + if (protoRows.getSerializedSize() >= maxRequestSize) { + if (protoRows.getSerializedRowsCount() > 1) { + // TODO(reuvenlax): Is it worth trying to handle this case by splitting the protoRows? + // Given that we split + // the ProtoRows iterable at 2MB and the max request size is 10MB, this scenario seems + // nearly impossible. + LOG.error( + "A request containing more than one row is over the request size limit of " + + maxRequestSize + + ". This is unexpected. All rows in the request will be sent to the failed-rows PCollection."); + } + for (ByteString rowBytes : protoRows.getSerializedRowsList()) { + TableRow failedRow = + TableRowToStorageApiProto.tableRowFromMessage( + DynamicMessage.parseFrom(descriptor.get().descriptor, rowBytes)); + o.get(failedRowsTag) + .output( + new BigQueryStorageApiInsertError( + failedRow, "Row payload too large. Maximum size " + maxRequestSize)); } + } else { + ++numAppends; + // RetryManager + AppendRowsContext context = new AppendRowsContext(element.getKey(), protoRows); + contexts.add(context); + retryManager.addOperation(runOperation, onError, onSuccess, context); + recordsAppended.inc(protoRows.getSerializedRowsCount()); + appendSizeDistribution.update(context.protoRows.getSerializedRowsCount()); } } - appendSplitDistribution.update(numSplits); - java.time.Duration timeElapsed = java.time.Duration.between(now, Instant.now()); - appendLatencyDistribution.update(timeElapsed.toMillis()); + if (numAppends > 0) { + initializeContexts.accept(contexts, false); + try { + retryManager.run(true); + } finally { + // Make sure that all pins are removed. + for (AppendRowsContext context : contexts) { + if (context.client != null) { + runAsyncIgnoreFailure(closeWriterExecutor, context.client::unpin); + } + } + } + appendSplitDistribution.update(numAppends); + + java.time.Duration timeElapsed = java.time.Duration.between(now, Instant.now()); + appendLatencyDistribution.update(timeElapsed.toMillis()); + } idleTimer.offset(streamIdleTime).withNoOutputTimestamp().setRelative(); } @@ -530,15 +643,16 @@ public String toString() { private void finalizeStream( @AlwaysFetched @StateId("streamName") ValueState streamName, @AlwaysFetched @StateId("streamOffset") ValueState streamOffset, - OutputReceiver> o, + MultiOutputReceiver o, org.joda.time.Instant finalizeElementTs) { String stream = MoreObjects.firstNonNull(streamName.read(), ""); if (!Strings.isNullOrEmpty(stream)) { // Finalize the stream long nextOffset = MoreObjects.firstNonNull(streamOffset.read(), 0L); - o.outputWithTimestamp( - KV.of(stream, new Operation(nextOffset - 1, true)), finalizeElementTs); + o.get(flushTag) + .outputWithTimestamp( + KV.of(stream, new Operation(nextOffset - 1, true)), finalizeElementTs); streamName.clear(); streamOffset.clear(); // Make sure that the stream object is closed. @@ -550,7 +664,7 @@ private void finalizeStream( public void onTimer( @AlwaysFetched @StateId("streamName") ValueState streamName, @AlwaysFetched @StateId("streamOffset") ValueState streamOffset, - OutputReceiver> o, + MultiOutputReceiver o, BoundedWindow window) { // Stream is idle - clear it. // Note: this is best effort. We are explicitly emiting a timestamp that is before @@ -566,7 +680,7 @@ public void onTimer( public void onWindowExpiration( @AlwaysFetched @StateId("streamName") ValueState streamName, @AlwaysFetched @StateId("streamOffset") ValueState streamOffset, - OutputReceiver> o, + MultiOutputReceiver o, BoundedWindow window) { // Window is done - usually because the pipeline has been drained. Make sure to clean up // streams so that they are not leaked. diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/DatastoreV1.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/DatastoreV1.java index 06b7c5292fd5..6a7dc725fbc3 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/DatastoreV1.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/datastore/DatastoreV1.java @@ -247,7 +247,7 @@ public class DatastoreV1 { /** * When choosing the number of updates in a single RPC, do not go below this value. The actual * number of entities per request may be lower when we flush for the end of a bundle or if we hit - * {@link DatastoreV1.DATASTORE_BATCH_UPDATE_BYTES_LIMIT}. + * {@link #DATASTORE_BATCH_UPDATE_BYTES_LIMIT}. */ @VisibleForTesting static final int DATASTORE_BATCH_UPDATE_ENTITIES_MIN = 5; @@ -398,7 +398,7 @@ private static long queryLatestStatisticsTimestamp( throw new NoSuchElementException("Datastore total statistics unavailable"); } Entity entity = batch.getEntityResults(0).getEntity(); - return entity.getProperties().get("timestamp").getTimestampValue().getSeconds() * 1000000; + return entity.getPropertiesOrThrow("timestamp").getTimestampValue().getSeconds() * 1000000; } /** @@ -451,7 +451,7 @@ static long getEstimatedSizeBytes( throws DatastoreException { String ourKind = query.getKind(0).getName(); Entity entity = getLatestTableStats(ourKind, namespace, datastore, readTime); - return entity.getProperties().get("entity_bytes").getIntegerValue(); + return entity.getPropertiesOrThrow("entity_bytes").getIntegerValue(); } private static PartitionId.Builder forNamespace(@Nullable String namespace) { @@ -684,7 +684,7 @@ public long getNumEntities( options, v1Options.getProjectId(), v1Options.getLocalhost()); Entity entity = getLatestTableStats(ourKind, namespace, datastore, getReadTime()); - return entity.getProperties().get("count").getIntegerValue(); + return entity.getPropertiesOrThrow("count").getIntegerValue(); } catch (Exception e) { return -1; } diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/BigqueryClient.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/BigqueryClient.java index 6224729aa91a..f5752797acd6 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/BigqueryClient.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/BigqueryClient.java @@ -288,7 +288,8 @@ private QueryResponse getTypedTableRows(QueryResponse response) { /** Performs a query without flattening results. */ @Nonnull - public List queryUnflattened(String query, String projectId, boolean typed) + public List queryUnflattened( + String query, String projectId, boolean typed, boolean useStandardSql) throws IOException, InterruptedException { Random rnd = new Random(System.currentTimeMillis()); String temporaryDatasetId = "_dataflow_temporary_dataset_" + rnd.nextInt(1000000); @@ -308,6 +309,7 @@ public List queryUnflattened(String query, String projectId, boolean t .setFlattenResults(false) .setAllowLargeResults(true) .setDestinationTable(tempTableReference) + .setUseLegacySql(!useStandardSql) .setQuery(query); JobConfiguration jc = new JobConfiguration().setQuery(jcQuery); diff --git a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java index 44f73bd56cb2..948c75cb756d 100644 --- a/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java +++ b/sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/testing/FakeDatasetService.java @@ -32,6 +32,7 @@ import com.google.api.services.bigquery.model.TableSchema; import com.google.cloud.bigquery.storage.v1.AppendRowsResponse; import com.google.cloud.bigquery.storage.v1.BatchCommitWriteStreamsResponse; +import com.google.cloud.bigquery.storage.v1.Exceptions; import com.google.cloud.bigquery.storage.v1.FinalizeWriteStreamResponse; import com.google.cloud.bigquery.storage.v1.FlushRowsResponse; import com.google.cloud.bigquery.storage.v1.ProtoRows; @@ -43,6 +44,7 @@ import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.DynamicMessage; import com.google.protobuf.Timestamp; +import com.google.rpc.Code; import java.io.IOException; import java.io.Serializable; import java.util.HashMap; @@ -50,6 +52,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.regex.Pattern; import javax.annotation.Nullable; import org.apache.beam.sdk.annotations.Internal; @@ -148,6 +151,8 @@ void commit() { } } + Function shouldFailRow = + (Function & Serializable) tr -> false; Map> insertErrors = Maps.newHashMap(); // The counter for the number of insertions performed. @@ -162,6 +167,10 @@ public static void setUp() { } } + public void setShouldFailRow(Function shouldFailRow) { + this.shouldFailRow = shouldFailRow; + } + @Override public Table getTable(TableReference tableRef) throws InterruptedException, IOException { if (tableRef.getProjectId() == null) { @@ -504,6 +513,7 @@ public StreamAppendClient getStreamAppendClient( @Override public ApiFuture appendRows(long offset, ProtoRows rows) throws Exception { + AppendRowsResponse.Builder responseBuilder = AppendRowsResponse.newBuilder(); synchronized (FakeDatasetService.class) { Stream stream = writeStreams.get(streamName); if (stream == null) { @@ -511,18 +521,32 @@ public ApiFuture appendRows(long offset, ProtoRows rows) } List tableRows = Lists.newArrayListWithExpectedSize(rows.getSerializedRowsCount()); - for (ByteString bytes : rows.getSerializedRowsList()) { + Map rowIndexToErrorMessage = Maps.newHashMap(); + for (int i = 0; i < rows.getSerializedRowsCount(); ++i) { + ByteString bytes = rows.getSerializedRows(i); DynamicMessage msg = DynamicMessage.parseFrom(protoDescriptor, bytes); if (msg.getUnknownFields() != null && !msg.getUnknownFields().asMap().isEmpty()) { throw new RuntimeException("Unknown fields set in append! " + msg.getUnknownFields()); } - tableRows.add( + TableRow tableRow = TableRowToStorageApiProto.tableRowFromMessage( - DynamicMessage.parseFrom(protoDescriptor, bytes))); + DynamicMessage.parseFrom(protoDescriptor, bytes)); + if (shouldFailRow.apply(tableRow)) { + rowIndexToErrorMessage.put(i, "Failing row " + tableRow.toPrettyString()); + } + tableRows.add(tableRow); + } + if (!rowIndexToErrorMessage.isEmpty()) { + return ApiFutures.immediateFailedFuture( + new Exceptions.AppendSerializtionError( + Code.INVALID_ARGUMENT.getNumber(), + "Append serialization failed for writer: " + streamName, + stream.streamName, + rowIndexToErrorMessage)); } stream.appendRows(offset, tableRows); } - return ApiFutures.immediateFuture(AppendRowsResponse.newBuilder().build()); + return ApiFutures.immediateFuture(responseBuilder.build()); } @Override diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOWriteTest.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOWriteTest.java index 7f529bfa3489..1e1749e8569a 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOWriteTest.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIOWriteTest.java @@ -64,6 +64,7 @@ import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Function; import java.util.function.LongFunction; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -2583,11 +2584,15 @@ public void testStorageApiErrors() throws Exception { TableRow goodNested = new TableRow().set("number", "42"); TableRow badNested = new TableRow().set("number", "nAn"); + final String failValue = "failme"; List goodRows = ImmutableList.of( new TableRow().set("name", "n1").set("number", "1"), + new TableRow().set("name", failValue).set("number", "1"), new TableRow().set("name", "n2").set("number", "2"), - new TableRow().set("name", "parent1").set("nested", goodNested)); + new TableRow().set("name", failValue).set("number", "2"), + new TableRow().set("name", "parent1").set("nested", goodNested), + new TableRow().set("name", failValue).set("number", "1")); List badRows = ImmutableList.of( // Unknown field. @@ -2614,6 +2619,11 @@ public void testStorageApiErrors() throws Exception { // Invalid nested row new TableRow().set("name", "parent2").set("nested", badNested)); + Function shouldFailRow = + (Function & Serializable) + tr -> tr.containsKey("name") && tr.get("name").equals(failValue); + fakeDatasetService.setShouldFailRow(shouldFailRow); + WriteResult result = p.apply(Create.of(Iterables.concat(goodRows, badRows))) .apply( @@ -2632,12 +2642,17 @@ public void testStorageApiErrors() throws Exception { .apply( MapElements.into(TypeDescriptor.of(TableRow.class)) .via(BigQueryStorageApiInsertError::getRow)); - PAssert.that(deadRows).containsInAnyOrder(badRows); + + PAssert.that(deadRows) + .containsInAnyOrder( + Iterables.concat(badRows, Iterables.filter(goodRows, shouldFailRow::apply))); p.run(); assertThat( fakeDatasetService.getAllRows("project-id", "dataset-id", "table"), - containsInAnyOrder(Iterables.toArray(goodRows, TableRow.class))); + containsInAnyOrder( + Iterables.toArray( + Iterables.filter(goodRows, r -> !shouldFailRow.apply(r)), TableRow.class))); } @Test diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryNestedRecordsIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryNestedRecordsIT.java index 698ef660293c..b85dc62c5fe9 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryNestedRecordsIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryNestedRecordsIT.java @@ -97,12 +97,13 @@ private static void runPipeline(Options options) throws Exception { TableRow queryUnflattened = bigQueryClient - .queryUnflattened(options.getInput(), bigQueryOptions.getProject(), true) + .queryUnflattened(options.getInput(), bigQueryOptions.getProject(), true, false) .get(0); TableRow queryUnflattenable = bigQueryClient - .queryUnflattened(options.getUnflattenableInput(), bigQueryOptions.getProject(), true) + .queryUnflattened( + options.getUnflattenableInput(), bigQueryOptions.getProject(), true, false) .get(0); // Verify that the results are the same. diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkFailedRowsIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkFailedRowsIT.java new file mode 100644 index 000000000000..465bebbf1389 --- /dev/null +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/StorageApiSinkFailedRowsIT.java @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.gcp.bigquery; + +import static org.hamcrest.MatcherAssert.assertThat; + +import com.google.api.services.bigquery.model.Table; +import com.google.api.services.bigquery.model.TableFieldSchema; +import com.google.api.services.bigquery.model.TableReference; +import com.google.api.services.bigquery.model.TableRow; +import com.google.api.services.bigquery.model.TableSchema; +import java.io.IOException; +import java.util.List; +import org.apache.beam.sdk.Pipeline; +import org.apache.beam.sdk.extensions.gcp.options.GcpOptions; +import org.apache.beam.sdk.io.gcp.testing.BigqueryClient; +import org.apache.beam.sdk.testing.PAssert; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.MapElements; +import org.apache.beam.sdk.values.PCollection; +import org.apache.beam.sdk.values.TypeDescriptor; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableList; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Iterables; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Lists; +import org.hamcrest.Matchers; +import org.joda.time.Duration; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Integration test for failed-rows handling when using the storage API. */ +@RunWith(Parameterized.class) +public class StorageApiSinkFailedRowsIT { + @Parameterized.Parameters + public static Iterable data() { + return ImmutableList.of( + new Object[] {true, false, false}, + new Object[] {false, true, false}, + new Object[] {false, false, true}, + new Object[] {true, false, true}); + } + + @Parameterized.Parameter(0) + public boolean useStreamingExactlyOnce; + + @Parameterized.Parameter(1) + public boolean useAtLeastOnce; + + @Parameterized.Parameter(2) + public boolean useBatch; + + private static final Logger LOG = LoggerFactory.getLogger(StorageApiSinkFailedRowsIT.class); + private static final BigqueryClient BQ_CLIENT = new BigqueryClient("StorageApiSinkFailedRowsIT"); + private static final String PROJECT = + TestPipeline.testingPipelineOptions().as(GcpOptions.class).getProject(); + private static final String BIG_QUERY_DATASET_ID = + "storage_api_sink_failed_rows" + System.nanoTime(); + + private static final List FIELDS = + ImmutableList.builder() + .add(new TableFieldSchema().setType("STRING").setName("str")) + .add(new TableFieldSchema().setType("INT64").setName("i64")) + .add(new TableFieldSchema().setType("DATE").setName("date")) + .add(new TableFieldSchema().setType("STRING").setMaxLength(1L).setName("strone")) + .add(new TableFieldSchema().setType("BYTES").setName("bytes")) + .add(new TableFieldSchema().setType("JSON").setName("json")) + .add( + new TableFieldSchema() + .setType("STRING") + .setMaxLength(1L) + .setMode("REPEATED") + .setName("stronearray")) + .build(); + + private static final TableSchema BASE_TABLE_SCHEMA = + new TableSchema() + .setFields( + ImmutableList.builder() + .addAll(FIELDS) + .add(new TableFieldSchema().setType("STRUCT").setFields(FIELDS).setName("inner")) + .build()); + + private static final byte[] BIG_BYTES = new byte[11 * 1024 * 1024]; + + private BigQueryIO.Write.Method getMethod() { + return useAtLeastOnce + ? BigQueryIO.Write.Method.STORAGE_API_AT_LEAST_ONCE + : BigQueryIO.Write.Method.STORAGE_WRITE_API; + } + + @BeforeClass + public static void setUpTestEnvironment() throws IOException, InterruptedException { + // Create one BQ dataset for all test cases. + BQ_CLIENT.createNewDataset(PROJECT, BIG_QUERY_DATASET_ID); + } + + @AfterClass + public static void cleanup() { + LOG.info("Start to clean up tables and datasets."); + BQ_CLIENT.deleteDataset(PROJECT, BIG_QUERY_DATASET_ID); + } + + @Test + public void testSchemaMismatchCaughtByBeam() throws IOException, InterruptedException { + String tableSpec = createTable(BASE_TABLE_SCHEMA); + TableRow good1 = new TableRow().set("str", "foo").set("i64", "42"); + TableRow good2 = new TableRow().set("str", "foo").set("i64", "43"); + Iterable goodRows = + ImmutableList.of( + good1.clone().set("inner", new TableRow()), + good2.clone().set("inner", new TableRow()), + new TableRow().set("inner", good1), + new TableRow().set("inner", good2)); + + TableRow bad1 = new TableRow().set("str", "foo").set("i64", "baad"); + TableRow bad2 = new TableRow().set("str", "foo").set("i64", "42").set("unknown", "foobar"); + Iterable badRows = + ImmutableList.of( + bad1, bad2, new TableRow().set("inner", bad1), new TableRow().set("inner", bad2)); + + runPipeline( + getMethod(), + useStreamingExactlyOnce, + tableSpec, + Iterables.concat(goodRows, badRows), + badRows); + assertGoodRowsWritten(tableSpec, goodRows); + } + + @Test + public void testInvalidRowCaughtByBigquery() throws IOException, InterruptedException { + String tableSpec = createTable(BASE_TABLE_SCHEMA); + + TableRow good1 = + new TableRow() + .set("str", "foo") + .set("i64", "42") + .set("date", "2022-08-16") + .set("stronearray", Lists.newArrayList()); + TableRow good2 = + new TableRow().set("str", "foo").set("i64", "43").set("stronearray", Lists.newArrayList()); + Iterable goodRows = + ImmutableList.of( + good1.clone().set("inner", new TableRow().set("stronearray", Lists.newArrayList())), + good2.clone().set("inner", new TableRow().set("stronearray", Lists.newArrayList())), + new TableRow().set("inner", good1).set("stronearray", Lists.newArrayList()), + new TableRow().set("inner", good2).set("stronearray", Lists.newArrayList())); + + TableRow bad1 = new TableRow().set("str", "foo").set("i64", "42").set("date", "10001-08-16"); + TableRow bad2 = new TableRow().set("str", "foo").set("i64", "42").set("strone", "ab"); + TableRow bad3 = new TableRow().set("str", "foo").set("i64", "42").set("json", "BAADF00D"); + TableRow bad4 = + new TableRow() + .set("str", "foo") + .set("i64", "42") + .set("stronearray", Lists.newArrayList("toolong")); + TableRow bad5 = new TableRow().set("bytes", BIG_BYTES); + Iterable badRows = + ImmutableList.of( + bad1, + bad2, + bad3, + bad4, + bad5, + new TableRow().set("inner", bad1), + new TableRow().set("inner", bad2), + new TableRow().set("inner", bad3)); + + runPipeline( + getMethod(), + useStreamingExactlyOnce, + tableSpec, + Iterables.concat(goodRows, badRows), + badRows); + assertGoodRowsWritten(tableSpec, goodRows); + } + + private static String createTable(TableSchema tableSchema) + throws IOException, InterruptedException { + String table = "table" + System.nanoTime(); + BQ_CLIENT.deleteTable(PROJECT, BIG_QUERY_DATASET_ID, table); + BQ_CLIENT.createNewTable( + PROJECT, + BIG_QUERY_DATASET_ID, + new Table() + .setSchema(tableSchema) + .setTableReference( + new TableReference() + .setTableId(table) + .setDatasetId(BIG_QUERY_DATASET_ID) + .setProjectId(PROJECT))); + return PROJECT + "." + BIG_QUERY_DATASET_ID + "." + table; + } + + private void assertGoodRowsWritten(String tableSpec, Iterable goodRows) + throws IOException, InterruptedException { + TableRow queryResponse = + Iterables.getOnlyElement( + BQ_CLIENT.queryUnflattened( + String.format("SELECT COUNT(*) FROM %s", tableSpec), PROJECT, true, true)); + int numRowsWritten = Integer.parseInt((String) queryResponse.get("f0_")); + if (useAtLeastOnce) { + assertThat(numRowsWritten, Matchers.greaterThanOrEqualTo(Iterables.size(goodRows))); + } else { + assertThat(numRowsWritten, Matchers.equalTo(Iterables.size(goodRows))); + } + } + + private static void runPipeline( + BigQueryIO.Write.Method method, + boolean triggered, + String tableSpec, + Iterable tableRows, + Iterable expectedFailedRows) { + Pipeline p = Pipeline.create(); + + BigQueryIO.Write write = + BigQueryIO.writeTableRows() + .to(tableSpec) + .withSchema(BASE_TABLE_SCHEMA) + .withMethod(method) + .withCreateDisposition(BigQueryIO.Write.CreateDisposition.CREATE_NEVER); + if (method == BigQueryIO.Write.Method.STORAGE_WRITE_API) { + write = write.withNumStorageWriteApiStreams(1); + if (triggered) { + write = write.withTriggeringFrequency(Duration.standardSeconds(1)); + } + } + PCollection input = p.apply("Create test cases", Create.of(tableRows)); + if (triggered) { + input = input.setIsBoundedInternal(PCollection.IsBounded.UNBOUNDED); + } + WriteResult result = input.apply("Write using Storage Write API", write); + + PCollection failedRows = + result + .getFailedStorageApiInserts() + .apply( + MapElements.into(TypeDescriptor.of(TableRow.class)) + .via(BigQueryStorageApiInsertError::getRow)); + + PAssert.that(failedRows).containsInAnyOrder(expectedFailedRows); + + p.run().waitUntilFinish(); + } +} diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProtoIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProtoIT.java index b2d9e04ffe22..5f488da0210b 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProtoIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/bigquery/TableRowToStorageApiProtoIT.java @@ -337,7 +337,8 @@ public void testBaseTableRow() throws IOException, InterruptedException { runPipeline(tableSpec, Collections.singleton(BASE_TABLE_ROW)); List actualTableRows = - BQ_CLIENT.queryUnflattened(String.format("SELECT * FROM [%s]", tableSpec), PROJECT, true); + BQ_CLIENT.queryUnflattened( + String.format("SELECT * FROM %s", tableSpec), PROJECT, true, true); assertEquals(1, actualTableRows.size()); assertEquals(BASE_TABLE_ROW_EXPECTED, actualTableRows.get(0)); @@ -362,7 +363,8 @@ public void testNestedRichTypesAndNull() throws IOException, InterruptedExceptio runPipeline(tableSpec, Collections.singleton(tableRow)); List actualTableRows = - BQ_CLIENT.queryUnflattened(String.format("SELECT * FROM [%s]", tableSpec), PROJECT, true); + BQ_CLIENT.queryUnflattened( + String.format("SELECT * FROM %s", tableSpec), PROJECT, true, true); assertEquals(1, actualTableRows.size()); assertEquals(BASE_TABLE_ROW_EXPECTED, actualTableRows.get(0).get("nestedValue1")); @@ -391,7 +393,7 @@ private static String createTable(TableSchema tableSchema) .setTableId(table) .setDatasetId(BIG_QUERY_DATASET_ID) .setProjectId(PROJECT))); - return PROJECT + ":" + BIG_QUERY_DATASET_ID + "." + table; + return PROJECT + "." + BIG_QUERY_DATASET_ID + "." + table; } private static void runPipeline(String tableSpec, Iterable tableRows) { diff --git a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamIT.java b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamIT.java index 202179bd9152..de837a173bcd 100644 --- a/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamIT.java +++ b/sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/it/SpannerChangeStreamIT.java @@ -26,6 +26,7 @@ import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Key; import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Options; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Statement; @@ -33,6 +34,10 @@ import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.apache.beam.sdk.io.gcp.spanner.SpannerConfig; import org.apache.beam.sdk.io.gcp.spanner.SpannerIO; import org.apache.beam.sdk.io.gcp.spanner.changestreams.model.DataChangeRecord; @@ -40,10 +45,12 @@ import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.Filter; import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.values.PCollection; import org.apache.commons.lang3.tuple.Pair; import org.joda.time.Instant; +import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -141,6 +148,83 @@ public void testReadSpannerChangeStream() { assertMetadataTableHasBeenDropped(); } + @Test + public void testReadSpannerChangeStreamFilteredByTransactionTag() { + // Defines how many rows are going to be inserted / updated / deleted in the test + final int numRows = 5; + // Inserts numRows rows and uses the first commit timestamp as the startAt for reading the + // change stream + final Pair insertTimestamps = insertRows(numRows); + final Timestamp startAt = insertTimestamps.getLeft(); + // Updates the created rows + updateRows(numRows); + // Delete the created rows and uses the last commit timestamp as the endAt for reading the + // change stream + final Pair deleteTimestamps = deleteRows(numRows); + final Timestamp endAt = deleteTimestamps.getRight(); + + final SpannerConfig spannerConfig = + SpannerConfig.create() + .withProjectId(projectId) + .withInstanceId(instanceId) + .withDatabaseId(databaseId); + + // Filter records to only those from transactions with tag "app=beam;action=update" + final PCollection tokens = + pipeline + .apply( + SpannerIO.readChangeStream() + .withSpannerConfig(spannerConfig) + .withChangeStreamName(changeStreamName) + .withMetadataDatabase(databaseId) + .withMetadataTable(metadataTableName) + .withInclusiveStartAt(startAt) + .withInclusiveEndAt(endAt)) + .apply( + Filter.by( + record -> + !record.isSystemTransaction() + && record + .getTransactionTag() + .equalsIgnoreCase("app=beam;action=update"))) + .apply(ParDo.of(new ModsToString())); + + // Each row is composed by the following data + // + PAssert.that(tokens) + .satisfies( + stringTokens -> { + Set setTokens = + StreamSupport.stream(stringTokens.spliterator(), false) + .collect(Collectors.toSet()); + Assert.assertTrue( + Stream.of( + "UPDATE,1,First Name 1,Last Name 1,Updated First Name 1,Updated Last Name 1", + "UPDATE,2,First Name 2,Last Name 2,Updated First Name 2,Updated Last Name 2", + "UPDATE,3,First Name 3,Last Name 3,Updated First Name 3,Updated Last Name 3", + "UPDATE,4,First Name 4,Last Name 4,Updated First Name 4,Updated Last Name 4", + "UPDATE,5,First Name 5,Last Name 5,Updated First Name 5,Updated Last Name 5") + .allMatch(setTokens::contains)); + Assert.assertTrue( + Stream.of( + "INSERT,1,null,null,First Name 1,Last Name 1", + "INSERT,2,null,null,First Name 2,Last Name 2", + "INSERT,3,null,null,First Name 3,Last Name 3", + "INSERT,4,null,null,First Name 4,Last Name 4", + "INSERT,5,null,null,First Name 5,Last Name 5", + "DELETE,1,Updated First Name 1,Updated Last Name 1,null,null", + "DELETE,2,Updated First Name 2,Updated Last Name 2,null,null", + "DELETE,3,Updated First Name 3,Updated Last Name 3,null,null", + "DELETE,4,Updated First Name 4,Updated Last Name 4,null,null", + "DELETE,5,Updated First Name 5,Updated Last Name 5,null,null") + .noneMatch(setTokens::contains)); + return null; + }); + pipeline.run().waitUntilFinish(); + + assertMetadataTableHasBeenDropped(); + } + private static void assertMetadataTableHasBeenDropped() { try (ResultSet resultSet = databaseClient @@ -187,34 +271,43 @@ private static Pair deleteRows(int n) { } private static Timestamp insertRow(int singerId) { - return databaseClient.write( - Collections.singletonList( - Mutation.newInsertBuilder(changeStreamTableName) - .set("SingerId") - .to(singerId) - .set("FirstName") - .to("First Name " + singerId) - .set("LastName") - .to("Last Name " + singerId) - .build())); + return databaseClient + .writeWithOptions( + Collections.singletonList( + Mutation.newInsertBuilder(changeStreamTableName) + .set("SingerId") + .to(singerId) + .set("FirstName") + .to("First Name " + singerId) + .set("LastName") + .to("Last Name " + singerId) + .build()), + Options.tag("app=beam;action=insert")) + .getCommitTimestamp(); } private static Timestamp updateRow(int singerId) { - return databaseClient.write( - Collections.singletonList( - Mutation.newUpdateBuilder(changeStreamTableName) - .set("SingerId") - .to(singerId) - .set("FirstName") - .to("Updated First Name " + singerId) - .set("LastName") - .to("Updated Last Name " + singerId) - .build())); + return databaseClient + .writeWithOptions( + Collections.singletonList( + Mutation.newUpdateBuilder(changeStreamTableName) + .set("SingerId") + .to(singerId) + .set("FirstName") + .to("Updated First Name " + singerId) + .set("LastName") + .to("Updated Last Name " + singerId) + .build()), + Options.tag("app=beam;action=update")) + .getCommitTimestamp(); } private static Timestamp deleteRow(int singerId) { - return databaseClient.write( - Collections.singletonList(Mutation.delete(changeStreamTableName, Key.of(singerId)))); + return databaseClient + .writeWithOptions( + Collections.singletonList(Mutation.delete(changeStreamTableName, Key.of(singerId))), + Options.tag("app=beam;action=delete")) + .getCommitTimestamp(); } private static class ModsToString extends DoFn { diff --git a/sdks/java/io/hadoop-format/build.gradle b/sdks/java/io/hadoop-format/build.gradle index 702d37175d53..ec70824a5ab8 100644 --- a/sdks/java/io/hadoop-format/build.gradle +++ b/sdks/java/io/hadoop-format/build.gradle @@ -40,14 +40,6 @@ hadoopVersions.each {kv -> configurations.create("hadoopVersion$kv.key")} def elastic_search_version = "7.12.0" -configurations.create("sparkRunner") -configurations.sparkRunner { - // Ban certain dependencies to prevent a StackOverflow within Spark - // because JUL -> SLF4J -> JUL, and similarly JDK14 -> SLF4J -> JDK14 - exclude group: "org.slf4j", module: "jul-to-slf4j" - exclude group: "org.slf4j", module: "slf4j-jdk14" -} - // Ban dependencies from the test runtime classpath configurations.testRuntimeClasspath { // Prevent a StackOverflow because of wiring LOG4J -> SLF4J -> LOG4J @@ -115,15 +107,6 @@ dependencies { testRuntimeOnly library.java.slf4j_jdk14 testRuntimeOnly project(path: ":runners:direct-java", configuration: "shadow") - delegate.add("sparkRunner", project(path: ":sdks:java:io:hadoop-format", configuration: "testRuntimeMigration")) - - sparkRunner project(path: ":examples:java", configuration: "testRuntimeMigration") - sparkRunner project(path: ":examples:java:twitter", configuration: "testRuntimeMigration") - sparkRunner project(":runners:spark:2") - sparkRunner project(":sdks:java:io:hadoop-file-system") - sparkRunner library.java.spark_streaming - sparkRunner library.java.spark_core - hadoopVersions.each {kv -> "hadoopVersion$kv.key" "org.apache.hadoop:hadoop-common:$kv.value" "hadoopVersion$kv.key" "org.apache.hadoop:hadoop-mapreduce-client-core:$kv.value" @@ -169,29 +152,6 @@ task createTargetDirectoryForCassandra() { } test.dependsOn createTargetDirectoryForCassandra -def runnerClass = "org.apache.beam.runners.spark.TestSparkRunner" -task sparkRunner(type: Test) { - group = "Verification" - def beamTestPipelineOptions = [ - "--project=hadoop-format", - "--tempRoot=/tmp/hadoop-format/", - "--streaming=false", - "--runner=" + runnerClass, - "--enableSparkMetricSinks=false", - ] - classpath = configurations.sparkRunner - include "**/HadoopFormatIOSequenceFileTest.class" - useJUnit { - includeCategories 'org.apache.beam.sdk.testing.ValidatesRunner' - } - forkEvery 1 - maxParallelForks 4 - systemProperty "spark.ui.enabled", "false" - systemProperty "spark.ui.showConsoleProgress", "false" - systemProperty "beam.spark.test.reuseSparkContext", "true" - systemProperty "beamTestPipelineOptions", JsonOutput.toJson(beamTestPipelineOptions) -} - task hadoopVersionsTest(group: "Verification") { description = "Runs Hadoop format tests with different Hadoop versions" dependsOn createTaskNames(hadoopVersions, "Test") diff --git a/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/JdbcUtil.java b/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/JdbcUtil.java index 01894c621ca4..5a675eb5f9f1 100644 --- a/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/JdbcUtil.java +++ b/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/JdbcUtil.java @@ -193,7 +193,7 @@ static JdbcIO.PreparedStatementSetCaller getPreparedStatementSetCaller( String logicalTypeName = fieldType.getLogicalType().getIdentifier(); // Special case of Timestamp and Numeric which are logical types in Portable framework - // but has their own fieldType in Java. + // but have their own fieldType in Java. if (logicalTypeName.equals(MicrosInstant.IDENTIFIER)) { // Process timestamp of MicrosInstant kind, which should only be passed from other type // systems such as SQL and other Beam SDKs. @@ -207,50 +207,46 @@ static JdbcIO.PreparedStatementSetCaller getPreparedStatementSetCaller( return (element, ps, i, fieldWithIndex) -> { ps.setBigDecimal(i + 1, element.getDecimal(fieldWithIndex.getIndex())); }; - } - - JDBCType jdbcType = JDBCType.valueOf(logicalTypeName); - switch (jdbcType) { - case DATE: - return (element, ps, i, fieldWithIndex) -> { - ReadableDateTime value = element.getDateTime(fieldWithIndex.getIndex()); - ps.setDate( - i + 1, - value == null - ? null - : new Date( - getDateOrTimeOnly(value.toDateTime(), true).getTime().getTime())); - }; - case TIME: - return (element, ps, i, fieldWithIndex) -> { - ReadableDateTime value = element.getDateTime(fieldWithIndex.getIndex()); - ps.setTime( - i + 1, - value == null - ? null - : new Time( - getDateOrTimeOnly( - element.getDateTime(fieldWithIndex.getIndex()).toDateTime(), - false) - .getTime() - .getTime())); - }; - case TIMESTAMP_WITH_TIMEZONE: - return (element, ps, i, fieldWithIndex) -> { - ReadableDateTime value = element.getDateTime(fieldWithIndex.getIndex()); - if (value == null) { - ps.setTimestamp(i + 1, null); - } else { - Calendar calendar = withTimestampAndTimezone(value.toDateTime()); - ps.setTimestamp(i + 1, new Timestamp(calendar.getTime().getTime()), calendar); - } - }; - case OTHER: - return (element, ps, i, fieldWithIndex) -> - ps.setObject( - i + 1, element.getValue(fieldWithIndex.getIndex()), java.sql.Types.OTHER); - default: - return getPreparedStatementSetCaller(fieldType.getLogicalType().getBaseType()); + } else if (logicalTypeName.equals("DATE")) { + return (element, ps, i, fieldWithIndex) -> { + ReadableDateTime value = element.getDateTime(fieldWithIndex.getIndex()); + ps.setDate( + i + 1, + value == null + ? null + : new Date(getDateOrTimeOnly(value.toDateTime(), true).getTime().getTime())); + }; + } else if (logicalTypeName.equals("TIME")) { + return (element, ps, i, fieldWithIndex) -> { + ReadableDateTime value = element.getDateTime(fieldWithIndex.getIndex()); + ps.setTime( + i + 1, + value == null + ? null + : new Time( + getDateOrTimeOnly( + element.getDateTime(fieldWithIndex.getIndex()).toDateTime(), + false) + .getTime() + .getTime())); + }; + } else if (logicalTypeName.equals("TIMESTAMP_WITH_TIMEZONE")) { + return (element, ps, i, fieldWithIndex) -> { + ReadableDateTime value = element.getDateTime(fieldWithIndex.getIndex()); + if (value == null) { + ps.setTimestamp(i + 1, null); + } else { + Calendar calendar = withTimestampAndTimezone(value.toDateTime()); + ps.setTimestamp(i + 1, new Timestamp(calendar.getTime().getTime()), calendar); + } + }; + } else if (logicalTypeName.equals("OTHER")) { + return (element, ps, i, fieldWithIndex) -> + ps.setObject( + i + 1, element.getValue(fieldWithIndex.getIndex()), java.sql.Types.OTHER); + } else { + // generic beam logic type (such as portable logical types) + return getPreparedStatementSetCaller(fieldType.getLogicalType().getBaseType()); } } default: diff --git a/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/LogicalTypes.java b/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/LogicalTypes.java index e61295889590..f21aa5b6f299 100644 --- a/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/LogicalTypes.java +++ b/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/LogicalTypes.java @@ -17,20 +17,20 @@ */ package org.apache.beam.sdk.io.jdbc; -import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument; - import java.sql.JDBCType; import java.time.Instant; -import java.util.Arrays; import java.util.Objects; -import org.apache.beam.repackaged.core.org.apache.commons.lang3.StringUtils; import org.apache.beam.sdk.annotations.Experimental; import org.apache.beam.sdk.annotations.Experimental.Kind; import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; +import org.apache.beam.sdk.schemas.logicaltypes.FixedBytes; import org.apache.beam.sdk.schemas.logicaltypes.FixedPrecisionNumeric; +import org.apache.beam.sdk.schemas.logicaltypes.FixedString; import org.apache.beam.sdk.schemas.logicaltypes.PassThroughLogicalType; import org.apache.beam.sdk.schemas.logicaltypes.UuidLogicalType; +import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; +import org.apache.beam.sdk.schemas.logicaltypes.VariableString; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.annotations.VisibleForTesting; import org.checkerframework.checker.nullness.qual.Nullable; @@ -78,22 +78,22 @@ class LogicalTypes { @VisibleForTesting static Schema.FieldType fixedLengthString(JDBCType jdbcType, int length) { - return Schema.FieldType.logicalType(FixedLengthString.of(jdbcType.getName(), length)); + return Schema.FieldType.logicalType(FixedString.of(jdbcType.getName(), length)); } @VisibleForTesting static Schema.FieldType fixedLengthBytes(JDBCType jdbcType, int length) { - return Schema.FieldType.logicalType(FixedLengthBytes.of(jdbcType.getName(), length)); + return Schema.FieldType.logicalType(FixedBytes.of(jdbcType.getName(), length)); } @VisibleForTesting static Schema.FieldType variableLengthString(JDBCType jdbcType, int length) { - return Schema.FieldType.logicalType(VariableLengthString.of(jdbcType.getName(), length)); + return Schema.FieldType.logicalType(VariableString.of(jdbcType.getName(), length)); } @VisibleForTesting static Schema.FieldType variableLengthBytes(JDBCType jdbcType, int length) { - return Schema.FieldType.logicalType(VariableLengthBytes.of(jdbcType.getName(), length)); + return Schema.FieldType.logicalType(VariableBytes.of(jdbcType.getName(), length)); } @VisibleForTesting @@ -101,6 +101,21 @@ static Schema.FieldType numeric(int precision, int scale) { return Schema.FieldType.logicalType(FixedPrecisionNumeric.of(precision, scale)); } + /** + * Returns a {@link FixedBytes}, or {@link VariableBytes} when length is Integer.MAX_VALUE. + * + *

    In some database, certain variable bytes type (e.g. bytea in postgresql) also returns BINARY + * jdbc type. This helper method make BINARY(Integer.MAX_VALUE) returns a variable bytes logical + * type thus avoid out-of-memory due to padding in fixed-length bytes. + */ + static Schema.LogicalType fixedOrVariableBytes(String name, int length) { + if (length == Integer.MAX_VALUE) { + return VariableBytes.of(name, length); + } else { + return FixedBytes.of(name, length); + } + } + /** Base class for JDBC logical types. */ abstract static class JdbcLogicalType implements Schema.LogicalType { protected final String identifier; @@ -164,88 +179,4 @@ public int hashCode() { return Objects.hash(identifier, baseType, argument); } } - - /** Fixed length string types such as CHAR. */ - static final class FixedLengthString extends JdbcLogicalType { - private final int length; - - static FixedLengthString of(String identifier, int length) { - return new FixedLengthString(identifier, length); - } - - private FixedLengthString(String identifier, int length) { - super(identifier, FieldType.INT32, Schema.FieldType.STRING, length); - this.length = length; - } - - @Override - public String toInputType(String base) { - checkArgument(base == null || base.length() <= length); - return StringUtils.rightPad(base, length); - } - } - - /** Fixed length byte types such as BINARY. */ - static final class FixedLengthBytes extends JdbcLogicalType { - private final int length; - - static FixedLengthBytes of(String identifier, int length) { - return new FixedLengthBytes(identifier, length); - } - - private FixedLengthBytes(String identifier, int length) { - super(identifier, FieldType.INT32, Schema.FieldType.BYTES, length); - this.length = length; - } - - @Override - public byte[] toInputType(byte[] base) { - checkArgument(base == null || base.length <= length); - if (base == null || base.length == length) { - return base; - } else { - return Arrays.copyOf(base, length); - } - } - } - - /** Variable length string types such as VARCHAR and LONGVARCHAR. */ - static final class VariableLengthString extends JdbcLogicalType { - private final int maxLength; - - static VariableLengthString of(String identifier, int maxLength) { - return new VariableLengthString(identifier, maxLength); - } - - private VariableLengthString(String identifier, int maxLength) { - super(identifier, FieldType.INT32, Schema.FieldType.STRING, maxLength); - this.maxLength = maxLength; - } - - @Override - public String toInputType(String base) { - checkArgument(base == null || base.length() <= maxLength); - return base; - } - } - - /** Variable length bytes types such as VARBINARY and LONGVARBINARY. */ - static final class VariableLengthBytes extends JdbcLogicalType { - private final int maxLength; - - static VariableLengthBytes of(String identifier, int maxLength) { - return new VariableLengthBytes(identifier, maxLength); - } - - private VariableLengthBytes(String identifier, int maxLength) { - super(identifier, FieldType.INT32, Schema.FieldType.BYTES, maxLength); - this.maxLength = maxLength; - } - - @Override - public byte[] toInputType(byte[] base) { - checkArgument(base == null || base.length <= maxLength); - return base; - } - } } diff --git a/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/SchemaUtil.java b/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/SchemaUtil.java index a34ef7847ef9..b466564cfeca 100644 --- a/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/SchemaUtil.java +++ b/sdks/java/io/jdbc/src/main/java/org/apache/beam/sdk/io/jdbc/SchemaUtil.java @@ -53,6 +53,9 @@ import org.apache.beam.sdk.schemas.Schema; import org.apache.beam.sdk.schemas.Schema.FieldType; import org.apache.beam.sdk.schemas.logicaltypes.FixedPrecisionNumeric; +import org.apache.beam.sdk.schemas.logicaltypes.FixedString; +import org.apache.beam.sdk.schemas.logicaltypes.VariableBytes; +import org.apache.beam.sdk.schemas.logicaltypes.VariableString; import org.apache.beam.sdk.values.Row; import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableMap; import org.joda.time.DateTime; @@ -73,7 +76,6 @@ class SchemaUtil { interface ResultSetFieldExtractor extends Serializable { Object extract(ResultSet rs, Integer index) throws SQLException; } - // ResultSetExtractors for primitive schema types (excluding arrays, structs and logical types). private static final EnumMap RESULTSET_FIELD_EXTRACTORS = @@ -114,13 +116,13 @@ private static BeamFieldConverter jdbcTypeToBeamFieldConverter( case BIGINT: return beamFieldOfType(Schema.FieldType.INT64); case BINARY: - return beamLogicalField(BINARY.getName(), LogicalTypes.FixedLengthBytes::of); + return beamLogicalField(BINARY.getName(), LogicalTypes::fixedOrVariableBytes); case BIT: return beamFieldOfType(LogicalTypes.JDBC_BIT_TYPE); case BOOLEAN: return beamFieldOfType(Schema.FieldType.BOOLEAN); case CHAR: - return beamLogicalField(CHAR.getName(), LogicalTypes.FixedLengthString::of); + return beamLogicalField(CHAR.getName(), FixedString::of); case DATE: return beamFieldOfType(LogicalTypes.JDBC_DATE_TYPE); case DECIMAL: @@ -132,17 +134,17 @@ private static BeamFieldConverter jdbcTypeToBeamFieldConverter( case INTEGER: return beamFieldOfType(Schema.FieldType.INT32); case LONGNVARCHAR: - return beamLogicalField(LONGNVARCHAR.getName(), LogicalTypes.VariableLengthString::of); + return beamLogicalField(LONGNVARCHAR.getName(), VariableString::of); case LONGVARBINARY: - return beamLogicalField(LONGVARBINARY.getName(), LogicalTypes.VariableLengthBytes::of); + return beamLogicalField(LONGVARBINARY.getName(), VariableBytes::of); case LONGVARCHAR: - return beamLogicalField(LONGVARCHAR.getName(), LogicalTypes.VariableLengthString::of); + return beamLogicalField(LONGVARCHAR.getName(), VariableString::of); case NCHAR: - return beamLogicalField(NCHAR.getName(), LogicalTypes.FixedLengthString::of); + return beamLogicalField(NCHAR.getName(), FixedString::of); case NUMERIC: return beamLogicalNumericField(); case NVARCHAR: - return beamLogicalField(NVARCHAR.getName(), LogicalTypes.VariableLengthString::of); + return beamLogicalField(NVARCHAR.getName(), VariableString::of); case REAL: return beamFieldOfType(Schema.FieldType.FLOAT); case SMALLINT: @@ -156,9 +158,9 @@ private static BeamFieldConverter jdbcTypeToBeamFieldConverter( case TINYINT: return beamFieldOfType(Schema.FieldType.BYTE); case VARBINARY: - return beamLogicalField(VARBINARY.getName(), LogicalTypes.VariableLengthBytes::of); + return beamLogicalField(VARBINARY.getName(), VariableBytes::of); case VARCHAR: - return beamLogicalField(VARCHAR.getName(), LogicalTypes.VariableLengthString::of); + return beamLogicalField(VARCHAR.getName(), VariableString::of); case BLOB: return beamFieldOfType(FieldType.BYTES); case CLOB: @@ -290,26 +292,17 @@ private static ResultSetFieldExtractor createLogicalTypeExtracto final Schema.LogicalType fieldType) { String logicalTypeName = fieldType.getIdentifier(); - JDBCType underlyingType; - if (Objects.equals(fieldType, LogicalTypes.JDBC_UUID_TYPE.getLogicalType())) { return OBJECT_EXTRACTOR; - } else if (Objects.equals(logicalTypeName, FixedPrecisionNumeric.IDENTIFIER)) { - underlyingType = JDBCType.NUMERIC; + } else if (logicalTypeName.equals("DATE")) { + return DATE_EXTRACTOR; + } else if (logicalTypeName.equals("TIME")) { + return TIME_EXTRACTOR; + } else if (logicalTypeName.equals("TIMESTAMP_EXTRACTOR")) { + return TIMESTAMP_EXTRACTOR; } else { - underlyingType = JDBCType.valueOf(logicalTypeName); - } - - switch (underlyingType) { - case DATE: - return DATE_EXTRACTOR; - case TIME: - return TIME_EXTRACTOR; - case TIMESTAMP_WITH_TIMEZONE: - return TIMESTAMP_EXTRACTOR; - default: - ResultSetFieldExtractor extractor = createFieldExtractor(fieldType.getBaseType()); - return (rs, index) -> fieldType.toInputType((BaseT) extractor.extract(rs, index)); + ResultSetFieldExtractor extractor = createFieldExtractor(fieldType.getBaseType()); + return (rs, index) -> fieldType.toInputType((BaseT) extractor.extract(rs, index)); } } diff --git a/sdks/java/io/neo4j/src/test/java/org/apache/beam/sdk/io/neo4j/Neo4jIOIT.java b/sdks/java/io/neo4j/src/test/java/org/apache/beam/sdk/io/neo4j/Neo4jIOIT.java index fc8d712b6cca..e5f606642a5f 100644 --- a/sdks/java/io/neo4j/src/test/java/org/apache/beam/sdk/io/neo4j/Neo4jIOIT.java +++ b/sdks/java/io/neo4j/src/test/java/org/apache/beam/sdk/io/neo4j/Neo4jIOIT.java @@ -70,7 +70,7 @@ public static void setup() throws Exception { new Neo4jContainer<>(DockerImageName.parse("neo4j").withTag(Neo4jTestUtil.NEO4J_VERSION)) .withStartupAttempts(1) .withAdminPassword(Neo4jTestUtil.NEO4J_PASSWORD) - .withEnv("NEO4J_dbms_default_listen_address", "0.0.0.0") + .withEnv("dbms_default_listen_address", "0.0.0.0") .withNetworkAliases(Neo4jTestUtil.NEO4J_NETWORK_ALIAS) .withSharedMemorySize(256 * 1024 * 1024L); // 256MB @@ -88,7 +88,7 @@ public static void setup() throws Exception { Neo4jTestUtil.executeOnNeo4j( containerHostname, containerPort, - "CREATE CONSTRAINT something_id_unique ON (n:Something) ASSERT n.id IS UNIQUE", + "CREATE CONSTRAINT something_id_unique FOR (n:Something) REQUIRE n.id IS UNIQUE", true); } diff --git a/sdks/java/io/parquet/src/main/java/org/apache/beam/sdk/io/parquet/ParquetIO.java b/sdks/java/io/parquet/src/main/java/org/apache/beam/sdk/io/parquet/ParquetIO.java index ee67989c5f42..433a53a20fe1 100644 --- a/sdks/java/io/parquet/src/main/java/org/apache/beam/sdk/io/parquet/ParquetIO.java +++ b/sdks/java/io/parquet/src/main/java/org/apache/beam/sdk/io/parquet/ParquetIO.java @@ -46,9 +46,7 @@ import org.apache.beam.sdk.coders.StringUtf8Coder; import org.apache.beam.sdk.io.FileIO; import org.apache.beam.sdk.io.FileIO.ReadableFile; -import org.apache.beam.sdk.io.fs.ResourceId; import org.apache.beam.sdk.io.hadoop.SerializableConfiguration; -import org.apache.beam.sdk.io.parquet.ParquetIO.ReadFiles.ReadFn; import org.apache.beam.sdk.io.parquet.ParquetIO.ReadFiles.SplitReadFn; import org.apache.beam.sdk.io.range.OffsetRange; import org.apache.beam.sdk.options.ValueProvider; @@ -77,7 +75,6 @@ import org.apache.parquet.filter2.compat.FilterCompat; import org.apache.parquet.filter2.compat.FilterCompat.Filter; import org.apache.parquet.hadoop.ParquetFileReader; -import org.apache.parquet.hadoop.ParquetReader; import org.apache.parquet.hadoop.ParquetWriter; import org.apache.parquet.hadoop.api.InitContext; import org.apache.parquet.hadoop.api.ReadSupport; @@ -136,18 +133,8 @@ * PCollection output = files.apply(ParquetIO.readFiles(SCHEMA)); * }

    * - *

    Splittable reading can be enabled by allowing the use of Splittable DoFn. It initially split - * the files into blocks of 64MB and may dynamically split further for higher read efficiency. It - * can be enabled by using {@link ParquetIO.Read#withSplit()}. - * - *

    For example: - * - *

    {@code
    - * PCollection records = pipeline.apply(ParquetIO.read(SCHEMA).from("/foo/bar").withSplit());
    - * ...
    - * }
    - * - *

    Since Beam version 2.35.0 the splittable reading is enabled by default. + *

    ParquetIO leverages splittable reading by using Splittable DoFn. It initially splits the files + * into the blocks of 64MB and may dynamically split further for higher read efficiency. * *

    Reading with projection can be enabled with the projection schema as following. Splittable * reading is enabled when reading with projection. The projection_schema contains only the column @@ -271,7 +258,6 @@ public static Read read(Schema schema) { return new AutoValue_ParquetIO_Read.Builder() .setSchema(schema) .setInferBeamSchema(false) - .setSplittable(true) .build(); } @@ -283,7 +269,6 @@ public static ReadFiles readFiles(Schema schema) { return new AutoValue_ParquetIO_ReadFiles.Builder() .setSchema(schema) .setInferBeamSchema(false) - .setSplittable(true) .build(); } @@ -292,10 +277,7 @@ public static ReadFiles readFiles(Schema schema) { * pattern) and converts to user defined type using provided parseFn. */ public static Parse parseGenericRecords(SerializableFunction parseFn) { - return new AutoValue_ParquetIO_Parse.Builder() - .setParseFn(parseFn) - .setSplittable(true) - .build(); + return new AutoValue_ParquetIO_Parse.Builder().setParseFn(parseFn).build(); } /** @@ -304,10 +286,7 @@ public static Parse parseGenericRecords(SerializableFunction ParseFiles parseFilesGenericRecords( SerializableFunction parseFn) { - return new AutoValue_ParquetIO_ParseFiles.Builder() - .setParseFn(parseFn) - .setSplittable(true) - .build(); + return new AutoValue_ParquetIO_ParseFiles.Builder().setParseFn(parseFn).build(); } /** Implementation of {@link #read(Schema)}. */ @@ -328,8 +307,6 @@ public abstract static class Read extends PTransform filepattern); abstract Builder setSchema(Schema schema); @@ -367,7 +342,6 @@ public Read from(String filepattern) { public Read withProjection(Schema projectionSchema, Schema encoderSchema) { return toBuilder() .setProjectionSchema(projectionSchema) - .setSplittable(true) .setEncoderSchema(encoderSchema) .build(); } @@ -389,28 +363,6 @@ public Read withBeamSchemas(boolean inferBeamSchema) { return toBuilder().setInferBeamSchema(inferBeamSchema).build(); } - /** - * Enable the Splittable reading. - * - * @deprecated as of version 2.35.0. Splittable reading is enabled by default. - */ - @Deprecated - public Read withSplit() { - return toBuilder().setSplittable(true).build(); - } - - /** - * Disable the Splittable reading. - * - * @deprecated This method may currently be used to opt-out of the default, splittable, - * behavior. However, this will be removed in a future release assuming no issues are - * discovered. - */ - @Deprecated - public Read withoutSplit() { - return toBuilder().setSplittable(false).build(); - } - /** * Define the Avro data model; see {@link AvroParquetReader.Builder#withDataModel(GenericData)}. */ @@ -431,10 +383,8 @@ public PCollection expand(PBegin input) { ReadFiles readFiles = readFiles(getSchema()) .withBeamSchemas(getInferBeamSchema()) - .withAvroDataModel(getAvroDataModel()); - if (isSplittable()) { - readFiles = readFiles.withSplit().withProjection(getProjectionSchema(), getEncoderSchema()); - } + .withAvroDataModel(getAvroDataModel()) + .withProjection(getProjectionSchema(), getEncoderSchema()); if (getConfiguration() != null) { readFiles = readFiles.withConfiguration(getConfiguration().get()); } @@ -452,7 +402,6 @@ public void populateDisplayData(DisplayData.Builder builder) { .add( DisplayData.item("inferBeamSchema", getInferBeamSchema()) .withLabel("Infer Beam Schema")) - .add(DisplayData.item("splittable", isSplittable())) .addIfNotNull(DisplayData.item("projectionSchema", String.valueOf(getProjectionSchema()))) .addIfNotNull(DisplayData.item("avroDataModel", String.valueOf(getAvroDataModel()))); if (this.getConfiguration() != null) { @@ -477,8 +426,6 @@ public abstract static class Parse extends PTransform> abstract @Nullable SerializableConfiguration getConfiguration(); - abstract boolean isSplittable(); - abstract Builder toBuilder(); @AutoValue.Builder @@ -491,8 +438,6 @@ abstract static class Builder { abstract Builder setConfiguration(SerializableConfiguration configuration); - abstract Builder setSplittable(boolean splittable); - abstract Parse build(); } @@ -521,28 +466,6 @@ public Parse withConfiguration(Configuration configuration) { return toBuilder().setConfiguration(new SerializableConfiguration(configuration)).build(); } - /** - * Enable the Splittable reading. - * - * @deprecated as of version 2.35.0. Splittable reading is enabled by default. - */ - @Deprecated - public Parse withSplit() { - return toBuilder().setSplittable(true).build(); - } - - /** - * Disable the Splittable reading. - * - * @deprecated This method may currently be used to opt-out of the default, splittable, - * behavior. However, this will be removed in a future release assuming no issues are - * discovered. - */ - @Deprecated - public Parse withoutSplit() { - return toBuilder().setSplittable(false).build(); - } - @Override public PCollection expand(PBegin input) { checkNotNull(getFilepattern(), "Filepattern cannot be null."); @@ -554,7 +477,6 @@ public PCollection expand(PBegin input) { parseFilesGenericRecords(getParseFn()) .toBuilder() .setCoder(getCoder()) - .setSplittable(isSplittable()) .setConfiguration(getConfiguration()) .build()); } @@ -565,7 +487,6 @@ public void populateDisplayData(DisplayData.Builder builder) { builder .addIfNotNull( DisplayData.item("filePattern", getFilepattern()).withLabel("Input File Pattern")) - .add(DisplayData.item("splittable", isSplittable())) .add(DisplayData.item("parseFn", getParseFn().getClass()).withLabel("Parse function")); if (this.getCoder() != null) { builder.add(DisplayData.item("coder", getCoder().getClass())); @@ -592,8 +513,6 @@ public abstract static class ParseFiles abstract @Nullable SerializableConfiguration getConfiguration(); - abstract boolean isSplittable(); - abstract Builder toBuilder(); @AutoValue.Builder @@ -604,8 +523,6 @@ abstract static class Builder { abstract Builder setConfiguration(SerializableConfiguration configuration); - abstract Builder setSplittable(boolean split); - abstract ParseFiles build(); } @@ -626,43 +543,19 @@ public ParseFiles withConfiguration(Configuration configuration) { return toBuilder().setConfiguration(new SerializableConfiguration(configuration)).build(); } - /** - * Enable the Splittable reading. - * - * @deprecated as of version 2.35.0. Splittable reading is enabled by default. - */ - @Deprecated - public ParseFiles withSplit() { - return toBuilder().setSplittable(true).build(); - } - - /** - * Disable the Splittable reading. - * - * @deprecated This method may currently be used to opt-out of the default, splittable, - * behavior. However, this will be removed in a future release assuming no issues are - * discovered. - */ - @Deprecated - public ParseFiles withoutSplit() { - return toBuilder().setSplittable(false).build(); - } - @Override public PCollection expand(PCollection input) { checkArgument(!isGenericRecordOutput(), "Parse can't be used for reading as GenericRecord."); return input - .apply(ParDo.of(buildFileReadingFn())) + .apply(ParDo.of(new SplitReadFn<>(null, null, getParseFn(), getConfiguration()))) .setCoder(inferCoder(input.getPipeline().getCoderRegistry())); } @Override public void populateDisplayData(DisplayData.Builder builder) { super.populateDisplayData(builder); - builder - .add(DisplayData.item("splittable", isSplittable())) - .add(DisplayData.item("parseFn", getParseFn().getClass()).withLabel("Parse function")); + builder.add(DisplayData.item("parseFn", getParseFn().getClass()).withLabel("Parse function")); if (this.getCoder() != null) { builder.add(DisplayData.item("coder", getCoder().getClass())); } @@ -676,13 +569,6 @@ public void populateDisplayData(DisplayData.Builder builder) { } } - /** Returns Splittable or normal Parquet file reading DoFn. */ - private DoFn buildFileReadingFn() { - return isSplittable() - ? new SplitReadFn<>(null, null, getParseFn(), getConfiguration()) - : new ReadFn<>(null, getParseFn(), getConfiguration()); - } - /** Returns true if expected output is {@code PCollection}. */ private boolean isGenericRecordOutput() { String outputType = TypeDescriptors.outputOf(getParseFn()).getType().getTypeName(); @@ -735,8 +621,6 @@ public abstract static class ReadFiles abstract boolean getInferBeamSchema(); - abstract boolean isSplittable(); - abstract Builder toBuilder(); @AutoValue.Builder @@ -753,8 +637,6 @@ abstract static class Builder { abstract Builder setInferBeamSchema(boolean inferBeamSchema); - abstract Builder setSplittable(boolean split); - abstract ReadFiles build(); } @@ -769,7 +651,6 @@ public ReadFiles withProjection(Schema projectionSchema, Schema encoderSchema) { return toBuilder() .setProjectionSchema(projectionSchema) .setEncoderSchema(encoderSchema) - .setSplittable(true) .build(); } @@ -790,32 +671,18 @@ public ReadFiles withBeamSchemas(boolean inferBeamSchema) { return toBuilder().setInferBeamSchema(inferBeamSchema).build(); } - /** - * Enable the Splittable reading. - * - * @deprecated as of version 2.35.0. Splittable reading is enabled by default. - */ - @Deprecated - public ReadFiles withSplit() { - return toBuilder().setSplittable(true).build(); - } - - /** - * Disable the Splittable reading. - * - * @deprecated This method may currently be used to opt-out of the default, splittable, - * behavior. However, this will be removed in a future release assuming no issues are - * discovered. - */ - @Deprecated - public ReadFiles withoutSplit() { - return toBuilder().setSplittable(false).build(); - } - @Override public PCollection expand(PCollection input) { checkNotNull(getSchema(), "Schema can not be null"); - return input.apply(ParDo.of(getReaderFn())).setCoder(getCollectionCoder()); + return input + .apply( + ParDo.of( + new SplitReadFn<>( + getAvroDataModel(), + getProjectionSchema(), + GenericRecordPassthroughFn.create(), + getConfiguration()))) + .setCoder(getCollectionCoder()); } @Override @@ -826,7 +693,6 @@ public void populateDisplayData(DisplayData.Builder builder) { .add( DisplayData.item("inferBeamSchema", getInferBeamSchema()) .withLabel("Infer Beam Schema")) - .add(DisplayData.item("splittable", isSplittable())) .addIfNotNull(DisplayData.item("projectionSchema", String.valueOf(getProjectionSchema()))) .addIfNotNull(DisplayData.item("avroDataModel", String.valueOf(getAvroDataModel()))); if (this.getConfiguration() != null) { @@ -839,26 +705,13 @@ public void populateDisplayData(DisplayData.Builder builder) { } } - /** Returns Parquet file reading function based on {@link #isSplittable()}. */ - private DoFn getReaderFn() { - return isSplittable() - ? new SplitReadFn<>( - getAvroDataModel(), - getProjectionSchema(), - GenericRecordPassthroughFn.create(), - getConfiguration()) - : new ReadFn<>( - getAvroDataModel(), GenericRecordPassthroughFn.create(), getConfiguration()); - } - /** * Returns {@link org.apache.beam.sdk.schemas.SchemaCoder} when using Beam schemas, {@link * AvroCoder} when not using Beam schema. */ @Experimental(Kind.SCHEMAS) private Coder getCollectionCoder() { - Schema coderSchema = - getProjectionSchema() != null && isSplittable() ? getEncoderSchema() : getSchema(); + Schema coderSchema = getProjectionSchema() != null ? getEncoderSchema() : getSchema(); return getInferBeamSchema() ? AvroUtils.schemaCoder(coderSchema) : AvroCoder.of(coderSchema); } @@ -1126,59 +979,6 @@ public Progress getProgress() { } } - /** - * @deprecated as of version 2.35.0. Splittable reading with {@link SplitReadFn} should be used - * instead. - */ - @Deprecated - static class ReadFn extends DoFn { - - private final Class modelClass; - - private final SerializableFunction parseFn; - - private final SerializableConfiguration configuration; - - ReadFn( - GenericData model, - SerializableFunction parseFn, - SerializableConfiguration configuration) { - this.modelClass = model != null ? model.getClass() : null; - this.parseFn = checkNotNull(parseFn, "GenericRecord parse function is null"); - this.configuration = configuration; - } - - @ProcessElement - public void processElement(@Element ReadableFile file, OutputReceiver receiver) - throws Exception { - if (!file.getMetadata().isReadSeekEfficient()) { - ResourceId filename = file.getMetadata().resourceId(); - throw new RuntimeException(String.format("File has to be seekable: %s", filename)); - } - - SeekableByteChannel seekableByteChannel = file.openSeekable(); - - AvroParquetReader.Builder builder = - (AvroParquetReader.Builder) - AvroParquetReader.builder( - new BeamParquetInputFile(seekableByteChannel)) - .withConf(SerializableConfiguration.newConfiguration(configuration)); - if (modelClass != null) { - // all GenericData implementations have a static get method - builder = builder.withDataModel(buildModelObject(modelClass)); - } - - try (ParquetReader reader = builder.build()) { - GenericRecord read; - while ((read = reader.read()) != null) { - receiver.output(parseFn.apply(read)); - } - } - - seekableByteChannel.close(); - } - } - private static class BeamParquetInputFile implements InputFile { private final SeekableByteChannel seekableByteChannel; diff --git a/sdks/java/io/parquet/src/test/java/org/apache/beam/sdk/io/parquet/ParquetIOTest.java b/sdks/java/io/parquet/src/test/java/org/apache/beam/sdk/io/parquet/ParquetIOTest.java index 5576be2a59f0..6dd67e3e511c 100644 --- a/sdks/java/io/parquet/src/test/java/org/apache/beam/sdk/io/parquet/ParquetIOTest.java +++ b/sdks/java/io/parquet/src/test/java/org/apache/beam/sdk/io/parquet/ParquetIOTest.java @@ -184,7 +184,6 @@ public void testWriteAndRead() { mainPipeline.run().waitUntilFinish(); ParquetIO.Read read = ParquetIO.read(SCHEMA); - assertTrue(read.isSplittable()); PCollection readBack = readPipeline.apply(read.from(temporaryFolder.getRoot().getAbsolutePath() + "/*")); @@ -210,27 +209,6 @@ public void testWriteWithRowGroupSizeAndRead() { readPipeline.run().waitUntilFinish(); } - @Test - public void testWriteAndReadWithoutSplit() { - List records = generateGenericRecords(1000); - - mainPipeline - .apply(Create.of(records).withCoder(AvroCoder.of(SCHEMA))) - .apply( - FileIO.write() - .via(ParquetIO.sink(SCHEMA)) - .to(temporaryFolder.getRoot().getAbsolutePath())); - mainPipeline.run().waitUntilFinish(); - - PCollection readBackWithSplit = - readPipeline.apply( - ParquetIO.read(SCHEMA) - .from(temporaryFolder.getRoot().getAbsolutePath() + "/*") - .withoutSplit()); - PAssert.that(readBackWithSplit).containsInAnyOrder(records); - readPipeline.run().waitUntilFinish(); - } - @Test public void testWriteAndReadWithBeamSchema() { List records = generateGenericRecords(1000); @@ -255,7 +233,7 @@ public void testWriteAndReadWithBeamSchema() { } @Test - public void testWriteAndReadFilesAsJsonForWithSplitForUnknownSchema() { + public void testWriteAndReadFilesAsJsonForUnknownSchema() { List records = generateGenericRecords(1000); mainPipeline @@ -266,13 +244,12 @@ public void testWriteAndReadFilesAsJsonForWithSplitForUnknownSchema() { .to(temporaryFolder.getRoot().getAbsolutePath())); mainPipeline.run().waitUntilFinish(); - PCollection readBackAsJsonWithSplit = + PCollection readBackAsJson = readPipeline.apply( ParquetIO.parseGenericRecords(ParseGenericRecordAsJsonFn.create()) - .from(temporaryFolder.getRoot().getAbsolutePath() + "/*") - .withSplit()); + .from(temporaryFolder.getRoot().getAbsolutePath() + "/*")); - PAssert.that(readBackAsJsonWithSplit).containsInAnyOrder(convertRecordsToJson(records)); + PAssert.that(readBackAsJson).containsInAnyOrder(convertRecordsToJson(records)); readPipeline.run().waitUntilFinish(); } @@ -281,7 +258,6 @@ public void testWriteAndReadFiles() { List records = generateGenericRecords(1000); ParquetIO.ReadFiles readFiles = ParquetIO.readFiles(SCHEMA); - assertTrue(readFiles.isSplittable()); PCollection writeThenRead = mainPipeline @@ -308,7 +284,6 @@ public void testReadFilesAsJsonForUnknownSchemaFiles() { ParquetIO.ParseFiles parseFiles = ParquetIO.parseFilesGenericRecords(ParseGenericRecordAsJsonFn.create()); - assertTrue(parseFiles.isSplittable()); PCollection writeThenRead = mainPipeline @@ -401,7 +376,6 @@ public void testReadDisplayData() { DisplayData.from( ParquetIO.read(SCHEMA) .from("foo.parquet") - .withSplit() .withProjection(REQUESTED_SCHEMA, SCHEMA) .withAvroDataModel(GenericData.get()) .withConfiguration(configuration)); @@ -409,7 +383,6 @@ public void testReadDisplayData() { assertThat(displayData, hasDisplayItem("filePattern", "foo.parquet")); assertThat(displayData, hasDisplayItem("schema", SCHEMA.toString())); assertThat(displayData, hasDisplayItem("inferBeamSchema", false)); - assertThat(displayData, hasDisplayItem("splittable", true)); assertThat(displayData, hasDisplayItem("projectionSchema", REQUESTED_SCHEMA.toString())); assertThat(displayData, hasDisplayItem("avroDataModel", GenericData.get().toString())); assertThat(displayData, hasDisplayItem("parquet.foo", "foo")); @@ -445,29 +418,6 @@ public void testWriteAndReadUsingReflectDataSchemaWithoutDataModelThrowsExceptio readPipeline.run().waitUntilFinish(); } - @Test(expected = org.apache.beam.sdk.Pipeline.PipelineExecutionException.class) - public void testWriteAndReadWithSplitUsingReflectDataSchemaWithoutDataModelThrowsException() { - Schema testRecordSchema = ReflectData.get().getSchema(TestRecord.class); - - List records = generateGenericRecords(1000); - mainPipeline - .apply(Create.of(records).withCoder(AvroCoder.of(testRecordSchema))) - .apply( - FileIO.write() - .via(ParquetIO.sink(testRecordSchema)) - .to(temporaryFolder.getRoot().getAbsolutePath())); - mainPipeline.run().waitUntilFinish(); - - PCollection readBack = - readPipeline.apply( - ParquetIO.read(testRecordSchema) - .withSplit() - .from(temporaryFolder.getRoot().getAbsolutePath() + "/*")); - - PAssert.that(readBack).containsInAnyOrder(records); - readPipeline.run().waitUntilFinish(); - } - @Test public void testWriteAndReadUsingReflectDataSchemaWithDataModel() { Schema testRecordSchema = ReflectData.get().getSchema(TestRecord.class); @@ -491,30 +441,6 @@ public void testWriteAndReadUsingReflectDataSchemaWithDataModel() { readPipeline.run().waitUntilFinish(); } - @Test - public void testWriteAndReadWithSplitUsingReflectDataSchemaWithDataModel() { - Schema testRecordSchema = ReflectData.get().getSchema(TestRecord.class); - - List records = generateGenericRecords(1000); - mainPipeline - .apply(Create.of(records).withCoder(AvroCoder.of(testRecordSchema))) - .apply( - FileIO.write() - .via(ParquetIO.sink(testRecordSchema)) - .to(temporaryFolder.getRoot().getAbsolutePath())); - mainPipeline.run().waitUntilFinish(); - - PCollection readBack = - readPipeline.apply( - ParquetIO.read(testRecordSchema) - .withSplit() - .withAvroDataModel(GenericData.get()) - .from(temporaryFolder.getRoot().getAbsolutePath() + "/*")); - - PAssert.that(readBack).containsInAnyOrder(records); - readPipeline.run().waitUntilFinish(); - } - @Test public void testWriteAndReadUsingGenericDataSchemaWithDataModel() { Schema schema = new Schema.Parser().parse(SCHEMA_STRING); @@ -538,30 +464,6 @@ public void testWriteAndReadUsingGenericDataSchemaWithDataModel() { readPipeline.run().waitUntilFinish(); } - @Test - public void testWriteAndReadwithSplitUsingGenericDataSchemaWithDataModel() { - Schema schema = new Schema.Parser().parse(SCHEMA_STRING); - - List records = generateGenericRecords(1000); - mainPipeline - .apply(Create.of(records).withCoder(AvroCoder.of(schema))) - .apply( - FileIO.write() - .via(ParquetIO.sink(schema).withAvroDataModel(GenericData.get())) - .to(temporaryFolder.getRoot().getAbsolutePath())); - mainPipeline.run().waitUntilFinish(); - - PCollection readBack = - readPipeline.apply( - ParquetIO.read(schema) - .withSplit() - .withAvroDataModel(GenericData.get()) - .from(temporaryFolder.getRoot().getAbsolutePath() + "/*")); - - PAssert.that(readBack).containsInAnyOrder(records); - readPipeline.run().waitUntilFinish(); - } - @Test public void testWriteAndReadWithConfiguration() { List records = generateGenericRecords(10); @@ -583,8 +485,7 @@ public void testWriteAndReadWithConfiguration() { readPipeline.apply( ParquetIO.read(SCHEMA) .from(temporaryFolder.getRoot().getAbsolutePath() + "/*") - .withConfiguration(configuration) - .withSplit()); + .withConfiguration(configuration)); PAssert.that(readBack).containsInAnyOrder(expectedRecords); readPipeline.run().waitUntilFinish(); } diff --git a/sdks/java/io/sparkreceiver/README.md b/sdks/java/io/sparkreceiver/README.md new file mode 100644 index 000000000000..6ce48efd58fe --- /dev/null +++ b/sdks/java/io/sparkreceiver/README.md @@ -0,0 +1,38 @@ + + +SparkReceiverIO contains I/O transforms which allow you to read messages from Spark Receiver (org.apache.spark.streaming.receiver.Receiver). + +## Dependencies + +To use SparkReceiverIO you must first add a dependency on `beam-sdks-java-io-sparkreceiver`. + +```maven + + org.apache.beam + beam-sdks-java-io-sparkreceiver + ... + +``` + +## Documentation + +The documentation is maintained in JavaDoc for SparkReceiverIO class. It includes +usage examples and primary concepts. +- [SparkReceiverIO.java](src/main/java/org/apache/beam/sdk/io/sparkreceiver/SparkReceiverIO.java) diff --git a/sdks/java/io/sparkreceiver/build.gradle b/sdks/java/io/sparkreceiver/build.gradle index 8d4b96f298cd..52c6a6340499 100644 --- a/sdks/java/io/sparkreceiver/build.gradle +++ b/sdks/java/io/sparkreceiver/build.gradle @@ -33,9 +33,10 @@ ext.summary = """Apache Beam SDK provides a simple, Java-based interface for streaming integration with CDAP plugins.""" configurations.all { - exclude group: 'org.slf4j', module: 'slf4j-log4j12' + exclude group: 'ch.qos.logback', module: 'logback-classic' exclude group: 'org.slf4j', module: 'slf4j-jdk14' - exclude group: 'org.slf4j', module: 'slf4j-simple' + exclude group: 'org.slf4j', module: 'slf4j-log4j12' + exclude group: 'org.slf4j', module: 'slf4j-reload4j' } dependencies { @@ -47,8 +48,11 @@ dependencies { implementation library.java.vendored_guava_26_0_jre implementation project(path: ":sdks:java:core", configuration: "shadow") compileOnly "org.scala-lang:scala-library:2.11.12" - testImplementation project(path: ":sdks:java:io:cdap", configuration: "testRuntimeMigration") testImplementation library.java.junit + testImplementation library.java.testcontainers_rabbitmq testImplementation project(path: ":runners:direct-java", configuration: "shadow") - testImplementation project(path: ":examples:java", configuration: "testRuntimeMigration") + testImplementation project(":sdks:java:io:synthetic") + testImplementation project(path: ":sdks:java:io:common", configuration: "testRuntimeMigration") + testImplementation project(path: ":sdks:java:testing:test-utils", configuration: "testRuntimeMigration") + testImplementation "com.rabbitmq:amqp-client:5.16.0" } diff --git a/sdks/java/io/sparkreceiver/src/main/java/org/apache/beam/sdk/io/sparkreceiver/ReadFromSparkReceiverWithOffsetDoFn.java b/sdks/java/io/sparkreceiver/src/main/java/org/apache/beam/sdk/io/sparkreceiver/ReadFromSparkReceiverWithOffsetDoFn.java index c51a5168ce39..8b2fdcb01ad1 100644 --- a/sdks/java/io/sparkreceiver/src/main/java/org/apache/beam/sdk/io/sparkreceiver/ReadFromSparkReceiverWithOffsetDoFn.java +++ b/sdks/java/io/sparkreceiver/src/main/java/org/apache/beam/sdk/io/sparkreceiver/ReadFromSparkReceiverWithOffsetDoFn.java @@ -19,6 +19,9 @@ import static org.apache.beam.sdk.util.Preconditions.checkStateNotNull; +import java.math.BigDecimal; +import java.math.MathContext; +import java.nio.ByteBuffer; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; @@ -30,15 +33,19 @@ import org.apache.beam.sdk.transforms.splittabledofn.ManualWatermarkEstimator; import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker; import org.apache.beam.sdk.transforms.splittabledofn.RestrictionTracker; +import org.apache.beam.sdk.transforms.splittabledofn.SplitResult; import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimator; import org.apache.beam.sdk.transforms.splittabledofn.WatermarkEstimators; import org.apache.beam.sdk.transforms.windowing.BoundedWindow; +import org.apache.commons.lang3.SerializationUtils; import org.apache.spark.SparkConf; import org.apache.spark.streaming.receiver.Receiver; import org.checkerframework.checker.nullness.qual.Nullable; import org.joda.time.Instant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import scala.collection.Iterator; +import scala.collection.mutable.ArrayBuffer; /** * A SplittableDoFn which reads from {@link Receiver} that implements {@link HasOffset}. By default, @@ -56,7 +63,7 @@ class ReadFromSparkReceiverWithOffsetDoFn extends DoFn { LoggerFactory.getLogger(ReadFromSparkReceiverWithOffsetDoFn.class); /** Constant waiting time after the {@link Receiver} starts. Required to prepare for polling */ - private static final int START_POLL_TIMEOUT_MS = 1000; + private static final int START_POLL_TIMEOUT_MS = 2000; private final SerializableFunction> createWatermarkEstimatorFn; @@ -104,10 +111,68 @@ public double getSize(@Element byte[] element, @Restriction OffsetRange offsetRa return restrictionTracker(element, offsetRange).getProgress().getWorkRemaining(); } + /** + * {@link OffsetRangeTracker} that performs basic split only in {@link + * OffsetRangeTracker#checkDone}. This behavior allows reading from primary range until resume, + * and then split to {alreadyReadRange, residualRange}. + */ + private static class CustomOffsetRangeTracker extends OffsetRangeTracker { + + public CustomOffsetRangeTracker(OffsetRange range) { + super(range); + } + + @SuppressWarnings("nullness") // Base method can return null + @Override + public SplitResult trySplit(double fractionOfRemainder) { + if (lastAttemptedOffset != null) { + if (range.getTo() == Long.MAX_VALUE) { + // Do not split, just use primary range + return null; + } else { + // Need to add residual range + OffsetRange res = new OffsetRange(range.getTo(), Long.MAX_VALUE); + this.range = new OffsetRange(range.getFrom(), range.getTo()); + return SplitResult.of(range, res); + } + } + // Basic split logic when lastAttemptedOffset is null + + // Convert to BigDecimal in computation to prevent overflow, which may result in loss of + // precision. + BigDecimal cur = + BigDecimal.valueOf(range.getFrom()).subtract(BigDecimal.ONE, MathContext.DECIMAL128); + // split = cur + max(1, (range.getTo() - cur) * fractionOfRemainder) + BigDecimal splitPos = + cur.add( + BigDecimal.valueOf(range.getTo()) + .subtract(cur, MathContext.DECIMAL128) + .multiply(BigDecimal.valueOf(fractionOfRemainder), MathContext.DECIMAL128) + .max(BigDecimal.ONE), + MathContext.DECIMAL128); + + long split = splitPos.longValue(); + if (split >= range.getTo()) { + return null; + } + OffsetRange res = new OffsetRange(split, range.getTo()); + this.range = new OffsetRange(range.getFrom(), split); + return SplitResult.of(range, res); + } + + @Override + public void checkDone() throws IllegalStateException { + if (lastAttemptedOffset != null && range.getTo() == Long.MAX_VALUE) { + // Perform basic split + super.trySplit(0); + } + } + } + @NewTracker public OffsetRangeTracker restrictionTracker( @Element byte[] element, @Restriction OffsetRange restriction) { - return new OffsetRangeTracker(restriction); + return new CustomOffsetRangeTracker(restriction); } @GetRestrictionCoder @@ -141,15 +206,46 @@ public boolean hasRecords() { @Override public void start(Receiver sparkReceiver) { this.sparkReceiver = sparkReceiver; - try { - new WrappedSupervisor( - sparkReceiver, - new SparkConf(), - objects -> { - V record = (V) objects[0]; - recordsQueue.offer(record); + + final SerializableFunction storeFn = + (input) -> { + if (input == null) { return null; - }); + } + /* + Use only [0] element - data. + The other elements are not needed because they are related to Spark environment options. + */ + Object data = input[0]; + + if (data instanceof ByteBuffer) { + final ByteBuffer byteBuffer = ((ByteBuffer) data).asReadOnlyBuffer(); + final byte[] bytes = new byte[byteBuffer.limit()]; + byteBuffer.get(bytes); + final V record = SerializationUtils.deserialize(bytes); + recordsQueue.offer(record); + } else if (data instanceof Iterator) { + final Iterator iterator = (Iterator) data; + while (iterator.hasNext()) { + V record = iterator.next(); + recordsQueue.offer(record); + } + } else if (data instanceof ArrayBuffer) { + final ArrayBuffer arrayBuffer = (ArrayBuffer) data; + final Iterator iterator = arrayBuffer.iterator(); + while (iterator.hasNext()) { + V record = iterator.next(); + recordsQueue.offer(record); + } + } else { + V record = (V) data; + recordsQueue.offer(record); + } + return null; + }; + + try { + new WrappedSupervisor(sparkReceiver, new SparkConf(), storeFn); } catch (Exception e) { LOG.error("Can not init Spark Receiver!", e); throw new IllegalStateException("Spark Receiver was not initialized"); @@ -188,26 +284,38 @@ public ProcessContinuation processElement( LOG.error("Can not build Spark Receiver", e); throw new IllegalStateException("Spark Receiver was not built!"); } + LOG.debug("Restriction {}", tracker.currentRestriction().toString()); sparkConsumer = new SparkConsumerWithOffset<>(tracker.currentRestriction().getFrom()); sparkConsumer.start(sparkReceiver); - while (sparkConsumer.hasRecords()) { - V record = sparkConsumer.poll(); - if (record != null) { - Long offset = getOffsetFn.apply(record); - if (!tracker.tryClaim(offset)) { - sparkConsumer.stop(); - LOG.debug("Stop for restriction: {}", tracker.currentRestriction().toString()); - return ProcessContinuation.stop(); + while (true) { + try { + TimeUnit.MILLISECONDS.sleep(START_POLL_TIMEOUT_MS); + } catch (InterruptedException e) { + LOG.error("SparkReceiver was interrupted before polling started", e); + throw new IllegalStateException("Spark Receiver was interrupted before polling started"); + } + if (!sparkConsumer.hasRecords()) { + sparkConsumer.stop(); + tracker.checkDone(); + LOG.debug("Resume for restriction: {}", tracker.currentRestriction().toString()); + return ProcessContinuation.resume(); + } + while (sparkConsumer.hasRecords()) { + V record = sparkConsumer.poll(); + if (record != null) { + Long offset = getOffsetFn.apply(record); + if (!tracker.tryClaim(offset)) { + sparkConsumer.stop(); + LOG.debug("Stop for restriction: {}", tracker.currentRestriction().toString()); + return ProcessContinuation.stop(); + } + Instant currentTimeStamp = getTimestampFn.apply(record); + ((ManualWatermarkEstimator) watermarkEstimator).setWatermark(currentTimeStamp); + receiver.outputWithTimestamp(record, currentTimeStamp); } - Instant currentTimeStamp = getTimestampFn.apply(record); - ((ManualWatermarkEstimator) watermarkEstimator).setWatermark(currentTimeStamp); - receiver.outputWithTimestamp(record, currentTimeStamp); } } - sparkConsumer.stop(); - LOG.debug("Resume for restriction: {}", tracker.currentRestriction().toString()); - return ProcessContinuation.resume(); } private static Instant ensureTimestampWithinBounds(Instant timestamp) { diff --git a/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ArrayBufferDataReceiver.java b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ArrayBufferDataReceiver.java new file mode 100644 index 000000000000..849ea0a1373e --- /dev/null +++ b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ArrayBufferDataReceiver.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.sparkreceiver; + +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.spark.storage.StorageLevel; +import org.apache.spark.streaming.receiver.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import scala.collection.mutable.ArrayBuffer; + +/** + * Imitation of Spark {@link Receiver} that implements {@link HasOffset} interface and pushes data + * passing the {@link ArrayBuffer}. Used to test {@link SparkReceiverIO#read()}. + */ +public class ArrayBufferDataReceiver extends Receiver implements HasOffset { + + private static final Logger LOG = LoggerFactory.getLogger(ArrayBufferDataReceiver.class); + private static final int TIMEOUT_MS = 500; + public static final int RECORDS_COUNT = 20; + + private Long startOffset; + + ArrayBufferDataReceiver() { + super(StorageLevel.MEMORY_AND_DISK_2()); + } + + @Override + public void setStartOffset(Long startOffset) { + if (startOffset != null) { + this.startOffset = startOffset; + } + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void onStart() { + Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().build()).submit(this::receive); + } + + @Override + public void onStop() {} + + @Override + public Long getEndOffset() { + return Long.MAX_VALUE; + } + + private void receive() { + Long currentOffset = startOffset; + while (!isStopped()) { + if (currentOffset < RECORDS_COUNT) { + ArrayBuffer dataArray = new ArrayBuffer<>(); + for (int i = 0; i < Math.max(2, RECORDS_COUNT / 10); i++) { + dataArray.$plus$eq(String.valueOf(currentOffset++)); + } + store(dataArray); + } else { + break; + } + try { + TimeUnit.MILLISECONDS.sleep(TIMEOUT_MS); + } catch (InterruptedException e) { + LOG.error("Interrupted", e); + } + } + } +} diff --git a/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ByteBufferDataReceiver.java b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ByteBufferDataReceiver.java new file mode 100644 index 000000000000..dcef495aa67a --- /dev/null +++ b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ByteBufferDataReceiver.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.sparkreceiver; + +import java.nio.ByteBuffer; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.commons.lang3.SerializationUtils; +import org.apache.spark.storage.StorageLevel; +import org.apache.spark.streaming.receiver.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Imitation of Spark {@link Receiver} that implements {@link HasOffset} interface and pushes data + * passing the {@link ByteBuffer}. Used to test {@link SparkReceiverIO#read()}. + */ +public class ByteBufferDataReceiver extends Receiver implements HasOffset { + + private static final Logger LOG = LoggerFactory.getLogger(ByteBufferDataReceiver.class); + private static final int TIMEOUT_MS = 500; + public static final int RECORDS_COUNT = 20; + + private Long startOffset; + + ByteBufferDataReceiver() { + super(StorageLevel.MEMORY_AND_DISK_2()); + } + + @Override + public void setStartOffset(Long startOffset) { + if (startOffset != null) { + this.startOffset = startOffset; + } + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void onStart() { + Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().build()).submit(this::receive); + } + + @Override + public void onStop() {} + + @Override + public Long getEndOffset() { + return Long.MAX_VALUE; + } + + private void receive() { + Long currentOffset = startOffset; + while (!isStopped()) { + if (currentOffset < RECORDS_COUNT) { + ByteBuffer dataBuffer = + ByteBuffer.wrap(SerializationUtils.serialize(String.valueOf(currentOffset++))); + store(dataBuffer); + } else { + break; + } + try { + TimeUnit.MILLISECONDS.sleep(TIMEOUT_MS); + } catch (InterruptedException e) { + LOG.error("Interrupted", e); + } + } + } +} diff --git a/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/IteratorDataReceiver.java b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/IteratorDataReceiver.java new file mode 100644 index 000000000000..8999802542c2 --- /dev/null +++ b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/IteratorDataReceiver.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.sparkreceiver; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.spark.storage.StorageLevel; +import org.apache.spark.streaming.receiver.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Imitation of Spark {@link Receiver} that implements {@link HasOffset} interface and pushes data + * passing the {@link Iterator}. Used to test {@link SparkReceiverIO#read()}. + */ +public class IteratorDataReceiver extends Receiver implements HasOffset { + + private static final Logger LOG = LoggerFactory.getLogger(IteratorDataReceiver.class); + private static final int TIMEOUT_MS = 500; + public static final int RECORDS_COUNT = 20; + + private Long startOffset; + + IteratorDataReceiver() { + super(StorageLevel.MEMORY_AND_DISK_2()); + } + + @Override + public void setStartOffset(Long startOffset) { + if (startOffset != null) { + this.startOffset = startOffset; + } + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void onStart() { + Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().build()).submit(this::receive); + } + + @Override + public void onStop() {} + + @Override + public Long getEndOffset() { + return Long.MAX_VALUE; + } + + private void receive() { + Long currentOffset = startOffset; + while (!isStopped()) { + if (currentOffset < RECORDS_COUNT) { + List dataArray = new ArrayList<>(); + for (int i = 0; i < Math.max(2, RECORDS_COUNT / 10); i++) { + dataArray.add(String.valueOf(currentOffset++)); + } + store(dataArray.iterator()); + } else { + break; + } + try { + TimeUnit.MILLISECONDS.sleep(TIMEOUT_MS); + } catch (InterruptedException e) { + LOG.error("Interrupted", e); + } + } + } +} diff --git a/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/RabbitMqReceiverWithOffset.java b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/RabbitMqReceiverWithOffset.java new file mode 100644 index 000000000000..362e6280eb29 --- /dev/null +++ b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/RabbitMqReceiverWithOffset.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.sparkreceiver; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.spark.storage.StorageLevel; +import org.apache.spark.streaming.receiver.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Imitation of Spark {@link Receiver} for RabbitMQ that implements {@link HasOffset} interface. + * Used to test {@link SparkReceiverIO#read()}. + */ +class RabbitMqReceiverWithOffset extends Receiver implements HasOffset { + + private static final Logger LOG = LoggerFactory.getLogger(RabbitMqReceiverWithOffset.class); + private static final int MAX_PREFETCH_COUNT = 65535; + + private final String rabbitmqUrl; + private final String streamName; + private final long totalMessagesNumber; + private long startOffset; + private static final int READ_TIMEOUT_IN_MS = 100; + + RabbitMqReceiverWithOffset( + final String uri, final String streamName, final long totalMessagesNumber) { + super(StorageLevel.MEMORY_AND_DISK_2()); + rabbitmqUrl = uri; + this.streamName = streamName; + this.totalMessagesNumber = totalMessagesNumber; + } + + @Override + public void setStartOffset(Long startOffset) { + this.startOffset = startOffset != null ? startOffset : 0; + } + + @Override + public Long getEndOffset() { + return Long.MAX_VALUE; + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void onStart() { + Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().build()).submit(this::receive); + } + + @Override + public void onStop() {} + + private void receive() { + long currentOffset = startOffset; + + final TestConsumer testConsumer; + final Connection connection; + final Channel channel; + + try { + LOG.info("Starting receiver with offset {}", currentOffset); + final ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setUri(rabbitmqUrl); + connectionFactory.setAutomaticRecoveryEnabled(true); + connectionFactory.setConnectionTimeout(600000); + connectionFactory.setNetworkRecoveryInterval(5000); + connectionFactory.setRequestedHeartbeat(60); + connectionFactory.setTopologyRecoveryEnabled(true); + connectionFactory.setRequestedChannelMax(0); + connectionFactory.setRequestedFrameMax(0); + connection = connectionFactory.newConnection(); + + channel = connection.createChannel(); + channel.queueDeclare( + streamName, true, false, false, Collections.singletonMap("x-queue-type", "stream")); + channel.basicQos(Math.min(MAX_PREFETCH_COUNT, (int) totalMessagesNumber)); + testConsumer = new TestConsumer(this, channel, this::store); + + channel.basicConsume( + streamName, + false, + Collections.singletonMap("x-stream-offset", currentOffset), + testConsumer); + } catch (Exception e) { + LOG.error("Can not basic consume", e); + throw new RuntimeException(e); + } + + while (!isStopped()) { + try { + TimeUnit.MILLISECONDS.sleep(READ_TIMEOUT_IN_MS); + } catch (InterruptedException e) { + LOG.error("Interrupted", e); + } + } + + try { + LOG.info("Stopping receiver"); + channel.close(); + connection.close(); + } catch (TimeoutException | IOException e) { + throw new RuntimeException(e); + } + } + + /** A simple RabbitMQ {@code Consumer}. */ + static class TestConsumer extends DefaultConsumer { + + private final java.util.function.Consumer messageConsumer; + private final Receiver receiver; + + public TestConsumer( + Receiver receiver, + Channel channel, + java.util.function.Consumer messageConsumer) { + super(channel); + this.receiver = receiver; + this.messageConsumer = messageConsumer; + } + + @Override + public void handleDelivery( + String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) { + try { + final String sMessage = new String(body, StandardCharsets.UTF_8); + LOG.trace("Adding message to consumer: {}", sMessage); + messageConsumer.accept(sMessage); + if (getChannel().isOpen() && !receiver.isStopped()) { + getChannel().basicAck(envelope.getDeliveryTag(), false); + } + } catch (Exception e) { + LOG.error("Can't read from RabbitMQ: {}", e.getMessage()); + } + } + } +} diff --git a/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ReadFromSparkReceiverWithOffsetDoFnTest.java b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ReadFromSparkReceiverWithOffsetDoFnTest.java new file mode 100644 index 000000000000..67b4e2cabba1 --- /dev/null +++ b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/ReadFromSparkReceiverWithOffsetDoFnTest.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.sparkreceiver; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.apache.beam.sdk.io.range.OffsetRange; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.splittabledofn.ManualWatermarkEstimator; +import org.apache.beam.sdk.transforms.splittabledofn.OffsetRangeTracker; +import org.apache.beam.sdk.transforms.splittabledofn.SplitResult; +import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.UnknownKeyFor; +import org.joda.time.Instant; +import org.junit.Test; + +/** Test class for {@link ReadFromSparkReceiverWithOffsetDoFn}. */ +public class ReadFromSparkReceiverWithOffsetDoFnTest { + + private static final byte[] TEST_ELEMENT = new byte[] {}; + + private final ReadFromSparkReceiverWithOffsetDoFn dofnInstance = + new ReadFromSparkReceiverWithOffsetDoFn<>(makeReadTransform()); + + private SparkReceiverIO.Read makeReadTransform() { + ReceiverBuilder receiverBuilder = + new ReceiverBuilder<>(CustomReceiverWithOffset.class).withConstructorArgs(); + return SparkReceiverIO.read() + .withSparkReceiverBuilder(receiverBuilder) + .withGetOffsetFn(Long::valueOf) + .withTimestampFn(Instant::parse); + } + + private static class MockOutputReceiver implements DoFn.OutputReceiver { + + private final List records = new ArrayList<>(); + + @Override + public void output(String output) {} + + @Override + public void outputWithTimestamp( + String output, @UnknownKeyFor @NonNull @Initialized Instant timestamp) { + records.add(output); + } + + public List getOutputs() { + return this.records; + } + } + + private final ManualWatermarkEstimator mockWatermarkEstimator = + new ManualWatermarkEstimator() { + + @Override + public void setWatermark(Instant watermark) { + // do nothing + } + + @Override + public Instant currentWatermark() { + return null; + } + + @Override + public Instant getState() { + return null; + } + }; + + private List createExpectedRecords(int numRecords) { + List records = new ArrayList<>(); + for (int i = 0; i < numRecords; i++) { + records.add(String.valueOf(i)); + } + return records; + } + + @Test + public void testInitialRestriction() { + long expectedStartOffset = 0L; + OffsetRange result = dofnInstance.initialRestriction(TEST_ELEMENT); + assertEquals(new OffsetRange(expectedStartOffset, Long.MAX_VALUE), result); + } + + @Test + public void testRestrictionTrackerSplit() { + OffsetRangeTracker offsetRangeTracker = + dofnInstance.restrictionTracker( + TEST_ELEMENT, dofnInstance.initialRestriction(TEST_ELEMENT)); + assertEquals(0L, offsetRangeTracker.currentRestriction().getFrom()); + assertEquals(Long.MAX_VALUE, offsetRangeTracker.currentRestriction().getTo()); + + assertEquals( + SplitResult.of(new OffsetRange(0, 0), new OffsetRange(0, Long.MAX_VALUE)), + offsetRangeTracker.trySplit(0d)); + + offsetRangeTracker = + dofnInstance.restrictionTracker( + TEST_ELEMENT, dofnInstance.initialRestriction(TEST_ELEMENT)); + + assertTrue(offsetRangeTracker.tryClaim(0L)); + assertNull(offsetRangeTracker.trySplit(0d)); + + offsetRangeTracker.checkDone(); + assertEquals( + SplitResult.of(new OffsetRange(0, 1), new OffsetRange(1, Long.MAX_VALUE)), + offsetRangeTracker.trySplit(0d)); + } + + @Test + public void testProcessElement() { + MockOutputReceiver receiver = new MockOutputReceiver(); + DoFn.ProcessContinuation result = + dofnInstance.processElement( + TEST_ELEMENT, + dofnInstance.restrictionTracker( + TEST_ELEMENT, dofnInstance.initialRestriction(TEST_ELEMENT)), + mockWatermarkEstimator, + receiver); + assertEquals(DoFn.ProcessContinuation.resume(), result); + assertEquals( + createExpectedRecords(CustomReceiverWithOffset.RECORDS_COUNT), receiver.getOutputs()); + } +} diff --git a/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/SparkReceiverIOIT.java b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/SparkReceiverIOIT.java new file mode 100644 index 000000000000..b335aab2ed53 --- /dev/null +++ b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/SparkReceiverIOIT.java @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.beam.sdk.io.sparkreceiver; + +import static org.apache.beam.sdk.io.synthetic.SyntheticOptions.fromJsonString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.cloud.Timestamp; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.MessageProperties; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeoutException; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.LongStream; +import org.apache.beam.sdk.PipelineResult; +import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.io.common.IOITHelper; +import org.apache.beam.sdk.io.common.IOTestPipelineOptions; +import org.apache.beam.sdk.io.synthetic.SyntheticSourceOptions; +import org.apache.beam.sdk.metrics.Counter; +import org.apache.beam.sdk.metrics.Metrics; +import org.apache.beam.sdk.options.Default; +import org.apache.beam.sdk.options.Description; +import org.apache.beam.sdk.options.ExperimentalOptions; +import org.apache.beam.sdk.options.PipelineOptionsFactory; +import org.apache.beam.sdk.options.StreamingOptions; +import org.apache.beam.sdk.options.Validation; +import org.apache.beam.sdk.testing.TestPipeline; +import org.apache.beam.sdk.testing.TestPipelineOptions; +import org.apache.beam.sdk.testutils.NamedTestResult; +import org.apache.beam.sdk.testutils.metrics.IOITMetrics; +import org.apache.beam.sdk.testutils.metrics.MetricsReader; +import org.apache.beam.sdk.testutils.metrics.TimeMonitor; +import org.apache.beam.sdk.testutils.publishing.InfluxDBSettings; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableSet; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.joda.time.Duration; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * IO Integration test for {@link org.apache.beam.sdk.io.sparkreceiver.SparkReceiverIO}. + * + *

    {@see https://beam.apache.org/documentation/io/testing/#i-o-transform-integration-tests} for + * more details. + * + *

    NOTE: This test sets retention policy of the messages so that all messages are retained in the + * topic so that we could read them back after writing. + */ +@RunWith(JUnit4.class) +public class SparkReceiverIOIT { + + private static final Logger LOG = LoggerFactory.getLogger(SparkReceiverIOIT.class); + + private static final String READ_TIME_METRIC_NAME = "read_time"; + + private static final String RUN_TIME_METRIC_NAME = "run_time"; + + private static final String READ_ELEMENT_METRIC_NAME = "spark_read_element_count"; + + private static final String NAMESPACE = SparkReceiverIOIT.class.getName(); + + private static final String TEST_ID = UUID.randomUUID().toString(); + + private static final String TIMESTAMP = Timestamp.now().toString(); + + private static final String TEST_MESSAGE_PREFIX = "Test "; + + private static Options options; + + private static SyntheticSourceOptions sourceOptions; + + private static GenericContainer rabbitMqContainer; + + private static InfluxDBSettings settings; + + private static final ExperimentalOptions sdfPipelineOptions; + + static { + sdfPipelineOptions = PipelineOptionsFactory.create().as(ExperimentalOptions.class); + sdfPipelineOptions.as(TestPipelineOptions.class).setBlockOnRun(false); + } + + @Rule public TestPipeline readPipeline = TestPipeline.fromOptions(sdfPipelineOptions); + + @BeforeClass + public static void setup() throws IOException { + options = IOITHelper.readIOTestPipelineOptions(Options.class); + sourceOptions = fromJsonString(options.getSourceOptions(), SyntheticSourceOptions.class); + if (options.isWithTestcontainers()) { + setupRabbitMqContainer(); + } else { + settings = + InfluxDBSettings.builder() + .withHost(options.getInfluxHost()) + .withDatabase(options.getInfluxDatabase()) + .withMeasurement(options.getInfluxMeasurement()) + .get(); + } + clearRabbitMQ(); + } + + @AfterClass + public static void afterClass() { + if (rabbitMqContainer != null) { + rabbitMqContainer.stop(); + } + + clearRabbitMQ(); + } + + private static void setupRabbitMqContainer() { + rabbitMqContainer = + new RabbitMQContainer( + DockerImageName.parse("rabbitmq").withTag(options.getRabbitMqContainerVersion())) + .withExposedPorts(5672, 15672); + rabbitMqContainer.start(); + options.setRabbitMqBootstrapServerAddress( + getBootstrapServers( + rabbitMqContainer.getHost(), rabbitMqContainer.getMappedPort(5672).toString())); + } + + private static String getBootstrapServers(String host, String port) { + return String.format("amqp://guest:guest@%s:%s", host, port); + } + + /** Pipeline options specific for this test. */ + public interface Options extends IOTestPipelineOptions, StreamingOptions { + + @Description("Options for synthetic source.") + @Validation.Required + @Default.String("{\"numRecords\": \"500\",\"keySizeBytes\": \"1\",\"valueSizeBytes\": \"90\"}") + String getSourceOptions(); + + void setSourceOptions(String sourceOptions); + + @Description("RabbitMQ bootstrap server address") + @Default.String("amqp://guest:guest@localhost:5672") + String getRabbitMqBootstrapServerAddress(); + + void setRabbitMqBootstrapServerAddress(String address); + + @Description("RabbitMQ stream") + @Default.String("rabbitMqTestStream") + String getStreamName(); + + void setStreamName(String streamName); + + @Description("Whether to use testcontainers") + @Default.Boolean(false) + Boolean isWithTestcontainers(); + + void setWithTestcontainers(Boolean withTestcontainers); + + @Description("RabbitMQ container version. Use when useTestcontainers is true") + @Nullable + @Default.String("3.9-alpine") + String getRabbitMqContainerVersion(); + + void setRabbitMqContainerVersion(String rabbitMqContainerVersion); + + @Description("Time to wait for the events to be processed by the read pipeline (in seconds)") + @Default.Integer(50) + @Validation.Required + Integer getReadTimeout(); + + void setReadTimeout(Integer readTimeout); + } + + private void writeToRabbitMq(final List messages) + throws URISyntaxException, NoSuchAlgorithmException, KeyManagementException, IOException, + TimeoutException { + + final ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setUri(options.getRabbitMqBootstrapServerAddress()); + Map arguments = new HashMap<>(); + arguments.put("x-queue-type", "stream"); + + try (Connection connection = connectionFactory.newConnection(); + Channel channel = connection.createChannel()) { + channel.queueDeclare(options.getStreamName(), true, false, false, arguments); + + messages.forEach( + message -> { + try { + channel.basicPublish( + "", + options.getStreamName(), + MessageProperties.PERSISTENT_TEXT_PLAIN, + message.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } + + private SparkReceiverIO.Read readFromRabbitMqWithOffset() { + final ReceiverBuilder receiverBuilder = + new ReceiverBuilder<>(RabbitMqReceiverWithOffset.class) + .withConstructorArgs( + options.getRabbitMqBootstrapServerAddress(), + options.getStreamName(), + sourceOptions.numRecords); + + return SparkReceiverIO.read() + .withGetOffsetFn( + rabbitMqMessage -> + Long.valueOf(rabbitMqMessage.substring(TEST_MESSAGE_PREFIX.length()))) + .withSparkReceiverBuilder(receiverBuilder); + } + + /** + * Since streams in RabbitMQ are durable by definition, we have to clean them up after test + * execution. The simplest way is to delete the whole stream after test execution. + */ + private static void clearRabbitMQ() { + final ConnectionFactory connectionFactory = new ConnectionFactory(); + + try { + connectionFactory.setUri(options.getRabbitMqBootstrapServerAddress()); + try (Connection connection = connectionFactory.newConnection(); + Channel channel = connection.createChannel()) { + channel.queueDelete(options.getStreamName()); + } + } catch (URISyntaxException + | NoSuchAlgorithmException + | KeyManagementException + | IOException + | TimeoutException e) { + LOG.error("Error during RabbitMQ clean up", e); + } + } + + /** Function for counting processed pipeline elements. */ + private static class CountingFn extends DoFn { + + private final Counter elementCounter; + + CountingFn(String namespace, String name) { + elementCounter = Metrics.counter(namespace, name); + } + + @ProcessElement + public void processElement() { + elementCounter.inc(1L); + } + } + + private void cancelIfTimeout(PipelineResult readResult, PipelineResult.State readState) + throws IOException { + if (readState == null) { + readResult.cancel(); + } + } + + private long readElementMetric(PipelineResult result) { + MetricsReader metricsReader = new MetricsReader(result, SparkReceiverIOIT.NAMESPACE); + return metricsReader.getCounterMetric(SparkReceiverIOIT.READ_ELEMENT_METRIC_NAME); + } + + private Set readMetrics(PipelineResult readResult) { + BiFunction supplier = + (reader, metricName) -> { + long start = reader.getStartTimeMetric(metricName); + long end = reader.getEndTimeMetric(metricName); + return NamedTestResult.create(TEST_ID, TIMESTAMP, metricName, (end - start) / 1e3); + }; + + NamedTestResult readTime = + supplier.apply(new MetricsReader(readResult, NAMESPACE), READ_TIME_METRIC_NAME); + NamedTestResult runTime = + NamedTestResult.create(TEST_ID, TIMESTAMP, RUN_TIME_METRIC_NAME, readTime.getValue()); + + return ImmutableSet.of(readTime, runTime); + } + + @Test + public void testSparkReceiverIOReadsInStreamingWithOffset() throws IOException { + + final List messages = + LongStream.range(0, sourceOptions.numRecords) + .mapToObj(number -> TEST_MESSAGE_PREFIX + number) + .collect(Collectors.toList()); + + try { + writeToRabbitMq(messages); + } catch (Exception e) { + LOG.error("Can not write to rabbit {}", e.getMessage()); + fail(); + } + LOG.info(sourceOptions.numRecords + " records were successfully written to RabbitMQ"); + + // Use streaming pipeline to read RabbitMQ records. + readPipeline.getOptions().as(Options.class).setStreaming(true); + readPipeline + .apply("Read from unbounded RabbitMq", readFromRabbitMqWithOffset()) + .setCoder(StringUtf8Coder.of()) + .apply("Measure read time", ParDo.of(new TimeMonitor<>(NAMESPACE, READ_TIME_METRIC_NAME))) + .apply("Counting element", ParDo.of(new CountingFn(NAMESPACE, READ_ELEMENT_METRIC_NAME))); + + final PipelineResult readResult = readPipeline.run(); + final PipelineResult.State readState = + readResult.waitUntilFinish(Duration.standardSeconds(options.getReadTimeout())); + + cancelIfTimeout(readResult, readState); + + assertEquals(sourceOptions.numRecords, readElementMetric(readResult)); + + if (!options.isWithTestcontainers()) { + Set metrics = readMetrics(readResult); + IOITMetrics.publishToInflux(TEST_ID, TIMESTAMP, metrics, settings); + } + } +} diff --git a/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/SparkReceiverIOTest.java b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/SparkReceiverIOTest.java index e81dca5150e5..6931e7199926 100644 --- a/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/SparkReceiverIOTest.java +++ b/sdks/java/io/sparkreceiver/src/test/java/org/apache/beam/sdk/io/sparkreceiver/SparkReceiverIOTest.java @@ -20,14 +20,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; import org.apache.beam.sdk.coders.StringUtf8Coder; +import org.apache.beam.sdk.testing.PAssert; import org.apache.beam.sdk.testing.TestPipeline; import org.apache.beam.sdk.testing.TestPipelineOptions; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.ParDo; import org.apache.beam.sdk.transforms.SerializableFunction; +import org.apache.beam.sdk.values.PCollection; import org.joda.time.Duration; import org.joda.time.Instant; import org.junit.Rule; @@ -110,11 +110,13 @@ public void testReadFromCustomReceiverWithOffset() { .withTimestampFn(Instant::parse) .withSparkReceiverBuilder(receiverBuilder); + List expected = new ArrayList<>(); for (int i = 0; i < CustomReceiverWithOffset.RECORDS_COUNT; i++) { - TestOutputDoFn.EXPECTED_RECORDS.add(String.valueOf(i)); + expected.add(String.valueOf(i)); } - pipeline.apply(reader).setCoder(StringUtf8Coder.of()).apply(ParDo.of(new TestOutputDoFn())); + PCollection actual = pipeline.apply(reader).setCoder(StringUtf8Coder.of()); + PAssert.that(actual).containsInAnyOrder(expected); pipeline.run().waitUntilFinish(Duration.standardSeconds(15)); } @@ -129,28 +131,73 @@ public void testReadFromCustomReceiverWithOffsetFailsAndReread() { .withTimestampFn(Instant::parse) .withSparkReceiverBuilder(receiverBuilder); + List expected = new ArrayList<>(); for (int i = 0; i < CustomReceiverWithOffset.RECORDS_COUNT; i++) { - TestOutputDoFn.EXPECTED_RECORDS.add(String.valueOf(i)); + expected.add(String.valueOf(i)); } - pipeline.apply(reader).setCoder(StringUtf8Coder.of()).apply(ParDo.of(new TestOutputDoFn())); + PCollection actual = pipeline.apply(reader).setCoder(StringUtf8Coder.of()); + PAssert.that(actual).containsInAnyOrder(expected); pipeline.run().waitUntilFinish(Duration.standardSeconds(15)); + } + + @Test + public void testReadFromReceiverArrayBufferData() { + ReceiverBuilder receiverBuilder = + new ReceiverBuilder<>(ArrayBufferDataReceiver.class).withConstructorArgs(); + SparkReceiverIO.Read reader = + SparkReceiverIO.read() + .withGetOffsetFn(Long::valueOf) + .withTimestampFn(Instant::parse) + .withSparkReceiverBuilder(receiverBuilder); + + List expected = new ArrayList<>(); + for (int i = 0; i < ArrayBufferDataReceiver.RECORDS_COUNT; i++) { + expected.add(String.valueOf(i)); + } + PCollection actual = pipeline.apply(reader).setCoder(StringUtf8Coder.of()); + + PAssert.that(actual).containsInAnyOrder(expected); + pipeline.run().waitUntilFinish(Duration.standardSeconds(15)); + } + + @Test + public void testReadFromReceiverByteBufferData() { + ReceiverBuilder receiverBuilder = + new ReceiverBuilder<>(ByteBufferDataReceiver.class).withConstructorArgs(); + SparkReceiverIO.Read reader = + SparkReceiverIO.read() + .withGetOffsetFn(Long::valueOf) + .withTimestampFn(Instant::parse) + .withSparkReceiverBuilder(receiverBuilder); - assertEquals(0, TestOutputDoFn.EXPECTED_RECORDS.size()); + List expected = new ArrayList<>(); + for (int i = 0; i < ByteBufferDataReceiver.RECORDS_COUNT; i++) { + expected.add(String.valueOf(i)); + } + PCollection actual = pipeline.apply(reader).setCoder(StringUtf8Coder.of()); + + PAssert.that(actual).containsInAnyOrder(expected); + pipeline.run().waitUntilFinish(Duration.standardSeconds(15)); } - /** {@link DoFn} that throws {@code RuntimeException} if receives unexpected element. */ - private static class TestOutputDoFn extends DoFn { - private static final Set EXPECTED_RECORDS = new HashSet<>(); - - @ProcessElement - public void processElement(@Element String element, OutputReceiver outputReceiver) { - if (!EXPECTED_RECORDS.contains(element)) { - throw new RuntimeException("Received unexpected element: " + element); - } else { - EXPECTED_RECORDS.remove(element); - outputReceiver.output(element); - } + @Test + public void testReadFromReceiverIteratorData() { + ReceiverBuilder receiverBuilder = + new ReceiverBuilder<>(IteratorDataReceiver.class).withConstructorArgs(); + SparkReceiverIO.Read reader = + SparkReceiverIO.read() + .withGetOffsetFn(Long::valueOf) + .withTimestampFn(Instant::parse) + .withSparkReceiverBuilder(receiverBuilder); + + List expected = new ArrayList<>(); + for (int i = 0; i < IteratorDataReceiver.RECORDS_COUNT; i++) { + expected.add(String.valueOf(i)); } + PCollection actual = pipeline.apply(reader).setCoder(StringUtf8Coder.of()); + + PAssert.that(actual).containsInAnyOrder(expected); + pipeline.run().waitUntilFinish(Duration.standardSeconds(15)); } } diff --git a/sdks/java/maven-archetypes/examples/build.gradle b/sdks/java/maven-archetypes/examples/build.gradle index 148015f43898..6a034029f10e 100644 --- a/sdks/java/maven-archetypes/examples/build.gradle +++ b/sdks/java/maven-archetypes/examples/build.gradle @@ -36,7 +36,7 @@ processResources { 'libraries-bom.version': dependencies.create(project.library.java.google_cloud_platform_libraries_bom).getVersion(), 'pubsub.version': dependencies.create(project.library.java.google_api_services_pubsub).getVersion(), 'slf4j.version': dependencies.create(project.library.java.slf4j_api).getVersion(), - 'spark.version': dependencies.create(project.library.java.spark_core).getVersion(), + 'spark.version': dependencies.create(project.library.java.spark3_core).getVersion(), 'nemo.version': dependencies.create(project.library.java.nemo_compiler_frontend_beam).getVersion(), 'hadoop.version': dependencies.create(project.library.java.hadoop_client).getVersion(), 'mockito.version': dependencies.create(project.library.java.mockito_core).getVersion(), diff --git a/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml b/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml index 50515b812078..5560ca93257e 100644 --- a/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml +++ b/sdks/java/maven-archetypes/examples/src/main/resources/archetype-resources/pom.xml @@ -220,15 +220,11 @@ spark-runner - - - 4.1.17.Final - + org.apache.beam - beam-runners-spark + beam-runners-spark-3 ${beam.version} runtime @@ -246,7 +242,7 @@ org.apache.spark - spark-streaming_2.11 + spark-streaming_2.12 ${spark.version} runtime @@ -258,26 +254,10 @@ com.fasterxml.jackson.module - jackson-module-scala_2.11 + jackson-module-scala_2.12 ${jackson.version} runtime - - - org.apache.beam - beam-sdks-java-io-google-cloud-platform - ${beam.version} - - - io.grpc - grpc-netty - - - io.netty - netty-handler - - - diff --git a/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle b/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle index 0e4f394170e5..af06bfc41d8e 100644 --- a/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle +++ b/sdks/java/maven-archetypes/gcp-bom-examples/build.gradle @@ -35,7 +35,7 @@ processResources { 'junit.version': dependencies.create(project.library.java.junit).getVersion(), 'pubsub.version': dependencies.create(project.library.java.google_api_services_pubsub).getVersion(), 'slf4j.version': dependencies.create(project.library.java.slf4j_api).getVersion(), - 'spark.version': dependencies.create(project.library.java.spark_core).getVersion(), + 'spark.version': dependencies.create(project.library.java.spark3_core).getVersion(), 'nemo.version': dependencies.create(project.library.java.nemo_compiler_frontend_beam).getVersion(), 'hadoop.version': dependencies.create(project.library.java.hadoop_client).getVersion(), 'mockito.version': dependencies.create(project.library.java.mockito_core).getVersion(), diff --git a/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml b/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml index 863a465f0fdc..a87a506a3a8b 100644 --- a/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml +++ b/sdks/java/maven-archetypes/gcp-bom-examples/src/main/resources/archetype-resources/pom.xml @@ -216,13 +216,10 @@ spark-runner - - 4.1.17.Final - org.apache.beam - beam-runners-spark + beam-runners-spark-3 runtime @@ -238,7 +235,7 @@ org.apache.spark - spark-streaming_2.11 + spark-streaming_2.12 runtime @@ -249,25 +246,10 @@ com.fasterxml.jackson.module - jackson-module-scala_2.11 + jackson-module-scala_2.12 ${jackson.version} runtime - - - org.apache.beam - beam-sdks-java-io-google-cloud-platform - - - io.grpc - grpc-netty - - - io.netty - netty-handler - - - @@ -342,6 +324,13 @@ beam-sdks-java-io-google-cloud-platform + + + org.apache.beam + beam-sdks-java-extensions-python + ${beam.version} + + com.google.api-client diff --git a/sdks/java/testing/jpms-tests/build.gradle b/sdks/java/testing/jpms-tests/build.gradle index 9aa3f41b73a0..f781c29b8480 100644 --- a/sdks/java/testing/jpms-tests/build.gradle +++ b/sdks/java/testing/jpms-tests/build.gradle @@ -78,9 +78,6 @@ configurations { sparkRunnerIntegrationTest.extendsFrom(baseIntegrationTest) } -def spark_version = '3.1.1' -def spark_scala_version = '2.12' - dependencies { implementation project(path: ":sdks:java:core", configuration: "shadow") implementation project(path: ":sdks:java:extensions:google-cloud-platform-core") @@ -93,8 +90,6 @@ dependencies { flinkRunnerIntegrationTest project(":runners:flink:${project.ext.latestFlinkVersion}") dataflowRunnerIntegrationTest project(":runners:google-cloud-dataflow-java") sparkRunnerIntegrationTest project(":runners:spark:3") - sparkRunnerIntegrationTest "org.apache.spark:spark-sql_$spark_scala_version:$spark_version" - sparkRunnerIntegrationTest "org.apache.spark:spark-streaming_$spark_scala_version:$spark_version" } /* diff --git a/sdks/java/testing/load-tests/build.gradle b/sdks/java/testing/load-tests/build.gradle index 2d93993a5657..e157f2fabf32 100644 --- a/sdks/java/testing/load-tests/build.gradle +++ b/sdks/java/testing/load-tests/build.gradle @@ -39,7 +39,7 @@ def runnerDependency = (project.hasProperty(runnerProperty) : ":runners:direct-java") def loadTestRunnerVersionProperty = "runner.version" def loadTestRunnerVersion = project.findProperty(loadTestRunnerVersionProperty) -def shouldProvideSpark = ":runners:spark:2".equals(runnerDependency) +def isSparkRunner = runnerDependency.startsWith(":runners:spark:") def isDataflowRunner = ":runners:google-cloud-dataflow-java".equals(runnerDependency) def isDataflowRunnerV2 = isDataflowRunner && "V2".equals(loadTestRunnerVersion) def runnerConfiguration = ":runners:direct-java".equals(runnerDependency) ? "shadow" : null @@ -82,20 +82,9 @@ dependencies { gradleRun project(project.path) gradleRun project(path: runnerDependency, configuration: runnerConfiguration) - - // The Spark runner requires the user to provide a Spark dependency. For self-contained - // runs with the Spark runner, we can provide such a dependency. This is deliberately phrased - // to not hardcode any runner other than :runners:direct-java - if (shouldProvideSpark) { - gradleRun library.java.spark_streaming - gradleRun library.java.spark_core, { - exclude group:"org.slf4j", module:"jul-to-slf4j" - } - gradleRun library.java.spark_sql - } } -if (shouldProvideSpark) { +if (isSparkRunner) { configurations.gradleRun { // Using Spark runner causes a StackOverflowError if slf4j-jdk14 is on the classpath exclude group: "org.slf4j", module: "slf4j-jdk14" diff --git a/sdks/java/testing/nexmark/build.gradle b/sdks/java/testing/nexmark/build.gradle index 3a8d3440c80b..a7fbf2e08ad4 100644 --- a/sdks/java/testing/nexmark/build.gradle +++ b/sdks/java/testing/nexmark/build.gradle @@ -38,8 +38,7 @@ def nexmarkRunnerDependency = project.findProperty(nexmarkRunnerProperty) ?: ":runners:direct-java" def nexmarkRunnerVersionProperty = "nexmark.runner.version" def nexmarkRunnerVersion = project.findProperty(nexmarkRunnerVersionProperty) -def shouldProvideSpark2 = ":runners:spark:2".equals(nexmarkRunnerDependency) -def shouldProvideSpark3 = ":runners:spark:3".equals(nexmarkRunnerDependency) +def isSparkRunner = nexmarkRunnerDependency.startsWith(":runners:spark:") def isDataflowRunner = ":runners:google-cloud-dataflow-java".equals(nexmarkRunnerDependency) def isDataflowRunnerV2 = isDataflowRunner && "V2".equals(nexmarkRunnerVersion) def runnerConfiguration = ":runners:direct-java".equals(nexmarkRunnerDependency) ? "shadow" : null @@ -91,39 +90,15 @@ dependencies { testImplementation project(path: ":sdks:java:testing:test-utils", configuration: "testRuntimeMigration") gradleRun project(project.path) gradleRun project(path: nexmarkRunnerDependency, configuration: runnerConfiguration) - - // The Spark runner requires the user to provide a Spark dependency. For self-contained - // runs with the Spark runner, we can provide such a dependency. This is deliberately phrased - // to not hardcode any runner other than :runners:direct-java - if (shouldProvideSpark2) { - gradleRun library.java.spark_core, { - exclude group:"org.slf4j", module:"jul-to-slf4j" - } - gradleRun library.java.spark_sql - gradleRun library.java.spark_streaming - } - if (shouldProvideSpark3) { - gradleRun library.java.spark3_core, { - exclude group:"org.slf4j", module:"jul-to-slf4j" - } - - gradleRun library.java.spark3_sql - gradleRun library.java.spark3_streaming - } } -if (shouldProvideSpark2) { - configurations.gradleRun { - // Using Spark runner causes a StackOverflowError if slf4j-jdk14 is on the classpath - exclude group: "org.slf4j", module: "slf4j-jdk14" - } -} -if (shouldProvideSpark3) { +if (isSparkRunner) { configurations.gradleRun { // Using Spark runner causes a StackOverflowError if slf4j-jdk14 is on the classpath exclude group: "org.slf4j", module: "slf4j-jdk14" } } + def getNexmarkArgs = { def nexmarkArgsStr = project.findProperty(nexmarkArgsProperty) ?: "" def nexmarkArgsList = new ArrayList() @@ -155,6 +130,12 @@ def getNexmarkArgs = { } } } + + if(isSparkRunner) { + // For transparency, be explicit about configuration of local Spark + nexmarkArgsList.add("--sparkMaster=local[4]") + } + return nexmarkArgsList } @@ -162,7 +143,7 @@ def getNexmarkArgs = { // // Parameters: // -Pnexmark.runner -// Specify a runner subproject, such as ":runners:spark:2" or ":runners:flink:1.13" +// Specify a runner subproject, such as ":runners:spark:3" or ":runners:flink:1.13" // Defaults to ":runners:direct-java" // // -Pnexmark.args @@ -177,6 +158,14 @@ task run(type: JavaExec) { dependsOn ":runners:google-cloud-dataflow-java:worker:legacy-worker:shadowJar" } } + if(isSparkRunner) { + // Disable UI + systemProperty "spark.ui.enabled", "false" + systemProperty "spark.ui.showConsoleProgress", "false" + // Dataset runner only + systemProperty "spark.sql.shuffle.partitions", "4" + } + mainClass = "org.apache.beam.sdk.nexmark.Main" classpath = configurations.gradleRun args nexmarkArgsList.toArray() diff --git a/sdks/java/testing/tpcds/README.md b/sdks/java/testing/tpcds/README.md index 247b5cbe9300..85826e341ffb 100644 --- a/sdks/java/testing/tpcds/README.md +++ b/sdks/java/testing/tpcds/README.md @@ -55,10 +55,10 @@ To run a query using ZetaSQL planner (currently Query96 can be run using ZetaSQL ## Spark Runner -To execute TPC-DS benchmark with Query3 for 1Gb dataset on Apache Spark 2.x, run the following example command from the command line: +To execute TPC-DS benchmark with Query3 for 1Gb dataset on Apache Spark 3.x, run the following example command from the command line: ```bash -./gradlew :sdks:java:testing:tpcds:run -Ptpcds.runner=":runners:spark:2" -Ptpcds.args=" \ +./gradlew :sdks:java:testing:tpcds:run -Ptpcds.runner=":runners:spark:3" -Ptpcds.args=" \ --runner=SparkRunner \ --queries=3 \ --tpcParallel=1 \ diff --git a/sdks/java/testing/tpcds/build.gradle b/sdks/java/testing/tpcds/build.gradle index e9537cfe50ca..325222e8e8f1 100644 --- a/sdks/java/testing/tpcds/build.gradle +++ b/sdks/java/testing/tpcds/build.gradle @@ -94,7 +94,7 @@ if (isSpark) { // // Parameters: // -Ptpcds.runner -// Specify a runner subproject, such as ":runners:spark:2" or ":runners:flink:1.13" +// Specify a runner subproject, such as ":runners:spark:3" or ":runners:flink:1.13" // Defaults to ":runners:direct-java" // // -Ptpcds.args diff --git a/sdks/java/testing/tpcds/src/main/java/org/apache/beam/sdk/tpcds/SqlTransformRunner.java b/sdks/java/testing/tpcds/src/main/java/org/apache/beam/sdk/tpcds/SqlTransformRunner.java index cf3c7433f08e..cd337e87d876 100644 --- a/sdks/java/testing/tpcds/src/main/java/org/apache/beam/sdk/tpcds/SqlTransformRunner.java +++ b/sdks/java/testing/tpcds/src/main/java/org/apache/beam/sdk/tpcds/SqlTransformRunner.java @@ -177,7 +177,6 @@ private static PCollection getTableParquet( "Read " + tableName + " (parquet)", ParquetIO.read(schema) .from(filepattern) - .withSplit() .withProjection(schemaProjected, schemaProjected) .withBeamSchemas(true)); } diff --git a/sdks/python/apache_beam/coders/coder_impl.py b/sdks/python/apache_beam/coders/coder_impl.py index 9e48a3a8f8e6..094687ce68d8 100644 --- a/sdks/python/apache_beam/coders/coder_impl.py +++ b/sdks/python/apache_beam/coders/coder_impl.py @@ -112,6 +112,7 @@ globals()['create_OutputStream'] = create_OutputStream globals()['ByteCountingOutputStream'] = ByteCountingOutputStream # pylint: enable=wrong-import-order, wrong-import-position, ungrouped-imports + is_compiled = True _LOGGER = logging.getLogger(__name__) @@ -611,6 +612,12 @@ class BytesCoderImpl(CoderImpl): A coder for bytes/str objects.""" def encode_to_stream(self, value, out, nested): # type: (bytes, create_OutputStream, bool) -> None + + # value might be of type np.bytes if passed from encode_batch, and cython + # does not recognize it as bytes. + if is_compiled and isinstance(value, np.bytes_): + value = bytes(value) + out.write(value, nested) def decode_from_stream(self, in_stream, nested): diff --git a/sdks/python/apache_beam/examples/complete/autocomplete_it_test.py b/sdks/python/apache_beam/examples/complete/autocomplete_it_test.py index 28312b7303b2..a19af5873186 100644 --- a/sdks/python/apache_beam/examples/complete/autocomplete_it_test.py +++ b/sdks/python/apache_beam/examples/complete/autocomplete_it_test.py @@ -27,28 +27,8 @@ from apache_beam.examples.complete import autocomplete from apache_beam.testing.test_pipeline import TestPipeline - -# Protect against environments where gcsio library is not available. -try: - from apache_beam.io.gcp import gcsio -except ImportError: - gcsio = None - - -def read_gcs_output_file(file_pattern): - gcs = gcsio.GcsIO() - file_names = gcs.list_prefix(file_pattern).keys() - output = [] - for file_name in file_names: - output.append(gcs.open(file_name).read().decode('utf-8').strip()) - return '\n'.join(output) - - -def create_content_input_file(path, contents): - logging.info('Creating file: %s', path) - gcs = gcsio.GcsIO() - with gcs.open(path, 'w') as f: - f.write(str.encode(contents, 'utf-8')) +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern def format_output_file(output_string): @@ -99,13 +79,13 @@ def test_autocomplete_output_files_on_small_input(self): INPUT_FILE_DIR = \ 'gs://temp-storage-for-end-to-end-tests/py-it-cloud/input' input = '/'.join([INPUT_FILE_DIR, str(uuid.uuid4()), 'input.txt']) - create_content_input_file(input, ' '.join(self.WORDS)) + create_file(input, ' '.join(self.WORDS)) extra_opts = {'input': input, 'output': output} autocomplete.run(test_pipeline.get_full_options_as_args(**extra_opts)) # Load result file and compare. - result = read_gcs_output_file(output).strip() + result = read_files_from_pattern('%s*' % output).strip() self.assertEqual( sorted(self.EXPECTED_PREFIXES), sorted(format_output_file(result))) diff --git a/sdks/python/apache_beam/examples/complete/distribopt_test.py b/sdks/python/apache_beam/examples/complete/distribopt_test.py index 50d20d3d62cd..b9d507410267 100644 --- a/sdks/python/apache_beam/examples/complete/distribopt_test.py +++ b/sdks/python/apache_beam/examples/complete/distribopt_test.py @@ -20,9 +20,8 @@ # pytype: skip-file import logging -import os -import tempfile import unittest +import uuid from ast import literal_eval as make_tuple import numpy as np @@ -30,7 +29,9 @@ from mock import MagicMock from mock import patch -from apache_beam.testing.util import open_shards +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern FILE_CONTENTS = 'OP01,8,12,0,12\n' \ 'OP02,30,14,3,12\n' \ @@ -44,16 +45,18 @@ class DistribOptimizationTest(unittest.TestCase): - def create_file(self, path, contents): - logging.info('Creating temp file: %s', path) - with open(path, 'w') as f: - f.write(contents) - + #TODO(https://github.com/apache/beam/issues/23606) Fix and enable + @pytest.mark.sickbay_dataflow @pytest.mark.examples_postcommit def test_basics(self): + test_pipeline = TestPipeline(is_integration_test=True) + # Setup the files with expected content. - temp_folder = tempfile.mkdtemp() - self.create_file(os.path.join(temp_folder, 'input.txt'), FILE_CONTENTS) + temp_location = test_pipeline.get_option('temp_location') + input = '/'.join([temp_location, str(uuid.uuid4()), 'input.txt']) + output = '/'.join([temp_location, str(uuid.uuid4()), 'result']) + create_file(input, FILE_CONTENTS) + extra_opts = {'input': input, 'output': output} # Run pipeline # Avoid dependency on SciPy @@ -64,16 +67,12 @@ def test_basics(self): with patch.dict('sys.modules', modules): from apache_beam.examples.complete import distribopt - distribopt.run([ - '--input=%s/input.txt' % temp_folder, - '--output', - os.path.join(temp_folder, 'result') - ], - save_main_session=False) + distribopt.run( + test_pipeline.get_full_options_as_args(**extra_opts), + save_main_session=False) # Load result file and compare. - with open_shards(os.path.join(temp_folder, 'result-*-of-*')) as result_file: - lines = result_file.readlines() + lines = read_files_from_pattern('%s*' % output).splitlines() # Only 1 result self.assertEqual(len(lines), 1) diff --git a/sdks/python/apache_beam/examples/complete/estimate_pi_it_test.py b/sdks/python/apache_beam/examples/complete/estimate_pi_it_test.py index cda92e5da4cb..bf6f8fc76c11 100644 --- a/sdks/python/apache_beam/examples/complete/estimate_pi_it_test.py +++ b/sdks/python/apache_beam/examples/complete/estimate_pi_it_test.py @@ -27,21 +27,7 @@ from apache_beam.examples.complete import estimate_pi from apache_beam.testing.test_pipeline import TestPipeline - -# Protect against environments where gcsio library is not available. -try: - from apache_beam.io.gcp import gcsio -except ImportError: - gcsio = None - - -def read_gcs_output_file(file_pattern): - gcs = gcsio.GcsIO() - file_names = gcs.list_prefix(file_pattern).keys() - output = [] - for file_name in file_names: - output.append(gcs.open(file_name).read().decode('utf-8')) - return '\n'.join(output) +from apache_beam.testing.test_utils import read_files_from_pattern class EstimatePiIT(unittest.TestCase): @@ -55,7 +41,7 @@ def test_estimate_pi_output_file(self): extra_opts = {'output': output} estimate_pi.run(test_pipeline.get_full_options_as_args(**extra_opts)) # Load result file and compare. - result = read_gcs_output_file(output) + result = read_files_from_pattern('%s*' % output) [_, _, estimated_pi] = json.loads(result.strip()) # Note: Probabilistically speaking this test can fail with a probability # that is very small (VERY) given that we run at least 100 thousand diff --git a/sdks/python/apache_beam/examples/complete/tfidf.py b/sdks/python/apache_beam/examples/complete/tfidf.py index 16ce2b8471a7..d7829f9d1c7d 100644 --- a/sdks/python/apache_beam/examples/complete/tfidf.py +++ b/sdks/python/apache_beam/examples/complete/tfidf.py @@ -24,13 +24,13 @@ # pytype: skip-file import argparse -import glob import math import re import apache_beam as beam from apache_beam.io import ReadFromText from apache_beam.io import WriteToText +from apache_beam.io.filesystems import FileSystems from apache_beam.options.pipeline_options import PipelineOptions from apache_beam.options.pipeline_options import SetupOptions from apache_beam.pvalue import AsSingleton @@ -200,7 +200,9 @@ def run(argv=None, save_main_session=True): with beam.Pipeline(options=pipeline_options) as p: # Read documents specified by the uris command line option. - pcoll = read_documents(p, glob.glob(known_args.uris)) + metadata_list = FileSystems.match([known_args.uris])[0].metadata_list + uris = [metadata.path for metadata in metadata_list] + pcoll = read_documents(p, uris) # Compute TF-IDF information for each word. output = pcoll | TfIdf() # Write the output using a "Write" transform that has side effects. diff --git a/sdks/python/apache_beam/examples/complete/tfidf_it_test.py b/sdks/python/apache_beam/examples/complete/tfidf_it_test.py new file mode 100644 index 000000000000..fe1649bbfa35 --- /dev/null +++ b/sdks/python/apache_beam/examples/complete/tfidf_it_test.py @@ -0,0 +1,75 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""End-to-end test for TF-IDF example.""" + +# pytype: skip-file + +import logging +import re +import unittest +import uuid + +import pytest + +from apache_beam.examples.complete import tfidf +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern + +EXPECTED_RESULTS = set([ + ('ghi', '1.txt', 0.3662040962227032), ('abc', '1.txt', 0.0), + ('abc', '3.txt', 0.0), ('abc', '2.txt', 0.0), + ('def', '1.txt', 0.13515503603605478), ('def', '2.txt', 0.2027325540540822) +]) + +EXPECTED_LINE_RE = r'\(u?\'([a-z]*)\', \(\'.*([0-9]\.txt)\', (.*)\)\)' + + +class TfIdfIT(unittest.TestCase): + @pytest.mark.examples_postcommit + def test_basics(self): + test_pipeline = TestPipeline(is_integration_test=True) + + # Setup the files with expected content. + temp_location = test_pipeline.get_option('temp_location') + input_folder = '/'.join([temp_location, str(uuid.uuid4())]) + create_file('/'.join([input_folder, '1.txt']), 'abc def ghi') + create_file('/'.join([input_folder, '2.txt']), 'abc def') + create_file('/'.join([input_folder, '3.txt']), 'abc') + output = '/'.join([temp_location, str(uuid.uuid4()), 'result']) + + extra_opts = {'uris': '%s/**' % input_folder, 'output': output} + tfidf.run( + test_pipeline.get_full_options_as_args(**extra_opts), + save_main_session=False) + + # Parse result file and compare. + results = [] + lines = read_files_from_pattern('%s*' % output).splitlines() + for line in lines: + match = re.search(EXPECTED_LINE_RE, line) + logging.info('Result line: %s', line) + if match is not None: + results.append((match.group(1), match.group(2), float(match.group(3)))) + logging.info('Computed results: %s', set(results)) + self.assertEqual(set(results), EXPECTED_RESULTS) + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + unittest.main() diff --git a/sdks/python/apache_beam/examples/complete/tfidf_test.py b/sdks/python/apache_beam/examples/complete/tfidf_test.py index 07138cd11e4f..085b9e2dd186 100644 --- a/sdks/python/apache_beam/examples/complete/tfidf_test.py +++ b/sdks/python/apache_beam/examples/complete/tfidf_test.py @@ -20,19 +20,13 @@ # pytype: skip-file import logging -import os -import re -import tempfile import unittest -import pytest - import apache_beam as beam from apache_beam.examples.complete import tfidf from apache_beam.testing.test_pipeline import TestPipeline from apache_beam.testing.util import assert_that from apache_beam.testing.util import equal_to -from apache_beam.testing.util import open_shards EXPECTED_RESULTS = set([ ('ghi', '1.txt', 0.3662040962227032), ('abc', '1.txt', 0.0), @@ -40,15 +34,8 @@ ('def', '1.txt', 0.13515503603605478), ('def', '2.txt', 0.2027325540540822) ]) -EXPECTED_LINE_RE = r'\(u?\'([a-z]*)\', \(\'.*([0-9]\.txt)\', (.*)\)\)' - class TfIdfTest(unittest.TestCase): - def create_file(self, path, contents): - logging.info('Creating temp file: %s', path) - with open(path, 'wb') as f: - f.write(contents.encode('utf-8')) - def test_tfidf_transform(self): with TestPipeline() as p: @@ -65,31 +52,6 @@ def re_key(word_uri_tfidf): # To actually trigger the check the pipeline must be run (e.g. by # exiting the with context). - @pytest.mark.examples_postcommit - def test_basics(self): - # Setup the files with expected content. - temp_folder = tempfile.mkdtemp() - self.create_file(os.path.join(temp_folder, '1.txt'), 'abc def ghi') - self.create_file(os.path.join(temp_folder, '2.txt'), 'abc def') - self.create_file(os.path.join(temp_folder, '3.txt'), 'abc') - tfidf.run([ - '--uris=%s/*' % temp_folder, - '--output', - os.path.join(temp_folder, 'result') - ], - save_main_session=False) - # Parse result file and compare. - results = [] - with open_shards(os.path.join(temp_folder, 'result-*-of-*')) as result_file: - for line in result_file: - match = re.search(EXPECTED_LINE_RE, line) - logging.info('Result line: %s', line) - if match is not None: - results.append( - (match.group(1), match.group(2), float(match.group(3)))) - logging.info('Computed results: %s', set(results)) - self.assertEqual(set(results), EXPECTED_RESULTS) - if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) diff --git a/sdks/python/apache_beam/examples/complete/top_wikipedia_sessions_it_test.py b/sdks/python/apache_beam/examples/complete/top_wikipedia_sessions_it_test.py index 64dcd06c8ecb..caae4a32d8e7 100644 --- a/sdks/python/apache_beam/examples/complete/top_wikipedia_sessions_it_test.py +++ b/sdks/python/apache_beam/examples/complete/top_wikipedia_sessions_it_test.py @@ -27,28 +27,8 @@ from apache_beam.examples.complete import top_wikipedia_sessions from apache_beam.testing.test_pipeline import TestPipeline - -# Protect against environments where gcsio library is not available. -try: - from apache_beam.io.gcp import gcsio -except ImportError: - gcsio = None - - -def read_gcs_output_file(file_pattern): - gcs = gcsio.GcsIO() - file_names = gcs.list_prefix(file_pattern).keys() - output = [] - for file_name in file_names: - output.append(gcs.open(file_name).read().decode('utf-8')) - return '\n'.join(output) - - -def create_content_input_file(path, contents): - logging.info('Creating file: %s', path) - gcs = gcsio.GcsIO() - with gcs.open(path, 'w') as f: - f.write(str.encode(contents, 'utf-8')) +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern class ComputeTopSessionsIT(unittest.TestCase): @@ -102,13 +82,13 @@ def test_top_wikipedia_sessions_output_files_on_small_input(self): INPUT_FILE_DIR = \ 'gs://temp-storage-for-end-to-end-tests/py-it-cloud/input' input = '/'.join([INPUT_FILE_DIR, str(uuid.uuid4()), 'input.txt']) - create_content_input_file(input, '\n'.join(self.EDITS)) + create_file(input, '\n'.join(self.EDITS)) extra_opts = {'input': input, 'output': output, 'sampling_threshold': '1.0'} top_wikipedia_sessions.run( test_pipeline.get_full_options_as_args(**extra_opts)) # Load result file and compare. - result = read_gcs_output_file(output).strip().splitlines() + result = read_files_from_pattern('%s*' % output).strip().splitlines() self.assertEqual(self.EXPECTED, sorted(result, key=lambda x: x.split()[0])) diff --git a/sdks/python/apache_beam/examples/cookbook/coders_it_test.py b/sdks/python/apache_beam/examples/cookbook/coders_it_test.py index c40200348507..941311ce5dc3 100644 --- a/sdks/python/apache_beam/examples/cookbook/coders_it_test.py +++ b/sdks/python/apache_beam/examples/cookbook/coders_it_test.py @@ -27,28 +27,8 @@ from apache_beam.examples.cookbook import coders from apache_beam.testing.test_pipeline import TestPipeline - -# Protect against environments where gcsio library is not available. -try: - from apache_beam.io.gcp import gcsio -except ImportError: - gcsio = None - - -def read_gcs_output_file(file_pattern): - gcs = gcsio.GcsIO() - file_names = gcs.list_prefix(file_pattern).keys() - output = [] - for file_name in file_names: - output.append(gcs.open(file_name).read().decode('utf-8').strip()) - return '\n'.join(output) - - -def create_content_input_file(path, contents): - logging.info('Creating file: %s', path) - gcs = gcsio.GcsIO() - with gcs.open(path, 'w') as f: - f.write(str.encode(contents, 'utf-8')) +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern def format_result(result_string): @@ -87,13 +67,12 @@ def test_coders_output_files_on_small_input(self): INPUT_FILE_DIR = \ 'gs://temp-storage-for-end-to-end-tests/py-it-cloud/input' input = '/'.join([INPUT_FILE_DIR, str(uuid.uuid4()), 'input.txt']) - create_content_input_file( - input, '\n'.join(map(json.dumps, self.SAMPLE_RECORDS))) + create_file(input, '\n'.join(map(json.dumps, self.SAMPLE_RECORDS))) extra_opts = {'input': input, 'output': output} coders.run(test_pipeline.get_full_options_as_args(**extra_opts)) # Load result file and compare. - result = read_gcs_output_file(output).strip() + result = read_files_from_pattern('%s*' % output).strip() self.assertEqual( sorted(self.EXPECTED_RESULT), sorted(format_result(result))) diff --git a/sdks/python/apache_beam/examples/cookbook/custom_ptransform_it_test.py b/sdks/python/apache_beam/examples/cookbook/custom_ptransform_it_test.py new file mode 100644 index 000000000000..9ad0c52bf23c --- /dev/null +++ b/sdks/python/apache_beam/examples/cookbook/custom_ptransform_it_test.py @@ -0,0 +1,70 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""End-to-end test for Custom PTransform example.""" +# pytype: skip-file + +import logging +import unittest +import uuid + +import pytest + +from apache_beam.examples.cookbook import custom_ptransform +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern + + +def format_result(result_string): + def format_tuple(result_elem_list): + [country, counter] = result_elem_list + return country, int(counter.strip()) + + result_list = list( + map( + lambda result_elem: format_tuple(result_elem.split(',')), + result_string.replace('\'', '').replace('[', + '').replace(']', '').replace( + '\"', '').split('\n'))) + return result_list + + +class CustomPTransformIT(unittest.TestCase): + WORDS = ['CAT', 'DOG', 'CAT', 'CAT', 'DOG'] + EXPECTED_RESULT = "('CAT DOG CAT CAT DOG', 2)" + + @pytest.mark.examples_postcommit + def test_custom_ptransform_output_files_on_small_input(self): + test_pipeline = TestPipeline(is_integration_test=True) + + # Setup the files with expected content. + temp_location = test_pipeline.get_option('temp_location') + input = '/'.join([temp_location, str(uuid.uuid4()), 'input.txt']) + output = '/'.join([temp_location, str(uuid.uuid4()), 'result']) + create_file(input, ' '.join(self.WORDS)) + extra_opts = {'input': input, 'output': output} + custom_ptransform.run(test_pipeline.get_full_options_as_args(**extra_opts)) + + # Load result file and compare. + result = read_files_from_pattern('%s*' % output).strip() + self.assertEqual(result, self.EXPECTED_RESULT) + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + unittest.main() diff --git a/sdks/python/apache_beam/examples/cookbook/custom_ptransform_test.py b/sdks/python/apache_beam/examples/cookbook/custom_ptransform_test.py index 4bcd09566796..cac4eae15b93 100644 --- a/sdks/python/apache_beam/examples/cookbook/custom_ptransform_test.py +++ b/sdks/python/apache_beam/examples/cookbook/custom_ptransform_test.py @@ -20,18 +20,13 @@ # pytype: skip-file import logging -import os -import tempfile import unittest -import pytest - import apache_beam as beam from apache_beam.examples.cookbook import custom_ptransform from apache_beam.testing.test_pipeline import TestPipeline from apache_beam.testing.util import assert_that from apache_beam.testing.util import equal_to -from apache_beam.testing.util import open_shards class CustomCountTest(unittest.TestCase): @@ -59,26 +54,6 @@ def run_pipeline(self, count_implementation, factor=1): assert_that( result, equal_to([('CAT', (3 * factor)), ('DOG', (2 * factor))])) - @pytest.mark.examples_postcommit - def test_custom_ptransform_output_files_on_small_input(self): - EXPECTED_RESULT = "('CAT DOG CAT CAT DOG', 2)" - - # Setup the files with expected content. - temp_folder = tempfile.mkdtemp() - self.create_content_input_file( - os.path.join(temp_folder, 'input.txt'), ' '.join(self.WORDS)) - custom_ptransform.run([ - '--input=%s/input.txt' % temp_folder, - '--output', - os.path.join(temp_folder, 'result') - ]) - - # Load result file and compare. - with open_shards(os.path.join(temp_folder, 'result-*-of-*')) as result_file: - result = result_file.read().strip() - - self.assertEqual(result, EXPECTED_RESULT) - if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) diff --git a/sdks/python/apache_beam/examples/cookbook/group_with_coder_test.py b/sdks/python/apache_beam/examples/cookbook/group_with_coder_test.py index f6bdf3e146ad..9cf36e70e45a 100644 --- a/sdks/python/apache_beam/examples/cookbook/group_with_coder_test.py +++ b/sdks/python/apache_beam/examples/cookbook/group_with_coder_test.py @@ -20,13 +20,20 @@ # pytype: skip-file import logging -import tempfile import unittest +import uuid import pytest from apache_beam.examples.cookbook import group_with_coder -from apache_beam.testing.util import open_shards +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import read_files_from_pattern + +# Protect against environments where gcsio library is not available. +try: + from apache_beam.io.gcp import gcsio +except ImportError: + gcsio = None # Patch group_with_coder.PlayerCoder.decode(). To test that the PlayerCoder was # used, we do not strip the prepended 'x:' string when decoding a Player object. @@ -34,6 +41,16 @@ s.decode('utf-8')) +def create_content_input_file(path, records): + logging.info('Creating file: %s', path) + gcs = gcsio.GcsIO() + with gcs.open(path, 'w') as f: + for record in records: + f.write(b'%s\n' % record.encode('utf-8')) + return path + + +@unittest.skipIf(gcsio is None, 'GCP dependencies are not installed') @pytest.mark.examples_postcommit class GroupWithCoderTest(unittest.TestCase): @@ -49,27 +66,32 @@ class GroupWithCoderTest(unittest.TestCase): 'mary,1' ] - def create_temp_file(self, records): - with tempfile.NamedTemporaryFile(delete=False) as f: - for record in records: - f.write(b'%s\n' % record.encode('utf-8')) - return f.name + def setUp(self): + self.test_pipeline = TestPipeline(is_integration_test=True) + # Setup the file with expected content. + self.temp_location = self.test_pipeline.get_option('temp_location') + self.input_file = create_content_input_file( + '/'.join([self.temp_location, str(uuid.uuid4()), 'input.txt']), + self.SAMPLE_RECORDS) + #TODO(https://github.com/apache/beam/issues/23608) Fix and enable + @pytest.mark.sickbay_dataflow def test_basics_with_type_check(self): # Run the workflow with pipeline_type_check option. This will make sure # the typehints associated with all transforms will have non-default values # and therefore any custom coders will be used. In our case we want to make # sure the coder for the Player class will be used. - temp_path = self.create_temp_file(self.SAMPLE_RECORDS) + output = '/'.join([self.temp_location, str(uuid.uuid4()), 'result']) + extra_opts = {'input': self.input_file, 'output': output} group_with_coder.run( - ['--input=%s*' % temp_path, '--output=%s.result' % temp_path], + self.test_pipeline.get_full_options_as_args(**extra_opts), save_main_session=False) # Parse result file and compare. results = [] - with open_shards(temp_path + '.result-*-of-*') as result_file: - for line in result_file: - name, points = line.split(',') - results.append((name, int(points))) + lines = read_files_from_pattern('%s*' % output).splitlines() + for line in lines: + name, points = line.split(',') + results.append((name, int(points))) logging.info('result: %s', results) self.assertEqual( sorted(results), @@ -80,15 +102,13 @@ def test_basics_without_type_check(self): # the typehints associated with all transforms will have default values and # therefore any custom coders will not be used. The default coder (pickler) # will be used instead. - temp_path = self.create_temp_file(self.SAMPLE_RECORDS) + output = '/'.join([self.temp_location, str(uuid.uuid4()), 'result']) + extra_opts = {'input': self.input_file, 'output': output} with self.assertRaises(Exception) as context: # yapf: disable group_with_coder.run( - [ - '--no_pipeline_type_check', - '--input=%s*' % temp_path, - '--output=%s.result' % temp_path - ], + self.test_pipeline.get_full_options_as_args(**extra_opts) + + ['--no_pipeline_type_check'], save_main_session=False) self.assertIn('Unable to deterministically encode', str(context.exception)) self.assertIn('CombinePerKey(sum)/GroupByKey', str(context.exception)) diff --git a/sdks/python/apache_beam/examples/cookbook/mergecontacts_test.py b/sdks/python/apache_beam/examples/cookbook/mergecontacts_test.py index e6f790e71c23..e996084b81cd 100644 --- a/sdks/python/apache_beam/examples/cookbook/mergecontacts_test.py +++ b/sdks/python/apache_beam/examples/cookbook/mergecontacts_test.py @@ -20,13 +20,15 @@ # pytype: skip-file import logging -import tempfile import unittest +import uuid import pytest from apache_beam.examples.cookbook import mergecontacts -from apache_beam.testing.util import open_shards +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern class MergeContactsTest(unittest.TestCase): @@ -118,11 +120,6 @@ class MergeContactsTest(unittest.TestCase): EXPECTED_STATS = '\n'.join(['2 luddites', '1 writers', '3 nomads', '']) - def create_temp_file(self, contents): - with tempfile.NamedTemporaryFile(delete=False) as f: - f.write(contents.encode('utf-8')) - return f.name - def normalize_tsv_results(self, tsv_data): """Sort .tsv file data so we can compare it with expected output.""" lines_in = tsv_data.strip().split('\n') @@ -140,25 +137,38 @@ def normalize_tsv_results(self, tsv_data): @pytest.mark.examples_postcommit def test_mergecontacts(self): - path_email = self.create_temp_file(self.CONTACTS_EMAIL) - path_phone = self.create_temp_file(self.CONTACTS_PHONE) - path_snailmail = self.create_temp_file(self.CONTACTS_SNAILMAIL) - - result_prefix = self.create_temp_file('') - - mergecontacts.run([ - '--input_email=%s' % path_email, - '--input_phone=%s' % path_phone, - '--input_snailmail=%s' % path_snailmail, - '--output_tsv=%s.tsv' % result_prefix, - '--output_stats=%s.stats' % result_prefix - ], - assert_results=(2, 1, 3), - save_main_session=False) - - with open_shards('%s.tsv-*-of-*' % result_prefix) as f: - contents = f.read() - self.assertEqual(self.EXPECTED_TSV, self.normalize_tsv_results(contents)) + test_pipeline = TestPipeline(is_integration_test=True) + + # Setup the files with expected content. + temp_location = test_pipeline.get_option('temp_location') + input_folder = '/'.join([temp_location, str(uuid.uuid4())]) + path_email = create_file( + '/'.join([input_folder, 'path_email.txt']), self.CONTACTS_EMAIL) + path_phone = create_file( + '/'.join([input_folder, 'path_phone.txt']), self.CONTACTS_PHONE) + path_snailmail = create_file( + '/'.join([input_folder, 'path_snailmail.txt']), self.CONTACTS_SNAILMAIL) + + result_prefix = '/'.join([temp_location, str(uuid.uuid4()), 'result']) + extra_opts = { + 'input_email': path_email, + 'input_phone': path_phone, + 'input_snailmail': path_snailmail, + 'output_tsv': '%s.tsv' % result_prefix, + 'output_stats': '%s.stats' % result_prefix + } + + pipeline_opts = test_pipeline.get_full_options_as_args(**extra_opts) + # Prevent ambiguous option error between output in + # args and expected output_tsv and output_stats + output_arg = [i for i in pipeline_opts if i.startswith('--output=')] + if output_arg: + pipeline_opts.remove(output_arg[0]) + mergecontacts.run( + pipeline_opts, assert_results=(2, 1, 3), save_main_session=False) + + contents = read_files_from_pattern('%s*' % result_prefix) + self.assertEqual(self.EXPECTED_TSV, self.normalize_tsv_results(contents)) if __name__ == '__main__': diff --git a/sdks/python/apache_beam/examples/cookbook/multiple_output_pardo_test.py b/sdks/python/apache_beam/examples/cookbook/multiple_output_pardo_test.py index 62bf505fcbff..706cff70ba70 100644 --- a/sdks/python/apache_beam/examples/cookbook/multiple_output_pardo_test.py +++ b/sdks/python/apache_beam/examples/cookbook/multiple_output_pardo_test.py @@ -21,13 +21,15 @@ import logging import re -import tempfile import unittest +import uuid import pytest from apache_beam.examples.cookbook import multiple_output_pardo -from apache_beam.testing.util import open_shards +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern class MultipleOutputParDo(unittest.TestCase): @@ -37,39 +39,38 @@ class MultipleOutputParDo(unittest.TestCase): EXPECTED_WORDS = [('whole', 1), ('world', 1), ('fantastic', 1), ('point', 1), ('view', 1)] - def create_temp_file(self, contents): - with tempfile.NamedTemporaryFile(delete=False) as f: - f.write(contents.encode('utf-8')) - return f.name - def get_wordcount_results(self, result_path): results = [] - with open_shards(result_path) as result_file: - for line in result_file: - match = re.search(r'([A-Za-z]+): ([0-9]+)', line) - if match is not None: - results.append((match.group(1), int(match.group(2)))) + lines = read_files_from_pattern(result_path).splitlines() + for line in lines: + match = re.search(r'([A-Za-z]+): ([0-9]+)', line) + if match is not None: + results.append((match.group(1), int(match.group(2)))) return results @pytest.mark.examples_postcommit def test_multiple_output_pardo(self): - temp_path = self.create_temp_file(self.SAMPLE_TEXT) - result_prefix = temp_path + '.result' + test_pipeline = TestPipeline(is_integration_test=True) + + # Setup the files with expected content. + temp_location = test_pipeline.get_option('temp_location') + input_folder = '/'.join([temp_location, str(uuid.uuid4())]) + input = create_file('/'.join([input_folder, 'input.txt']), self.SAMPLE_TEXT) + result_prefix = '/'.join([temp_location, str(uuid.uuid4()), 'result']) + extra_opts = {'input': input, 'output': result_prefix} multiple_output_pardo.run( - ['--input=%s*' % temp_path, '--output=%s' % result_prefix], + test_pipeline.get_full_options_as_args(**extra_opts), save_main_session=False) expected_char_count = len(''.join(self.SAMPLE_TEXT.split('\n'))) - with open_shards(result_prefix + '-chars-*-of-*') as f: - contents = f.read() - self.assertEqual(expected_char_count, int(contents)) + contents = read_files_from_pattern(result_prefix + '-chars*') + self.assertEqual(expected_char_count, int(contents)) - short_words = self.get_wordcount_results( - result_prefix + '-short-words-*-of-*') + short_words = self.get_wordcount_results(result_prefix + '-short-words*') self.assertEqual(sorted(short_words), sorted(self.EXPECTED_SHORT_WORDS)) - words = self.get_wordcount_results(result_prefix + '-words-*-of-*') + words = self.get_wordcount_results(result_prefix + '-words*') self.assertEqual(sorted(words), sorted(self.EXPECTED_WORDS)) diff --git a/sdks/python/apache_beam/examples/dataframe/wordcount_test.py b/sdks/python/apache_beam/examples/dataframe/wordcount_test.py index 12180b9506e4..25cd401cb9a9 100644 --- a/sdks/python/apache_beam/examples/dataframe/wordcount_test.py +++ b/sdks/python/apache_beam/examples/dataframe/wordcount_test.py @@ -23,13 +23,15 @@ import collections import logging import re -import tempfile import unittest +import uuid import pytest from apache_beam.examples.dataframe import wordcount -from apache_beam.testing.util import open_shards +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern class WordCountTest(unittest.TestCase): @@ -41,27 +43,27 @@ class WordCountTest(unittest.TestCase): loooooonger words """ - def create_temp_file(self, contents): - with tempfile.NamedTemporaryFile(delete=False) as f: - f.write(contents.encode('utf-8')) - return f.name - @pytest.mark.examples_postcommit def test_basics(self): - temp_path = self.create_temp_file(self.SAMPLE_TEXT) + test_pipeline = TestPipeline(is_integration_test=True) + # Setup the files with expected content. + temp_location = test_pipeline.get_option('temp_location') + temp_path = '/'.join([temp_location, str(uuid.uuid4())]) + input = create_file('/'.join([temp_path, 'input.txt']), self.SAMPLE_TEXT) expected_words = collections.defaultdict(int) for word in re.findall(r'[\w]+', self.SAMPLE_TEXT): expected_words[word] += 1 - wordcount.run(['--input=%s*' % temp_path, '--output=%s.result' % temp_path]) + extra_opts = {'input': input, 'output': '%s.result' % temp_path} + wordcount.run(test_pipeline.get_full_options_as_args(**extra_opts)) # Parse result file and compare. results = [] - with open_shards(temp_path + '.result-*') as result_file: - for line in result_file: - match = re.search(r'(\S+),([0-9]+)', line) - if match is not None: - results.append((match.group(1), int(match.group(2)))) - elif line.strip(): - self.assertEqual(line.strip(), 'word,count') + lines = read_files_from_pattern(temp_path + '.result*').splitlines() + for line in lines: + match = re.search(r'(\S+),([0-9]+)', line) + if match is not None: + results.append((match.group(1), int(match.group(2)))) + elif line.strip(): + self.assertEqual(line.strip(), 'word,count') self.assertEqual(sorted(results), sorted(expected_words.items())) diff --git a/sdks/python/apache_beam/examples/inference/runinference_metrics/__init__.py b/sdks/python/apache_beam/examples/inference/runinference_metrics/__init__.py new file mode 100644 index 000000000000..cce3acad34a4 --- /dev/null +++ b/sdks/python/apache_beam/examples/inference/runinference_metrics/__init__.py @@ -0,0 +1,16 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diff --git a/sdks/python/apache_beam/examples/inference/runinference_metrics/config.py b/sdks/python/apache_beam/examples/inference/runinference_metrics/config.py new file mode 100644 index 000000000000..61b9a21bea4a --- /dev/null +++ b/sdks/python/apache_beam/examples/inference/runinference_metrics/config.py @@ -0,0 +1,30 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""The file defines global variables.""" + +PROJECT_ID = "" +REGION = "us-central1" +JOB_NAME = "benchmarking-runinference" +NUM_WORKERS = 1 +TOKENIZER_NAME = "distilbert-base-uncased-finetuned-sst-2-english" +MODEL_STATE_DICT_PATH = ( + f"gs://{PROJECT_ID}-ml-examples/{TOKENIZER_NAME}/pytorch_model.bin") +MODEL_CONFIG_PATH = TOKENIZER_NAME +IMG_NAME = "kfp-components-preprocessing/pytorch-gpu" +TAG = "latest" +DOCKER_IMG = f"{REGION}-docker.pkg.dev/{PROJECT_ID}/{IMG_NAME}:{TAG}" diff --git a/sdks/python/apache_beam/examples/inference/runinference_metrics/main.py b/sdks/python/apache_beam/examples/inference/runinference_metrics/main.py new file mode 100644 index 000000000000..7feeda4ea8e0 --- /dev/null +++ b/sdks/python/apache_beam/examples/inference/runinference_metrics/main.py @@ -0,0 +1,127 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""This file contains the pipeline for loading a ML model, and exploring +the different RunInference metrics.""" +import argparse +import logging +import sys + +import apache_beam as beam +import config as cfg +from apache_beam.ml.inference import RunInference +from apache_beam.ml.inference.base import KeyedModelHandler +from apache_beam.ml.inference.pytorch_inference import PytorchModelHandlerKeyedTensor +from pipeline.options import get_pipeline_options +from pipeline.transformations import CustomPytorchModelHandlerKeyedTensor +from pipeline.transformations import HuggingFaceStripBatchingWrapper +from pipeline.transformations import PostProcessor +from pipeline.transformations import Tokenize +from transformers import DistilBertConfig + + +def parse_arguments(argv): + """ + Parses the arguments passed to the command line and + returns them as an object + Args: + argv: The arguments passed to the command line. + Returns: + The arguments that are being passed in. + """ + parser = argparse.ArgumentParser(description="benchmark-runinference") + + parser.add_argument( + "-m", + "--mode", + help="Mode to run pipeline in.", + choices=["local", "cloud"], + default="local", + ) + parser.add_argument( + "-p", + "--project", + help="GCP project to run pipeline on.", + default=cfg.PROJECT_ID, + ) + parser.add_argument( + "-d", + "--device", + help="Device to run the dataflow job on", + choices=["CPU", "GPU"], + default="CPU", + ) + + args, _ = parser.parse_known_args(args=argv) + return args + + +def run(): + """ + Runs the pipeline that loads a transformer based text classification model + and does inference on a list of sentences. + At the end of pipeline, different metrics like latency, + throughput and others are printed. + """ + args = parse_arguments(sys.argv) + + inputs = [ + "This is the worst food I have ever eaten", + "In my soul and in my heart, I’m convinced I’m wrong!", + "Be with me always—take any form—drive me mad!"\ + "only do not leave me in this abyss, where I cannot find you!", + "Do I want to live? Would you like to live with your soul in the grave?", + "Honest people don’t hide their deeds.", + "Nelly, I am Heathcliff! He’s always,"\ + "always in my mind: not as a pleasure,"\ + "any more than I am always a pleasure to myself, but as my own being.", + ] * 1000 + + pipeline_options = get_pipeline_options( + job_name=cfg.JOB_NAME, + num_workers=cfg.NUM_WORKERS, + project=args.project, + mode=args.mode, + device=args.device, + ) + model_handler_class = ( + PytorchModelHandlerKeyedTensor + if args.device == "GPU" else CustomPytorchModelHandlerKeyedTensor) + device = "cuda:0" if args.device == "GPU" else args.device + model_handler = model_handler_class( + state_dict_path=cfg.MODEL_STATE_DICT_PATH, + model_class=HuggingFaceStripBatchingWrapper, + model_params={ + "config": DistilBertConfig.from_pretrained(cfg.MODEL_CONFIG_PATH) + }, + device=device, + ) + + with beam.Pipeline(options=pipeline_options) as pipeline: + _ = ( + pipeline + | "Create inputs" >> beam.Create(inputs) + | "Tokenize" >> beam.ParDo(Tokenize(cfg.TOKENIZER_NAME)) + | "Inference" >> + RunInference(model_handler=KeyedModelHandler(model_handler)) + | "Decode Predictions" >> beam.ParDo(PostProcessor())) + metrics = pipeline.result.metrics().query(beam.metrics.MetricsFilter()) + logging.info(metrics) + + +if __name__ == "__main__": + run() diff --git a/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/__init__.py b/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/__init__.py new file mode 100644 index 000000000000..cce3acad34a4 --- /dev/null +++ b/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/__init__.py @@ -0,0 +1,16 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diff --git a/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/options.py b/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/options.py new file mode 100644 index 000000000000..b32200ed7331 --- /dev/null +++ b/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/options.py @@ -0,0 +1,74 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""This file contains the pipeline options to configure +the Dataflow pipeline.""" + +from datetime import datetime +from typing import Any + +import config as cfg +from apache_beam.options.pipeline_options import PipelineOptions + + +def get_pipeline_options( + project: str, + job_name: str, + mode: str, + device: str, + num_workers: int = cfg.NUM_WORKERS, + **kwargs: Any, +) -> PipelineOptions: + """Function to retrieve the pipeline options. + Args: + project: GCP project to run on + mode: Indicator to run local, cloud or template + num_workers: Number of Workers for running the job parallely + Returns: + Dataflow pipeline options + """ + job_name = f'{job_name}-{datetime.now().strftime("%Y%m%d%H%M%S")}' + + staging_bucket = f"gs://{cfg.PROJECT_ID}-ml-examples" + + # For a list of available options, check: + # https://cloud.google.com/dataflow/docs/guides/specifying-exec-params#setting-other-cloud-dataflow-pipeline-options + dataflow_options = { + "runner": "DirectRunner" if mode == "local" else "DataflowRunner", + "job_name": job_name, + "project": project, + "region": cfg.REGION, + "staging_location": f"{staging_bucket}/dflow-staging", + "temp_location": f"{staging_bucket}/dflow-temp", + "setup_file": "./setup.py", + } + flags = [] + if device == "GPU": + flags = [ + "--experiment=worker_accelerator=type:nvidia-tesla-p4;count:1;"\ + "install-nvidia-driver", + "--experiment=use_runner_v2", + ] + dataflow_options.update({ + "sdk_container_image": cfg.DOCKER_IMG, + "machine_type": "n1-standard-4", + }) + + # Optional parameters + if num_workers: + dataflow_options.update({"num_workers": num_workers}) + return PipelineOptions(flags=flags, **dataflow_options) diff --git a/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/transformations.py b/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/transformations.py new file mode 100644 index 000000000000..e7f6f9d44689 --- /dev/null +++ b/sdks/python/apache_beam/examples/inference/runinference_metrics/pipeline/transformations.py @@ -0,0 +1,94 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""This file contains the transformations and utility functions for +the pipeline.""" +import apache_beam as beam +import torch +from apache_beam.io.filesystems import FileSystems +from apache_beam.ml.inference.pytorch_inference import PytorchModelHandlerKeyedTensor +from transformers import DistilBertForSequenceClassification +from transformers import DistilBertTokenizer + + +class CustomPytorchModelHandlerKeyedTensor(PytorchModelHandlerKeyedTensor): + """Wrapper around PytorchModelHandlerKeyedTensor to load a model on CPU.""" + def load_model(self) -> torch.nn.Module: + """Loads and initializes a Pytorch model for processing.""" + model = self._model_class(**self._model_params) + model.to(self._device) + file = FileSystems.open(self._state_dict_path, "rb") + model.load_state_dict(torch.load(file, map_location=self._device)) + model.eval() + return model + + +# Can be removed once https://github.com/apache/beam/issues/21863 is fixed +class HuggingFaceStripBatchingWrapper(DistilBertForSequenceClassification): + """Wrapper around HuggingFace model because RunInference requires a batch + as a list of dicts instead of a dict of lists. Another workaround + can be found here where they disable batching instead. + https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_language_modeling.py""" + def forward(self, **kwargs): + output = super().forward(**kwargs) + return [dict(zip(output, v)) for v in zip(*output.values())] + + +class Tokenize(beam.DoFn): + """A DoFn for tokenizing texts""" + def __init__(self, model_name: str): + """Initialises a tokenizer based on the model_name""" + self._model_name = model_name + + def setup(self): + """Loads the tokenizer""" + self._tokenizer = DistilBertTokenizer.from_pretrained(self._model_name) + + def process(self, text_input: str): + """Prepocesses the text using the tokenizer""" + # We need to pad the tokens tensors to max length to make sure + # that all the tensors are of the same length and hence + # stack-able by the RunInference API, normally you would batch first + # and tokenize the batch after and pad each tensor + # the the max length in the batch. + tokens = self._tokenizer( + text_input, return_tensors="pt", padding="max_length", max_length=512) + # squeeze because tokenization add an extra dimension, which is empty + # in this case because we're tokenizing one element at a time. + tokens = {key: torch.squeeze(val) for key, val in tokens.items()} + return [(text_input, tokens)] + + +class PostProcessor(beam.DoFn): + """Postprocess the RunInference output""" + def process(self, element): + """ + Takes the input text and the prediction result, and returns a dictionary + with the input text and the softmax probabilities + + Args: + element: The tuple of input text and the prediction result + + Returns: + A list of dictionaries, each containing the input text + and the softmax output. + """ + text_input, prediction_result = element + softmax = ( + torch.nn.Softmax(dim=-1)( + prediction_result.inference["logits"]).detach().numpy()) + return [{"input": text_input, "softmax": softmax}] diff --git a/sdks/python/apache_beam/examples/inference/runinference_metrics/setup.py b/sdks/python/apache_beam/examples/inference/runinference_metrics/setup.py new file mode 100644 index 000000000000..d6fb9742ac4c --- /dev/null +++ b/sdks/python/apache_beam/examples/inference/runinference_metrics/setup.py @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Setup.py module for the workflow's worker utilities. + +All the workflow related code is gathered in a package that will be built as a +source distribution, staged in the staging area for the workflow being run and +then installed in the workers when they start running. + +This behavior is triggered by specifying the --setup_file command line option +when running the workflow for remote execution. +""" + +import setuptools +from setuptools import find_packages + +REQUIREMENTS = [ + "apache-beam[gcp]==2.41.0", "transformers==4.21.0", "torch==1.12.0" +] + +setuptools.setup( + name="write-to-pubsub-pipeline", + version="1.1.1", + install_requires=REQUIREMENTS, + packages=find_packages(), + author="Apache Software Foundation", + author_email="dev@beam.apache.org", + py_modules=["config"], +) diff --git a/sdks/python/apache_beam/examples/ml-orchestration/README.md b/sdks/python/apache_beam/examples/ml-orchestration/README.md new file mode 100644 index 000000000000..2f886f09e582 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/README.md @@ -0,0 +1,22 @@ + + +# Example ML workflow orchestration with Kubeflow Pipelines and Tensorflow Extended + +This module contains two examples of simple, orchestrated machine learning workflows that rely on Apache Beam for data preprocessing. A detailed explanation can be found on the Beam website [here](https://beam.apache.org/documentation/ml/orchestration/) \ No newline at end of file diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/Dockerfile b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/Dockerfile new file mode 100644 index 000000000000..98f9262c7f3e --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/Dockerfile @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM python:3.9-slim + +# optional: install extra dependencies + +# install python packages +# (the requirements file is currently empty +# because this is a stub ingestion example) +COPY requirements.txt / +RUN python3 -m pip install --no-cache-dir -r requirements.txt + +# copy src files and set working directory +COPY src /src +WORKDIR /src diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/component.yaml b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/component.yaml new file mode 100644 index 000000000000..e25d240c5f77 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/component.yaml @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Ingestion +description: Component that mimicks scraping data from the web and outputs it to a jsonlines format file +inputs: + - name: base_artifact_path + description: base path to store data + type: String +outputs: + - name: ingested_dataset_path + description: target uri for the ingested dataset + type: String +implementation: + container: + image: + command: [ + python3, + ingest.py, + --base-artifact-path, + {inputValue: base_artifact_path}, + --ingested-dataset-path, + {outputPath: ingested_dataset_path} + ] diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/requirements.txt b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/requirements.txt new file mode 100644 index 000000000000..91eacc92e8be --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/requirements.txt @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/src/ingest.py b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/src/ingest.py new file mode 100644 index 000000000000..5369e95bf927 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/src/ingest.py @@ -0,0 +1,74 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Ingestion function that fetches data from one file and simply copies it to another.""" + +import argparse +import time +from pathlib import Path + + +def parse_args(): + """Parse ingestion arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--ingested-dataset-path", + type=str, + help="Path to save the ingested dataset to.", + required=True) + parser.add_argument( + "--base-artifact-path", + type=str, + help="Base path to store pipeline artifacts.", + required=True) + return parser.parse_args() + + +def ingest_data(ingested_dataset_path: str, base_artifact_path: str): + """Data ingestion step that returns an uri + to the data it has 'ingested' as jsonlines. + + Args: + data_ingestion_target (str): uri to the data that was scraped and + ingested by the component""" + # timestamp as unique id for the component execution + timestamp = int(time.time()) + + # create directory to store the actual data + target_path = f"{base_artifact_path}/ingestion/ingested_dataset_{timestamp}.jsonl" + # if the target path is a google cloud storage path convert the path to the gcsfuse path + target_path_gcsfuse = target_path.replace("gs://", "/gcs/") + Path(target_path_gcsfuse).parent.mkdir(parents=True, exist_ok=True) + + with open(target_path_gcsfuse, 'w') as f: + f.writelines([ + """{"image_id": 318556, "id": 255, "caption": "An angled view of a beautifully decorated bathroom.", "image_url": "http://farm4.staticflickr.com/3133/3378902101_3c9fa16b84_z.jpg", "image_name": "COCO_train2014_000000318556.jpg", "image_license": "Attribution-NonCommercial-ShareAlike License"}\n""", + """{"image_id": 476220, "id": 314, "caption": "An empty kitchen with white and black appliances.", "image_url": "http://farm7.staticflickr.com/6173/6207941582_b69380c020_z.jpg", "image_name": "COCO_train2014_000000476220.jpg", "image_license": "Attribution-NonCommercial License"}\n""", + """{"image_id": 134754, "id": 425, "caption": "Two people carrying surf boards on a beach.", "image_url": "http://farm9.staticflickr.com/8500/8398513396_b6a1f11a4b_z.jpg", "image_name": "COCO_train2014_000000134754.jpg", "image_license": "Attribution-NonCommercial-NoDerivs License"}""" + ]) + + # the directory where the output file is created may or may not exists + # so we have to create it. + # KFP v1 components can only write output to files. The output of this + # component is written to ingested_dataset_path and contains the path + # of the actual ingested data + Path(ingested_dataset_path).parent.mkdir(parents=True, exist_ok=True) + with open(ingested_dataset_path, 'w') as f: + f.write(target_path) + + +if __name__ == "__main__": + args = parse_args() + ingest_data(**vars(args)) diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/Dockerfile b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/Dockerfile new file mode 100644 index 000000000000..f46feded1da4 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/Dockerfile @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START component_dockerfile] +FROM python:3.9-slim + +# (Optional) install extra dependencies + +# install pypi dependencies +COPY requirements.txt / +RUN python3 -m pip install --no-cache-dir -r requirements.txt + +# copy src files and set working directory +COPY src /src +WORKDIR /src +# [END component_dockerfile] diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/component.yaml b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/component.yaml new file mode 100644 index 000000000000..f64c3c11fb69 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/component.yaml @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START preprocessing_component_definition] +name: preprocessing +description: Component that mimicks scraping data from the web and outputs it to a jsonlines format file +inputs: + - name: ingested_dataset_path + description: source uri of the data to scrape + type: String + - name: base_artifact_path + description: base path to store data + type: String + - name: gcp_project_id + description: ID for the google cloud project to deploy the pipeline to. + type: String + - name: region + description: Region in which to deploy the Dataflow pipeline. + type: String + - name: dataflow_staging_root + description: Path to staging directory for the dataflow runner. + type: String + - name: beam_runner + description: Beam runner, DataflowRunner or DirectRunner. + type: String +outputs: + - name: preprocessed_dataset_path + description: target uri for the ingested dataset + type: String +implementation: + container: + image: + command: [ + python3, + preprocess.py, + --ingested-dataset-path, + {inputValue: ingested_dataset_path}, + --base-artifact-path, + {inputValue: base_artifact_path}, + --preprocessed-dataset-path, + {outputPath: preprocessed_dataset_path}, + --gcp-project-id, + {inputValue: gcp_project_id}, + --region, + {inputValue: region}, + --dataflow-staging-root, + {inputValue: dataflow_staging_root}, + --beam-runner, + {inputValue: beam_runner}, + ] +# [END preprocessing_component_definition] + diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/requirements.txt b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/requirements.txt new file mode 100644 index 000000000000..2ebbd8cf2149 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/requirements.txt @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apache_beam[gcp]==2.40.0 +requests==2.28.1 +torch==1.12.0 +torchvision==0.13.0 +numpy==1.22.4 +Pillow==9.2.0 diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/src/preprocess.py b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/src/preprocess.py new file mode 100644 index 000000000000..7cf6d6ead4a3 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/src/preprocess.py @@ -0,0 +1,208 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functionality for the data preprocessing step.""" + +import re +import json +import io +import argparse +import time +from pathlib import Path +import logging +from collections.abc import Iterable + +import requests +from PIL import Image, UnidentifiedImageError +import numpy as np +import torch +import torchvision.transforms as T +import torchvision.transforms.functional as TF +import apache_beam as beam +from apache_beam.options.pipeline_options import PipelineOptions + +IMAGE_SIZE = (224, 244) + + +# [START preprocess_component_argparse] +def parse_args(): + """Parse preprocessing arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--ingested-dataset-path", + type=str, + help="Path to the ingested dataset", + required=True) + parser.add_argument( + "--preprocessed-dataset-path", + type=str, + help="The target directory for the ingested dataset.", + required=True) + parser.add_argument( + "--base-artifact-path", + type=str, + help="Base path to store pipeline artifacts.", + required=True) + parser.add_argument( + "--gcp-project-id", + type=str, + help="ID for the google cloud project to deploy the pipeline to.", + required=True) + parser.add_argument( + "--region", + type=str, + help="Region in which to deploy the pipeline.", + required=True) + parser.add_argument( + "--dataflow-staging-root", + type=str, + help="Path to staging directory for dataflow.", + required=True) + parser.add_argument( + "--beam-runner", + type=str, + help="Beam runner: DataflowRunner or DirectRunner.", + default="DirectRunner") + + return parser.parse_args() + + +# [END preprocess_component_argparse] + + +def preprocess_dataset( + ingested_dataset_path: str, + preprocessed_dataset_path: str, + base_artifact_path: str, + gcp_project_id: str, + region: str, + dataflow_staging_root: str, + beam_runner: str): + """Preprocess the ingested raw dataset and write the result to avro format. + + Args: + ingested_dataset_path (str): Path to the ingested dataset + preprocessed_dataset_path (str): Path to where the preprocessed dataset will be saved + base_artifact_path (str): path to the base directory of where artifacts can be stored for + this component. + gcp_project_id (str): ID for the google cloud project to deploy the pipeline to. + region (str): Region in which to deploy the pipeline. + dataflow_staging_root (str): Path to staging directory for the dataflow runner. + beam_runner (str): Beam runner: DataflowRunner or DirectRunner. + """ + # [START kfp_component_input_output] + timestamp = time.time() + target_path = f"{base_artifact_path}/preprocessing/preprocessed_dataset_{timestamp}" + + # the directory where the output file is created may or may not exists + # so we have to create it. + Path(preprocessed_dataset_path).parent.mkdir(parents=True, exist_ok=True) + with open(preprocessed_dataset_path, 'w') as f: + f.write(target_path) + # [END kfp_component_input_output] + + # [START deploy_preprocessing_beam_pipeline] + # We use the save_main_session option because one or more DoFn's in this + # workflow rely on global context (e.g., a module imported at module level). + pipeline_options = PipelineOptions( + runner=beam_runner, + project=gcp_project_id, + job_name=f'preprocessing-{int(time.time())}', + temp_location=dataflow_staging_root, + region=region, + requirements_file="/requirements.txt", + save_main_session=True, + ) + + with beam.Pipeline(options=pipeline_options) as pipeline: + ( + pipeline + | "Read input jsonlines file" >> + beam.io.ReadFromText(ingested_dataset_path) + | "Load json" >> beam.Map(json.loads) + | "Filter licenses" >> beam.Filter(valid_license) + | "Download image from URL" >> beam.FlatMap(download_image_from_url) + | "Resize image" >> beam.Map(resize_image, size=IMAGE_SIZE) + | "Clean Text" >> beam.Map(clean_text) + | "Serialize Example" >> beam.Map(serialize_example) + | "Write to Avro files" >> beam.io.WriteToAvro( + file_path_prefix=target_path, + schema={ + "namespace": "preprocessing.example", + "type": "record", + "name": "Sample", + "fields": [{ + "name": "id", "type": "int" + }, { + "name": "caption", "type": "string" + }, { + "name": "image", "type": "bytes" + }] + }, + file_name_suffix=".avro")) + # [END deploy_preprocessing_beam_pipeline] + + +def download_image_from_url(element: dict) -> Iterable[dict]: + """download the images from their uri.""" + response = requests.get(element['image_url']) + try: + image = Image.open(io.BytesIO(response.content)) + image = T.ToTensor()(image) + yield {**element, 'image': image} + except UnidentifiedImageError as e: + logging.exception(e) + + +def resize_image(element: dict, size=(256, 256)): + "Resize the element's PIL image to the target resolution." + image = TF.resize(element['image'], size) + return {**element, 'image': image} + + +def clean_text(element: dict): + """Perform a series of string cleaning operations.""" + text = element['caption'] + text = text.lower() # lower case + text = re.sub(r"http\S+", "", text) # remove urls + text = re.sub("\s+", " ", text) # remove extra spaces (including \n and \t) + text = re.sub( + "[()[\].,|:;?!=+~\-\/{}]", ",", + text) # all puncutation are replace w commas + text = f" {text}" # always start with a space + text = text.strip(',') # remove commas at the start or end of the caption + text = text[:-1] if text and text[-1] == "," else text + text = text[1:] if text and text[0] == "," else text + return {**element, "preprocessed_caption": text} + + +def valid_license(element): + """Checks whether an element's image has the correct license for our use case.""" + license = element['image_license'] + return license in ["Attribution License", "No known copyright restrictions"] + + +def serialize_example(element): + """Serialize an elements image.""" + buffer = io.BytesIO() + torch.save(element['image'], buffer) + buffer.seek(0) + image = buffer.read() + return {**element, 'image': image} + + +if __name__ == "__main__": + args = parse_args() + preprocess_dataset(**vars(args)) diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/Dockerfile b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/Dockerfile new file mode 100644 index 000000000000..8e2bf86d8113 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/Dockerfile @@ -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. + +FROM python:3.9-slim + +# optional install extra dependencies + +# install pypi dependencies +COPY requirements.txt / +RUN python3 -m pip install --no-cache-dir -r requirements.txt + +# copy src files and set working directory +COPY src /src +WORKDIR /src diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/component.yaml b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/component.yaml new file mode 100644 index 000000000000..240ed13acf9a --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/component.yaml @@ -0,0 +1,41 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: model training +description: Train a pytorch model +inputs: + - name: base_artifact_path + description: base path to store data + type: String + - name: preprocessed_dataset_path + description: path to the preprocessed dataset + type: String +outputs: + - name: trained_model_path + description: trained model file + type: String +implementation: + container: + image: + command: [ + python3, + train.py, + --preprocessed-dataset-path, + {inputValue: preprocessed_dataset_path}, + --base-artifact-path, + {inputValue: base_artifact_path}, + --trained-model-path, + {outputPath: trained_model_path} + ] diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/requirements.txt b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/requirements.txt new file mode 100644 index 000000000000..72eb22959697 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/requirements.txt @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +torch==1.12.0 +numpy==1.22.4 +Pillow==9.2.0 \ No newline at end of file diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/src/train.py b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/src/train.py new file mode 100644 index 000000000000..b15473f22483 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/src/train.py @@ -0,0 +1,83 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Simple training function that loads a pretrained model from the torch hub and saves it.""" + +import argparse +import time +from pathlib import Path + +import torch + + +def parse_args(): + """Parse ingestion arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--preprocessed-dataset-path", + type=str, + help="Path to the preprocessed dataset.", + required=True) + parser.add_argument( + "--trained-model-path", + type=str, + help="Output path to the trained model.", + required=True) + parser.add_argument( + "--base-artifact-path", + type=str, + help="Base path to store pipeline artifacts.", + required=True) + return parser.parse_args() + + +def train_model( + preprocessed_dataset_path: str, + trained_model_path: str, + base_artifact_path: str): + """Placeholder method to load a model from the torch hub and save it. + + Args: + preprocessed_dataset_path (str): Path to the preprocessed dataset + trained_model_path (str): Output path for the trained model + base_artifact_path (str): path to the base directory of where artifacts can be stored for + this component + """ + # timestamp for the component execution + timestamp = time.time() + + # create model or load a pretrained one + model = torch.hub.load('pytorch/vision:v0.10.0', 'vgg16', pretrained=True) + + # to implement: train on preprocessed dataset + # + + # create directory to export the model to + target_path = f"{base_artifact_path}/training/trained_model_{timestamp}.pt" + target_path_gcsfuse = target_path.replace("gs://", "/gcs/") + Path(target_path_gcsfuse).parent.mkdir(parents=True, exist_ok=True) + + # save and export the model + torch.save(model.state_dict(), target_path_gcsfuse) + + # Write the model path to the component output file + Path(trained_model_path).parent.mkdir(parents=True, exist_ok=True) + with open(trained_model_path, 'w') as f: + f.write(target_path) + + +if __name__ == "__main__": + args = parse_args() + train_model(**vars(args)) diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.json b/sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.json new file mode 100644 index 000000000000..a39bb8d253ad --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.json @@ -0,0 +1,247 @@ +{ + "pipelineSpec": { + "components": { + "comp-ingestion": { + "executorLabel": "exec-ingestion", + "inputDefinitions": { + "parameters": { + "base_artifact_path": { + "type": "STRING" + } + } + }, + "outputDefinitions": { + "parameters": { + "ingested_dataset_path": { + "type": "STRING" + } + } + } + }, + "comp-model-training": { + "executorLabel": "exec-model-training", + "inputDefinitions": { + "parameters": { + "base_artifact_path": { + "type": "STRING" + }, + "preprocessed_dataset_path": { + "type": "STRING" + } + } + }, + "outputDefinitions": { + "parameters": { + "trained_model_path": { + "type": "STRING" + } + } + } + }, + "comp-preprocessing": { + "executorLabel": "exec-preprocessing", + "inputDefinitions": { + "parameters": { + "base_artifact_path": { + "type": "STRING" + }, + "beam_runner": { + "type": "STRING" + }, + "dataflow_staging_root": { + "type": "STRING" + }, + "gcp_project_id": { + "type": "STRING" + }, + "ingested_dataset_path": { + "type": "STRING" + }, + "region": { + "type": "STRING" + } + } + }, + "outputDefinitions": { + "parameters": { + "preprocessed_dataset_path": { + "type": "STRING" + } + } + } + } + }, + "deploymentSpec": { + "executors": { + "exec-ingestion": { + "container": { + "command": [ + "python3", + "ingest.py", + "--base-artifact-path", + "{{$.inputs.parameters['base_artifact_path']}}", + "--ingested-dataset-path", + "{{$.outputs.parameters['ingested_dataset_path'].output_file}}" + ], + "image": "" + } + }, + "exec-model-training": { + "container": { + "command": [ + "python3", + "train.py", + "--preprocessed-dataset-path", + "{{$.inputs.parameters['preprocessed_dataset_path']}}", + "--base-artifact-path", + "{{$.inputs.parameters['base_artifact_path']}}", + "--trained-model-path", + "{{$.outputs.parameters['trained_model_path'].output_file}}" + ], + "image": "" + } + }, + "exec-preprocessing": { + "container": { + "command": [ + "python3", + "preprocess.py", + "--ingested-dataset-path", + "{{$.inputs.parameters['ingested_dataset_path']}}", + "--base-artifact-path", + "{{$.inputs.parameters['base_artifact_path']}}", + "--preprocessed-dataset-path", + "{{$.outputs.parameters['preprocessed_dataset_path'].output_file}}", + "--gcp-project-id", + "{{$.inputs.parameters['gcp_project_id']}}", + "--region", + "{{$.inputs.parameters['region']}}", + "--dataflow-staging-root", + "{{$.inputs.parameters['dataflow_staging_root']}}", + "--beam-runner", + "{{$.inputs.parameters['beam_runner']}}" + ], + "image": "" + } + } + } + }, + "pipelineInfo": { + "name": "beam-preprocessing-kfp-example" + }, + "root": { + "dag": { + "tasks": { + "ingestion": { + "cachingOptions": { + "enableCache": true + }, + "componentRef": { + "name": "comp-ingestion" + }, + "inputs": { + "parameters": { + "base_artifact_path": { + "componentInputParameter": "component_artifact_root" + } + } + }, + "taskInfo": { + "name": "ingestion" + } + }, + "model-training": { + "cachingOptions": { + "enableCache": true + }, + "componentRef": { + "name": "comp-model-training" + }, + "dependentTasks": [ + "preprocessing" + ], + "inputs": { + "parameters": { + "base_artifact_path": { + "componentInputParameter": "component_artifact_root" + }, + "preprocessed_dataset_path": { + "taskOutputParameter": { + "outputParameterKey": "preprocessed_dataset_path", + "producerTask": "preprocessing" + } + } + } + }, + "taskInfo": { + "name": "model-training" + } + }, + "preprocessing": { + "cachingOptions": { + "enableCache": true + }, + "componentRef": { + "name": "comp-preprocessing" + }, + "dependentTasks": [ + "ingestion" + ], + "inputs": { + "parameters": { + "base_artifact_path": { + "componentInputParameter": "component_artifact_root" + }, + "beam_runner": { + "componentInputParameter": "beam_runner" + }, + "dataflow_staging_root": { + "componentInputParameter": "dataflow_staging_root" + }, + "gcp_project_id": { + "componentInputParameter": "gcp_project_id" + }, + "ingested_dataset_path": { + "taskOutputParameter": { + "outputParameterKey": "ingested_dataset_path", + "producerTask": "ingestion" + } + }, + "region": { + "componentInputParameter": "region" + } + } + }, + "taskInfo": { + "name": "preprocessing" + } + } + } + }, + "inputDefinitions": { + "parameters": { + "beam_runner": { + "type": "STRING" + }, + "component_artifact_root": { + "type": "STRING" + }, + "dataflow_staging_root": { + "type": "STRING" + }, + "gcp_project_id": { + "type": "STRING" + }, + "region": { + "type": "STRING" + } + } + } + }, + "schemaVersion": "2.0.0", + "sdkVersion": "kfp-1.8.14" + }, + "runtimeConfig": { + "gcsOutputDirectory": "gs://test/test" + } +} \ No newline at end of file diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py b/sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py new file mode 100644 index 000000000000..f687f3dd6477 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py @@ -0,0 +1,132 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +import kfp +from kfp import components as comp +from kfp.v2 import dsl +from kfp.v2.compiler import Compiler + + +def parse_args(): + """Parse arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--gcp-project-id", + type=str, + help="ID for the google cloud project to deploy the pipeline to.", + required=True) + parser.add_argument( + "--region", + type=str, + help="Region in which to deploy the pipeline.", + required=True) + parser.add_argument( + "--pipeline-root", + type=str, + help= + "Path to artifact repository where Kubeflow Pipelines stores a pipeline’s artifacts.", + required=True) + parser.add_argument( + "--component-artifact-root", + type=str, + help= + "Path to artifact repository where Kubeflow Pipelines components can store artifacts.", + required=True) + parser.add_argument( + "--dataflow-staging-root", + type=str, + help="Path to staging directory for dataflow.", + required=True) + parser.add_argument( + "--beam-runner", + type=str, + help="Beam runner: DataflowRunner or DirectRunner.", + default="DirectRunner") + return parser.parse_args() + + +# arguments are parsed as a global variable so +# they can be used in the pipeline decorator below +ARGS = parse_args() +PIPELINE_ROOT = vars(ARGS)['pipeline_root'] + +# [START load_kfp_components] +# load the kfp components from their yaml files +DataIngestOp = comp.load_component('components/ingestion/component.yaml') +DataPreprocessingOp = comp.load_component( + 'components/preprocessing/component.yaml') +TrainModelOp = comp.load_component('components/train/component.yaml') +# [END load_kfp_components] + + +# [START define_kfp_pipeline] +@dsl.pipeline( + pipeline_root=PIPELINE_ROOT, + name="beam-preprocessing-kfp-example", + description="Pipeline to show an apache beam preprocessing example in KFP") +def pipeline( + gcp_project_id: str, + region: str, + component_artifact_root: str, + dataflow_staging_root: str, + beam_runner: str): + """KFP pipeline definition. + + Args: + gcp_project_id (str): ID for the google cloud project to deploy the pipeline to. + region (str): Region in which to deploy the pipeline. + component_artifact_root (str): Path to artifact repository where Kubeflow Pipelines + components can store artifacts. + dataflow_staging_root (str): Path to staging directory for the dataflow runner. + beam_runner (str): Beam runner: DataflowRunner or DirectRunner. + """ + + ingest_data_task = DataIngestOp(base_artifact_path=component_artifact_root) + + data_preprocessing_task = DataPreprocessingOp( + ingested_dataset_path=ingest_data_task.outputs["ingested_dataset_path"], + base_artifact_path=component_artifact_root, + gcp_project_id=gcp_project_id, + region=region, + dataflow_staging_root=dataflow_staging_root, + beam_runner=beam_runner) + + train_model_task = TrainModelOp( + preprocessed_dataset_path=data_preprocessing_task. + outputs["preprocessed_dataset_path"], + base_artifact_path=component_artifact_root) + + +# [END define_kfp_pipeline] + +if __name__ == "__main__": + # [START compile_kfp_pipeline] + Compiler().compile(pipeline_func=pipeline, package_path="pipeline.json") + # [END compile_kfp_pipeline] + + run_arguments = vars(ARGS) + del run_arguments['pipeline_root'] + + # [START execute_kfp_pipeline] + client = kfp.Client() + experiment = client.create_experiment("KFP orchestration example") + run_result = client.run_pipeline( + experiment_id=experiment.id, + job_name="KFP orchestration job", + pipeline_package_path="pipeline.json", + params=run_arguments) + # [END execute_kfp_pipeline] diff --git a/sdks/python/apache_beam/examples/ml-orchestration/kfp/requirements.txt b/sdks/python/apache_beam/examples/ml-orchestration/kfp/requirements.txt new file mode 100644 index 000000000000..7b2ec602a0a2 --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/kfp/requirements.txt @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# requirements to compile the pipeline and execute it on Dataflow +kfp==1.8.13 +google-cloud-aiplatform==1.15 \ No newline at end of file diff --git a/sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_local.py b/sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_local.py new file mode 100644 index 000000000000..2204285a14bf --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_local.py @@ -0,0 +1,141 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Preprocessing example with TFX with the LocalDagRunner and +either the beam DirectRunner or DataflowRunner""" +import argparse +import os + +from tfx import v1 as tfx + + +def parse_args(): + """Parse arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--gcp-project-id", + type=str, + help="ID for the google cloud project to deploy the pipeline to.", + required=True) + parser.add_argument( + "--region", + type=str, + help="Region in which to deploy the pipeline.", + required=True) + parser.add_argument( + "--pipeline-name", + type=str, + help="Name for the Beam pipeline.", + required=True) + parser.add_argument( + "--pipeline-root", + type=str, + help= + "Path to artifact repository where TFX stores a pipeline’s artifacts.", + required=True) + parser.add_argument( + "--csv-file", type=str, help="Path to the csv input file.", required=True) + parser.add_argument( + "--csv-file", type=str, help="Path to the csv input file.", required=True) + parser.add_argument( + "--module-file", + type=str, + help="Path to module file containing the preprocessing_fn and run_fn.", + default="coco_captions_utils.py") + parser.add_argument( + "--beam-runner", + type=str, + help="Beam runner: DataflowRunner or DirectRunner.", + default="DirectRunner") + parser.add_argument( + "--metadata-file", + type=str, + help="Path to store a metadata file as a mock metadata database", + default="metadata.db") + return parser.parse_args() + + +# [START tfx_pipeline] +def create_pipeline( + gcp_project_id, + region, + pipeline_name, + pipeline_root, + csv_file, + module_file, + beam_runner, + metadata_file): + """Create the TFX pipeline. + + Args: + gcp_project_id (str): ID for the google cloud project to deploy the pipeline to. + region (str): Region in which to deploy the pipeline. + pipeline_name (str): Name for the Beam pipeline + pipeline_root (str): Path to artifact repository where TFX + stores a pipeline’s artifacts. + csv_file (str): Path to the csv input file. + module_file (str): Path to module file containing the preprocessing_fn and run_fn. + beam_runner (str): Beam runner: DataflowRunner or DirectRunner. + metadata_file (str): Path to store a metadata file as a mock metadata database. + """ + example_gen = tfx.components.CsvExampleGen(input_base=csv_file) + + # Computes statistics over data for visualization and example validation. + statistics_gen = tfx.components.StatisticsGen( + examples=example_gen.outputs['examples']) + + schema_gen = tfx.components.SchemaGen( + statistics=statistics_gen.outputs['statistics'], infer_feature_shape=True) + + transform = tfx.components.Transform( + examples=example_gen.outputs['examples'], + schema=schema_gen.outputs['schema'], + module_file=module_file) + + trainer = tfx.components.Trainer( + module_file=module_file, + examples=transform.outputs['transformed_examples'], + transform_graph=transform.outputs['transform_graph']) + + components = [example_gen, statistics_gen, schema_gen, transform, trainer] + + beam_pipeline_args_by_runner = { + 'DirectRunner': [], + 'DataflowRunner': [ + '--runner=DataflowRunner', + '--project=' + gcp_project_id, + '--temp_location=' + os.path.join(pipeline_root, 'tmp'), + '--region=' + region, + ] + } + + return tfx.dsl.Pipeline( + pipeline_name=pipeline_name, + pipeline_root=pipeline_root, + components=components, + enable_cache=True, + metadata_connection_config=tfx.orchestration.metadata. + sqlite_metadata_connection_config(metadata_file), + beam_pipeline_args=beam_pipeline_args_by_runner[beam_runner]) + + +# [END tfx_pipeline] + +if __name__ == "__main__": + + # [START tfx_execute_pipeline] + args = parse_args() + tfx.orchestration.LocalDagRunner().run(create_pipeline(**vars(args))) + # [END tfx_execute_pipeline] diff --git a/sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py b/sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py new file mode 100644 index 000000000000..c28f54cae19e --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py @@ -0,0 +1,87 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Implementation of the tfx component functions for the coco captions example.""" + +import tempfile + +import tensorflow as tf +import tensorflow_transform as tft +import tensorflow_transform.beam as tft_beam +from tfx import v1 as tfx + + +# [START tfx_run_fn] +def run_fn(fn_args: tfx.components.FnArgs) -> None: + """Build the TF model, train it and export it.""" + # create a model + model = tf.keras.Sequential() + model.add(tf.keras.layers.Dense(1, input_dim=10)) + model.compile() + + # train the model on the preprocessed data + # model.fit(...) + + # Save model to fn_args.serving_model_dir. + model.save(fn_args.serving_model_dir) + + +# [END tfx_run_fn] + + +# [START tfx_preprocessing_fn] +def preprocessing_fn(inputs): + """Transform raw data.""" + # convert the captions to lowercase + # split the captions into separate words + lower = tf.strings.lower(inputs['caption']) + + # compute the vocabulary of the captions during a full pass + # over the dataset and use this to tokenize. + mean_length = tft.mean(tf.strings.length(lower)) + # + + return { + 'caption_lower': lower, + } + + +# [END tfx_preprocessing_fn] + +# [START tfx_analyze_and_transform] +if __name__ == "__main__": + # Test processing_fn directly without the tfx pipeline + raw_data = [ + { + "caption": "A bicycle replica with a clock as the front wheel." + }, { + "caption": "A black Honda motorcycle parked in front of a garage." + }, { + "caption": "A room with blue walls and a white sink and door." + } + ] + + # define the feature_spec (in a tfx pipeline this would be generated by a SchemaGen component) + feature_spec = dict(caption=tf.io.FixedLenFeature([], tf.string)) + raw_data_metadata = tft.DatasetMetadata.from_feature_spec(feature_spec) + + # test out the beam implementation of the + # processing_fn with AnalyzeAndTransformDataset + with tft_beam.Context(temp_dir=tempfile.mkdtemp()): + transformed_dataset, transform_fn = ( + (raw_data, raw_data_metadata) + | tft_beam.AnalyzeAndTransformDataset(preprocessing_fn)) + transformed_data, transformed_metadata = transformed_dataset +# [END tfx_analyze_and_transform] diff --git a/sdks/python/apache_beam/examples/ml-orchestration/tfx/requirements.txt b/sdks/python/apache_beam/examples/ml-orchestration/tfx/requirements.txt new file mode 100644 index 000000000000..3e43eb6dc3cb --- /dev/null +++ b/sdks/python/apache_beam/examples/ml-orchestration/tfx/requirements.txt @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +tfx==1.9.0 +tensorflow==2.9.1 \ No newline at end of file diff --git a/sdks/python/apache_beam/examples/wordcount_debugging_test.py b/sdks/python/apache_beam/examples/wordcount_debugging_test.py index a36354b389f6..8a19bce777ad 100644 --- a/sdks/python/apache_beam/examples/wordcount_debugging_test.py +++ b/sdks/python/apache_beam/examples/wordcount_debugging_test.py @@ -21,13 +21,15 @@ import logging import re -import tempfile import unittest +import uuid import pytest from apache_beam.examples import wordcount_debugging -from apache_beam.testing.util import open_shards +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern @pytest.mark.examples_postcommit @@ -35,25 +37,25 @@ class WordCountDebuggingTest(unittest.TestCase): SAMPLE_TEXT = 'xx yy Flourish\n zz Flourish Flourish stomach\n aa\n bb cc dd' - def create_temp_file(self, contents): - with tempfile.NamedTemporaryFile(delete=False) as f: - f.write(contents.encode('utf-8')) - return f.name - def get_results(self, temp_path): results = [] - with open_shards(temp_path + '.result-*-of-*') as result_file: - for line in result_file: - match = re.search(r'([A-Za-z]+): ([0-9]+)', line) - if match is not None: - results.append((match.group(1), int(match.group(2)))) + lines = read_files_from_pattern(temp_path + '.result*').splitlines() + for line in lines: + match = re.search(r'([A-Za-z]+): ([0-9]+)', line) + if match is not None: + results.append((match.group(1), int(match.group(2)))) return results def test_basics(self): - temp_path = self.create_temp_file(self.SAMPLE_TEXT) + test_pipeline = TestPipeline(is_integration_test=True) + # Setup the files with expected content. + temp_location = test_pipeline.get_option('temp_location') + temp_path = '/'.join([temp_location, str(uuid.uuid4())]) + input = create_file('/'.join([temp_path, 'input.txt']), self.SAMPLE_TEXT) + extra_opts = {'input': input, 'output': '%s.result' % temp_path} expected_words = [('Flourish', 3), ('stomach', 1)] wordcount_debugging.run( - ['--input=%s*' % temp_path, '--output=%s.result' % temp_path], + test_pipeline.get_full_options_as_args(**extra_opts), save_main_session=False) # Parse result file and compare. diff --git a/sdks/python/apache_beam/examples/wordcount_minimal_test.py b/sdks/python/apache_beam/examples/wordcount_minimal_test.py index 732b808cc33a..b4882d2dd32c 100644 --- a/sdks/python/apache_beam/examples/wordcount_minimal_test.py +++ b/sdks/python/apache_beam/examples/wordcount_minimal_test.py @@ -35,13 +35,15 @@ import collections import logging import re -import tempfile import unittest +import uuid import pytest from apache_beam.examples import wordcount_minimal -from apache_beam.testing.util import open_shards +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern @pytest.mark.examples_postcommit @@ -50,26 +52,29 @@ class WordCountMinimalTest(unittest.TestCase): SAMPLE_TEXT = 'a b c a b a\n aa bb cc aa bb aa' - def create_temp_file(self, contents): - with tempfile.NamedTemporaryFile(delete=False) as f: - f.write(contents.encode('utf-8')) - return f.name - def test_basics(self): - temp_path = self.create_temp_file(self.SAMPLE_TEXT) + test_pipeline = TestPipeline(is_integration_test=True) + + # Setup the files with expected content. + temp_location = test_pipeline.get_option('temp_location') + temp_path = '/'.join([temp_location, str(uuid.uuid4())]) + input = create_file('/'.join([temp_path, 'input.txt']), self.SAMPLE_TEXT) + + extra_opts = {'input': input, 'output': '%s.result' % temp_path} expected_words = collections.defaultdict(int) for word in re.findall(r'\w+', self.SAMPLE_TEXT): expected_words[word] += 1 wordcount_minimal.main( - ['--input=%s*' % temp_path, '--output=%s.result' % temp_path], + test_pipeline.get_full_options_as_args(**extra_opts), save_main_session=False) + # Parse result file and compare. results = [] - with open_shards(temp_path + '.result-*-of-*') as result_file: - for line in result_file: - match = re.search(r'([a-z]+): ([0-9]+)', line) - if match is not None: - results.append((match.group(1), int(match.group(2)))) + lines = read_files_from_pattern(temp_path + '.result*').splitlines() + for line in lines: + match = re.search(r'([a-z]+): ([0-9]+)', line) + if match is not None: + results.append((match.group(1), int(match.group(2)))) self.assertEqual(sorted(results), sorted(expected_words.items())) diff --git a/sdks/python/apache_beam/examples/wordcount_test.py b/sdks/python/apache_beam/examples/wordcount_test.py index 96428c2ee075..7a0f1093c244 100644 --- a/sdks/python/apache_beam/examples/wordcount_test.py +++ b/sdks/python/apache_beam/examples/wordcount_test.py @@ -36,13 +36,15 @@ import collections import logging import re -import tempfile import unittest +import uuid import pytest from apache_beam.examples import wordcount -from apache_beam.testing.util import open_shards +from apache_beam.testing.test_pipeline import TestPipeline +from apache_beam.testing.test_utils import create_file +from apache_beam.testing.test_utils import read_files_from_pattern @pytest.mark.examples_postcommit @@ -51,25 +53,26 @@ class WordCountTest(unittest.TestCase): SAMPLE_TEXT = ( u'a b c a b a\nacento gráfico\nJuly 30, 2018\n\n aa bb cc aa bb aa') - def create_temp_file(self, contents): - with tempfile.NamedTemporaryFile(delete=False) as f: - f.write(contents.encode('utf-8')) - return f.name - def test_basics(self): - temp_path = self.create_temp_file(self.SAMPLE_TEXT) + test_pipeline = TestPipeline(is_integration_test=True) + # Setup the files with expected content. + temp_location = test_pipeline.get_option('temp_location') + temp_path = '/'.join([temp_location, str(uuid.uuid4())]) + input = create_file('/'.join([temp_path, 'input.txt']), self.SAMPLE_TEXT) + extra_opts = {'input': input, 'output': '%s.result' % temp_path} expected_words = collections.defaultdict(int) for word in re.findall(r'[\w\']+', self.SAMPLE_TEXT, re.UNICODE): expected_words[word] += 1 - wordcount.run(['--input=%s*' % temp_path, '--output=%s.result' % temp_path], - save_main_session=False) + wordcount.run( + test_pipeline.get_full_options_as_args(**extra_opts), + save_main_session=False) # Parse result file and compare. results = [] - with open_shards(temp_path + '.result-*-of-*') as result_file: - for line in result_file: - match = re.search(r'(\S+): ([0-9]+)', line) - if match is not None: - results.append((match.group(1), int(match.group(2)))) + lines = read_files_from_pattern(temp_path + '.result*').splitlines() + for line in lines: + match = re.search(r'(\S+): ([0-9]+)', line) + if match is not None: + results.append((match.group(1), int(match.group(2)))) self.assertEqual(sorted(results), sorted(expected_words.items())) diff --git a/sdks/python/apache_beam/internal/gcp/auth.py b/sdks/python/apache_beam/internal/gcp/auth.py index 699ec79d4b8a..47c3416babd4 100644 --- a/sdks/python/apache_beam/internal/gcp/auth.py +++ b/sdks/python/apache_beam/internal/gcp/auth.py @@ -22,8 +22,10 @@ import logging import socket import threading +from typing import Optional from apache_beam.options.pipeline_options import GoogleCloudOptions +from apache_beam.options.pipeline_options import PipelineOptions # google.auth is only available when Beam is installed with the gcp extra. try: @@ -63,6 +65,8 @@ def set_running_in_gce(worker_executing_project): def get_service_credentials(pipeline_options): + # type: (PipelineOptions) -> Optional[google.auth.credentials.Credentials] + """For internal use only; no backwards-compatibility guarantees. Get credentials to access Google services. @@ -115,6 +119,7 @@ class _Credentials(object): @classmethod def get_service_credentials(cls, pipeline_options): + # type: (PipelineOptions) -> Optional[google.auth.credentials.Credentials] with cls._credentials_lock: if cls._credentials_init: return cls._credentials @@ -134,6 +139,7 @@ def get_service_credentials(cls, pipeline_options): @staticmethod def _get_service_credentials(pipeline_options): + # type: (PipelineOptions) -> Optional[google.auth.credentials.Credentials] if not _GOOGLE_AUTH_AVAILABLE: _LOGGER.warning( 'Unable to find default credentials because the google-auth library ' diff --git a/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py b/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py index 75dad46124f4..1dcb56c51eca 100644 --- a/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py +++ b/sdks/python/apache_beam/io/external/xlang_jdbcio_it_test.py @@ -17,7 +17,6 @@ # pytype: skip-file -import datetime import logging import time import typing @@ -26,7 +25,6 @@ from typing import Callable from typing import Union -import pytz from parameterized import parameterized import apache_beam as beam @@ -58,18 +56,13 @@ ROW_COUNT = 10 -JdbcReadTestRow = typing.NamedTuple( - "JdbcReadTestRow", - [("f_int", int), ("f_timestamp", Timestamp), ("f_decimal", Decimal)], +JdbcTestRow = typing.NamedTuple( + "JdbcTestRow", + [("f_id", int), ("f_float", float), ("f_char", str), ("f_varchar", str), + ("f_bytes", bytes), ("f_varbytes", bytes), ("f_timestamp", Timestamp), + ("f_decimal", Decimal)], ) -coders.registry.register_coder(JdbcReadTestRow, coders.RowCoder) - -JdbcWriteTestRow = typing.NamedTuple( - "JdbcWriteTestRow", - [("f_id", int), ("f_real", float), ("f_string", str), - ("f_timestamp", Timestamp), ("f_decimal", Decimal)], -) -coders.registry.register_coder(JdbcWriteTestRow, coders.RowCoder) +coders.registry.register_coder(JdbcTestRow, coders.RowCoder) @unittest.skipIf(sqlalchemy is None, 'sql alchemy package is not installed.') @@ -123,29 +116,60 @@ def tearDown(self): logging.error('Could not stop the postgreSQL container.') @parameterized.expand(['postgres', 'mysql']) - def test_xlang_jdbc_write(self, database): + def test_xlang_jdbc_write_read(self, database): container_init, classpath, db_string, driver = ( CrossLanguageJdbcIOTest.DB_CONTAINER_CLASSPATH_STRING[database]) self._setUpTestCase(container_init, db_string, driver) - table_name = 'jdbc_external_test_write' + table_name = 'jdbc_external_test' + if database == 'postgres': + # postgres does not have BINARY and VARBINARY type, use equvalent. + binary_type = ('BYTEA', 'BYTEA') + else: + binary_type = ('BINARY(10)', 'VARBINARY(10)') + self.engine.execute( - "CREATE TABLE {}(f_id INTEGER, f_real FLOAT, f_string VARCHAR(100), f_timestamp TIMESTAMP(3), f_decimal DECIMAL(10, 2))" # pylint: disable=line-too-long - .format(table_name)) + "CREATE TABLE IF NOT EXISTS {}".format(table_name) + "(f_id INTEGER, " + + "f_float DOUBLE PRECISION, " + "f_char CHAR(10), " + + "f_varchar VARCHAR(10), " + f"f_bytes {binary_type[0]}, " + + f"f_varbytes {binary_type[1]}, " + "f_timestamp TIMESTAMP(3), " + + "f_decimal DECIMAL(10, 2))") inserted_rows = [ - JdbcWriteTestRow( + JdbcTestRow( i, i + 0.1, - 'Test{}'.format(i), + f'Test{i}', + f'Test{i}', + f'Test{i}'.encode(), + f'Test{i}'.encode(), # In alignment with Java Instant which supports milli precision. Timestamp.of(seconds=round(time.time(), 3)), + # Test both positive and negative numbers. Decimal(f'{i-1}.23')) for i in range(ROW_COUNT) ] + expected_row = [] + for row in inserted_rows: + f_char = row.f_char + ' ' * (10 - len(row.f_char)) + if database != 'postgres': + # padding expected results + f_bytes = row.f_bytes + b'\0' * (10 - len(row.f_bytes)) + else: + f_bytes = row.f_bytes + expected_row.append( + JdbcTestRow( + row.f_id, + row.f_float, + f_char, + row.f_varchar, + f_bytes, + row.f_bytes, + row.f_timestamp, + row.f_decimal)) with TestPipeline() as p: p.not_use_test_runner_api = True _ = ( p - | beam.Create(inserted_rows).with_output_types(JdbcWriteTestRow) + | beam.Create(inserted_rows).with_output_types(JdbcTestRow) # TODO(https://github.com/apache/beam/issues/20446) Add test with # overridden write_statement | 'Write to jdbc' >> WriteToJdbc( @@ -157,46 +181,6 @@ def test_xlang_jdbc_write(self, database): classpath=classpath, )) - fetched_data = self.engine.execute("SELECT * FROM {}".format(table_name)) - fetched_rows = [ - JdbcWriteTestRow( - int(row[0]), - float(row[1]), - str(row[2]), - Timestamp.from_utc_datetime(row[3].replace(tzinfo=pytz.UTC)), - Decimal(row[4])) for row in fetched_data - ] - - self.assertEqual( - set(fetched_rows), - set(inserted_rows), - 'Inserted data does not fit data fetched from table', - ) - - @parameterized.expand(['postgres', 'mysql']) - def test_xlang_jdbc_read(self, database): - container_init, classpath, db_string, driver = ( - CrossLanguageJdbcIOTest.DB_CONTAINER_CLASSPATH_STRING[database]) - self._setUpTestCase(container_init, db_string, driver) - table_name = 'jdbc_external_test_read' - self.engine.execute( - "CREATE TABLE {}(f_int INTEGER, f_timestamp TIMESTAMP, f_decimal DECIMAL(10,2))" # pylint: disable=line-too-long - .format(table_name)) - - all_timestamps = [] - for i in range(ROW_COUNT): - # prepare timestamp - strtime = Timestamp.now().to_utc_datetime().strftime('%Y-%m-%dT%H:%M:%S') - dttime = datetime.datetime.strptime( - strtime, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=pytz.UTC) - all_timestamps.append(Timestamp.from_utc_datetime(dttime)) - decimal_value = Decimal(f'{i-1}.23') - - # write records using sqlalchemy engine - self.engine.execute( - "INSERT INTO {} VALUES({},'{}','{}')".format( - table_name, i, strtime, decimal_value)) - # Register MillisInstant logical type to override the mapping from Timestamp # originally handled by MicrosInstant. LogicalType.register_logical_type(MillisInstant) @@ -215,12 +199,7 @@ def test_xlang_jdbc_read(self, database): password=self.password, classpath=classpath)) - assert_that( - result, - equal_to([ - JdbcReadTestRow(i, all_timestamps[i], Decimal(f'{i-1}.23')) - for i in range(ROW_COUNT) - ])) + assert_that(result, equal_to(expected_row)) # Creating a container with testcontainers sometimes raises ReadTimeout # error. In java there are 2 retries set by default. diff --git a/sdks/python/apache_beam/io/gcp/bigquery.py b/sdks/python/apache_beam/io/gcp/bigquery.py index bad20f69243f..7233326ce0c2 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery.py +++ b/sdks/python/apache_beam/io/gcp/bigquery.py @@ -369,6 +369,7 @@ def chain_after(result): from apache_beam.io.avroio import _create_avro_source as create_avro_source from apache_beam.io.filesystems import CompressionTypes from apache_beam.io.filesystems import FileSystems +from apache_beam.io.gcp import bigquery_schema_tools from apache_beam.io.gcp import bigquery_tools from apache_beam.io.gcp.bigquery_io_metadata import create_bigquery_io_metadata from apache_beam.io.gcp.bigquery_read_internal import _BigQueryReadSplit @@ -2471,9 +2472,9 @@ def _expand_output_type(self, output_pcollection): raise TypeError( '%s: table must be of type string' '; got a callable instead' % self.__class__.__name__) - return output_pcollection | beam.io.gcp.bigquery_schema_tools.\ + return output_pcollection | bigquery_schema_tools.\ convert_to_usertype( - beam.io.gcp.bigquery.bigquery_tools.BigQueryWrapper().get_table( + bigquery_tools.BigQueryWrapper().get_table( project_id=table_details.projectId, dataset_id=table_details.datasetId, table_id=table_details.tableId).schema) diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py index 8b899a343d35..f438949d428b 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads.py @@ -437,18 +437,18 @@ def process(self, element, schema_mod_job_name_prefix): # Trigger potential schema modification by loading zero rows into the # destination table with the temporary table schema. schema_update_job_reference = self.bq_wrapper.perform_load_job( - destination=table_reference, - source_stream=io.BytesIO(), # file with zero rows - job_id=job_name, - schema=temp_table_schema, - write_disposition='WRITE_APPEND', - create_disposition='CREATE_NEVER', - additional_load_parameters=additional_parameters, - job_labels=self._bq_io_metadata.add_additional_bq_job_labels(), - # JSON format is hardcoded because zero rows load(unlike AVRO) and - # a nested schema(unlike CSV, which a default one) is permitted. - source_format="NEWLINE_DELIMITED_JSON", - load_job_project_id=self._load_job_project_id) + destination=table_reference, + source_stream=io.BytesIO(), # file with zero rows + job_id=job_name, + schema=temp_table_schema, + write_disposition='WRITE_APPEND', + create_disposition='CREATE_NEVER', + additional_load_parameters=additional_parameters, + job_labels=self._bq_io_metadata.add_additional_bq_job_labels(), + # JSON format is hardcoded because zero rows load(unlike AVRO) and + # a nested schema(unlike CSV, which a default one) is permitted. + source_format="NEWLINE_DELIMITED_JSON", + load_job_project_id=self._load_job_project_id) self.pending_jobs.append( GlobalWindows.windowed_value( (destination, schema_update_job_reference))) @@ -597,6 +597,7 @@ class TriggerLoadJobs(beam.DoFn): """ TEMP_TABLES = 'TemporaryTables' + ONGOING_JOBS = 'OngoingJobs' def __init__( self, @@ -718,6 +719,8 @@ def process(self, element, load_job_name_prefix, *schema_side_inputs): source_format=self.source_format, job_labels=self.bq_io_metadata.add_additional_bq_job_labels(), load_job_project_id=self.load_job_project_id) + yield pvalue.TaggedOutput( + TriggerLoadJobs.ONGOING_JOBS, (destination, job_reference)) self.pending_jobs.append( GlobalWindows.windowed_value((destination, job_reference))) @@ -761,6 +764,13 @@ def process(self, element): files = element[1] partitions = [] + if not files: + _LOGGER.warning( + 'Ignoring a BigQuery batch load partition to %s ' + 'that contains no source URIs.', + destination) + return + latest_partition = PartitionFiles.Partition( self.max_partition_size, self.max_files_per_partition) @@ -1054,13 +1064,17 @@ def _load_data( load_job_project_id=self.load_job_project_id), load_job_name_pcv, *self.schema_side_inputs).with_outputs( - TriggerLoadJobs.TEMP_TABLES, main='main')) + TriggerLoadJobs.TEMP_TABLES, + TriggerLoadJobs.ONGOING_JOBS, + main='main')) - temp_tables_load_job_ids_pc = trigger_loads_outputs['main'] + finished_temp_tables_load_job_ids_pc = trigger_loads_outputs['main'] + temp_tables_load_job_ids_pc = trigger_loads_outputs[ + TriggerLoadJobs.ONGOING_JOBS] temp_tables_pc = trigger_loads_outputs[TriggerLoadJobs.TEMP_TABLES] schema_mod_job_ids_pc = ( - temp_tables_load_job_ids_pc + finished_temp_tables_load_job_ids_pc | beam.ParDo( UpdateDestinationSchema( project=self.project, @@ -1072,7 +1086,7 @@ def _load_data( schema_mod_job_name_pcv)) copy_job_outputs = ( - temp_tables_load_job_ids_pc + finished_temp_tables_load_job_ids_pc | beam.ParDo( TriggerCopyJobs( project=self.project, @@ -1113,7 +1127,9 @@ def _load_data( step_name=step_name, load_job_project_id=self.load_job_project_id), load_job_name_pcv, - *self.schema_side_inputs)) + *self.schema_side_inputs).with_outputs( + TriggerLoadJobs.ONGOING_JOBS, main='main') + )[TriggerLoadJobs.ONGOING_JOBS] destination_load_job_ids_pc = ( (temp_tables_load_job_ids_pc, destination_load_job_ids_pc) diff --git a/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py b/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py index 724032abfa7e..0c0e136eae4b 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_file_loads_test.py @@ -400,6 +400,23 @@ def test_partition_files_dofn_size_split(self): class TestBigQueryFileLoads(_TestCaseWithTempDirCleanUp): + def test_trigger_load_jobs_with_empty_files(self): + destination = "project:dataset.table" + empty_files = [] + load_job_prefix = "test_prefix" + + with beam.Pipeline() as p: + partitions = ( + p + | beam.Create([(destination, empty_files)]) + | beam.ParDo(bqfl.PartitionFiles(1000, 10)).with_outputs( + bqfl.PartitionFiles.MULTIPLE_PARTITIONS_TAG, + bqfl.PartitionFiles.SINGLE_PARTITION_TAG)) + + _ = ( + partitions[bqfl.PartitionFiles.SINGLE_PARTITION_TAG] + | beam.ParDo(bqfl.TriggerLoadJobs(), load_job_prefix)) + def test_records_traverse_transform_with_mocks(self): destination = 'project1:dataset1.table1' diff --git a/sdks/python/apache_beam/io/gcp/bigquery_schema_tools.py b/sdks/python/apache_beam/io/gcp/bigquery_schema_tools.py index e78f7bd5a7f7..4c25aa62e0bd 100644 --- a/sdks/python/apache_beam/io/gcp/bigquery_schema_tools.py +++ b/sdks/python/apache_beam/io/gcp/bigquery_schema_tools.py @@ -28,9 +28,13 @@ import numpy as np import apache_beam as beam +import apache_beam.io.gcp.bigquery_tools +import apache_beam.typehints.schemas +import apache_beam.utils.proto_utils import apache_beam.utils.timestamp from apache_beam.io.gcp.internal.clients import bigquery from apache_beam.portability.api import schema_pb2 +from apache_beam.transforms import DoFn # BigQuery types as listed in # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types @@ -91,13 +95,11 @@ def bq_field_to_type(field, mode): def convert_to_usertype(table_schema): - usertype = beam.io.gcp.bigquery_schema_tools. \ - generate_user_type_from_bq_schema(table_schema) - return beam.ParDo( - beam.io.gcp.bigquery_schema_tools.BeamSchemaConversionDoFn(usertype)) + usertype = generate_user_type_from_bq_schema(table_schema) + return beam.ParDo(BeamSchemaConversionDoFn(usertype)) -class BeamSchemaConversionDoFn(beam.DoFn): +class BeamSchemaConversionDoFn(DoFn): def __init__(self, pcoll_val_ctor): self._pcoll_val_ctor = pcoll_val_ctor @@ -113,8 +115,9 @@ def infer_output_type(self, input_type): @classmethod def _from_serialized_schema(cls, schema_str): return cls( - beam.typehints.schemas.named_tuple_from_schema( - beam.utils.proto_utils.parse_Bytes(schema_str, schema_pb2.Schema))) + apache_beam.typehints.schemas.named_tuple_from_schema( + apache_beam.utils.proto_utils.parse_Bytes( + schema_str, schema_pb2.Schema))) def __reduce__(self): # when pickling, use bytes representation of the schema. diff --git a/sdks/python/apache_beam/io/gcp/gcsio.py b/sdks/python/apache_beam/io/gcp/gcsio.py index d4ceeda4bd9d..e34a0b774535 100644 --- a/sdks/python/apache_beam/io/gcp/gcsio.py +++ b/sdks/python/apache_beam/io/gcp/gcsio.py @@ -38,6 +38,8 @@ import time import traceback from itertools import islice +from typing import Optional +from typing import Union import apache_beam from apache_beam.internal.http_client import get_new_http @@ -49,6 +51,7 @@ from apache_beam.io.filesystemio import UploaderStream from apache_beam.io.gcp import resource_identifiers from apache_beam.metrics import monitoring_infos +from apache_beam.options.pipeline_options import PipelineOptions from apache_beam.utils import retry __all__ = ['GcsIO'] @@ -158,7 +161,12 @@ class GcsIOError(IOError, retry.PermanentException): class GcsIO(object): """Google Cloud Storage I/O client.""" def __init__(self, storage_client=None, pipeline_options=None): + # type: (Optional[storage.StorageV1], Optional[Union[dict, PipelineOptions]]) -> None if storage_client is None: + if not pipeline_options: + pipeline_options = PipelineOptions() + elif isinstance(pipeline_options, dict): + pipeline_options = PipelineOptions.from_dictionary(pipeline_options) storage_client = storage.StorageV1( credentials=auth.get_service_credentials(pipeline_options), get_credentials=False, diff --git a/sdks/python/apache_beam/io/kinesis.py b/sdks/python/apache_beam/io/kinesis.py index c87dc0122a2e..bc5e1fa787b4 100644 --- a/sdks/python/apache_beam/io/kinesis.py +++ b/sdks/python/apache_beam/io/kinesis.py @@ -199,7 +199,7 @@ class ReadDataFromKinesis(ExternalTransform): Experimental; no backwards compatibility guarantees. """ - URN = 'beam:transform:org.apache.beam:kinesis_read:v1' + URN = 'beam:transform:org.apache.beam:kinesis_read_data:v1' def __init__( self, diff --git a/sdks/python/apache_beam/io/parquetio.py b/sdks/python/apache_beam/io/parquetio.py index acbf1e23f203..dfcc1abec29a 100644 --- a/sdks/python/apache_beam/io/parquetio.py +++ b/sdks/python/apache_beam/io/parquetio.py @@ -43,6 +43,7 @@ from apache_beam.transforms import DoFn from apache_beam.transforms import ParDo from apache_beam.transforms import PTransform +from apache_beam.transforms import window try: import pyarrow as pa @@ -60,7 +61,8 @@ 'ReadAllFromParquet', 'ReadFromParquetBatched', 'ReadAllFromParquetBatched', - 'WriteToParquet' + 'WriteToParquet', + 'WriteToParquetBatched' ] @@ -83,6 +85,67 @@ def process(self, table, with_filename=False): yield row +class _RowDictionariesToArrowTable(DoFn): + """ A DoFn that consumes python dictionarys and yields a pyarrow table.""" + def __init__( + self, + schema, + row_group_buffer_size=64 * 1024 * 1024, + record_batch_size=1000): + self._schema = schema + self._row_group_buffer_size = row_group_buffer_size + self._buffer = [[] for _ in range(len(schema.names))] + self._buffer_size = record_batch_size + self._record_batches = [] + self._record_batches_byte_size = 0 + + def process(self, row): + if len(self._buffer[0]) >= self._buffer_size: + self._flush_buffer() + + if self._record_batches_byte_size >= self._row_group_buffer_size: + table = self._create_table() + yield table + + # reorder the data in columnar format. + for i, n in enumerate(self._schema.names): + self._buffer[i].append(row[n]) + + def finish_bundle(self): + if len(self._buffer[0]) > 0: + self._flush_buffer() + if self._record_batches_byte_size > 0: + table = self._create_table() + yield window.GlobalWindows.windowed_value_at_end_of_window(table) + + def display_data(self): + res = super().display_data() + res['row_group_buffer_size'] = str(self._row_group_buffer_size) + res['buffer_size'] = str(self._buffer_size) + + return res + + def _create_table(self): + table = pa.Table.from_batches(self._record_batches, schema=self._schema) + self._record_batches = [] + self._record_batches_byte_size = 0 + return table + + def _flush_buffer(self): + arrays = [[] for _ in range(len(self._schema.names))] + for x, y in enumerate(self._buffer): + arrays[x] = pa.array(y, type=self._schema.types[x]) + self._buffer[x] = [] + rb = pa.RecordBatch.from_arrays(arrays, schema=self._schema) + self._record_batches.append(rb) + size = 0 + for x in arrays: + for b in x.buffers(): + if b is not None: + size = size + b.size + self._record_batches_byte_size = self._record_batches_byte_size + size + + class ReadFromParquetBatched(PTransform): """A :class:`~apache_beam.transforms.ptransform.PTransform` for reading Parquet files as a `PCollection` of `pyarrow.Table`. This `PTransform` is @@ -453,13 +516,127 @@ def __init__( A WriteToParquet transform usable for writing. """ super().__init__() + self._schema = schema + self._row_group_buffer_size = row_group_buffer_size + self._record_batch_size = record_batch_size + + self._sink = \ + _create_parquet_sink( + file_path_prefix, + schema, + codec, + use_deprecated_int96_timestamps, + use_compliant_nested_type, + file_name_suffix, + num_shards, + shard_name_template, + mime_type + ) + + def expand(self, pcoll): + return pcoll | ParDo( + _RowDictionariesToArrowTable( + self._schema, self._row_group_buffer_size, + self._record_batch_size)) | Write(self._sink) + + def display_data(self): + return { + 'sink_dd': self._sink, + 'row_group_buffer_size': str(self._row_group_buffer_size) + } + + +class WriteToParquetBatched(PTransform): + """A ``PTransform`` for writing parquet files from a `PCollection` of + `pyarrow.Table`. + + This ``PTransform`` is currently experimental. No backward-compatibility + guarantees. + """ + def __init__( + self, + file_path_prefix, + schema=None, + codec='none', + use_deprecated_int96_timestamps=False, + use_compliant_nested_type=False, + file_name_suffix='', + num_shards=0, + shard_name_template=None, + mime_type='application/x-parquet', + ): + """Initialize a WriteToParquetBatched transform. + + Writes parquet files from a :class:`~apache_beam.pvalue.PCollection` of + records. Each record is a pa.Table Schema must be specified like the + example below. + + .. testsetup:: batched + + from tempfile import NamedTemporaryFile + import glob + import os + import pyarrow + + filename = NamedTemporaryFile(delete=False).name + + .. testcode:: batched + + table = pyarrow.Table.from_pylist([{'name': 'foo', 'age': 10}, + {'name': 'bar', 'age': 20}]) + with beam.Pipeline() as p: + records = p | 'Read' >> beam.Create([table]) + _ = records | 'Write' >> beam.io.WriteToParquetBatched(filename, + pyarrow.schema( + [('name', pyarrow.string()), ('age', pyarrow.int64())] + ) + ) + + .. testcleanup:: batched + + for output in glob.glob('{}*'.format(filename)): + os.remove(output) + + For more information on supported types and schema, please see the pyarrow + document. + + Args: + file_path_prefix: The file path to write to. The files written will begin + with this prefix, followed by a shard identifier (see num_shards), and + end in a common extension, if given by file_name_suffix. In most cases, + only this argument is specified and num_shards, shard_name_template, and + file_name_suffix use default values. + schema: The schema to use, as type of ``pyarrow.Schema``. + codec: The codec to use for block-level compression. Any string supported + by the pyarrow specification is accepted. + use_deprecated_int96_timestamps: Write nanosecond resolution timestamps to + INT96 Parquet format. Defaults to False. + use_compliant_nested_type: Write compliant Parquet nested type (lists). + file_name_suffix: Suffix for the files written. + num_shards: The number of files (shards) used for output. If not set, the + service will decide on the optimal number of shards. + Constraining the number of shards is likely to reduce + the performance of a pipeline. Setting this value is not recommended + unless you require a specific number of output files. + shard_name_template: A template string containing placeholders for + the shard number and shard count. When constructing a filename for a + particular shard number, the upper-case letters 'S' and 'N' are + replaced with the 0-padded shard number and shard count respectively. + This argument can be '' in which case it behaves as if num_shards was + set to 1 and only one file will be generated. The default pattern used + is '-SSSSS-of-NNNNN' if None is passed as the shard_name_template. + mime_type: The MIME type to use for the produced files, if the filesystem + supports specifying MIME types. + + Returns: + A WriteToParquetBatched transform usable for writing. + """ + super().__init__() self._sink = \ _create_parquet_sink( file_path_prefix, schema, codec, - row_group_buffer_size, - record_batch_size, use_deprecated_int96_timestamps, use_compliant_nested_type, file_name_suffix, @@ -479,8 +656,6 @@ def _create_parquet_sink( file_path_prefix, schema, codec, - row_group_buffer_size, - record_batch_size, use_deprecated_int96_timestamps, use_compliant_nested_type, file_name_suffix, @@ -492,8 +667,6 @@ def _create_parquet_sink( file_path_prefix, schema, codec, - row_group_buffer_size, - record_batch_size, use_deprecated_int96_timestamps, use_compliant_nested_type, file_name_suffix, @@ -504,14 +677,12 @@ def _create_parquet_sink( class _ParquetSink(filebasedsink.FileBasedSink): - """A sink for parquet files.""" + """A sink for parquet files from batches.""" def __init__( self, file_path_prefix, schema, codec, - row_group_buffer_size, - record_batch_size, use_deprecated_int96_timestamps, use_compliant_nested_type, file_name_suffix, @@ -535,7 +706,6 @@ def __init__( "Due to ARROW-9424, writing with LZ4 compression is not supported in " "pyarrow 1.x, please use a different pyarrow version or a different " f"codec. Your pyarrow version: {pa.__version__}") - self._row_group_buffer_size = row_group_buffer_size self._use_deprecated_int96_timestamps = use_deprecated_int96_timestamps if use_compliant_nested_type and ARROW_MAJOR_VERSION < 4: raise ValueError( @@ -543,10 +713,6 @@ def __init__( "pyarrow version >= 4.x, please use a different pyarrow version. " f"Your pyarrow version: {pa.__version__}") self._use_compliant_nested_type = use_compliant_nested_type - self._buffer = [[] for _ in range(len(schema.names))] - self._buffer_size = record_batch_size - self._record_batches = [] - self._record_batches_byte_size = 0 self._file_handle = None def open(self, temp_path): @@ -564,23 +730,10 @@ def open(self, temp_path): use_deprecated_int96_timestamps=self._use_deprecated_int96_timestamps, use_compliant_nested_type=self._use_compliant_nested_type) - def write_record(self, writer, value): - if len(self._buffer[0]) >= self._buffer_size: - self._flush_buffer() - - if self._record_batches_byte_size >= self._row_group_buffer_size: - self._write_batches(writer) - - # reorder the data in columnar format. - for i, n in enumerate(self._schema.names): - self._buffer[i].append(value[n]) + def write_record(self, writer, table: pa.Table): + writer.write_table(table) def close(self, writer): - if len(self._buffer[0]) > 0: - self._flush_buffer() - if self._record_batches_byte_size > 0: - self._write_batches(writer) - writer.close() if self._file_handle: self._file_handle.close() @@ -590,25 +743,4 @@ def display_data(self): res = super().display_data() res['codec'] = str(self._codec) res['schema'] = str(self._schema) - res['row_group_buffer_size'] = str(self._row_group_buffer_size) return res - - def _write_batches(self, writer): - table = pa.Table.from_batches(self._record_batches, schema=self._schema) - self._record_batches = [] - self._record_batches_byte_size = 0 - writer.write_table(table) - - def _flush_buffer(self): - arrays = [[] for _ in range(len(self._schema.names))] - for x, y in enumerate(self._buffer): - arrays[x] = pa.array(y, type=self._schema.types[x]) - self._buffer[x] = [] - rb = pa.RecordBatch.from_arrays(arrays, schema=self._schema) - self._record_batches.append(rb) - size = 0 - for x in arrays: - for b in x.buffers(): - if b is not None: - size = size + b.size - self._record_batches_byte_size = self._record_batches_byte_size + size diff --git a/sdks/python/apache_beam/io/parquetio_test.py b/sdks/python/apache_beam/io/parquetio_test.py index 454a45493c4a..df018a3a776f 100644 --- a/sdks/python/apache_beam/io/parquetio_test.py +++ b/sdks/python/apache_beam/io/parquetio_test.py @@ -40,6 +40,7 @@ from apache_beam.io.parquetio import ReadFromParquet from apache_beam.io.parquetio import ReadFromParquetBatched from apache_beam.io.parquetio import WriteToParquet +from apache_beam.io.parquetio import WriteToParquetBatched from apache_beam.io.parquetio import _create_parquet_sink from apache_beam.io.parquetio import _create_parquet_source from apache_beam.testing.test_pipeline import TestPipeline @@ -284,8 +285,6 @@ def test_sink_display_data(self): file_name, self.SCHEMA, 'none', - 1024 * 1024, - 1000, False, False, '.end', @@ -299,7 +298,6 @@ def test_sink_display_data(self): 'file_pattern', 'some_parquet_sink-%(shard_num)05d-of-%(num_shards)05d.end'), DisplayDataItemMatcher('codec', 'none'), - DisplayDataItemMatcher('row_group_buffer_size', str(1024 * 1024)), DisplayDataItemMatcher('compression', 'uncompressed') ] hc.assert_that(dd.items, hc.contains_inanyorder(*expected_items)) @@ -308,6 +306,7 @@ def test_write_display_data(self): file_name = 'some_parquet_sink' write = WriteToParquet(file_name, self.SCHEMA) dd = DisplayData.create_from(write) + expected_items = [ DisplayDataItemMatcher('codec', 'none'), DisplayDataItemMatcher('schema', str(self.SCHEMA)), @@ -319,6 +318,21 @@ def test_write_display_data(self): ] hc.assert_that(dd.items, hc.contains_inanyorder(*expected_items)) + def test_write_batched_display_data(self): + file_name = 'some_parquet_sink' + write = WriteToParquetBatched(file_name, self.SCHEMA) + dd = DisplayData.create_from(write) + + expected_items = [ + DisplayDataItemMatcher('codec', 'none'), + DisplayDataItemMatcher('schema', str(self.SCHEMA)), + DisplayDataItemMatcher( + 'file_pattern', + 'some_parquet_sink-%(shard_num)05d-of-%(num_shards)05d'), + DisplayDataItemMatcher('compression', 'uncompressed') + ] + hc.assert_that(dd.items, hc.contains_inanyorder(*expected_items)) + def test_sink_transform_int96(self): with tempfile.NamedTemporaryFile() as dst: path = dst.name @@ -348,6 +362,22 @@ def test_sink_transform(self): | Map(json.dumps) assert_that(readback, equal_to([json.dumps(r) for r in self.RECORDS])) + def test_sink_transform_batched(self): + with TemporaryDirectory() as tmp_dirname: + path = os.path.join(tmp_dirname + "tmp_filename") + with TestPipeline() as p: + _ = p \ + | Create([self._records_as_arrow()]) \ + | WriteToParquetBatched( + path, self.SCHEMA, num_shards=1, shard_name_template='') + with TestPipeline() as p: + # json used for stable sortability + readback = \ + p \ + | ReadFromParquet(path) \ + | Map(json.dumps) + assert_that(readback, equal_to([json.dumps(r) for r in self.RECORDS])) + def test_sink_transform_compliant_nested_type(self): if ARROW_MAJOR_VERSION < 4: return unittest.skip( diff --git a/sdks/python/apache_beam/ml/gcp/visionml.py b/sdks/python/apache_beam/ml/gcp/visionml.py index 3e556b903c44..dd29dd377388 100644 --- a/sdks/python/apache_beam/ml/gcp/visionml.py +++ b/sdks/python/apache_beam/ml/gcp/visionml.py @@ -80,7 +80,7 @@ def __init__( metadata=None): """ Args: - features: (List[``vision.types.Feature.enums.Feature``]) Required. + features: (List[``vision.Feature``]) Required. The Vision API features to detect retry: (google.api_core.retry.Retry) Optional. A retry object used to retry requests. @@ -107,9 +107,9 @@ def __init__( image_contexts = [(''gs://cloud-samples-data/vision/ocr/sign.jpg'', Union[dict, - ``vision.types.ImageContext()``]), + ``vision.ImageContext()``]), (''gs://cloud-samples-data/vision/ocr/sign.jpg'', Union[dict, - ``vision.types.ImageContext()``]),] + ``vision.ImageContext()``]),] context_side_input = ( @@ -152,9 +152,8 @@ def expand(self, pvalue): client_options=self.client_options, metadata=self.metadata))) - @typehints.with_input_types( - Union[str, bytes], Optional[vision.types.ImageContext]) - @typehints.with_output_types(List[vision.types.AnnotateImageRequest]) + @typehints.with_input_types(Union[str, bytes], Optional[vision.ImageContext]) + @typehints.with_output_types(List[vision.AnnotateImageRequest]) def _create_image_annotation_pairs(self, element, context_side_input): if context_side_input: # If we have a side input image context, use that image_context = context_side_input.get(element) @@ -162,13 +161,18 @@ def _create_image_annotation_pairs(self, element, context_side_input): image_context = None if isinstance(element, str): - image = vision.types.Image( - source=vision.types.ImageSource(image_uri=element)) + + image = vision.Image( + {'source': vision.ImageSource({'image_uri': element})}) + else: # Typehint checks only allows str or bytes - image = vision.types.Image(content=element) + image = vision.Image(content=element) - request = vision.types.AnnotateImageRequest( - image=image, features=self.features, image_context=image_context) + request = vision.AnnotateImageRequest({ + 'image': image, + 'features': self.features, + 'image_context': image_context + }) yield request @@ -181,7 +185,7 @@ class AnnotateImageWithContext(AnnotateImage): Element is a tuple of:: (Union[str, bytes], - Optional[``vision.types.ImageContext``]) + Optional[``vision.ImageContext``]) where the former is either an URI (e.g. a GCS URI) or bytes base64-encoded image data. @@ -197,7 +201,7 @@ def __init__( metadata=None): """ Args: - features: (List[``vision.types.Feature.enums.Feature``]) Required. + features: (List[``vision.Feature``]) Required. The Vision API features to detect retry: (google.api_core.retry.Retry) Optional. A retry object used to retry requests. @@ -244,25 +248,28 @@ def expand(self, pvalue): metadata=self.metadata))) @typehints.with_input_types( - Tuple[Union[str, bytes], Optional[vision.types.ImageContext]]) - @typehints.with_output_types(List[vision.types.AnnotateImageRequest]) + Tuple[Union[str, bytes], Optional[vision.ImageContext]]) + @typehints.with_output_types(List[vision.AnnotateImageRequest]) def _create_image_annotation_pairs(self, element, **kwargs): element, image_context = element # Unpack (image, image_context) tuple if isinstance(element, str): - image = vision.types.Image( - source=vision.types.ImageSource(image_uri=element)) + image = vision.Image( + {'source': vision.ImageSource({'image_uri': element})}) else: # Typehint checks only allows str or bytes - image = vision.types.Image(content=element) + image = vision.Image({"content": element}) - request = vision.types.AnnotateImageRequest( - image=image, features=self.features, image_context=image_context) + request = vision.AnnotateImageRequest({ + 'image': image, + 'features': self.features, + 'image_context': image_context + }) yield request -@typehints.with_input_types(List[vision.types.AnnotateImageRequest]) +@typehints.with_input_types(List[vision.AnnotateImageRequest]) class _ImageAnnotateFn(DoFn): """A DoFn that sends each input element to the GCP Vision API. - Returns ``google.cloud.vision.types.BatchAnnotateImagesResponse``. + Returns ``google.cloud.vision.BatchAnnotateImagesResponse``. """ def __init__(self, features, retry, timeout, client_options, metadata): super().__init__() diff --git a/sdks/python/apache_beam/ml/gcp/visionml_test.py b/sdks/python/apache_beam/ml/gcp/visionml_test.py index f038442468f8..479b3d80e4de 100644 --- a/sdks/python/apache_beam/ml/gcp/visionml_test.py +++ b/sdks/python/apache_beam/ml/gcp/visionml_test.py @@ -45,12 +45,13 @@ def setUp(self): self._mock_client = mock.Mock() self._mock_client.batch_annotate_images.return_value = None - feature_type = vision.enums.Feature.Type.TEXT_DETECTION + feature_type = vision.Feature.Type.TEXT_DETECTION self.features = [ - vision.types.Feature( - type=feature_type, max_results=3, model="builtin/stable") + vision.Feature({ + 'type': feature_type, 'max_results': 3, 'model': "builtin/stable" + }) ] - self.img_ctx = vision.types.ImageContext() + self.img_ctx = vision.ImageContext() self.min_batch_size = 1 self.max_batch_size = 1 diff --git a/sdks/python/apache_beam/ml/gcp/visionml_test_it.py b/sdks/python/apache_beam/ml/gcp/visionml_test_it.py index 4413266dcc5c..ea3fc9768ff5 100644 --- a/sdks/python/apache_beam/ml/gcp/visionml_test_it.py +++ b/sdks/python/apache_beam/ml/gcp/visionml_test_it.py @@ -47,7 +47,8 @@ def test_text_detection_with_language_hint(self): IMAGES_TO_ANNOTATE = [ 'gs://apache-beam-samples/advanced_analytics/vision/sign.jpg' ] - IMAGE_CONTEXT = [vision.types.ImageContext(language_hints=['en'])] + + IMAGE_CONTEXT = [vision.ImageContext({'language_hints': ['en']})] with TestPipeline(is_integration_test=True) as p: contexts = p | 'Create context' >> beam.Create( @@ -57,7 +58,9 @@ def test_text_detection_with_language_hint(self): p | beam.Create(IMAGES_TO_ANNOTATE) | AnnotateImage( - features=[vision.types.Feature(type='TEXT_DETECTION')], + features=[ + vision.Feature({'type_': vision.Feature.Type.TEXT_DETECTION}) + ], context_side_input=beam.pvalue.AsDict(contexts)) | beam.ParDo(extract)) diff --git a/sdks/python/apache_beam/options/pipeline_options.py b/sdks/python/apache_beam/options/pipeline_options.py index 54eaaf19ed8e..036613ca5469 100644 --- a/sdks/python/apache_beam/options/pipeline_options.py +++ b/sdks/python/apache_beam/options/pipeline_options.py @@ -1497,9 +1497,10 @@ def _add_argparse_args(cls, parser): 'For example, http://hostname:6066') parser.add_argument( '--spark_version', - default='2', - choices=['2', '3'], - help='Spark major version to use.') + default='3', + choices=['3', '2'], + help='Spark major version to use. ' + 'Note, Spark 2 support is deprecated') class TestOptions(PipelineOptions): diff --git a/sdks/python/apache_beam/options/pipeline_options_test.py b/sdks/python/apache_beam/options/pipeline_options_test.py index 38d362d6870f..3f51c3f52b74 100644 --- a/sdks/python/apache_beam/options/pipeline_options_test.py +++ b/sdks/python/apache_beam/options/pipeline_options_test.py @@ -218,8 +218,6 @@ def _add_argparse_args(cls, parser): parser.add_argument( '--fake_multi_option', action='append', help='fake multi option') - @unittest.skip( - "TODO(https://github.com/apache/beam/issues/21116): Flaky test.") def test_display_data(self): for case in PipelineOptionsTest.TEST_CASES: options = PipelineOptions(flags=case['flags']) diff --git a/sdks/python/apache_beam/portability/common_urns.py b/sdks/python/apache_beam/portability/common_urns.py index 199ce4d7058f..3b47f1ab1e40 100644 --- a/sdks/python/apache_beam/portability/common_urns.py +++ b/sdks/python/apache_beam/portability/common_urns.py @@ -83,3 +83,7 @@ micros_instant = LogicalTypes.Enum.MICROS_INSTANT millis_instant = LogicalTypes.Enum.MILLIS_INSTANT python_callable = LogicalTypes.Enum.PYTHON_CALLABLE +fixed_bytes = LogicalTypes.Enum.FIXED_BYTES +var_bytes = LogicalTypes.Enum.VAR_BYTES +fixed_char = LogicalTypes.Enum.FIXED_CHAR +var_char = LogicalTypes.Enum.VAR_CHAR diff --git a/sdks/python/apache_beam/runners/dask/__init__.py b/sdks/python/apache_beam/runners/dask/__init__.py new file mode 100644 index 000000000000..cce3acad34a4 --- /dev/null +++ b/sdks/python/apache_beam/runners/dask/__init__.py @@ -0,0 +1,16 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diff --git a/sdks/python/apache_beam/runners/dask/dask_runner.py b/sdks/python/apache_beam/runners/dask/dask_runner.py new file mode 100644 index 000000000000..109c4379b45d --- /dev/null +++ b/sdks/python/apache_beam/runners/dask/dask_runner.py @@ -0,0 +1,182 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""DaskRunner, executing remote jobs on Dask.distributed. + +The DaskRunner is a runner implementation that executes a graph of +transformations across processes and workers via Dask distributed's +scheduler. +""" +import argparse +import dataclasses +import typing as t + +from apache_beam import pvalue +from apache_beam.options.pipeline_options import PipelineOptions +from apache_beam.pipeline import AppliedPTransform +from apache_beam.pipeline import PipelineVisitor +from apache_beam.runners.dask.overrides import dask_overrides +from apache_beam.runners.dask.transform_evaluator import TRANSLATIONS +from apache_beam.runners.dask.transform_evaluator import NoOp +from apache_beam.runners.direct.direct_runner import BundleBasedDirectRunner +from apache_beam.runners.runner import PipelineResult +from apache_beam.runners.runner import PipelineState +from apache_beam.utils.interactive_utils import is_in_notebook + + +class DaskOptions(PipelineOptions): + @staticmethod + def _parse_timeout(candidate): + try: + return int(candidate) + except (TypeError, ValueError): + import dask + return dask.config.no_default + + @classmethod + def _add_argparse_args(cls, parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '--dask_client_address', + dest='address', + type=str, + default=None, + help='Address of a dask Scheduler server. Will default to a ' + '`dask.LocalCluster()`.') + parser.add_argument( + '--dask_connection_timeout', + dest='timeout', + type=DaskOptions._parse_timeout, + help='Timeout duration for initial connection to the scheduler.') + parser.add_argument( + '--dask_scheduler_file', + dest='scheduler_file', + type=str, + default=None, + help='Path to a file with scheduler information if available.') + # TODO(alxr): Add options for security. + parser.add_argument( + '--dask_client_name', + dest='name', + type=str, + default=None, + help='Gives the client a name that will be included in logs generated ' + 'on the scheduler for matters relating to this client.') + parser.add_argument( + '--dask_connection_limit', + dest='connection_limit', + type=int, + default=512, + help='The number of open comms to maintain at once in the connection ' + 'pool.') + + +@dataclasses.dataclass +class DaskRunnerResult(PipelineResult): + from dask import distributed + + client: distributed.Client + futures: t.Sequence[distributed.Future] + + def __post_init__(self): + super().__init__(PipelineState.RUNNING) + + def wait_until_finish(self, duration=None) -> str: + try: + if duration is not None: + # Convert milliseconds to seconds + duration /= 1000 + self.client.wait_for_workers(timeout=duration) + self.client.gather(self.futures, errors='raise') + self._state = PipelineState.DONE + except: # pylint: disable=broad-except + self._state = PipelineState.FAILED + raise + return self._state + + def cancel(self) -> str: + self._state = PipelineState.CANCELLING + self.client.cancel(self.futures) + self._state = PipelineState.CANCELLED + return self._state + + def metrics(self): + # TODO(alxr): Collect and return metrics... + raise NotImplementedError('collecting metrics will come later!') + + +class DaskRunner(BundleBasedDirectRunner): + """Executes a pipeline on a Dask distributed client.""" + @staticmethod + def to_dask_bag_visitor() -> PipelineVisitor: + from dask import bag as db + + @dataclasses.dataclass + class DaskBagVisitor(PipelineVisitor): + bags: t.Dict[AppliedPTransform, + db.Bag] = dataclasses.field(default_factory=dict) + + def visit_transform(self, transform_node: AppliedPTransform) -> None: + op_class = TRANSLATIONS.get(transform_node.transform.__class__, NoOp) + op = op_class(transform_node) + + inputs = list(transform_node.inputs) + if inputs: + bag_inputs = [] + for input_value in inputs: + if isinstance(input_value, pvalue.PBegin): + bag_inputs.append(None) + + prev_op = input_value.producer + if prev_op in self.bags: + bag_inputs.append(self.bags[prev_op]) + + if len(bag_inputs) == 1: + self.bags[transform_node] = op.apply(bag_inputs[0]) + else: + self.bags[transform_node] = op.apply(bag_inputs) + + else: + self.bags[transform_node] = op.apply(None) + + return DaskBagVisitor() + + @staticmethod + def is_fnapi_compatible(): + return False + + def run_pipeline(self, pipeline, options): + # TODO(alxr): Create interactive notebook support. + if is_in_notebook(): + raise NotImplementedError('interactive support will come later!') + + try: + import dask.distributed as ddist + except ImportError: + raise ImportError( + 'DaskRunner is not available. Please install apache_beam[dask].') + + dask_options = options.view_as(DaskOptions).get_all_options( + drop_default=True) + client = ddist.Client(**dask_options) + + pipeline.replace_all(dask_overrides()) + + dask_visitor = self.to_dask_bag_visitor() + pipeline.visit(dask_visitor) + + futures = client.compute(list(dask_visitor.bags.values())) + return DaskRunnerResult(client, futures) diff --git a/sdks/python/apache_beam/runners/dask/dask_runner_test.py b/sdks/python/apache_beam/runners/dask/dask_runner_test.py new file mode 100644 index 000000000000..d8b3e17d8a56 --- /dev/null +++ b/sdks/python/apache_beam/runners/dask/dask_runner_test.py @@ -0,0 +1,94 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import inspect +import unittest + +import apache_beam as beam +from apache_beam.options.pipeline_options import PipelineOptions +from apache_beam.testing import test_pipeline +from apache_beam.testing.util import assert_that +from apache_beam.testing.util import equal_to + +try: + from apache_beam.runners.dask.dask_runner import DaskOptions + from apache_beam.runners.dask.dask_runner import DaskRunner + import dask + import dask.distributed as ddist +except (ImportError, ModuleNotFoundError): + raise unittest.SkipTest('Dask must be installed to run tests.') + + +class DaskOptionsTest(unittest.TestCase): + def test_parses_connection_timeout__defaults_to_none(self): + default_options = PipelineOptions([]) + default_dask_options = default_options.view_as(DaskOptions) + self.assertEqual(None, default_dask_options.timeout) + + def test_parses_connection_timeout__parses_int(self): + conn_options = PipelineOptions('--dask_connection_timeout 12'.split()) + dask_conn_options = conn_options.view_as(DaskOptions) + self.assertEqual(12, dask_conn_options.timeout) + + def test_parses_connection_timeout__handles_bad_input(self): + err_options = PipelineOptions('--dask_connection_timeout foo'.split()) + dask_err_options = err_options.view_as(DaskOptions) + self.assertEqual(dask.config.no_default, dask_err_options.timeout) + + def test_parser_destinations__agree_with_dask_client(self): + options = PipelineOptions( + '--dask_client_address localhost:8080 --dask_connection_timeout 600 ' + '--dask_scheduler_file foobar.cfg --dask_client_name charlie ' + '--dask_connection_limit 1024'.split()) + dask_options = options.view_as(DaskOptions) + + # Get the argument names for the constructor. + client_args = list(inspect.signature(ddist.Client).parameters) + + for opt_name in dask_options.get_all_options(drop_default=True).keys(): + with self.subTest(f'{opt_name} in dask.distributed.Client constructor'): + self.assertIn(opt_name, client_args) + + +class DaskRunnerRunPipelineTest(unittest.TestCase): + """Test class used to introspect the dask runner via a debugger.""" + def setUp(self) -> None: + self.pipeline = test_pipeline.TestPipeline(runner=DaskRunner()) + + def test_create(self): + with self.pipeline as p: + pcoll = p | beam.Create([1]) + assert_that(pcoll, equal_to([1])) + + def test_create_and_map(self): + def double(x): + return x * 2 + + with self.pipeline as p: + pcoll = p | beam.Create([1]) | beam.Map(double) + assert_that(pcoll, equal_to([2])) + + def test_create_map_and_groupby(self): + def double(x): + return x * 2, x + + with self.pipeline as p: + pcoll = p | beam.Create([1]) | beam.Map(double) | beam.GroupByKey() + assert_that(pcoll, equal_to([(2, [1])])) + + +if __name__ == '__main__': + unittest.main() diff --git a/sdks/python/apache_beam/runners/dask/overrides.py b/sdks/python/apache_beam/runners/dask/overrides.py new file mode 100644 index 000000000000..d07c7cd518af --- /dev/null +++ b/sdks/python/apache_beam/runners/dask/overrides.py @@ -0,0 +1,145 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import dataclasses +import typing as t + +import apache_beam as beam +from apache_beam import typehints +from apache_beam.io.iobase import SourceBase +from apache_beam.pipeline import AppliedPTransform +from apache_beam.pipeline import PTransformOverride +from apache_beam.runners.direct.direct_runner import _GroupAlsoByWindowDoFn +from apache_beam.transforms import ptransform +from apache_beam.transforms.window import GlobalWindows + +K = t.TypeVar("K") +V = t.TypeVar("V") + + +@dataclasses.dataclass +class _Create(beam.PTransform): + values: t.Tuple[t.Any] + + def expand(self, input_or_inputs): + return beam.pvalue.PCollection.from_(input_or_inputs) + + def get_windowing(self, inputs: t.Any) -> beam.Windowing: + return beam.Windowing(GlobalWindows()) + + +@typehints.with_input_types(K) +@typehints.with_output_types(K) +class _Reshuffle(beam.PTransform): + def expand(self, input_or_inputs): + return beam.pvalue.PCollection.from_(input_or_inputs) + + +@dataclasses.dataclass +class _Read(beam.PTransform): + source: SourceBase + + def expand(self, input_or_inputs): + return beam.pvalue.PCollection.from_(input_or_inputs) + + +@typehints.with_input_types(t.Tuple[K, V]) +@typehints.with_output_types(t.Tuple[K, t.Iterable[V]]) +class _GroupByKeyOnly(beam.PTransform): + def expand(self, input_or_inputs): + return beam.pvalue.PCollection.from_(input_or_inputs) + + def infer_output_type(self, input_type): + + key_type, value_type = typehints.trivial_inference.key_value_types( + input_type + ) + return typehints.KV[key_type, typehints.Iterable[value_type]] + + +@typehints.with_input_types(t.Tuple[K, t.Iterable[V]]) +@typehints.with_output_types(t.Tuple[K, t.Iterable[V]]) +class _GroupAlsoByWindow(beam.ParDo): + """Not used yet...""" + def __init__(self, windowing): + super().__init__(_GroupAlsoByWindowDoFn(windowing)) + self.windowing = windowing + + def expand(self, input_or_inputs): + return beam.pvalue.PCollection.from_(input_or_inputs) + + +@typehints.with_input_types(t.Tuple[K, V]) +@typehints.with_output_types(t.Tuple[K, t.Iterable[V]]) +class _GroupByKey(beam.PTransform): + def expand(self, input_or_inputs): + return input_or_inputs | "GroupByKey" >> _GroupByKeyOnly() + + +class _Flatten(beam.PTransform): + def expand(self, input_or_inputs): + is_bounded = all(pcoll.is_bounded for pcoll in input_or_inputs) + return beam.pvalue.PCollection(self.pipeline, is_bounded=is_bounded) + + +def dask_overrides() -> t.List[PTransformOverride]: + class CreateOverride(PTransformOverride): + def matches(self, applied_ptransform: AppliedPTransform) -> bool: + return applied_ptransform.transform.__class__ == beam.Create + + def get_replacement_transform_for_applied_ptransform( + self, applied_ptransform: AppliedPTransform) -> ptransform.PTransform: + return _Create(t.cast(beam.Create, applied_ptransform.transform).values) + + class ReshuffleOverride(PTransformOverride): + def matches(self, applied_ptransform: AppliedPTransform) -> bool: + return applied_ptransform.transform.__class__ == beam.Reshuffle + + def get_replacement_transform_for_applied_ptransform( + self, applied_ptransform: AppliedPTransform) -> ptransform.PTransform: + return _Reshuffle() + + class ReadOverride(PTransformOverride): + def matches(self, applied_ptransform: AppliedPTransform) -> bool: + return applied_ptransform.transform.__class__ == beam.io.Read + + def get_replacement_transform_for_applied_ptransform( + self, applied_ptransform: AppliedPTransform) -> ptransform.PTransform: + return _Read(t.cast(beam.io.Read, applied_ptransform.transform).source) + + class GroupByKeyOverride(PTransformOverride): + def matches(self, applied_ptransform: AppliedPTransform) -> bool: + return applied_ptransform.transform.__class__ == beam.GroupByKey + + def get_replacement_transform_for_applied_ptransform( + self, applied_ptransform: AppliedPTransform) -> ptransform.PTransform: + return _GroupByKey() + + class FlattenOverride(PTransformOverride): + def matches(self, applied_ptransform: AppliedPTransform) -> bool: + return applied_ptransform.transform.__class__ == beam.Flatten + + def get_replacement_transform_for_applied_ptransform( + self, applied_ptransform: AppliedPTransform) -> ptransform.PTransform: + return _Flatten() + + return [ + CreateOverride(), + ReshuffleOverride(), + ReadOverride(), + GroupByKeyOverride(), + FlattenOverride(), + ] diff --git a/sdks/python/apache_beam/runners/dask/transform_evaluator.py b/sdks/python/apache_beam/runners/dask/transform_evaluator.py new file mode 100644 index 000000000000..c4aac7f2111f --- /dev/null +++ b/sdks/python/apache_beam/runners/dask/transform_evaluator.py @@ -0,0 +1,103 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Transform Beam PTransforms into Dask Bag operations. + +A minimum set of operation substitutions, to adap Beam's PTransform model +to Dask Bag functions. + +TODO(alxr): Translate ops from https://docs.dask.org/en/latest/bag-api.html. +""" +import abc +import dataclasses +import typing as t + +import apache_beam +import dask.bag as db +from apache_beam.pipeline import AppliedPTransform +from apache_beam.runners.dask.overrides import _Create +from apache_beam.runners.dask.overrides import _Flatten +from apache_beam.runners.dask.overrides import _GroupByKeyOnly + +OpInput = t.Union[db.Bag, t.Sequence[db.Bag], None] + + +@dataclasses.dataclass +class DaskBagOp(abc.ABC): + applied: AppliedPTransform + + @property + def transform(self): + return self.applied.transform + + @abc.abstractmethod + def apply(self, input_bag: OpInput) -> db.Bag: + pass + + +class NoOp(DaskBagOp): + def apply(self, input_bag: OpInput) -> db.Bag: + return input_bag + + +class Create(DaskBagOp): + def apply(self, input_bag: OpInput) -> db.Bag: + assert input_bag is None, 'Create expects no input!' + original_transform = t.cast(_Create, self.transform) + items = original_transform.values + return db.from_sequence(items) + + +class ParDo(DaskBagOp): + def apply(self, input_bag: db.Bag) -> db.Bag: + transform = t.cast(apache_beam.ParDo, self.transform) + return input_bag.map( + transform.fn.process, *transform.args, **transform.kwargs).flatten() + + +class Map(DaskBagOp): + def apply(self, input_bag: db.Bag) -> db.Bag: + transform = t.cast(apache_beam.Map, self.transform) + return input_bag.map( + transform.fn.process, *transform.args, **transform.kwargs) + + +class GroupByKey(DaskBagOp): + def apply(self, input_bag: db.Bag) -> db.Bag: + def key(item): + return item[0] + + def value(item): + k, v = item + return k, [elm[1] for elm in v] + + return input_bag.groupby(key).map(value) + + +class Flatten(DaskBagOp): + def apply(self, input_bag: OpInput) -> db.Bag: + assert type(input_bag) is list, 'Must take a sequence of bags!' + return db.concat(input_bag) + + +TRANSLATIONS = { + _Create: Create, + apache_beam.ParDo: ParDo, + apache_beam.Map: Map, + _GroupByKeyOnly: GroupByKey, + _Flatten: Flatten, +} diff --git a/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py b/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py index e16c88ea9ee2..d581c48cee13 100644 --- a/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py +++ b/sdks/python/apache_beam/runners/dataflow/dataflow_runner.py @@ -1207,8 +1207,6 @@ def run_Read(self, transform_node, options): traceback.format_exc()) step.add_property(PropertyNames.SOURCE_STEP_INPUT, source_dict) - elif transform.source.format == 'text': - step.add_property(PropertyNames.FILE_PATTERN, transform.source.path) elif transform.source.format == 'pubsub': if not standard_options.streaming: raise ValueError( @@ -1274,54 +1272,7 @@ def run__NativeWrite(self, transform_node, options): TransformNames.WRITE, transform_node.full_label, transform_node) # TODO(mairbek): refactor if-else tree to use registerable functions. # Initialize the sink specific properties. - if transform.sink.format == 'text': - # Note that it is important to use typed properties (@type/value dicts) - # for non-string properties and also for empty strings. For example, - # in the code below the num_shards must have type and also - # file_name_suffix and shard_name_template (could be empty strings). - step.add_property( - PropertyNames.FILE_NAME_PREFIX, - transform.sink.file_name_prefix, - with_type=True) - step.add_property( - PropertyNames.FILE_NAME_SUFFIX, - transform.sink.file_name_suffix, - with_type=True) - step.add_property( - PropertyNames.SHARD_NAME_TEMPLATE, - transform.sink.shard_name_template, - with_type=True) - if transform.sink.num_shards > 0: - step.add_property( - PropertyNames.NUM_SHARDS, transform.sink.num_shards, with_type=True) - # TODO(silviuc): Implement sink validation. - step.add_property(PropertyNames.VALIDATE_SINK, False, with_type=True) - elif transform.sink.format == 'bigquery': - # TODO(silviuc): Add table validation if transform.sink.validate. - step.add_property( - PropertyNames.BIGQUERY_DATASET, - transform.sink.table_reference.datasetId) - step.add_property( - PropertyNames.BIGQUERY_TABLE, transform.sink.table_reference.tableId) - # If project owning the table was not specified then the project owning - # the workflow (current project) will be used. - if transform.sink.table_reference.projectId is not None: - step.add_property( - PropertyNames.BIGQUERY_PROJECT, - transform.sink.table_reference.projectId) - step.add_property( - PropertyNames.BIGQUERY_CREATE_DISPOSITION, - transform.sink.create_disposition) - step.add_property( - PropertyNames.BIGQUERY_WRITE_DISPOSITION, - transform.sink.write_disposition) - if transform.sink.table_schema is not None: - step.add_property( - PropertyNames.BIGQUERY_SCHEMA, transform.sink.schema_as_json()) - if transform.sink.kms_key is not None: - step.add_property( - PropertyNames.BIGQUERY_KMS_KEY, transform.sink.kms_key) - elif transform.sink.format == 'pubsub': + if transform.sink.format == 'pubsub': standard_options = options.view_as(StandardOptions) if not standard_options.streaming: raise ValueError( diff --git a/sdks/python/apache_beam/runners/dataflow/internal/names.py b/sdks/python/apache_beam/runners/dataflow/internal/names.py index 4ec94616d6ab..aa6de4f7085e 100644 --- a/sdks/python/apache_beam/runners/dataflow/internal/names.py +++ b/sdks/python/apache_beam/runners/dataflow/internal/names.py @@ -36,10 +36,10 @@ # Update this version to the next version whenever there is a change that will # require changes to legacy Dataflow worker execution environment. -BEAM_CONTAINER_VERSION = 'beam-master-20221018' +BEAM_CONTAINER_VERSION = 'beam-master-20221021' # Update this version to the next version whenever there is a change that # requires changes to SDK harness container or SDK harness launcher. -BEAM_FNAPI_CONTAINER_VERSION = 'beam-master-20221018' +BEAM_FNAPI_CONTAINER_VERSION = 'beam-master-20221021' DATAFLOW_CONTAINER_IMAGE_REPOSITORY = 'gcr.io/cloud-dataflow/v1beta3' diff --git a/sdks/python/apache_beam/runners/portability/expansion_service_test.py b/sdks/python/apache_beam/runners/portability/expansion_service_test.py index e99a7aa90c7c..7aa2e5f16e5b 100644 --- a/sdks/python/apache_beam/runners/portability/expansion_service_test.py +++ b/sdks/python/apache_beam/runners/portability/expansion_service_test.py @@ -376,6 +376,8 @@ def cleanup(unused_signum, unused_frame): def main(unused_argv): + # TODO: use the regular expansion service (expansion_service_main) instead of + # this custom service for testing. PyPIArtifactRegistry.register_artifact('beautifulsoup4', '>=4.9,<5.0') parser = argparse.ArgumentParser() parser.add_argument( @@ -388,8 +390,14 @@ def main(unused_argv): options.fully_qualified_name_glob): server = grpc.server(thread_pool_executor.shared_unbounded_instance()) expansion_servicer = expansion_service.ExpansionServiceServicer( - PipelineOptions( - ["--experiments", "beam_fn_api", "--sdk_location", "container"])) + PipelineOptions([ + "--experiments", + "beam_fn_api", + "--sdk_location", + "container", + "--pickle_library", + "cloudpickle" + ])) update_sklearn_model_dependency(expansion_servicer._default_environment) beam_expansion_api_pb2_grpc.add_ExpansionServiceServicer_to_server( expansion_servicer, server) diff --git a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py index eae78747f628..a76cdaf997a5 100644 --- a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py @@ -165,6 +165,7 @@ def get_output_batch_type(self, input_element_type): assert_that(res, equal_to([6, 12, 18])) + @unittest.skip('https://github.com/apache/beam/issues/23944') def test_batch_pardo_trigger_flush(self): try: utils.check_compiled('apache_beam.coders.coder_impl') @@ -368,6 +369,7 @@ def infer_output_type(self, input_type): assert_that(res, equal_to([6, 12, 12, 18, 18, 18])) + @unittest.skip('https://github.com/apache/beam/issues/23944') def test_pardo_large_input(self): try: utils.check_compiled('apache_beam.coders.coder_impl') diff --git a/sdks/python/apache_beam/runners/portability/spark_runner.py b/sdks/python/apache_beam/runners/portability/spark_runner.py index b1d754d89836..b4c46c0dac06 100644 --- a/sdks/python/apache_beam/runners/portability/spark_runner.py +++ b/sdks/python/apache_beam/runners/portability/spark_runner.py @@ -88,15 +88,15 @@ def path_to_jar(self): 'Unable to parse jar URL "%s". If using a full URL, make sure ' 'the scheme is specified. If using a local file path, make sure ' 'the file exists; you may have to first build the job server ' - 'using `./gradlew runners:spark:2:job-server:shadowJar`.' % + 'using `./gradlew runners:spark:3:job-server:shadowJar`.' % self._jar) return self._jar else: - if self._spark_version == '3': - return self.path_to_beam_jar(':runners:spark:3:job-server:shadowJar') - return self.path_to_beam_jar( - ':runners:spark:2:job-server:shadowJar', - artifact_id='beam-runners-spark-job-server') + if self._spark_version == '2': + return self.path_to_beam_jar( + ':runners:spark:2:job-server:shadowJar', + artifact_id='beam-runners-spark-job-server') + return self.path_to_beam_jar(':runners:spark:3:job-server:shadowJar') def java_arguments( self, job_port, artifact_port, expansion_port, artifacts_dir): diff --git a/sdks/python/apache_beam/runners/portability/spark_runner_test.py b/sdks/python/apache_beam/runners/portability/spark_runner_test.py index 488222f2f2fa..5530caa1e971 100644 --- a/sdks/python/apache_beam/runners/portability/spark_runner_test.py +++ b/sdks/python/apache_beam/runners/portability/spark_runner_test.py @@ -84,7 +84,7 @@ def parse_options(self, request): self.set_spark_job_server_jar( known_args.spark_job_server_jar or job_server.JavaJarJobServer.path_to_beam_jar( - ':runners:spark:2:job-server:shadowJar')) + ':runners:spark:3:job-server:shadowJar')) self.environment_type = known_args.environment_type self.environment_options = known_args.environment_options diff --git a/sdks/python/apache_beam/runners/portability/spark_uber_jar_job_server.py b/sdks/python/apache_beam/runners/portability/spark_uber_jar_job_server.py index 832f3142cb63..97fa6b629cee 100644 --- a/sdks/python/apache_beam/runners/portability/spark_uber_jar_job_server.py +++ b/sdks/python/apache_beam/runners/portability/spark_uber_jar_job_server.py @@ -69,17 +69,17 @@ def executable_jar(self): 'Unable to parse jar URL "%s". If using a full URL, make sure ' 'the scheme is specified. If using a local file path, make sure ' 'the file exists; you may have to first build the job server ' - 'using `./gradlew runners:spark:2:job-server:shadowJar`.' % + 'using `./gradlew runners:spark:3:job-server:shadowJar`.' % self._executable_jar) url = self._executable_jar else: - if self._spark_version == '3': - url = job_server.JavaJarJobServer.path_to_beam_jar( - ':runners:spark:3:job-server:shadowJar') - else: + if self._spark_version == '2': url = job_server.JavaJarJobServer.path_to_beam_jar( ':runners:spark:2:job-server:shadowJar', artifact_id='beam-runners-spark-job-server') + else: + url = job_server.JavaJarJobServer.path_to_beam_jar( + ':runners:spark:3:job-server:shadowJar') return job_server.JavaJarJobServer.local_jar(url) def create_beam_job(self, job_id, job_name, pipeline, options): diff --git a/sdks/python/apache_beam/runners/portability/stager.py b/sdks/python/apache_beam/runners/portability/stager.py index e06c71c917d2..abcef4679c20 100644 --- a/sdks/python/apache_beam/runners/portability/stager.py +++ b/sdks/python/apache_beam/runners/portability/stager.py @@ -224,9 +224,14 @@ def create_job_resources(options, # type: PipelineOptions 'The file %s cannot be found. It was specified in the ' '--requirements_file command line option.' % setup_options.requirements_file) + extra_packages, thinned_requirements_file = ( + Stager._extract_local_packages(setup_options.requirements_file)) + if extra_packages: + setup_options.extra_packages = ( + setup_options.extra_packages or []) + extra_packages resources.append( Stager._create_file_stage_to_artifact( - setup_options.requirements_file, REQUIREMENTS_FILE)) + thinned_requirements_file, REQUIREMENTS_FILE)) # Populate cache with packages from the requirement file option and # stage the files in the cache. if not use_beam_default_container: @@ -683,6 +688,25 @@ def _remove_dependency_from_requirements( return tmp_requirements_filename + @staticmethod + def _extract_local_packages(requirements_file): + local_deps = [] + pypi_deps = [] + with open(requirements_file, 'r') as fin: + for line in fin: + dep = line.strip() + if os.path.exists(dep): + local_deps.append(dep) + else: + pypi_deps.append(dep) + if local_deps: + with tempfile.NamedTemporaryFile(suffix='-requirements.txt', + delete=False) as fout: + fout.write('\n'.join(pypi_deps).encode('utf-8')) + return local_deps, fout.name + else: + return [], requirements_file + @staticmethod def _get_platform_for_default_sdk_container(): """ diff --git a/sdks/python/apache_beam/runners/portability/stager_test.py b/sdks/python/apache_beam/runners/portability/stager_test.py index b221bb1ec6f6..c1806c384941 100644 --- a/sdks/python/apache_beam/runners/portability/stager_test.py +++ b/sdks/python/apache_beam/runners/portability/stager_test.py @@ -832,6 +832,49 @@ def test_populate_requirements_cache_with_sdist(self): self.assertTrue('.tar.gz' in f) self.assertTrue('.whl' not in f) + def test_populate_requirements_cache_with_local_files(self): + staging_dir = self.make_temp_dir() + requirements_cache_dir = self.make_temp_dir() + source_dir = self.make_temp_dir() + pkg_dir = self.make_temp_dir() + + options = PipelineOptions() + self.update_options(options) + + options.view_as(SetupOptions).requirements_cache = requirements_cache_dir + options.view_as(SetupOptions).requirements_file = os.path.join( + source_dir, stager.REQUIREMENTS_FILE) + local_package = os.path.join(pkg_dir, 'local_package.tar.gz') + self.create_temp_file(local_package, 'local-package-content') + self.create_temp_file( + os.path.join(source_dir, stager.REQUIREMENTS_FILE), + '\n'.join(['fake_pypi', local_package])) + with mock.patch('apache_beam.runners.portability.stager_test' + '.stager.Stager._populate_requirements_cache', + staticmethod(self._populate_requitements_cache_fake)): + options.view_as(SetupOptions).requirements_cache_only_sources = True + resources = self.stager.create_and_stage_job_resources( + options, staging_location=staging_dir)[1] + + self.assertEqual( + sorted([ + stager.REQUIREMENTS_FILE, + stager.EXTRA_PACKAGES_FILE, + 'nothing.tar.gz', + 'local_package.tar.gz' + ]), + sorted(resources)) + + with open(os.path.join(staging_dir, stager.REQUIREMENTS_FILE)) as fin: + requirements_contents = fin.read() + self.assertIn('fake_pypi', requirements_contents) + self.assertNotIn('local_package', requirements_contents) + + with open(os.path.join(staging_dir, stager.EXTRA_PACKAGES_FILE)) as fin: + extra_packages_contents = fin.read() + self.assertNotIn('fake_pypi', extra_packages_contents) + self.assertIn('local_package', extra_packages_contents) + class TestStager(stager.Stager): def stage_artifact(self, local_path_to_artifact, artifact_name, sha256): diff --git a/sdks/python/apache_beam/runners/worker/statecache.py b/sdks/python/apache_beam/runners/worker/statecache.py index e3f37fec1144..dde4243057dd 100644 --- a/sdks/python/apache_beam/runners/worker/statecache.py +++ b/sdks/python/apache_beam/runners/worker/statecache.py @@ -176,7 +176,7 @@ def get_deep_size(*objs): """Calculates the deep size of all the arguments in bytes.""" return objsize.get_deep_size( - objs, + *objs, get_size_func=_size_func, get_referents_func=_get_referents_func, filter_func=_filter_func) @@ -274,6 +274,7 @@ def get(self, key, loading_fn): self._miss_count += 1 loading_value = _LoadingValue() self._cache[key] = loading_value + self._current_weight += loading_value.weight() # Ensure that we unlock the lock while loading to allow for parallel gets self._lock.release() diff --git a/sdks/python/apache_beam/runners/worker/statecache_test.py b/sdks/python/apache_beam/runners/worker/statecache_test.py index 6850cb212840..a5d1ff2e01e3 100644 --- a/sdks/python/apache_beam/runners/worker/statecache_test.py +++ b/sdks/python/apache_beam/runners/worker/statecache_test.py @@ -20,11 +20,13 @@ import logging import re +import sys import threading import time import unittest import weakref +import objsize from hamcrest import assert_that from hamcrest import contains_string @@ -32,6 +34,7 @@ from apache_beam.runners.worker.statecache import StateCache from apache_beam.runners.worker.statecache import WeightedValue from apache_beam.runners.worker.statecache import _LoadingValue +from apache_beam.runners.worker.statecache import get_deep_size class StateCacheTest(unittest.TestCase): @@ -356,6 +359,54 @@ def get_referents_for_cache(self): 'used/max 1/5 MB, hit 100.00%, lookups 0, ' 'avg load time 0 ns, loads 0, evictions 0')) + def test_get_deep_size_builtin_objects(self): + """ + `statecache.get_deep_copy` should work same with objsize unless the `objs` + has `CacheAware` or a filtered object. They should return the same size for + built-in objects. + """ + primitive_test_objects = [ + 1, # int + 2.0, # float + 1+1j, # complex + True, # bool + 'hello,world', # str + b'\00\01\02', # bytes + ] + + collection_test_objects = [ + [3, 4, 5], # list + (6, 7), # tuple + {'a', 'b', 'c'}, # set + {'k': 8, 'l': 9}, # dict + ] + + for obj in primitive_test_objects: + self.assertEqual( + get_deep_size(obj), + objsize.get_deep_size(obj), + f'different size for obj: `{obj}`, type: {type(obj)}') + self.assertEqual( + get_deep_size(obj), + sys.getsizeof(obj), + f'different size for obj: `{obj}`, type: {type(obj)}') + + for obj in collection_test_objects: + self.assertEqual( + get_deep_size(obj), + objsize.get_deep_size(obj), + f'different size for obj: `{obj}`, type: {type(obj)}') + + def test_current_weight_between_get_and_put(self): + value = 1234567 + get_cache = StateCache(100) + get_cache.get("key", lambda k: value) + + put_cache = StateCache(100) + put_cache.put("key", value) + + self.assertEqual(get_cache._current_weight, put_cache._current_weight) + if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) diff --git a/sdks/python/apache_beam/runners/worker/worker_status.py b/sdks/python/apache_beam/runners/worker/worker_status.py index 7604bd0867a5..40f0d927c0ee 100644 --- a/sdks/python/apache_beam/runners/worker/worker_status.py +++ b/sdks/python/apache_beam/runners/worker/worker_status.py @@ -230,7 +230,7 @@ def close(self): def _log_lull_in_bundle_processor(self, bundle_process_cache): while True: time.sleep(2 * 60) - if bundle_process_cache.active_bundle_processors: + if bundle_process_cache and bundle_process_cache.active_bundle_processors: for instruction in list( bundle_process_cache.active_bundle_processors.keys()): processor = bundle_process_cache.lookup(instruction) diff --git a/sdks/python/apache_beam/testing/benchmarks/nexmark/nexmark_launcher.py b/sdks/python/apache_beam/testing/benchmarks/nexmark/nexmark_launcher.py index e4babe5f42e8..04c6d325e194 100644 --- a/sdks/python/apache_beam/testing/benchmarks/nexmark/nexmark_launcher.py +++ b/sdks/python/apache_beam/testing/benchmarks/nexmark/nexmark_launcher.py @@ -60,10 +60,15 @@ # pytype: skip-file import argparse +import json import logging +import os import time import uuid +import requests +from requests.auth import HTTPBasicAuth + import apache_beam as beam from apache_beam.options.pipeline_options import GoogleCloudOptions from apache_beam.options.pipeline_options import PipelineOptions @@ -123,6 +128,13 @@ def __init__(self): logging.info('creating sub %s', self.topic_name) sub.create() + self.export_influxdb = self.args.export_summary_to_influxdb + if self.export_influxdb: + self.influx_database = self.args.influx_database + self.influx_host = self.args.influx_host + self.influx_base = self.args.base_influx_measurement + self.influx_retention = self.args.influx_retention_policy + def parse_args(self): parser = argparse.ArgumentParser() @@ -170,6 +182,32 @@ def parse_args(self): choices=['PUBLISH_ONLY', 'SUBSCRIBE_ONLY', 'COMBINED'], help='Pubsub mode used in the pipeline.') + parser.add_argument( + '--export_summary_to_influxdb', + default=False, + action='store_true', + help='If set store results in influxdb') + parser.add_argument( + '--influx_database', + type=str, + default='beam_test_metrics', + help='Influx database name') + parser.add_argument( + '--influx_host', + type=str, + default='http://localhost:8086', + help='Influx database url') + parser.add_argument( + '--base_influx_measurement', + type=str, + default='nexmark', + help='Prefix to influx measurement') + parser.add_argument( + '--influx_retention_policy', + type=str, + default='forever', + help='Retention policy for stored results') + self.args, self.pipeline_args = parser.parse_known_args() logging.basicConfig( level=getattr(logging, self.args.loglevel, None), @@ -243,7 +281,8 @@ def read_from_pubsub(self): | 'deserialization' >> beam.ParDo(nexmark_util.ParseJsonEventFn())) return events - def run_query(self, query, query_args, pipeline_options, query_errors): + def run_query( + self, query_num, query, query_args, pipeline_options, query_errors): try: self.pipeline = beam.Pipeline(options=self.pipeline_options) nexmark_util.setup_coder() @@ -269,6 +308,8 @@ def run_query(self, query, query_args, pipeline_options, query_errors): result.wait_until_finish() perf = self.monitor(result, event_monitor, result_monitor) self.log_performance(perf) + if self.export_influxdb: + self.publish_performance_influxdb(query_num, perf) except Exception as exc: query_errors.append(str(exc)) @@ -349,6 +390,47 @@ def log_performance(perf): 'query run took %.1f seconds and processed %.1f events per second' % (perf.runtime_sec, perf.event_per_sec)) + def publish_performance_influxdb(self, query_num, perf): + processingMode = "streaming" if self.streaming else "batch" + measurement = "%s_%d_python_%s" % ( + self.influx_base, query_num, processingMode) + + tags = {'runner': self.pipeline_options.view_as(StandardOptions).runner} + + mt = ','.join([measurement] + [k + "=" + v for k, v in tags.items()]) + + fields = { + 'numResults': "%di" % (perf.result_count), + 'runtimeMs': "%di" % (perf.runtime_sec * 1000), + } + + ts = int(time.time()) + payload = '\n'.join( + ["%s %s=%s %d" % (mt, k, v, ts) for k, v in fields.items()]) + + url = '%s/write' % (self.influx_host) + query_str = { + 'db': self.influx_database, + 'rp': self.influx_retention, + 'precision': 's', + } + + user = os.getenv('INFLUXDB_USER') + password = os.getenv('INFLUXDB_USER_PASSWORD') + auth = HTTPBasicAuth(user, password) + + try: + response = requests.post(url, params=query_str, data=payload, auth=auth) + except requests.exceptions.RequestException as e: + logging.warning('Failed to publish metrics to InfluxDB: ' + str(e)) + else: + if response.status_code != 204: + content = json.loads(response.content) + logging.warning( + 'Failed to publish metrics to InfluxDB. Received status code %s ' + 'with an error message: %s' % + (response.status_code, content['error'])) + @staticmethod def get_performance(result, event_monitor, result_monitor): event_count = nexmark_util.get_counter_metric( @@ -429,6 +511,7 @@ def run(self): for i in self.args.query: logging.info('Running query %d', i) self.run_query( + i, queries[i], query_args, self.pipeline_options, diff --git a/sdks/python/apache_beam/testing/test_utils.py b/sdks/python/apache_beam/testing/test_utils.py index 72067fdb5fc4..049ccc0c3d49 100644 --- a/sdks/python/apache_beam/testing/test_utils.py +++ b/sdks/python/apache_beam/testing/test_utils.py @@ -206,3 +206,20 @@ def create_pull_response(responses): res.received_messages.append(received_message) return res + + +def create_file(path, contents): + """Create a file to use as input to test pipelines""" + with FileSystems.create(path) as f: + f.write(str.encode(contents, 'utf-8')) + return path + + +def read_files_from_pattern(file_pattern): + """Reads the files that match a pattern""" + metadata_list = FileSystems.match([file_pattern])[0].metadata_list + output = [] + for metadata in metadata_list: + with FileSystems.open(metadata.path) as f: + output.append(f.read().decode('utf-8').strip()) + return '\n'.join(output) diff --git a/sdks/python/apache_beam/transforms/batch_dofn_test.py b/sdks/python/apache_beam/transforms/batch_dofn_test.py index de35c29024a5..d2aceb371492 100644 --- a/sdks/python/apache_beam/transforms/batch_dofn_test.py +++ b/sdks/python/apache_beam/transforms/batch_dofn_test.py @@ -41,12 +41,12 @@ def process_batch(self, batch: List[int], *args, yield [element / 2 for element in batch] -class BatchDoFnNoReturnAnnotation(beam.DoFn): +class NoReturnAnnotation(beam.DoFn): def process_batch(self, batch: List[int], *args, **kwargs): yield [element * 2 for element in batch] -class BatchDoFnOverrideTypeInference(beam.DoFn): +class OverrideTypeInference(beam.DoFn): def process_batch(self, batch, *args, **kwargs): yield [element * 2 for element in batch] @@ -104,7 +104,7 @@ def get_test_class_name(cls, num, params_dict): "expected_output_batch_type": beam.typehints.List[float] }, { - "dofn": BatchDoFnNoReturnAnnotation(), + "dofn": NoReturnAnnotation(), "input_element_type": int, "expected_process_defined": False, "expected_process_batch_defined": True, @@ -112,7 +112,7 @@ def get_test_class_name(cls, num, params_dict): "expected_output_batch_type": beam.typehints.List[int] }, { - "dofn": BatchDoFnOverrideTypeInference(), + "dofn": OverrideTypeInference(), "input_element_type": int, "expected_process_defined": False, "expected_process_batch_defined": True, @@ -168,7 +168,7 @@ def test_can_yield_batches(self): self.assertEqual(self.dofn._can_yield_batches, expected) -class BatchDoFnNoInputAnnotation(beam.DoFn): +class NoInputAnnotation(beam.DoFn): def process_batch(self, batch, *args, **kwargs): yield [element * 2 for element in batch] @@ -198,6 +198,12 @@ def process_batch(self, batch: List[int], *args, **kwargs) -> Iterator[int]: yield batch[0] +class NoElementOutputAnnotation(beam.DoFn): + def process_batch(self, batch: List[int], *args, + **kwargs) -> Iterator[List[int]]: + yield [element * 2 for element in batch] + + class BatchDoFnTest(unittest.TestCase): def test_map_pardo(self): # verify batch dofn accessors work well with beam.Map generated DoFn @@ -213,12 +219,11 @@ def test_no_input_annotation_raises(self): p = beam.Pipeline() pc = p | beam.Create([1, 2, 3]) - with self.assertRaisesRegex(TypeError, - r'BatchDoFnNoInputAnnotation.process_batch'): - _ = pc | beam.ParDo(BatchDoFnNoInputAnnotation()) + with self.assertRaisesRegex(TypeError, r'NoInputAnnotation.process_batch'): + _ = pc | beam.ParDo(NoInputAnnotation()) def test_unsupported_dofn_param_raises(self): - class BatchDoFnBadParam(beam.DoFn): + class BadParam(beam.DoFn): @no_type_check def process_batch(self, batch: List[int], key=beam.DoFn.KeyParam): yield batch * key @@ -226,9 +231,8 @@ def process_batch(self, batch: List[int], key=beam.DoFn.KeyParam): p = beam.Pipeline() pc = p | beam.Create([1, 2, 3]) - with self.assertRaisesRegex(NotImplementedError, - r'BatchDoFnBadParam.*KeyParam'): - _ = pc | beam.ParDo(BatchDoFnBadParam()) + with self.assertRaisesRegex(NotImplementedError, r'BadParam.*KeyParam'): + _ = pc | beam.ParDo(BadParam()) def test_mismatched_batch_producer_raises(self): p = beam.Pipeline() @@ -256,6 +260,27 @@ def test_mismatched_element_producer_raises(self): r'(?ms)MismatchedElementProducingDoFn.*process:.*process_batch:'): _ = pc | beam.ParDo(MismatchedElementProducingDoFn()) + def test_cant_infer_batchconverter_input_raises(self): + p = beam.Pipeline() + pc = p | beam.Create(['a', 'b', 'c']) + + with self.assertRaisesRegex( + TypeError, + # Error should mention "input", and the name of the DoFn + r'input.*BatchDoFn.*'): + _ = pc | beam.ParDo(BatchDoFn()) + + def test_cant_infer_batchconverter_output_raises(self): + p = beam.Pipeline() + pc = p | beam.Create([1, 2, 3]) + + with self.assertRaisesRegex( + TypeError, + # Error should mention "output", the name of the DoFn, and suggest + # overriding DoFn.infer_output_type + r'output.*NoElementOutputAnnotation.*DoFn\.infer_output_type'): + _ = pc | beam.ParDo(NoElementOutputAnnotation()) + def test_element_to_batch_dofn_typehint(self): # Verify that element to batch DoFn sets the correct typehint on the output # PCollection. diff --git a/sdks/python/apache_beam/transforms/core.py b/sdks/python/apache_beam/transforms/core.py index 50ff32e57a33..69c003ee5b8c 100644 --- a/sdks/python/apache_beam/transforms/core.py +++ b/sdks/python/apache_beam/transforms/core.py @@ -1511,10 +1511,17 @@ def infer_batch_converters(self, input_element_type): "process_batch method on {self.fn!r} does not have " "an input type annoation") - # Generate a batch converter to convert between the input type and the - # (batch) input type of process_batch - self.fn.input_batch_converter = BatchConverter.from_typehints( - element_type=input_element_type, batch_type=input_batch_type) + try: + # Generate a batch converter to convert between the input type and the + # (batch) input type of process_batch + self.fn.input_batch_converter = BatchConverter.from_typehints( + element_type=input_element_type, batch_type=input_batch_type) + except TypeError as e: + raise TypeError( + "Failed to find a BatchConverter for the input types of DoFn " + f"{self.fn!r} (element_type={input_element_type!r}, " + f"batch_type={input_batch_type!r}).") from e + else: self.fn.input_batch_converter = None @@ -1530,8 +1537,16 @@ def infer_batch_converters(self, input_element_type): # Generate a batch converter to convert between the output type and the # (batch) output type of process_batch output_element_type = self.infer_output_type(input_element_type) - self.fn.output_batch_converter = BatchConverter.from_typehints( - element_type=output_element_type, batch_type=output_batch_type) + + try: + self.fn.output_batch_converter = BatchConverter.from_typehints( + element_type=output_element_type, batch_type=output_batch_type) + except TypeError as e: + raise TypeError( + "Failed to find a BatchConverter for the *output* types of DoFn " + f"{self.fn!r} (element_type={output_element_type!r}, " + f"batch_type={output_batch_type!r}). Maybe you need to override " + "DoFn.infer_output_type to set the output element type?") from e else: self.fn.output_batch_converter = None diff --git a/sdks/python/apache_beam/transforms/util.py b/sdks/python/apache_beam/transforms/util.py index cb4b86245e00..dca5628118d6 100644 --- a/sdks/python/apache_beam/transforms/util.py +++ b/sdks/python/apache_beam/transforms/util.py @@ -83,6 +83,7 @@ 'Distinct', 'Keys', 'KvSwap', + 'LogElements', 'Regex', 'Reify', 'RemoveDuplicates', @@ -1105,6 +1106,49 @@ def Iterables(delimiter=None): Kvs = Iterables +@typehints.with_input_types(T) +@typehints.with_output_types(T) +class LogElements(PTransform): + """ + PTransform for printing the elements of a PCollection. + """ + class _LoggingFn(DoFn): + def __init__(self, prefix='', with_timestamp=False, with_window=False): + super().__init__() + self.prefix = prefix + self.with_timestamp = with_timestamp + self.with_window = with_window + + def process( + self, + element, + timestamp=DoFn.TimestampParam, + window=DoFn.WindowParam, + **kwargs): + log_line = self.prefix + str(element) + + if self.with_timestamp: + log_line += ', timestamp=' + repr(timestamp.to_rfc3339()) + + if self.with_window: + log_line += ', window(start=' + window.start.to_rfc3339() + log_line += ', end=' + window.end.to_rfc3339() + ')' + + print(log_line) + yield element + + def __init__( + self, label=None, prefix='', with_timestamp=False, with_window=False): + super().__init__(label) + self.prefix = prefix + self.with_timestamp = with_timestamp + self.with_window = with_window + + def expand(self, input): + return input | ParDo( + self._LoggingFn(self.prefix, self.with_timestamp, self.with_window)) + + class Reify(object): """PTransforms for converting between explicit and implicit form of various Beam values.""" diff --git a/sdks/python/apache_beam/transforms/util_test.py b/sdks/python/apache_beam/transforms/util_test.py index f236e3ce768e..180b7ae5f8e7 100644 --- a/sdks/python/apache_beam/transforms/util_test.py +++ b/sdks/python/apache_beam/transforms/util_test.py @@ -26,8 +26,10 @@ import time import unittest import warnings +from datetime import datetime import pytest +import pytz import apache_beam as beam from apache_beam import GroupByKey @@ -1063,6 +1065,46 @@ def test_tostring_kvs_empty_delimeter(self): assert_that(result, equal_to(["one1", "two2"])) +class LogElementsTest(unittest.TestCase): + @pytest.fixture(scope="function") + def _capture_stdout_log(request, capsys): + with TestPipeline() as p: + result = ( + p | beam.Create([ + TimestampedValue( + "event", + datetime(2022, 10, 1, 0, 0, 0, 0, + tzinfo=pytz.UTC).timestamp()), + TimestampedValue( + "event", + datetime(2022, 10, 2, 0, 0, 0, 0, + tzinfo=pytz.UTC).timestamp()), + ]) + | beam.WindowInto(FixedWindows(60)) + | util.LogElements( + prefix='prefix_', with_window=True, with_timestamp=True)) + + request.captured_stdout = capsys.readouterr().out + return result + + @pytest.mark.usefixtures("_capture_stdout_log") + def test_stdout_logs(self): + assert self.captured_stdout == \ + ("prefix_event, timestamp='2022-10-01T00:00:00Z', " + "window(start=2022-10-01T00:00:00Z, end=2022-10-01T00:01:00Z)\n" + "prefix_event, timestamp='2022-10-02T00:00:00Z', " + "window(start=2022-10-02T00:00:00Z, end=2022-10-02T00:01:00Z)\n"), \ + f'Received from stdout: {self.captured_stdout}' + + def test_ptransform_output(self): + with TestPipeline() as p: + result = ( + p + | beam.Create(['a', 'b', 'c']) + | util.LogElements(prefix='prefix_')) + assert_that(result, equal_to(['a', 'b', 'c'])) + + class ReifyTest(unittest.TestCase): def test_timestamp(self): l = [ diff --git a/sdks/python/apache_beam/typehints/__init__.py b/sdks/python/apache_beam/typehints/__init__.py index 46a8579c6c65..81ffc9f307d9 100644 --- a/sdks/python/apache_beam/typehints/__init__.py +++ b/sdks/python/apache_beam/typehints/__init__.py @@ -29,3 +29,10 @@ pass else: from apache_beam.typehints.pandas_type_compatibility import * + +try: + import pyarrow as _ +except ImportError: + pass +else: + from apache_beam.typehints.arrow_type_compatibility import * diff --git a/sdks/python/apache_beam/typehints/arrow_batching_microbenchmark.py b/sdks/python/apache_beam/typehints/arrow_batching_microbenchmark.py new file mode 100644 index 000000000000..d17a9b1a1818 --- /dev/null +++ b/sdks/python/apache_beam/typehints/arrow_batching_microbenchmark.py @@ -0,0 +1,78 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A microbenchmark for pyarrow batch creation. + +This microbenchmark exercises the PyarrowBatchConverter.produce_batch method +for different batch sizes. +""" + +import argparse +import logging + +import pyarrow as pa + +from apache_beam.portability.api import schema_pb2 +from apache_beam.tools import utils +from apache_beam.typehints.arrow_type_compatibility import PyarrowBatchConverter +from apache_beam.typehints.arrow_type_compatibility import beam_schema_from_arrow_schema +from apache_beam.typehints.schemas import typing_from_runner_api + + +def benchmark_produce_batch(size): + batch = pa.Table.from_pydict({ + 'foo': pa.array(range(size), type=pa.int64()), + 'bar': pa.array([i / size for i in range(size)], type=pa.float64()), + 'baz': pa.array([str(i) for i in range(size)], type=pa.string()), + }) + beam_schema = beam_schema_from_arrow_schema(batch.schema) + element_type = typing_from_runner_api( + schema_pb2.FieldType(row_type=schema_pb2.RowType(schema=beam_schema))) + + batch_converter = PyarrowBatchConverter.from_typehints(element_type, pa.Table) + elements = list(batch_converter.explode_batch(batch)) + + def _do_benchmark(): + _ = batch_converter.produce_batch(elements) + + return _do_benchmark + + +def run_benchmark( + starting_point=1, num_runs=10, num_elements_step=300, verbose=True): + suite = [ + utils.LinearRegressionBenchmarkConfig( + benchmark_produce_batch, starting_point, num_elements_step, num_runs) + ] + return utils.run_benchmarks(suite, verbose=verbose) + + +if __name__ == '__main__': + logging.basicConfig() + + parser = argparse.ArgumentParser() + parser.add_argument('--num_runs', default=10, type=int) + parser.add_argument('--starting_point', default=50, type=int) + parser.add_argument('--increment', default=1000, type=int) + parser.add_argument('--verbose', default=True, type=bool) + options = parser.parse_args() + + run_benchmark( + options.starting_point, + options.num_runs, + options.increment, + options.verbose) diff --git a/sdks/python/apache_beam/typehints/arrow_type_compatibility.py b/sdks/python/apache_beam/typehints/arrow_type_compatibility.py new file mode 100644 index 000000000000..cad6ac8751ca --- /dev/null +++ b/sdks/python/apache_beam/typehints/arrow_type_compatibility.py @@ -0,0 +1,384 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Utilities for converting between Beam and Arrow schemas. + +For internal use only, no backward compatibility guarantees. +""" + +from functools import partial +from typing import Dict +from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple + +import pyarrow as pa + +from apache_beam.portability.api import schema_pb2 +from apache_beam.typehints.batch import BatchConverter +from apache_beam.typehints.row_type import RowTypeConstraint +from apache_beam.typehints.schemas import typing_from_runner_api +from apache_beam.typehints.schemas import typing_to_runner_api +from apache_beam.utils import proto_utils + +__all__ = [] + +# Get major, minor version +PYARROW_VERSION = tuple(map(int, pa.__version__.split('.')[0:2])) + +BEAM_SCHEMA_ID_KEY = b'beam:schema_id' +# We distinguish between schema and field options, because they have to be +# combined into arrow Field-level metadata for nested structs. +BEAM_SCHEMA_OPTION_KEY_PREFIX = b'beam:schema_option:' +BEAM_FIELD_OPTION_KEY_PREFIX = b'beam:field_option:' + + +def _hydrate_beam_option(encoded_option: bytes) -> schema_pb2.Option: + return proto_utils.parse_Bytes(encoded_option, schema_pb2.Option) + + +def beam_schema_from_arrow_schema(arrow_schema: pa.Schema) -> schema_pb2.Schema: + if arrow_schema.metadata: + schema_id = arrow_schema.metadata.get(BEAM_SCHEMA_ID_KEY, None) + schema_options = [ + _hydrate_beam_option(value) for key, + value in arrow_schema.metadata.items() + if key.startswith(BEAM_SCHEMA_OPTION_KEY_PREFIX) + ] + else: + schema_id = None + schema_options = [] + + return schema_pb2.Schema( + fields=[ + _beam_field_from_arrow_field(arrow_schema.field(i)) + for i in range(len(arrow_schema.types)) + ], + options=schema_options, + id=schema_id) + + +def _beam_field_from_arrow_field(arrow_field: pa.Field) -> schema_pb2.Field: + beam_fieldtype = _beam_fieldtype_from_arrow_field(arrow_field) + + if arrow_field.metadata: + field_options = [ + _hydrate_beam_option(value) for key, + value in arrow_field.metadata.items() + if key.startswith(BEAM_FIELD_OPTION_KEY_PREFIX) + ] + if isinstance(arrow_field.type, pa.StructType): + beam_fieldtype.row_type.schema.options.extend([ + _hydrate_beam_option(value) for key, + value in arrow_field.metadata.items() + if key.startswith(BEAM_SCHEMA_OPTION_KEY_PREFIX) + ]) + if BEAM_SCHEMA_ID_KEY in arrow_field.metadata: + beam_fieldtype.row_type.schema.id = arrow_field.metadata[ + BEAM_SCHEMA_ID_KEY] + + else: + field_options = None + + return schema_pb2.Field( + name=arrow_field.name, + type=beam_fieldtype, + options=field_options, + ) + + +def _beam_fieldtype_from_arrow_field( + arrow_field: pa.Field) -> schema_pb2.FieldType: + beam_fieldtype = _beam_fieldtype_from_arrow_type(arrow_field.type) + beam_fieldtype.nullable = arrow_field.nullable + + return beam_fieldtype + + +def _beam_fieldtype_from_arrow_type( + arrow_type: pa.DataType) -> schema_pb2.FieldType: + if arrow_type in PYARROW_TO_ATOMIC_TYPE: + return schema_pb2.FieldType(atomic_type=PYARROW_TO_ATOMIC_TYPE[arrow_type]) + elif isinstance(arrow_type, pa.ListType): + return schema_pb2.FieldType( + array_type=schema_pb2.ArrayType( + element_type=_beam_fieldtype_from_arrow_field( + arrow_type.value_field))) + elif isinstance(arrow_type, pa.MapType): + return schema_pb2.FieldType(map_type=_arrow_map_to_beam_map(arrow_type)) + elif isinstance(arrow_type, pa.StructType): + return schema_pb2.FieldType( + row_type=schema_pb2.RowType( + schema=schema_pb2.Schema( + fields=[ + _beam_field_from_arrow_field(arrow_type[i]) + for i in range(len(arrow_type)) + ], + ))) + + else: + raise ValueError(f"Unrecognized arrow type: {arrow_type!r}") + + +def _option_as_arrow_metadata(beam_option: schema_pb2.Option, *, + prefix: bytes) -> Tuple[bytes, bytes]: + return ( + prefix + beam_option.name.encode('UTF-8'), + beam_option.SerializeToString()) + + +_field_option_as_arrow_metadata = partial( + _option_as_arrow_metadata, prefix=BEAM_FIELD_OPTION_KEY_PREFIX) +_schema_option_as_arrow_metadata = partial( + _option_as_arrow_metadata, prefix=BEAM_SCHEMA_OPTION_KEY_PREFIX) + + +def arrow_schema_from_beam_schema(beam_schema: schema_pb2.Schema) -> pa.Schema: + return pa.schema( + [_arrow_field_from_beam_field(field) for field in beam_schema.fields], + { + BEAM_SCHEMA_ID_KEY: beam_schema.id, + **dict( + _schema_option_as_arrow_metadata(option) for option in beam_schema.options) # pylint: disable=line-too-long + }, + ) + + +def _arrow_field_from_beam_field(beam_field: schema_pb2.Field) -> pa.Field: + return _arrow_field_from_beam_fieldtype( + beam_field.type, name=beam_field.name, field_options=beam_field.options) + + +_ARROW_PRIMITIVE_MAPPING = [ + # TODO(https://github.com/apache/beam/issues/23816): Support unsigned ints + # and float16 + (schema_pb2.BYTE, pa.int8()), + (schema_pb2.INT16, pa.int16()), + (schema_pb2.INT32, pa.int32()), + (schema_pb2.INT64, pa.int64()), + (schema_pb2.FLOAT, pa.float32()), + (schema_pb2.DOUBLE, pa.float64()), + (schema_pb2.BOOLEAN, pa.bool_()), + (schema_pb2.STRING, pa.string()), + (schema_pb2.BYTES, pa.binary()), +] +ATOMIC_TYPE_TO_PYARROW = { + beam: arrow + for beam, arrow in _ARROW_PRIMITIVE_MAPPING +} +PYARROW_TO_ATOMIC_TYPE = { + arrow: beam + for beam, arrow in _ARROW_PRIMITIVE_MAPPING +} + + +def _arrow_field_from_beam_fieldtype( + beam_fieldtype: schema_pb2.FieldType, + name=b'', + field_options: Sequence[schema_pb2.Option] = None) -> pa.DataType: + arrow_type = _arrow_type_from_beam_fieldtype(beam_fieldtype) + if field_options is not None: + metadata = dict( + _field_option_as_arrow_metadata(field_option) + for field_option in field_options) + else: + metadata = {} + + type_info = beam_fieldtype.WhichOneof("type_info") + if type_info == "row_type": + schema = beam_fieldtype.row_type.schema + metadata.update( + dict( + _schema_option_as_arrow_metadata(schema_option) + for schema_option in schema.options)) + if schema.id: + metadata[BEAM_SCHEMA_ID_KEY] = schema.id + + return pa.field( + name=name, + type=arrow_type, + nullable=beam_fieldtype.nullable, + metadata=metadata, + ) + + +if PYARROW_VERSION < (6, 0): + # In pyarrow < 6.0.0 we cannot construct a MapType object from Field + # instances, pa.map_ will only accept DataType instances. This makes it + # impossible to propagate nullability. + # + # Note this was changed in: + # https://github.com/apache/arrow/commit/64bef2ad8d9cd2fea122cfa079f8ca3fea8cdf5d + # + # Here we define a custom arrow map conversion function to handle these cases + # and error as appropriate. + + def _make_arrow_map(beam_map_type: schema_pb2.MapType): + if beam_map_type.key_type.nullable: + raise TypeError('Arrow map key field cannot be nullable') + elif beam_map_type.value_type.nullable: + raise TypeError( + "pyarrow<6 does not support creating maps with nullable " + "values. Please use pyarrow>=6.0.0") + + return pa.map_( + _arrow_type_from_beam_fieldtype(beam_map_type.key_type), + _arrow_type_from_beam_fieldtype(beam_map_type.value_type)) + + def _arrow_map_to_beam_map(arrow_map_type): + return schema_pb2.MapType( + key_type=_beam_fieldtype_from_arrow_type(arrow_map_type.key_type), + value_type=_beam_fieldtype_from_arrow_type(arrow_map_type.item_type)) + +else: + + def _make_arrow_map(beam_map_type: schema_pb2.MapType): + return pa.map_( + _arrow_field_from_beam_fieldtype(beam_map_type.key_type), + _arrow_field_from_beam_fieldtype(beam_map_type.value_type)) + + def _arrow_map_to_beam_map(arrow_map_type): + return schema_pb2.MapType( + key_type=_beam_fieldtype_from_arrow_field(arrow_map_type.key_field), + value_type=_beam_fieldtype_from_arrow_field(arrow_map_type.item_field)) + + +def _arrow_type_from_beam_fieldtype( + beam_fieldtype: schema_pb2.FieldType, +) -> Tuple[pa.DataType, Optional[Dict[bytes, bytes]]]: + # Note this function is not concerned with beam_fieldtype.nullable, as + # nullability is a property of the Field in Arrow. + type_info = beam_fieldtype.WhichOneof("type_info") + if type_info == 'atomic_type': + try: + output_arrow_type = ATOMIC_TYPE_TO_PYARROW[beam_fieldtype.atomic_type] + except KeyError: + raise ValueError( + "Unsupported atomic type: {0}".format(beam_fieldtype.atomic_type)) + elif type_info == "array_type": + output_arrow_type = pa.list_( + _arrow_field_from_beam_fieldtype( + beam_fieldtype.array_type.element_type)) + elif type_info == "map_type": + output_arrow_type = _make_arrow_map(beam_fieldtype.map_type) + elif type_info == "row_type": + schema = beam_fieldtype.row_type.schema + # Note schema id and options are handled at the arrow field level, they are + # added at field-level metadata. + output_arrow_type = pa.struct( + [_arrow_field_from_beam_field(field) for field in schema.fields]) + elif type_info == "logical_type": + # TODO(https://github.com/apache/beam/issues/23817): Add support for logical + # types. + raise NotImplementedError( + "Beam logical types are not currently supported " + "in arrow_type_compatibility.") + else: + raise ValueError(f"Unrecognized type_info: {type_info!r}") + + return output_arrow_type + + +class PyarrowBatchConverter(BatchConverter): + def __init__(self, element_type: RowTypeConstraint): + super().__init__(pa.Table, element_type) + self._beam_schema = typing_to_runner_api(element_type).row_type.schema + arrow_schema = arrow_schema_from_beam_schema(self._beam_schema) + + self._arrow_schema = arrow_schema + + @staticmethod + @BatchConverter.register + def from_typehints(element_type, + batch_type) -> Optional['PyarrowBatchConverter']: + if isinstance(element_type, RowTypeConstraint) and batch_type == pa.Table: + return PyarrowBatchConverter(element_type) + + return None + + def produce_batch(self, elements): + arrays = [ + pa.array([getattr(el, name) for el in elements], + type=self._arrow_schema.field(name).type) for name, + _ in self._element_type._fields + ] + return pa.Table.from_arrays(arrays, schema=self._arrow_schema) + + def explode_batch(self, batch: pa.Table): + """Convert an instance of B to Generator[E].""" + for row_values in zip(*batch.columns): + yield self._element_type.user_type( + **{ + name: val.as_py() + for name, + val in zip(self._arrow_schema.names, row_values) + }) + + def combine_batches(self, batches: List[pa.Table]): + return pa.concat_tables(batches) + + def get_length(self, batch: pa.Table): + return batch.num_rows + + def estimate_byte_size(self, batch: pa.Table): + return batch.nbytes + + @staticmethod + def _from_serialized_schema(serialized_schema): + beam_schema = proto_utils.parse_Bytes(serialized_schema, schema_pb2.Schema) + element_type = typing_from_runner_api( + schema_pb2.FieldType(row_type=schema_pb2.RowType(schema=beam_schema))) + return PyarrowBatchConverter(element_type) + + def __reduce__(self): + return self._from_serialized_schema, ( + self._beam_schema.SerializeToString(), ) + + +class PyarrowArrayBatchConverter(BatchConverter): + def __init__(self, element_type: type): + super().__init__(pa.Array, element_type) + self._element_type = element_type + beam_fieldtype = typing_to_runner_api(element_type) + self._arrow_type = _arrow_type_from_beam_fieldtype(beam_fieldtype) + + @staticmethod + @BatchConverter.register + def from_typehints(element_type, + batch_type) -> Optional['PyarrowArrayBatchConverter']: + if batch_type == pa.Array: + return PyarrowArrayBatchConverter(element_type) + + return None + + def produce_batch(self, elements): + return pa.array(list(elements), type=self._arrow_type) + + def explode_batch(self, batch: pa.Array): + """Convert an instance of B to Generator[E].""" + for val in batch: + yield val.as_py() + + def combine_batches(self, batches: List[pa.Array]): + return pa.concat_arrays(batches) + + def get_length(self, batch: pa.Array): + return batch.num_rows + + def estimate_byte_size(self, batch: pa.Array): + return batch.nbytes diff --git a/sdks/python/apache_beam/typehints/arrow_type_compatibility_test.py b/sdks/python/apache_beam/typehints/arrow_type_compatibility_test.py new file mode 100644 index 000000000000..6a8649cff1ea --- /dev/null +++ b/sdks/python/apache_beam/typehints/arrow_type_compatibility_test.py @@ -0,0 +1,197 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Tests for arrow_type_compatibility.""" + +import logging +import unittest +from typing import Optional + +import pyarrow as pa +import pytest +from parameterized import parameterized +from parameterized import parameterized_class + +from apache_beam.typehints import row_type +from apache_beam.typehints import typehints +from apache_beam.typehints.arrow_type_compatibility import arrow_schema_from_beam_schema +from apache_beam.typehints.arrow_type_compatibility import beam_schema_from_arrow_schema +from apache_beam.typehints.batch import BatchConverter +from apache_beam.typehints.batch_test import temp_seed +from apache_beam.typehints.schemas_test import get_test_beam_schemas_protos + + +@pytest.mark.uses_pyarrow +class ArrowTypeCompatibilityTest(unittest.TestCase): + @parameterized.expand([(beam_schema, ) + for beam_schema in get_test_beam_schemas_protos()]) + def test_beam_schema_survives_roundtrip(self, beam_schema): + roundtripped = beam_schema_from_arrow_schema( + arrow_schema_from_beam_schema(beam_schema)) + + self.assertEqual(beam_schema, roundtripped) + +@parameterized_class([ + { + 'batch_typehint': pa.Table, + 'element_typehint': row_type.RowTypeConstraint.from_fields([ + ('foo', Optional[int]), + ('bar', Optional[float]), + ('baz', Optional[str]), + ]), + 'batch': pa.Table.from_pydict({ + 'foo': pa.array(range(100), type=pa.int64()), + 'bar': pa.array([i / 100 for i in range(100)], type=pa.float64()), + 'baz': pa.array([str(i) for i in range(100)], type=pa.string()), + }), + }, + { + 'batch_typehint': pa.Table, + 'element_typehint': row_type.RowTypeConstraint.from_fields([ + ('foo', Optional[int]), + ( + 'nested', + Optional[row_type.RowTypeConstraint.from_fields([ + ("bar", Optional[float]), # noqa: F821 + ("baz", Optional[str]), # noqa: F821 + ])]), + ]), + 'batch': pa.Table.from_pydict({ + 'foo': pa.array(range(100), type=pa.int64()), + 'nested': pa.array([ + None if i % 11 else { + 'bar': i / 100, 'baz': str(i) + } for i in range(100) + ]), + }), + }, + { + 'batch_typehint': pa.Array, + 'element_typehint': int, + 'batch': pa.array(range(100), type=pa.int64()), + }, + { + 'batch_typehint': pa.Array, + 'element_typehint': row_type.RowTypeConstraint.from_fields([ + ("bar", Optional[float]), # noqa: F821 + ("baz", Optional[str]), # noqa: F821 + ]), + 'batch': pa.array([ + { + 'bar': i / 100, 'baz': str(i) + } if i % 7 else None for i in range(100) + ]), + } +]) +@pytest.mark.uses_pyarrow +class ArrowBatchConverterTest(unittest.TestCase): + def create_batch_converter(self): + return BatchConverter.from_typehints( + element_type=self.element_typehint, batch_type=self.batch_typehint) + + def setUp(self): + self.converter = self.create_batch_converter() + self.normalized_batch_typehint = typehints.normalize(self.batch_typehint) + self.normalized_element_typehint = typehints.normalize( + self.element_typehint) + + def equality_check(self, left, right): + if isinstance(left, pa.Array): + self.assertTrue(left.equals(right)) + else: + self.assertEqual(left, right) + + def test_typehint_validates(self): + typehints.validate_composite_type_param(self.batch_typehint, '') + typehints.validate_composite_type_param(self.element_typehint, '') + + def test_type_check(self): + typehints.check_constraint(self.normalized_batch_typehint, self.batch) + + def test_type_check_element(self): + for element in self.converter.explode_batch(self.batch): + typehints.check_constraint(self.normalized_element_typehint, element) + + def test_explode_rebatch(self): + exploded = list(self.converter.explode_batch(self.batch)) + rebatched = self.converter.produce_batch(exploded) + + typehints.check_constraint(self.normalized_batch_typehint, rebatched) + self.equality_check(self.batch, rebatched) + + def test_estimate_byte_size_implemented(self): + # Just verify that we can call byte size + self.assertGreater(self.converter.estimate_byte_size(self.batch), 0) + + @parameterized.expand([ + (2, ), + (3, ), + (10, ), + ]) + def test_estimate_byte_size_partitions(self, N): + elements = list(self.converter.explode_batch(self.batch)) + + # Split elements into N contiguous partitions, create a batch out of each + batches = [ + self.converter.produce_batch( + elements[len(elements) * i // N:len(elements) * (i + 1) // N]) + for i in range(N) + ] + + # Some estimate_byte_size implementations use random samples, + # set a seed temporarily to make this test deterministic + with temp_seed(12345): + partitioned_size_estimate = sum( + self.converter.estimate_byte_size(batch) for batch in batches) + size_estimate = self.converter.estimate_byte_size(self.batch) + + # Assert that size estimate for partitions is within 10% of size estimate + # for the whole partition. + self.assertLessEqual( + abs(partitioned_size_estimate / size_estimate - 1), 0.1) + + @parameterized.expand([ + (2, ), + (3, ), + (10, ), + ]) + def test_combine_batches(self, N): + elements = list(self.converter.explode_batch(self.batch)) + + # Split elements into N contiguous partitions, create a batch out of each + batches = [ + self.converter.produce_batch( + elements[len(elements) * i // N:len(elements) * (i + 1) // N]) + for i in range(N) + ] + + # Combine the batches, output should be equivalent to the original batch + combined = self.converter.combine_batches(batches) + + self.equality_check(self.batch, combined) + + def test_equals(self): + self.assertTrue(self.converter == self.create_batch_converter()) + self.assertTrue(self.create_batch_converter() == self.converter) + + def test_hash(self): + self.assertEqual(hash(self.create_batch_converter()), hash(self.converter)) + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + unittest.main() diff --git a/sdks/python/apache_beam/typehints/batch.py b/sdks/python/apache_beam/typehints/batch.py index 322ba717a05c..de6c7fb71572 100644 --- a/sdks/python/apache_beam/typehints/batch.py +++ b/sdks/python/apache_beam/typehints/batch.py @@ -39,6 +39,8 @@ from apache_beam import coders from apache_beam.typehints import typehints +__all__ = ['BatchConverter'] + B = TypeVar('B') E = TypeVar('E') diff --git a/sdks/python/apache_beam/typehints/schemas.py b/sdks/python/apache_beam/typehints/schemas.py index 4371ca2de0da..55335bbd6b6e 100644 --- a/sdks/python/apache_beam/typehints/schemas.py +++ b/sdks/python/apache_beam/typehints/schemas.py @@ -34,13 +34,23 @@ bytes <-----> BYTES ByteString ------> BYTES Timestamp <-----> LogicalType(urn="beam:logical_type:micros_instant:v1") - Timestamp <------ LogicalType(urn="beam:logical_type:millis_instant:v1") Decimal <-----> LogicalType(urn="beam:logical_type:fixed_decimal:v1") Mapping <-----> MapType Sequence <-----> ArrayType NamedTuple <-----> RowType beam.Row ------> RowType +One direction mapping of Python types from Beam portable schemas: + + bytes + <------ LogicalType(urn="beam:logical_type:fixed_bytes:v1") + <------ LogicalType(urn="beam:logical_type:var_bytes:v1") + str + <------ LogicalType(urn="beam:logical_type:fixed_char:v1") + <------ LogicalType(urn="beam:logical_type:var_char:v1") + Timestamp + <------ LogicalType(urn="beam:logical_type:millis_instant:v1") + Note that some of these mappings are provided as conveniences, but they are lossy and will not survive a roundtrip from python to Beam schemas and back. For example, the Python type :code:`int` will map to :code:`INT64` in @@ -57,6 +67,7 @@ # pytype: skip-file import decimal +import logging from typing import Any from typing import ByteString from typing import Dict @@ -116,6 +127,8 @@ float: schema_pb2.DOUBLE, }) +_LOGGER = logging.getLogger(__name__) + def named_fields_to_schema( names_and_types: Union[Dict[str, type], Sequence[Tuple[str, type]]], @@ -171,6 +184,25 @@ def typing_from_runner_api( schema_registry=schema_registry).typing_from_runner_api(fieldtype_proto) +def value_to_runner_api( + type_proto: schema_pb2.FieldType, + value, + schema_registry: SchemaTypeRegistry = SCHEMA_REGISTRY +) -> schema_pb2.FieldValue: + return SchemaTranslation(schema_registry=schema_registry).value_to_runner_api( + type_proto, value) + + +def value_from_runner_api( + type_proto: schema_pb2.FieldType, + value_proto: schema_pb2.FieldValue, + schema_registry: SchemaTypeRegistry = SCHEMA_REGISTRY +) -> schema_pb2.FieldValue: + return SchemaTranslation( + schema_registry=schema_registry).value_from_runner_api( + type_proto, value_proto) + + def option_to_runner_api( option: Tuple[str, Any], schema_registry: SchemaTypeRegistry = SCHEMA_REGISTRY) -> schema_pb2.Option: @@ -269,59 +301,114 @@ def typing_to_runner_api(self, type_: type) -> schema_pb2.FieldType: logical_type=schema_pb2.LogicalType(urn=PYTHON_ANY_URN), nullable=True) else: - if logical_type.argument_type() is None: - return schema_pb2.FieldType( - logical_type=schema_pb2.LogicalType( - urn=logical_type.urn(), - representation=self.typing_to_runner_api( - logical_type.representation_type()))) - else: - # TODO(https://github.com/apache/beam/issues/23373): Complete support - # for logical types that require arguments. - # This include implement SchemaTranslation.value_to_runner_api (see Java - # SDK's SchemaTranslation.fieldValueToProto) - return schema_pb2.FieldType( - logical_type=schema_pb2.LogicalType( - urn=logical_type.urn(), - representation=self.typing_to_runner_api( - logical_type.representation_type()), - argument_type=self.typing_to_runner_api( - logical_type.argument_type()))) + argument_type = None + argument = None + if logical_type.argument_type() is not None: + argument_type = self.typing_to_runner_api(logical_type.argument_type()) + try: + argument = self.value_to_runner_api( + argument_type, logical_type.argument()) + except ValueError: + # TODO(https://github.com/apache/beam/issues/23373): Complete support + # for logical types that require arguments beyond atomic type. + # For now, skip arguments. + argument = None + return schema_pb2.FieldType( + logical_type=schema_pb2.LogicalType( + urn=logical_type.urn(), + representation=self.typing_to_runner_api( + logical_type.representation_type()), + argument_type=argument_type, + argument=argument)) + + def atomic_value_from_runner_api( + self, + atomic_type: schema_pb2.AtomicType, + atomic_value: schema_pb2.AtomicTypeValue): + if atomic_type == schema_pb2.BYTE: + value = np.int8(atomic_value.byte) + elif atomic_type == schema_pb2.INT16: + value = np.int16(atomic_value.int16) + elif atomic_type == schema_pb2.INT32: + value = np.int32(atomic_value.int32) + elif atomic_type == schema_pb2.INT64: + value = np.int64(atomic_value.int64) + elif atomic_type == schema_pb2.FLOAT: + value = np.float32(atomic_value.float) + elif atomic_type == schema_pb2.DOUBLE: + value = np.float64(atomic_value.double) + elif atomic_type == schema_pb2.STRING: + value = atomic_value.string + elif atomic_type == schema_pb2.BOOLEAN: + value = atomic_value.boolean + elif atomic_type == schema_pb2.BYTES: + value = atomic_value.bytes + else: + raise ValueError( + f"Unrecognized atomic_type ({atomic_type}) " + f"when decoding value {atomic_value!r}") + + return value + + def atomic_value_to_runner_api( + self, atomic_type: schema_pb2.AtomicType, + value) -> schema_pb2.AtomicTypeValue: + if atomic_type == schema_pb2.BYTE: + atomic_value = schema_pb2.AtomicTypeValue(byte=value) + elif atomic_type == schema_pb2.INT16: + atomic_value = schema_pb2.AtomicTypeValue(int16=value) + elif atomic_type == schema_pb2.INT32: + atomic_value = schema_pb2.AtomicTypeValue(int32=value) + elif atomic_type == schema_pb2.INT64: + atomic_value = schema_pb2.AtomicTypeValue(int64=value) + elif atomic_type == schema_pb2.FLOAT: + atomic_value = schema_pb2.AtomicTypeValue(float=value) + elif atomic_type == schema_pb2.DOUBLE: + atomic_value = schema_pb2.AtomicTypeValue(double=value) + elif atomic_type == schema_pb2.STRING: + atomic_value = schema_pb2.AtomicTypeValue(string=value) + elif atomic_type == schema_pb2.BOOLEAN: + atomic_value = schema_pb2.AtomicTypeValue(boolean=value) + elif atomic_type == schema_pb2.BYTES: + atomic_value = schema_pb2.AtomicTypeValue(bytes=value) + else: + raise ValueError( + "Unrecognized atomic_type {atomic_type} when encoding value {value}") - def option_from_runner_api( - self, option_proto: schema_pb2.Option) -> Tuple[str, Any]: - if not option_proto.HasField('type'): - return option_proto.name, None + return atomic_value - fieldtype_proto = option_proto.type - if fieldtype_proto.WhichOneof("type_info") != "atomic_type": + def value_from_runner_api( + self, + type_proto: schema_pb2.FieldType, + value_proto: schema_pb2.FieldValue): + if type_proto.WhichOneof("type_info") != "atomic_type": + # TODO: Allow other value types raise ValueError( "Encounterd option with unsupported type. Only " - f"atomic_type options are supported: {option_proto}") - - if fieldtype_proto.atomic_type == schema_pb2.BYTE: - value = np.int8(option_proto.value.atomic_value.byte) - elif fieldtype_proto.atomic_type == schema_pb2.INT16: - value = np.int16(option_proto.value.atomic_value.int16) - elif fieldtype_proto.atomic_type == schema_pb2.INT32: - value = np.int32(option_proto.value.atomic_value.int32) - elif fieldtype_proto.atomic_type == schema_pb2.INT64: - value = np.int64(option_proto.value.atomic_value.int64) - elif fieldtype_proto.atomic_type == schema_pb2.FLOAT: - value = np.float32(option_proto.value.atomic_value.float) - elif fieldtype_proto.atomic_type == schema_pb2.DOUBLE: - value = np.float64(option_proto.value.atomic_value.double) - elif fieldtype_proto.atomic_type == schema_pb2.STRING: - value = option_proto.value.atomic_value.string - elif fieldtype_proto.atomic_type == schema_pb2.BOOLEAN: - value = option_proto.value.atomic_value.boolean - elif fieldtype_proto.atomic_type == schema_pb2.BYTES: - value = option_proto.value.atomic_value.bytes - else: + f"atomic_type options are supported: {type_proto}") + + value = self.atomic_value_from_runner_api( + type_proto.atomic_type, value_proto.atomic_value) + return value + + def value_to_runner_api(self, typing_proto: schema_pb2.FieldType, value): + if typing_proto.WhichOneof("type_info") != "atomic_type": + # TODO: Allow other value types raise ValueError( - f"Unrecognized atomic_type ({fieldtype_proto.atomic_type}) " - f"when decoding option {option_proto!r}") + "Only atomic_type option values are currently supported in Python. " + f"Got {value!r}, which maps to fieldtype {typing_proto!r}.") + + atomic_value = self.atomic_value_to_runner_api( + typing_proto.atomic_type, value) + value_proto = schema_pb2.FieldValue(atomic_value=atomic_value) + return value_proto + + def option_from_runner_api( + self, option_proto: schema_pb2.Option) -> Tuple[str, Any]: + if not option_proto.HasField('type'): + return option_proto.name, None + value = self.value_from_runner_api(option_proto.type, option_proto.value) return option_proto.name, value def option_to_runner_api(self, option: Tuple[str, Any]) -> schema_pb2.Option: @@ -332,41 +419,9 @@ def option_to_runner_api(self, option: Tuple[str, Any]) -> schema_pb2.Option: # Don't set type, value return schema_pb2.Option(name=name) - fieldtype_proto = self.typing_to_runner_api(type(value)) - if fieldtype_proto.WhichOneof("type_info") != "atomic_type": - # TODO: Allow other value types - raise ValueError( - "Only atomic_type option values are currently supported in Python. " - f"Got {value!r}, which maps to fieldtype {fieldtype_proto!r}.") - - if fieldtype_proto.atomic_type == schema_pb2.BYTE: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(byte=value) - elif fieldtype_proto.atomic_type == schema_pb2.INT16: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(int16=value) - elif fieldtype_proto.atomic_type == schema_pb2.INT32: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(int32=value) - elif fieldtype_proto.atomic_type == schema_pb2.INT64: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(int64=value) - elif fieldtype_proto.atomic_type == schema_pb2.FLOAT: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(float=value) - elif fieldtype_proto.atomic_type == schema_pb2.DOUBLE: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(double=value) - elif fieldtype_proto.atomic_type == schema_pb2.STRING: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(string=value) - elif fieldtype_proto.atomic_type == schema_pb2.BOOLEAN: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(boolean=value) - elif fieldtype_proto.atomic_type == schema_pb2.BYTES: - atomictypevalue_proto = schema_pb2.AtomicTypeValue(bytes=value) - else: - raise ValueError( - "Unrecognized atomic_type in fieldtype_proto=" - f"{fieldtype_proto!r} when encoding option {option!r}") - - return schema_pb2.Option( - name=name, - type=fieldtype_proto, - value=schema_pb2.FieldValue(atomic_value=atomictypevalue_proto), - ) + type_proto = self.typing_to_runner_api(type(value)) + value_proto = self.value_to_runner_api(type_proto, value) + return schema_pb2.Option(name=name, type=type_proto, value=value_proto) def typing_from_runner_api( self, fieldtype_proto: schema_pb2.FieldType) -> type: @@ -463,12 +518,10 @@ def named_tuple_from_schema(self, schema: schema_pb2.Schema) -> type: # Define a reduce function, otherwise these types can't be pickled # (See BEAM-9574) - def __reduce__(self): - return ( - _hydrate_namedtuple_instance, - (schema.SerializeToString(), tuple(self))) - - setattr(user_type, '__reduce__', __reduce__) + setattr( + user_type, + '__reduce__', + _named_tuple_reduce_method(schema.SerializeToString())) self.schema_registry.add(user_type, schema) coders.registry.register_coder(user_type, coders.RowCoder) @@ -476,6 +529,13 @@ def __reduce__(self): return user_type +def _named_tuple_reduce_method(serialized_schema): + def __reduce__(self): + return _hydrate_namedtuple_instance, (serialized_schema, tuple(self)) + + return __reduce__ + + def _hydrate_namedtuple_instance(encoded_schema, values): return named_tuple_from_schema( proto_utils.parse_Bytes(encoded_schema, schema_pb2.Schema))(*values) @@ -643,8 +703,24 @@ def from_runner_api(cls, logical_type_proto): if logical_type is None: raise ValueError( "No logical type registered for URN '%s'" % logical_type_proto.urn) - # TODO(bhulette): Use argument - return logical_type() + if not logical_type_proto.HasField( + "argument_type") or not logical_type_proto.HasField("argument"): + # logical type_proto without argument + return logical_type() + else: + try: + argument = value_from_runner_api( + logical_type_proto.argument_type, logical_type_proto.argument) + except ValueError: + # TODO(https://github.com/apache/beam/issues/23373): Complete support + # for logical types that require arguments beyond atomic type. + # For now, skip arguments. + _LOGGER.warning( + 'Logical type %s with argument is currently unsupported. ' + 'Argument values are omitted', + logical_type_proto.urn) + return logical_type() + return logical_type(argument) class NoArgumentLogicalType(LogicalType[LanguageT, RepresentationT, None]): @@ -666,6 +742,28 @@ def _from_typing(cls, typ): return cls() +class PassThroughLogicalType(LogicalType[LanguageT, LanguageT, ArgT]): + """A base class for LogicalTypes that use the same type as the underlying + representation type. + """ + def to_language_type(self, value): + return value + + @classmethod + def representation_type(cls): + # type: () -> type + return cls.language_type() + + def to_representation_type(self, value): + return value + + @classmethod + def _from_typing(cls, typ): + # type: (type) -> LogicalType + # TODO(https://github.com/apache/beam/issues/23373): enable argument + return cls() + + MicrosInstantRepresentation = NamedTuple( 'MicrosInstantRepresentation', [('seconds', np.int64), ('micros', np.int64)]) @@ -851,3 +949,129 @@ def _from_typing(cls, typ): # TODO(yathu,BEAM-10722): Investigate and resolve conflicts in logical type # registration when more than one logical types sharing the same language type LogicalType.register_logical_type(DecimalLogicalType) + + +@LogicalType.register_logical_type +class FixedBytes(PassThroughLogicalType[bytes, np.int32]): + """A logical type for fixed-length bytes.""" + @classmethod + def urn(cls): + return common_urns.fixed_bytes.urn + + def __init__(self, length: np.int32): + self.length = length + + @classmethod + def language_type(cls) -> type: + return bytes + + def to_language_type(self, value: bytes): + length = len(value) + if length > self.length: + raise ValueError( + "value length {} > allowed length {}".format(length, self.length)) + elif length < self.length: + # padding at the end + value = value + b'\0' * (self.length - length) + + return value + + @classmethod + def argument_type(cls): + return np.int32 + + def argument(self): + return self.length + + +@LogicalType.register_logical_type +class VariableBytes(PassThroughLogicalType[bytes, np.int32]): + """A logical type for variable-length bytes with specified maximum length.""" + @classmethod + def urn(cls): + return common_urns.var_bytes.urn + + def __init__(self, max_length: np.int32 = np.iinfo(np.int32).max): + self.max_length = max_length + + @classmethod + def language_type(cls) -> type: + return bytes + + def to_language_type(self, value: bytes): + length = len(value) + if length > self.max_length: + raise ValueError( + "value length {} > allowed length {}".format(length, self.max_length)) + + return value + + @classmethod + def argument_type(cls): + return np.int32 + + def argument(self): + return self.max_length + + +@LogicalType.register_logical_type +class FixedString(PassThroughLogicalType[str, np.int32]): + """A logical type for fixed-length string.""" + @classmethod + def urn(cls): + return common_urns.fixed_char.urn + + def __init__(self, length: np.int32): + self.length = length + + @classmethod + def language_type(cls) -> type: + return str + + def to_language_type(self, value: str): + length = len(value) + if length > self.length: + raise ValueError( + "value length {} > allowed length {}".format(length, self.length)) + elif length < self.length: + # padding at the end + value = value + ' ' * (self.length - length) + + return value + + @classmethod + def argument_type(cls): + return np.int32 + + def argument(self): + return self.length + + +@LogicalType.register_logical_type +class VariableString(PassThroughLogicalType[str, np.int32]): + """A logical type for variable-length string with specified maximum length.""" + @classmethod + def urn(cls): + return common_urns.var_char.urn + + def __init__(self, max_length: np.int32 = np.iinfo(np.int32).max): + self.max_length = max_length + + @classmethod + def language_type(cls) -> type: + return str + + def to_language_type(self, value: str): + length = len(value) + if length > self.max_length: + raise ValueError( + "value length {} > allowed length {}".format(length, self.max_length)) + + return value + + @classmethod + def argument_type(cls): + return np.int32 + + def argument(self): + return self.max_length diff --git a/sdks/python/apache_beam/typehints/schemas_test.py b/sdks/python/apache_beam/typehints/schemas_test.py index 5d9434345cbc..de2ed829ab36 100644 --- a/sdks/python/apache_beam/typehints/schemas_test.py +++ b/sdks/python/apache_beam/typehints/schemas_test.py @@ -266,6 +266,39 @@ def get_test_beam_fieldtype_protos(): string='str'))), ]) for i, typ in enumerate(all_primitives) + ] + [ + schema_pb2.Field( + name='nested', + type=schema_pb2.FieldType( + row_type=schema_pb2.RowType( + schema=schema_pb2.Schema( + fields=[ + schema_pb2.Field( + name='nested_field', + type=schema_pb2.FieldType( + atomic_type=schema_pb2.INT64, + ), + options=[ + schema_pb2.Option( + name='a_nested_field_flag' + ), + ]), + ], + options=[ + schema_pb2.Option( + name='a_nested_schema_flag'), + schema_pb2.Option( + name='a_str', + type=schema_pb2.FieldType( + atomic_type=schema_pb2.STRING + ), + value=schema_pb2.FieldValue( + atomic_value=schema_pb2. + AtomicTypeValue( + string='str'))), + ], + ))), + ), ]))), schema_pb2.FieldType( row_type=schema_pb2.RowType( @@ -303,7 +336,7 @@ def get_test_beam_fieldtype_protos(): atomic_type=schema_pb2 .DOUBLE)))), ])))) - ]))) + ]))), ] return all_primitives + \ @@ -633,8 +666,10 @@ def test_generated_class_pickle_instance(self): self.assertEqual(instance, self.pickler.loads(self.pickler.dumps(instance))) - @unittest.skip("https://github.com/apache/beam/issues/22714") def test_generated_class_pickle(self): + if self.pickler in [pickle, dill]: + self.skipTest('https://github.com/apache/beam/issues/22714') + schema = schema_pb2.Schema( id="some-uuid", fields=[ diff --git a/sdks/python/container/py310/base_image_requirements.txt b/sdks/python/container/py310/base_image_requirements.txt index aeb8d0d990ba..651a332f909f 100644 --- a/sdks/python/container/py310/base_image_requirements.txt +++ b/sdks/python/container/py310/base_image_requirements.txt @@ -66,7 +66,7 @@ google-cloud-pubsublite==1.5.0 google-cloud-recommendations-ai==0.7.1 google-cloud-spanner==3.22.2 google-cloud-videointelligence==1.16.3 -google-cloud-vision==1.0.2 +google-cloud-vision==3.1.4 google-crc32c==1.5.0 google-pasta==0.2.0 google-resumable-media==2.4.0 @@ -102,7 +102,7 @@ overrides==6.5.0 packaging==21.3 pandas==1.4.4 parameterized==0.8.1 -pbr==5.10.0 +pbr==5.11.0 pluggy==1.0.0 proto-plus==1.22.1 protobuf==3.19.6 @@ -131,7 +131,7 @@ requests-mock==1.10.0 requests-oauthlib==1.3.1 rsa==4.9 scikit-learn==1.1.2 -scipy==1.9.2 +scipy==1.9.3 six==1.16.0 sortedcontainers==2.4.0 soupsieve==2.3.2.post1 diff --git a/sdks/python/container/py37/base_image_requirements.txt b/sdks/python/container/py37/base_image_requirements.txt index 53041ca821f0..2dbf7d1827db 100644 --- a/sdks/python/container/py37/base_image_requirements.txt +++ b/sdks/python/container/py37/base_image_requirements.txt @@ -70,7 +70,7 @@ google-cloud-recommendations-ai==0.7.1 google-cloud-spanner==3.22.2 google-cloud-storage==2.5.0 google-cloud-videointelligence==1.16.3 -google-cloud-vision==1.0.2 +google-cloud-vision==3.1.4 google-crc32c==1.5.0 google-pasta==0.2.0 google-python-cloud-debugger==3.1 @@ -109,7 +109,7 @@ overrides==6.5.0 packaging==21.3 pandas==1.3.5 parameterized==0.8.1 -pbr==5.10.0 +pbr==5.11.0 pluggy==1.0.0 proto-plus==1.22.1 protobuf==3.19.6 diff --git a/sdks/python/container/py38/base_image_requirements.txt b/sdks/python/container/py38/base_image_requirements.txt index e5f7fec58485..ca45ef30f856 100644 --- a/sdks/python/container/py38/base_image_requirements.txt +++ b/sdks/python/container/py38/base_image_requirements.txt @@ -70,7 +70,7 @@ google-cloud-recommendations-ai==0.7.1 google-cloud-spanner==3.22.2 google-cloud-storage==2.5.0 google-cloud-videointelligence==1.16.3 -google-cloud-vision==1.0.2 +google-cloud-vision==3.1.4 google-crc32c==1.5.0 google-pasta==0.2.0 google-python-cloud-debugger==3.1 @@ -109,7 +109,7 @@ overrides==6.5.0 packaging==21.3 pandas==1.4.4 parameterized==0.8.1 -pbr==5.10.0 +pbr==5.11.0 pluggy==1.0.0 proto-plus==1.22.1 protobuf==3.19.6 @@ -138,7 +138,7 @@ requests-mock==1.10.0 requests-oauthlib==1.3.1 rsa==4.9 scikit-learn==1.1.2 -scipy==1.9.2 +scipy==1.9.3 six==1.16.0 sortedcontainers==2.4.0 soupsieve==2.3.2.post1 diff --git a/sdks/python/container/py39/base_image_requirements.txt b/sdks/python/container/py39/base_image_requirements.txt index bc624e7df12c..f4b9cdca16a5 100644 --- a/sdks/python/container/py39/base_image_requirements.txt +++ b/sdks/python/container/py39/base_image_requirements.txt @@ -70,7 +70,7 @@ google-cloud-recommendations-ai==0.7.1 google-cloud-spanner==3.22.2 google-cloud-storage==2.5.0 google-cloud-videointelligence==1.16.3 -google-cloud-vision==1.0.2 +google-cloud-vision==3.1.4 google-crc32c==1.5.0 google-pasta==0.2.0 google-python-cloud-debugger==3.1 @@ -109,7 +109,7 @@ overrides==6.5.0 packaging==21.3 pandas==1.4.4 parameterized==0.8.1 -pbr==5.10.0 +pbr==5.11.0 pluggy==1.0.0 proto-plus==1.22.1 protobuf==3.19.6 @@ -138,7 +138,7 @@ requests-mock==1.10.0 requests-oauthlib==1.3.1 rsa==4.9 scikit-learn==1.1.2 -scipy==1.9.2 +scipy==1.9.3 six==1.16.0 sortedcontainers==2.4.0 soupsieve==2.3.2.post1 diff --git a/sdks/python/mypy.ini b/sdks/python/mypy.ini index 9309120a8cab..a628036d6682 100644 --- a/sdks/python/mypy.ini +++ b/sdks/python/mypy.ini @@ -89,6 +89,9 @@ ignore_errors = true [mypy-apache_beam.runners.direct.*] ignore_errors = true +[mypy-apache_beam.runners.dask.*] +ignore_errors = true + [mypy-apache_beam.runners.interactive.*] ignore_errors = true diff --git a/sdks/python/setup.py b/sdks/python/setup.py index c91fb2e71a85..61858fa5d978 100644 --- a/sdks/python/setup.py +++ b/sdks/python/setup.py @@ -314,7 +314,7 @@ def get_portability_package_data(): 'google-cloud-dlp>=3.0.0,<4', 'google-cloud-language>=1.3.0,<2', 'google-cloud-videointelligence>=1.8.0,<2', - 'google-cloud-vision>=0.38.0,<2', + 'google-cloud-vision>=2,<4', 'google-cloud-recommendations-ai>=0.1.0,<0.8.0' ], 'interactive': [ @@ -324,7 +324,7 @@ def get_portability_package_data(): 'ipython>=7,<8;python_version<="3.7"', 'ipython>=8,<9;python_version>"3.7"', 'ipykernel>=6,<7', - 'ipywidgets>=7.6.5,<8', + 'ipywidgets>=8,<9', # Skip version 6.1.13 due to # https://github.com/jupyter/jupyter_client/issues/637 'jupyter-client>=6.1.11,<6.1.13', @@ -350,7 +350,11 @@ def get_portability_package_data(): # This can be removed once dill is updated to version > 0.3.5.1 # Issue: https://github.com/apache/beam/issues/23566 'dataframe': ['pandas>=1.0,<1.5;python_version<"3.10"', - 'pandas>=1.4.3,<1.5;python_version>="3.10"'] + 'pandas>=1.4.3,<1.5;python_version>="3.10"'], + 'dask': [ + 'dask >= 2022.6', + 'distributed >= 2022.6', + ], }, zip_safe=False, # PyPI package information. diff --git a/sdks/python/test-suites/portable/common.gradle b/sdks/python/test-suites/portable/common.gradle index 79770e893256..0eae96c8bec9 100644 --- a/sdks/python/test-suites/portable/common.gradle +++ b/sdks/python/test-suites/portable/common.gradle @@ -172,15 +172,15 @@ task samzaValidatesRunner() { def createSparkRunnerTestTask(String workerType) { def taskName = "sparkCompatibilityMatrix${workerType}" - // `project(':runners:spark:2:job-server').shadowJar.archivePath` is not resolvable until runtime, so hard-code it here. - def jobServerJar = "${rootDir}/runners/spark/2/job-server/build/libs/beam-runners-spark-job-server-${version}.jar" + // `project(':runners:spark:3:job-server').shadowJar.archivePath` is not resolvable until runtime, so hard-code it here. + def jobServerJar = "${rootDir}/runners/spark/3/job-server/build/libs/beam-runners-spark-3-job-server-${version}.jar" def options = "--spark_job_server_jar=${jobServerJar} --environment_type=${workerType}" if (workerType == 'PROCESS') { options += " --environment_options=process_command=${buildDir.absolutePath}/sdk_worker.sh" } def task = toxTask(taskName, 'spark-runner-test', options) task.configure { - dependsOn ':runners:spark:2:job-server:shadowJar' + dependsOn ':runners:spark:3:job-server:shadowJar' if (workerType == 'DOCKER') { dependsOn pythonContainerTask } else if (workerType == 'PROCESS') { @@ -208,7 +208,7 @@ project.tasks.register("preCommitPy${pythonVersionSuffix}") { project.tasks.register("postCommitPy${pythonVersionSuffix}") { dependsOn = ['setupVirtualenv', "postCommitPy${pythonVersionSuffix}IT", - ':runners:spark:2:job-server:shadowJar', + ':runners:spark:3:job-server:shadowJar', 'portableLocalRunnerJuliaSetWithSetupPy', 'portableWordCountSparkRunnerBatch', 'portableLocalRunnerTestWithRequirementsFile'] @@ -248,13 +248,13 @@ project.tasks.register("sparkExamples") { dependsOn = [ 'setupVirtualenv', 'installGcpTest', - ':runners:spark:2:job-server:shadowJar' + ':runners:spark:3:job-server:shadowJar' ] doLast { def testOpts = [ "--log-cli-level=INFO", ] - def jobServerJar = "${rootDir}/runners/spark/2/job-server/build/libs/beam-runners-spark-job-server-${version}.jar" + def jobServerJar = "${rootDir}/runners/spark/3/job-server/build/libs/beam-runners-spark-3-job-server-${version}.jar" def pipelineOpts = [ "--runner=SparkRunner", "--project=apache-beam-testing", @@ -350,6 +350,13 @@ project.tasks.register("xlangSpannerIOIT") { "--environment_type=LOOPBACK", "--temp_location=gs://temp-storage-for-end-to-end-tests/temp-it", "--flink_job_server_jar=${project(":runners:flink:${latestFlinkVersion}:job-server").shadowJar.archivePath}", + '--sdk_harness_log_level_overrides=' + + // suppress info level flink.runtime log flood + '{\\"org.apache.flink.runtime\\":\\"WARN\\",' + + // suppress full __metricscontainers log printed in FlinkPipelineRunner.createPortablePipelineResult + '\\"org.apache.beam.runners.flink.FlinkPipelineRunner\\":\\"WARN\\",' + + // suppress metric name collision warning logs + '\\"org.apache.flink.runtime.metrics.groups\\":\\"ERROR\\"}' ] def cmdArgs = mapToArgString([ "test_opts": testOpts, @@ -388,7 +395,7 @@ def addTestJavaJarCreator(String runner, Task jobServerJarTask) { // TODO(BEAM-11333) Update and test multiple Flink versions. addTestJavaJarCreator("FlinkRunner", tasks.getByPath(":runners:flink:${latestFlinkVersion}:job-server:shadowJar")) -addTestJavaJarCreator("SparkRunner", tasks.getByPath(":runners:spark:2:job-server:shadowJar")) +addTestJavaJarCreator("SparkRunner", tasks.getByPath(":runners:spark:3:job-server:shadowJar")) def addTestFlinkUberJar(boolean saveMainSession) { project.tasks.register("testUberJarFlinkRunner${saveMainSession ? 'SaveMainSession' : ''}") { diff --git a/sdks/python/test-suites/tox/common.gradle b/sdks/python/test-suites/tox/common.gradle index 99afc1d72557..61802ac9c45e 100644 --- a/sdks/python/test-suites/tox/common.gradle +++ b/sdks/python/test-suites/tox/common.gradle @@ -24,6 +24,9 @@ test.dependsOn "testPython${pythonVersionSuffix}" toxTask "testPy${pythonVersionSuffix}Cloud", "py${pythonVersionSuffix}-cloud" test.dependsOn "testPy${pythonVersionSuffix}Cloud" +toxTask "testPy${pythonVersionSuffix}Dask", "py${pythonVersionSuffix}-dask" +test.dependsOn "testPy${pythonVersionSuffix}Dask" + toxTask "testPy${pythonVersionSuffix}Cython", "py${pythonVersionSuffix}-cython" test.dependsOn "testPy${pythonVersionSuffix}Cython" diff --git a/sdks/python/tox.ini b/sdks/python/tox.ini index 138a5410ead0..33ec39c41892 100644 --- a/sdks/python/tox.ini +++ b/sdks/python/tox.ini @@ -17,7 +17,7 @@ [tox] # new environments will be excluded by default unless explicitly added to envlist. -envlist = py37,py38,py39,py310,py37-{cloud,cython,lint,mypy},py38-{cloud,cython,docs,cloudcoverage},py39-{cloud,cython},py310-{cloud,cython},whitespacelint +envlist = py37,py38,py39,py310,py37-{cloud,cython,lint,mypy,dask},py38-{cloud,cython,docs,cloudcoverage,dask},py39-{cloud,cython},py310-{cloud,cython,dask},whitespacelint toxworkdir = {toxinidir}/target/{env:ENV_NAME:.tox} [pycodestyle] @@ -92,6 +92,10 @@ extras = test,gcp,interactive,dataframe,aws,azure commands = {toxinidir}/scripts/run_pytest.sh {envname} "{posargs}" +[testenv:py{37,38,39}-dask] +extras = test,dask +commands = + {toxinidir}/scripts/run_pytest.sh {envname} "{posargs}" [testenv:py38-cloudcoverage] deps = codecov @@ -129,6 +133,8 @@ commands = deps = -r build-requirements.txt mypy==0.782 + dask==2022.01.0 + distributed==2022.01.0 # make extras available in case any of these libs are typed extras = gcp @@ -136,8 +142,9 @@ commands = mypy --version python setup.py mypy + [testenv:py38-docs] -extras = test,gcp,docs,interactive,dataframe +extras = test,gcp,docs,interactive,dataframe,dask deps = Sphinx==1.8.5 sphinx_rtd_theme==0.4.3 diff --git a/website/ADD_CASE_STUDY.md b/website/ADD_CASE_STUDY.md new file mode 100644 index 000000000000..f5fd454cad8c --- /dev/null +++ b/website/ADD_CASE_STUDY.md @@ -0,0 +1,72 @@ + + +# How to add a new case study + +1. Fork [Apache Beam](https://github.com/apache/beam) repository +2. This [case study draft template](https://docs.google.com/document/d/1qRpXW-WM4jtlcy5VaqDaXgYap9KI1ii27Uwp641UOBM/edit#heading=h.l6lphj20eacs) provides some helpful tips, questions and ideas to prepare and organize your case study content +3. Copy [case study md template](https://github.com/apache/beam/tree/master/website/CASE_STUDY_TEMPLATE.md) to the `case-studies` folder and name your file with company or project name e.g., `beam/website/www/site/content/en/case-studies/YOUR_CASE_STUDY_NAME.md` +4. Add your case study content to the md file you just created. See [Case study md file recommendations](#case-study-md-file-recommendations) +5. Add images to the image folder [beam/website/www/site/static/images/case-study](https://github.com/apache/beam/tree/master/website/www/site/static/images/case-study)/company-name according to [Case study images recommendations](#case-study-images-recommendations) +6. Add case study quote card for the [Apache Beam](https://beam.apache.org/) website homepage `Case Studies Powered by Apache Beam` section. See [Add case study card to the Apache Beam website homepage](#Add-case-study-card-to-the-Apache-Beam-website-homepage) +7. Create pull request to the apache beam repository with your changes + + +## Case study md file recommendations + +Following properties determine how your case-study will looks on [Apache Beam case studies](https://beam.apache.org/case-studies/) listing and the case study page itself. + +| Field | Description | +|-------------------|---------------------------------------------------------------------------------------------------------| +| `title` | Case study title, usually 4-12 words | +| `name` | Company or project name | +| `icon` | Relative path to the company/project logo e.g. "/images/logos/powered-by/company_name.png" | +| `category` | `study` for case studies | +| `cardTitle` | Case study card title for Apache Beam [case studies](https://beam.apache.org/case-studies/) page | +| `cardDescription` | Description for [case studies](https://beam.apache.org/case-studies/) page, usually 30-40 words | +| `authorName` | Case study author | +| `authorPosition` | Case study author role | +| `authorImg` | Relative path for case study author photo, e.g. "/images/case-study/company/authorImg.png" | +| `publishDate` | Case study publish date for sorting at [case studies](https://beam.apache.org/case-studies/), e.g. `2022-10-14T01:56:00+00:00` | + +Other sections of the [case study md template](https://github.com/apache/beam/blob/master/website/CASE_STUDY_TEMPLATE.md) are organized to present the case study content. + +## Case study images recommendations + +1. Add case study company/project logo to the [images/logos/powered-by](https://github.com/apache/beam/tree/master/website/www/site/static/images/logos/powered-by) folder. Please use your company/project name e.g. `ricardo.png` +2. Create your company/project folder to group images used in your case study e.g., `beam/website/www/site/static/images/case-study/company-name` folder +3. Add author photo to `beam/website/www/site/static/images/case-study/company-name` folder +4. Add other images that your case study is using to `beam/website/www/site/static/images/case-study/company-name` folder + + +## Add case study card to the Apache Beam website homepage + +To add a new case study card to the Apache Beam website homepage, add the new case study entry to the [quotes.yaml](https://github.com/apache/beam/blob/master/website/www/site/data/en/quotes.yaml) using the following format: + +| Field | Description | +|-------------------|---------------------------------------------------------------------------------------------------------| +| `text` | Homepage case study text, recommended up to 215 characters or so | +| `icon` | Relative path to quotation marks logo, by default `icons/quote-icon.svg` | +| `logoUrl` | Relative path for company/project logo, e.g. `images/logos/powered-by/company_name.png` | +| `linkUrl` | Relative path to the case study web page, e.g., `case-studies/YOUR_CASE_STUDY_NAME/index.html` | +| `linkText` | Link text, by default using `Learn more` | + +Example: +``` + text: Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s. // recommendation to use no more than 215 symbols in the text + icon: icons/quote-icon.svg + logoUrl: images/logos/powered-by/company_name.png + linkUrl: case-studies/YOUR_CASE_STUDY_NAME/index.html + linkText: Learn more +``` diff --git a/website/ADD_LOGO.md b/website/ADD_LOGO.md index a478ab867641..b6beddf05bca 100644 --- a/website/ADD_LOGO.md +++ b/website/ADD_LOGO.md @@ -18,27 +18,31 @@ --> # How to add your logo - +Please follow these steps to add your company or project logo to Apache Beam [case studies](https://beam.apache.org/case-studies/) page: 1. Fork [Apache Beam](https://github.com/apache/beam) repository 2. Add file with company or project name to the [case-studies](https://github.com/apache/beam/tree/master/website/www/site/content/en/case-studies) folder e.g., `company.md` -3. Add project/company logo to +3. Add company/project logo to the [images/logos/powered-by](https://github.com/apache/beam/tree/master/website/www/site/static/images/logos/powered-by) folder. Please use your company/project name e.g. `ricardo.png` 4. Copy template below to the created file and replace next fields with your data -| Field | Name | -|-----------------|--------------------------------------------------| -| title | Project/Company name | -| icon | Path to the logo e.g. "/images/company_name.png" | -| cardDescription | Description of the project | +| Field | Description | +|-------------------|---------------------------------------------------------------------------------------------------------| +| `title` | Company/project name | +| `icon` | Path to the company/project logo e.g. "/images/logos/powered-by/company_name.png" | +| `hasNav` | Specified logo page has space for left & right nav menu | +| `hasLink` | Links logo image to the company/project website instead of displaying cardDescription, optional | +| `cardDescription` | Company or project description, optional | ``` --- title: "Cloud Dataflow" -icon: /images/company_name.png -cardDescription: "Project/Company description" +icon: /images/logos/powered-by/company_name.png +hasNav: true +hasLink: false +cardDescription: "Google Cloud Dataflow is a fully managed service for executing Apache Beam pipelines within the Google Cloud Platform ecosystem." --- ``` -5. Create pull request to the apache beam repository with your changes +5. Create pull request to the Apache Beam repository with your changes \ No newline at end of file diff --git a/website/CASE_STUDY_TEMPLATE.md b/website/CASE_STUDY_TEMPLATE.md new file mode 100644 index 000000000000..4c1faa1265df --- /dev/null +++ b/website/CASE_STUDY_TEMPLATE.md @@ -0,0 +1,97 @@ +--- +title: "Case study title" +name: "Company/project name" +icon: /images/logos/powered-by/company_name.png +hasNav: true +category: study +cardTitle: "Case study title (different for the Case Studies page listing)" +cardDescription: "Case study description for Case Studies page listing" +authorName: "Name LastName" +authorPosition: "Software Engineer @ companyName" +authorImg: /images/case-study/company/authorImg.png +publishDate: 2022-02-15T01:56:00+00:00 +--- + + +

    +
    + +
    +
    +

    + “Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.” +

    +
    +
    + +
    +
    +
    + Name LastName +
    +
    + Software Engineer @ companyName +
    +
    +
    +
    +
    + + +
    + +# Case Study Title + +## Background + +[Lorem Ipsum](https://www.lipsum.com/) is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. + +## Quote Section + +
    +

    + Lorem Ipsum is simply dummy text of the printing and typesetting industry +

    +
    +
    + +
    +
    +
    + Name LastName +
    +
    + Software Engineer @ companyName +
    +
    +
    +
    + +## Content Section + +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. + +
    + +
    + +## Results + +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. + + +{{< case_study_feedback Template >}} +
    +
    diff --git a/website/CONTRIBUTE.md b/website/CONTRIBUTE.md index 31a4550be237..b54139280df9 100644 --- a/website/CONTRIBUTE.md +++ b/website/CONTRIBUTE.md @@ -30,12 +30,15 @@ This guide consists of: - [Define TableOfContents](#define-tableofcontents) - [Language switching](#language-switching) - [Code highlighting](#code-highlighting) - - [Adding class to markdown text](#paragraph) + - [Adding class to markdown text](#adding-class-to-markdown-text) - [Table](#table) - [Github sample](#github-sample) - [Others](#others) + - [How to add relative links to JavaScript or CSS](#how-to-add-relative-links-to-javascript-or-css) - [What to be replaced in Jekyll](#what-to-be-replaced-in-jekyll) - [Translation guide](#translation-guide) +- [How to add new case study](#how-to-add-a-new-apache-beam-case-study) +- [How to add new logo](#how-to-add-a-new-logo-to-case-studies-page) ## Project structure @@ -45,10 +48,13 @@ www/ ├── site │   ├── archetypes # frontmatter template │   ├── assets -│ │ └── scss # styles +│ │ ├── icons # svg icons +│ │ ├── js # scripts +│ │ ├── scss # styles │   ├── content # pages │ │ └── en │ │ ├── blog +│ │ ├── case-studies │ │ ├── community │ │ ├── contribute │ │ ├── documentation @@ -63,7 +69,6 @@ www/ │ │ ├── downloads # downloaded files │ │ └── fonts │ │ └── images -│ │ └── js │   └── themes │ └── docsy ├── build_code_samples.sh @@ -162,7 +167,7 @@ $ hugo new about/_index.md $ hugo new -c content/pl about/_index.md ``` -## How to write in Hugo +## How to write in Hugo way This section will guide you how to use Hugo shortcodes in Apache Beam website. Please refer to the [Hugo documentation](https://gohugo.io/content-management/shortcodes/) for more details of usage. @@ -280,7 +285,7 @@ A table markdown here. {{< /table >}} ``` -### Code sample +### Github sample To retrieve a piece of code from Beam project. @@ -312,6 +317,15 @@ To get branch of the repository in markdown: To render capability matrix, please take a look at [this example](/www/site/content/en/documentation/runners/capability-matrix/#beam-capability-matrix). +### How to add relative links to JavaScript or CSS +Please take a note that relative links should be added with relative paths to JavaScript or CSS files with using Hugo syntax, so that they are able to form correct absolute links on localhost, staging and production. Examples: +``` +/themes/docsy/assets/js/search.js # var searchPage = "{{ "search/" | absURL }}?q=" + query; +/assets/js/page-nav.js # img.src = "{{ "images/arrow-expandable.svg" | absURL }}"; +/assets/js/copy-to-clipboard.js # +/assets/scss/_case_study.scss # background-image: url('{{ "images/open-quote.svg" | absURL }}'); +``` + ## What to be replaced in Jekyll This section will briefly let you know the replaced features of Jekyll in terms of writing a new blog post or documentation in Hugo. @@ -420,3 +434,9 @@ Now from your template: Similar to markdown content translation, there are two separated section menus `/www/site/layouts/partials/section-menu` corresponding to your languages. Your job is to take the section menus in `en` directory, translate and place them inside your `pl` directory. **Note**: if you get stuck at adding translation, please refer to [our example](https://github.com/PolideaInternal/beam/tree/example/i18n/). + +## How to add a new Apache Beam case study +Please follow this guide to [add a new case study](https://github.com/apache/beam/tree/master/website/ADD_CASE_STUDY.md) + +## How to add a new logo to case studies page +Please follow this guide to add [a new logo](https://github.com/apache/beam/tree/master/website/ADD_LOGO.md) to the [case studies](https://beam.apache.org/case-studies/) page. diff --git a/website/www/site/assets/js/shuffle-elements.js b/website/www/site/assets/js/shuffle-elements.js new file mode 100644 index 000000000000..2e5c4bc06dcd --- /dev/null +++ b/website/www/site/assets/js/shuffle-elements.js @@ -0,0 +1,25 @@ +// Licensed under the Apache License, Version 2.0 (the 'License'); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +$(document).ready(function() { + const logos = document.querySelector(".case-study-list--additional"); + const temp = logos.cloneNode(true); + let i = temp.children.length + 1; + + while( i-- > 0 ) { + temp.appendChild( temp.children[Math.random() * i |0] ); + } + + logos.parentNode.replaceChild(temp, logos); + + document.querySelector(".case-study-list--additional").style.visibility = "visible"; +}); diff --git a/website/www/site/assets/scss/_calendar.scss b/website/www/site/assets/scss/_calendar.scss index 8e27fccbd6ea..68d08872c961 100644 --- a/website/www/site/assets/scss/_calendar.scss +++ b/website/www/site/assets/scss/_calendar.scss @@ -94,6 +94,9 @@ padding: 24px 19.2px 24.7px 20px; margin-bottom: 24px; } + @media (max-width: $ak-breakpoint-xs) { + width: 260px; + } &:hover { text-decoration: none; box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.24), @@ -251,6 +254,9 @@ max-width: 327px; height: 356px; padding: 32px 20px; + @media (max-width: $ak-breakpoint-xs) { + max-width: 260px; + } .calendar-card-big-title { margin-top: 35px; diff --git a/website/www/site/assets/scss/_case_study.scss b/website/www/site/assets/scss/_case_study.scss index 00ed9fd6c933..cd9e809b1997 100644 --- a/website/www/site/assets/scss/_case_study.scss +++ b/website/www/site/assets/scss/_case_study.scss @@ -44,6 +44,7 @@ } .case-study-list--additional { + visibility: hidden; @media (min-width: $mobile) and (max-width: $tablet) { justify-content: center; } @@ -88,11 +89,19 @@ .case-study-used-by-card--responsive { @media (min-width: $mobile){ - width: 23%; + width: 18%; margin-right: 0; } } +.case-study-used-by-card--with-link { + &:hover { + .case-study-used-by-card-img { + display: block; + } + } +} + .case-study-card { padding: 16px; display: flex; diff --git a/website/www/site/assets/scss/_local.scss b/website/www/site/assets/scss/_local.scss index a297bc15bde8..f638f64a1185 100644 --- a/website/www/site/assets/scss/_local.scss +++ b/website/www/site/assets/scss/_local.scss @@ -15,8 +15,24 @@ * limitations under the License. */ +@import "media"; + .paragraph-wrap { a { word-break: break-word; } } + +.calendar-mobile--twitter { + @media (max-width: $ak-breakpoint-xs) { + iframe { + width: 260px !important; + } + } +} + +.calendar-mobile--events { + @media (max-width: $ak-breakpoint-xs) { + overflow-x: auto; + } +} diff --git a/website/www/site/content/en/case-studies/Amazon.md b/website/www/site/content/en/case-studies/Amazon.md new file mode 100644 index 000000000000..2fba8b727aa4 --- /dev/null +++ b/website/www/site/content/en/case-studies/Amazon.md @@ -0,0 +1,18 @@ +--- +title: "Amazon" +icon: /images/logos/powered-by/Amazon.png +--- + + diff --git a/website/www/site/content/en/case-studies/ML6.md b/website/www/site/content/en/case-studies/ML6.md new file mode 100644 index 000000000000..6f41ce5405ce --- /dev/null +++ b/website/www/site/content/en/case-studies/ML6.md @@ -0,0 +1,18 @@ +--- +title: "ML6" +icon: /images/logos/powered-by/ML6.jpg +--- + + diff --git a/website/www/site/content/en/case-studies/Strivr.md b/website/www/site/content/en/case-studies/Strivr.md new file mode 100644 index 000000000000..4443d771f5b2 --- /dev/null +++ b/website/www/site/content/en/case-studies/Strivr.md @@ -0,0 +1,17 @@ +--- +title: "Strivr" +icon: /images/logos/powered-by/Strivr.png +--- + diff --git a/website/www/site/content/en/case-studies/TrustPilot.md b/website/www/site/content/en/case-studies/TrustPilot.md new file mode 100644 index 000000000000..09053e72f208 --- /dev/null +++ b/website/www/site/content/en/case-studies/TrustPilot.md @@ -0,0 +1,17 @@ +--- +title: "TrustPilot" +icon: /images/logos/powered-by/Trustpilot.png +--- + diff --git a/website/www/site/content/en/case-studies/Twitter.md b/website/www/site/content/en/case-studies/Twitter.md new file mode 100644 index 000000000000..16ce225c4e74 --- /dev/null +++ b/website/www/site/content/en/case-studies/Twitter.md @@ -0,0 +1,17 @@ +--- +title: "Twitter" +icon: /images/logos/powered-by/Twitter.png +--- + diff --git a/website/www/site/content/en/case-studies/Wayfair.md b/website/www/site/content/en/case-studies/Wayfair.md new file mode 100644 index 000000000000..88210a020508 --- /dev/null +++ b/website/www/site/content/en/case-studies/Wayfair.md @@ -0,0 +1,17 @@ +--- +title: "Wayfair" +icon: /images/logos/powered-by/Wayfair.png +--- + diff --git a/website/www/site/content/en/case-studies/Wizeline.md b/website/www/site/content/en/case-studies/Wizeline.md new file mode 100644 index 000000000000..e971353f667f --- /dev/null +++ b/website/www/site/content/en/case-studies/Wizeline.md @@ -0,0 +1,17 @@ +--- +title: "Wizeline" +icon: /images/logos/powered-by/Wizeline.png +--- + diff --git a/website/www/site/content/en/case-studies/akvelon.md b/website/www/site/content/en/case-studies/akvelon.md index ad95073fb772..b119bafc770b 100644 --- a/website/www/site/content/en/case-studies/akvelon.md +++ b/website/www/site/content/en/case-studies/akvelon.md @@ -2,7 +2,7 @@ title: "Akvelon" icon: /images/logos/powered-by/akvelon.png hasNav: true -cardDescription: "Akvelon is a software engineering company that helps start-ups, SMBs, and Fortune 500 companies unlock the full potential of cloud, data, and AI/ML to empower their strategic advantage. Akvelon team has deep expertise in integrating Apache Beam with diverse data processing ecosystems and is an enthusiastic Apache Beam community contributor." +cardDescription: "

    Akvelon is a software engineering company that helps start-ups, SMBs, and Fortune 500 companies unlock the full potential of cloud, data, and AI/ML to empower their strategic advantage. Akvelon team has deep expertise in integrating Apache Beam with diverse data processing ecosystems and is an enthusiastic Apache Beam community contributor.

    " --- -Kio is a set of Kotlin extensions for Apache Beam to implement fluent-like API for Java SDK. +
    + +# Kio is a set of Kotlin extensions for Apache Beam to implement fluent-like API for Java SDK. ## Word Count example @@ -41,3 +43,5 @@ kio.execute().waitUntilDone() ## Documentation For more information about Kio, please see the documentation here: [https://code.chermenin.ru/kio](https://code.chermenin.ru/kio). +
    +
    diff --git a/website/www/site/content/en/contribute/release-guide.md b/website/www/site/content/en/contribute/release-guide.md index c5ea4442145b..5b48068d05a2 100644 --- a/website/www/site/content/en/contribute/release-guide.md +++ b/website/www/site/content/en/contribute/release-guide.md @@ -562,18 +562,18 @@ See the source of the script for more details, or to run commands manually in ca 1. Verify Docker images are published. How to find images: 1. Visit [https://hub.docker.com/u/apache](https://hub.docker.com/search?q=apache%2Fbeam&type=image) 2. Visit each repository and navigate to *tags* tab. - 3. Verify images are pushed with tags: ${RELEASE}_rc{RC_NUM} + 3. Verify images are pushed with tags: ${RELEASE_VERSION}_rc{RC_NUM} 1. Verify that third party licenses are included in Docker containers by logging in to the images. - For Python SDK images, there should be around 80 ~ 100 dependencies. Please note that dependencies for the SDKs with different Python versions vary. Need to verify all Python images by replacing `${ver}` with each supported Python version `X.Y`. ``` - docker run --rm -it --entrypoint=/bin/bash apache/beam_python${ver}_sdk:${RELEASE}_rc{RC_NUM} + docker run --rm -it --entrypoint=/bin/bash apache/beam_python${ver}_sdk:${RELEASE_VERSION}_rc{RC_NUM} ls -al /opt/apache/beam/third_party_licenses/ | wc -l ``` - For Java SDK images, there should be around 200 dependencies. ``` - docker run --rm -it --entrypoint=/bin/bash apache/beam_java${ver}_sdk:${RELEASE}_rc{RC_NUM} + docker run --rm -it --entrypoint=/bin/bash apache/beam_java${ver}_sdk:${RELEASE_VERSION}_rc{RC_NUM} ls -al /opt/apache/beam/third_party_licenses/ | wc -l ``` 1. Publish staging artifacts @@ -740,7 +740,7 @@ all major features and bug fixes, and all known issues. 1. Maven artifacts deployed to the staging repository of [repository.apache.org](https://repository.apache.org/content/repositories/) 1. Source distribution deployed to the dev repository of [dist.apache.org](https://dist.apache.org/repos/dist/dev/beam/) 1. Website pull request proposed to list the [release](https://beam.apache.org/get-started/downloads/), publish the [Java API reference manual](https://beam.apache.org/releases/javadoc/), and publish the [Python API reference manual](https://beam.apache.org/releases/pydoc/). -1. Docker images are published to [DockerHub](https://hub.docker.com/search?q=apache%2Fbeam&type=image) with tags: {RELEASE}_rc{RC_NUM}. +1. Docker images are published to [DockerHub](https://hub.docker.com/search?q=apache%2Fbeam&type=image) with tags: {RELEASE_VERSION}_rc{RC_NUM}. You can (optionally) also do additional verification by: 1. Check that Python zip file contains the `README.md`, `NOTICE`, and `LICENSE` files. @@ -787,8 +787,9 @@ Here’s an email template; please adjust as you see fit. * website pull request listing the release [6], the blog post [6], and publishing the API reference manual [7]. * Java artifacts were built with Gradle GRADLE_VERSION and OpenJDK/Oracle JDK JDK_VERSION. * Python artifacts are deployed along with the source release to the dist.apache.org [2] and PyPI[8]. - * Validation sheet with a tab for 1.2.3 release to help with validation [9]. - * Docker images published to Docker Hub [10]. + * Go artifacts and documentation are available at pkg.go.dev [9] + * Validation sheet with a tab for 1.2.3 release to help with validation [10]. + * Docker images published to Docker Hub [11]. The vote will be open for at least 72 hours. It is adopted by majority approval, with at least 3 PMC affirmative votes. @@ -805,8 +806,9 @@ Here’s an email template; please adjust as you see fit. [6] https://github.com/apache/beam/pull/... [7] https://github.com/apache/beam-site/pull/... [8] https://pypi.org/project/apache-beam/1.2.3rc3/ - [9] https://docs.google.com/spreadsheets/d/1qk-N5vjXvbcEk68GjbkSZTR8AGqyNUM-oLFo_ZXBpJw/edit#gid=... - [10] https://hub.docker.com/search?q=apache%2Fbeam&type=image + [9] https://pkg.go.dev/github.com/apache/beam/sdks/v2@v1.2.3-RC3/go/pkg/beam + [10] https://docs.google.com/spreadsheets/d/1qk-N5vjXvbcEk68GjbkSZTR8AGqyNUM-oLFo_ZXBpJw/edit#gid=... + [11] https://hub.docker.com/search?q=apache%2Fbeam&type=image If there are any issues found in the release candidate, reply on the vote thread to cancel the vote. There’s no need to wait 72 hours. @@ -877,7 +879,7 @@ _Note_: -Prepourl and -Pver can be found in the RC vote email sent by Release Ma ``` **Spark Local Runner** ``` - ./gradlew :runners:spark:2:runQuickstartJavaSpark \ + ./gradlew :runners:spark:3:runQuickstartJavaSpark \ -Prepourl=https://repository.apache.org/content/repositories/orgapachebeam-${KEY} \ -Pver=${RELEASE_VERSION} ``` @@ -1145,8 +1147,8 @@ All wheels should be published, in addition to the zip of the release source. ./beam/release/src/main/scripts/publish_docker_images.sh ``` * **Verify that:** - * Images are published at [DockerHub](https://hub.docker.com/search?q=apache%2Fbeam&type=image) with tags {RELEASE} and *latest*. - * Images with *latest* tag are pointing to current release by confirming the digest of the image with *latest* tag is the same as the one with {RELEASE} tag. + * Images are published at [DockerHub](https://hub.docker.com/search?q=apache%2Fbeam&type=image) with tags {RELEASE_VERSION} and *latest*. + * Images with *latest* tag are pointing to current release by confirming the digest of the image with *latest* tag is the same as the one with {RELEASE_VERSION} tag. (Optional) Clean up any unneeded local images afterward to save disk space. @@ -1165,7 +1167,7 @@ Create and push a new signed tag for the released version by copying the tag for # Optional: unlock the signing key by signing an arbitrary file. gpg --output ~/doc.sig --sign ~/.bashrc -VERSION_TAG="v${RELEASE}" +VERSION_TAG="v${RELEASE_VERSION}" # Tag for Go SDK git tag -s "sdks/$VERSION_TAG" "$RC_TAG" diff --git a/website/www/site/content/en/documentation/basics.md b/website/www/site/content/en/documentation/basics.md index e38b4c6b0dfd..548850246a9f 100644 --- a/website/www/site/content/en/documentation/basics.md +++ b/website/www/site/content/en/documentation/basics.md @@ -93,7 +93,7 @@ For more information about pipelines, see the following pages: * [Beam Programming Guide: Overview](/documentation/programming-guide/#overview) * [Beam Programming Guide: Creating a pipeline](/documentation/programming-guide/#creating-a-pipeline) * [Design your pipeline](/documentation/pipelines/design-your-pipeline) - * [Create your pipeline](/documentation/pipeline/create-your-pipeline) + * [Create your pipeline](/documentation/pipelines/create-your-pipeline) ### PCollection diff --git a/website/www/site/content/en/documentation/ml/data-processing.md b/website/www/site/content/en/documentation/ml/data-processing.md index 6304b6d4fe1b..70e72e1c983d 100755 --- a/website/www/site/content/en/documentation/ml/data-processing.md +++ b/website/www/site/content/en/documentation/ml/data-processing.md @@ -53,6 +53,8 @@ ib.collect(beam_df.describe()) ib.collect(beam_df.isnull()) ``` +For a full end-to-end example on how to implement data exploration and data preprocessing with Beam and the DataFrame API for your AI/ML project, you can follow the [Beam Dataframe API tutorial for AI/ML](https://github.com/apache/beam/tree/master/examples/notebooks/beam-ml/dataframe_api_preprocessing.ipynb). + ## Data pipeline for ML A typical data preprocessing pipeline consists of the following steps: 1. Reading and writing data: read/write the data from your filesystem, database or messaging queue. Beam has a rich set of [IO connectors](https://beam.apache.org/documentation/io/built-in/) for ingesting and writing data. diff --git a/website/www/site/content/en/documentation/ml/multi-model-pipelines.md b/website/www/site/content/en/documentation/ml/multi-model-pipelines.md index be614e4b5000..ad1f5ff80f46 100644 --- a/website/www/site/content/en/documentation/ml/multi-model-pipelines.md +++ b/website/www/site/content/en/documentation/ml/multi-model-pipelines.md @@ -90,7 +90,7 @@ with pipeline as p: ``` In -this [notebook](https://github.com/apache/beam/tree/master/examples/notebooks/beam-ml/run-inference-multi-model.ipynb) +this [notebook](https://github.com/apache/beam/tree/master/examples/notebooks/beam-ml/run_inference_multi_model.ipynb) , we show an end-to-end example of a cascade pipeline used for generating and ranking image captions. The solution consists of two open-source models: diff --git a/website/www/site/content/en/documentation/ml/orchestration.md b/website/www/site/content/en/documentation/ml/orchestration.md new file mode 100644 index 000000000000..e3f7b7169e40 --- /dev/null +++ b/website/www/site/content/en/documentation/ml/orchestration.md @@ -0,0 +1,223 @@ +--- +title: "Orchestration" +--- + + +# Workflow orchestration + +## Understanding the Beam DAG + + +Apache Beam is an open source, unified model for defining both batch and streaming data-parallel processing pipelines. One of the central concepts to the Beam programming model is the DAG (= Directed Acyclic Graph). Each Beam pipeline is a DAG that can be constructed through the Beam SDK in your programming language of choice (from the set of supported beam SDKs). Each node of this DAG represents a processing step (PTransform) that accepts a collection of data as input (PCollection) and outputs a transformed collection of data (PCollection). The edges define how data flows through the pipeline from one processing step to another. The image below shows an example of such a pipeline. + +![A standalone beam pipeline](/images/standalone-beam-pipeline.svg) + +Note that simply defining a pipeline and the corresponding DAG does not mean that data will start flowing through the pipeline. To actually execute the pipeline, it has to be deployed to one of the [supported Beam runners](https://beam.apache.org/documentation/runners/capability-matrix/). These distributed processing back-ends include Apache Flink, Apache Spark and Google Cloud Dataflow. A [Direct Runner](https://beam.apache.org/documentation/runners/direct/) is also provided to execute the pipeline locally on your machine for development and debugging purposes. Make sure to check out the [runner capability matrix](https://beam.apache.org/documentation/runners/capability-matrix/) to guarantee that the chosen runner supports the data processing steps defined in your pipeline, especially when using the Direct Runner. + +## Orchestrating frameworks + +Successfully delivering machine learning projects is about a lot more than training a model and calling it a day. A full ML workflow will often contain a range of other steps including data ingestion, data validation, data preprocessing, model evaluation, model deployment, data drift detection, etc. Furthermore, it’s essential to keep track of metadata and artifacts from your experiments to answer important questions like: +- What data was this model trained on and with which training parameters? +- When was this model deployed and what accuracy did it get on a test dataset? +Without this knowledge at your disposal, it will become increasingly difficult to troubleshoot, monitor and improve your ML solutions as they grow in size. + +The solution: MLOps. MLOps is an umbrella term used to describe best practices and guiding principles that aim to make the development and maintenance of machine learning systems seamless and efficient. Simply put, MLOps is most often about automating machine learning workflows throughout the model and data lifecycle. Popular frameworks to create these workflow DAGs are [Kubeflow Pipelines](https://www.kubeflow.org/docs/components/pipelines/introduction/), [Apache Airflow](https://airflow.apache.org/docs/apache-airflow/stable/index.html) and [TFX](https://www.tensorflow.org/tfx/guide). + +So what does all of this have to do with Beam? Well, since we established that Beam is a great tool for a range of ML tasks, a beam pipeline can either be used as a standalone data processing job or can be part of a larger sequence of steps in such a workflow. In the latter case, the beam DAG is just one node in the overarching DAG composed by the workflow orchestrator. This results in a DAG in a DAG, as illustrated by the example below. + +![An beam pipeline as part of a larger orchestrated workflow](/images/orchestrated-beam-pipeline.svg) + +It is important to understand the key difference between the Beam DAG and the orchestrating DAG. The Beam DAG processes data and passes that data between the nodes of its DAG. The focus of Beam is on parallelization and enabling both batch and streaming jobs. In contrast, the orchestration DAG schedules and monitors steps in the workflow and passed between the nodes of the DAG are execution parameters, metadata and artifacts. An example of such an artifact could be a trained model or a dataset. Such artifacts are often passed by a reference URI and not by value. + +Note: TFX creates a workflow DAG, which needs an orchestrator of its own to be executed. [Natively supported orchestrators for TFX](https://www.tensorflow.org/tfx/guide/custom_orchestrator) are Airflow, Kubeflow Pipelines and, here’s the kicker, Beam itself! As mentioned by the [TFX docs](https://www.tensorflow.org/tfx/guide/beam_orchestrator): + +> "Several TFX components rely on Beam for distributed data processing. In addition, TFX can use Apache Beam to orchestrate and execute the pipeline DAG. Beam orchestrator uses a different BeamRunner than the one which is used for component data processing." + +Caveat: The Beam orchestrator is not meant to be a TFX orchestrator to be used in production environments. It simply enables debugging TFX pipelines locally on Beam’s DirectRunner without the need for the extra setup that is needed for Airflow or Kubeflow. + +## Preprocessing example + +Let’s get practical and take a look at two such orchestrated ML workflows, one with Kubeflow Pipelines (KFP) and one with Tensorflow Extended (TFX). These two frameworks achieve the same goal of creating workflows, but have their own distinct advantages and disadvantages: KFP requires you to create your workflow components from scratch and requires a user to explicitly indicate which artifacts should be passed between components and in what way. In contrast, TFX offers a number of prebuilt components and takes care of the artifact passing more implicitly. Clearly, there is a trade-off to be considered between flexibility and programming overhead when choosing between the two frameworks. We will start by looking at an example with KFP and then transition to TFX to show TFX takes care of a lot of functionality that we had to define by hand in the KFP example. + +For simplicity, we will showcase workflows with only three components: data ingestion, data preprocessing and model training. Depending on the scenario, a range of extra components could be added such as model evaluation, model deployment, etc. We will focus our attention on the preprocessing component, since it showcases how to use Apache beam in an ML workflow for efficient and parallel processing of your ML data. + +The dataset we will use consists of image-caption pairs, i.e. images paired with a textual caption describing the content of the image. These pairs are taken from captions subset of the [MSCOCO 2014 dataset](https://cocodataset.org/#home). This multi-modal data (image + text) gives us the opportunity to experiment with preprocessing operations for both modalities. + +### Kubeflow pipelines (KFP) + +In order to execute our ML workflow with KFP we must perform three steps: + +1. Create the KFP components by specifying the interface to the components and by writing and containerizing the implementation of the component logic +2. Create the KFP pipeline by connecting the created components and specifying how inputs and outputs should be passed from between components and compiling the pipeline definition to a full pipeline definition. +3. Execute the KFP pipeline by submitting it to a KFP client endpoint. + +The full example code can be found [here](https://github.com/apache/beam/tree/master/sdks/python/apache_beam/examples/ml-orchestration/kfp) + +#### Create the KFP components + +This is our target file structure: + + kfp + ├── pipeline.py + ├── components + │ ├── ingestion + │ │ ├── Dockerfile + │ │ ├── component.yaml + │ │ ├── requirements.txt + │ │ └── src + │ │ └── ingest.py + │ ├── preprocessing + │ │ ├── Dockerfile + │ │ ├── component.yaml + │ │ ├── requirements.txt + │ │ └── src + │ │ └── preprocess.py + │ └── train + │ ├── Dockerfile + │ ├── component.yaml + │ ├── requirements.txt + │ └── src + │ └── train.py + └── requirements.txt + +Let’s start with the component specifications. The full preprocessing component specification is illustrated below. The inputs are the path where the ingested dataset was saved by the ingest component and a path to a directory where the component can store artifacts. Additionally, there are some inputs that specify how and where the Beam pipeline should run. The specifications for the ingestion and train component are similar and can be found [here](https://github.com/apache/beam/tree/master/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/ingestion/component.yaml) and [here](https://github.com/apache/beam/tree/master/sdks/python/apache_beam/examples/ml-orchestration/kfp/components/train/component.yaml), respectively. + +>Note: we are using the KFP v1 SDK, because v2 is still in [beta](https://www.kubeflow.org/docs/started/support/#application-status). The v2 SDK introduces some new options for specifying the component interface with more native support for input and output artifacts. To see how to migrate components from v1 to v2, consult the [KFP docs](https://www.kubeflow.org/docs/components/pipelines/sdk-v2/v2-component-io/). + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/component.yaml" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/component.yaml" preprocessing_component_definition >}} +{{< /highlight >}} + +In this case, each component shares an identical Dockerfile but extra component-specific dependencies could be added where necessary. + +{{< highlight language="Dockerfile" file="sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/Dockerfile" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/Dockerfile" component_dockerfile >}} +{{< /highlight >}} + +With the component specification and containerization out of the way we can look at the actual implementation of the preprocessing component. + +Since KFP provides the input and output arguments as command-line arguments, an `argumentparser` is needed. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/kf/components/preprocessing/src/preprocess.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/src/preprocess.py" preprocess_component_argparse >}} +{{< /highlight >}} + +The implementation of the `preprocess_dataset` function contains the Beam pipeline code and the Beam pipeline options to select the desired runner. The executed preprocessing involves downloading the image bytes from their url, converting them to a Torch Tensor and resizing to the desired size. The caption undergoes a series of string manipulations to ensure that our model receives clean uniform image descriptions (Tokenization is not yet done here, but could be included here as well if the vocabulary is known). Finally each element is serialized and written to [Avro](https://avro.apache.org/docs/1.2.0/) files (Alternative files formats could be used as well, e.g. TFRecords). + + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/src/preprocess.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/src/preprocess.py" deploy_preprocessing_beam_pipeline >}} +{{< /highlight >}} + +It also contains the necessary code to perform the component IO. First, a target path is constructed to store the preprocessed dataset based on the component input parameter `base_artifact_path` and a timestamp. Output values from components can only be returned as files so we write the value of the constructed target path to an output file that was provided by KFP to our component. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/src/preprocess.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/components/preprocessing/src/preprocess.py" kfp_component_input_output >}} +{{< /highlight >}} + +Since we are mainly interested in the preprocessing component to show how a Beam pipeline can be integrated into a larger ML workflow, we will not cover the implementation of the ingestion and train component in depth. Implementations of dummy components that mock their behavior are provided in the full example code. + +#### Create the pipeline definition + +`pipeline.py` first loads the created components from their specification `.yaml` file. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py" load_kfp_components >}} +{{< /highlight >}} + +After that, the pipeline is created and the required components inputs and outputs are specified manually. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py" define_kfp_pipeline >}} +{{< /highlight >}} + +Finally, the defined pipeline is compiled and a `pipeline.json` specification file is generated. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py" compile_kfp_pipeline >}} +{{< /highlight >}} + + +#### Execute the KFP pipeline + +Using the specification file and the snippet below with the necessary [requirements](https://github.com/apache/beam/tree/master/sdks/python/apache_beam/examples/ml-orchestration/kfp/requirements.txt) installed, the pipeline can now be executed. Consult the [docs](https://kubeflow-pipelines.readthedocs.io/en/latest/source/kfp.client.html#kfp.Client.run_pipeline) for more information. Note that, before executing the pipeline, a container for each component must be built and pushed to a container registry that can be accessed by your pipeline execution. Also make sure that the component specification `.yaml` files are updated to point to the correct container image. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/kfp/pipeline.py" execute_kfp_pipeline >}} +{{< /highlight >}} + + +### Tensorflow Extended (TFX) + +The way of working for TFX is similar to the approach for KFP as illustrated above: Define the individual workflow components, connect them in a pipeline object and run the pipeline in the target environment. However, what makes TFX different is that it has already built a set of Python packages that are libraries to create workflow components. So unlike the KFP example, we do not need to start from scratch by writing and containerizing our code. What is left for the users to do is pick which of those TFX components are relevant to their specific workflow and adapt their functionality to the specific use case using the library. The image below shows the available components and their corresponding libraries. The link with Apache Beam is that TFX relies heavily on it to implement data-parallel pipelines in these libraries. This means that components created with these libraries will need to be run on one of the support Beam runners. The full example code can again be found [here](https://github.com/apache/beam/tree/master/sdks/python/apache_beam/examples/ml-orchestration/tfx) + + +![TFX libraries and components](https://www.tensorflow.org/static/tfx/guide/images/libraries_components.png) + +We will work out a small example in a similar fashion as for KFP. There we used ingestion, preprocessing and trainer components. Translating this to TFX, we will need the ExampleGen, Transform and Trainer libraries. + +This time we will start by looking at the pipeline definition. Note that this looks very similar to our previous example. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_local.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_local.py" tfx_pipeline >}} +{{< /highlight >}} + +We will use the same data input as last time, i.e. a couple of image-captions pairs extracted from the [MSCOCO 2014 dataset](https://cocodataset.org/#home). This time, however, in CSV format because the ExampleGen component does not by default have support for jsonlines. (The formats that are supported out of the box are listed [here](https://www.tensorflow.org/tfx/guide/examplegen#data_sources_and_formats). Alternatively, it’s possible to write a [custom ExampleGen](https://www.tensorflow.org/tfx/guide/examplegen#custom_examplegen) as well.) + +Copy the snippet below to an input data csv file: + +{{< highlight >}} +image_id,id,caption,image_url,image_name,image_license +318556,255,"An angled view of a beautifully decorated bathroom.","http://farm4.staticflickr.com/3133/3378902101_3c9fa16b84_z.jpg","COCO_train2014_000000318556.jpg","Attribution-NonCommercial-ShareAlike License" +476220,14,"An empty kitchen with white and black appliances.","http://farm7.staticflickr.com/6173/6207941582_b69380c020_z.jpg","COCO_train2014_000000476220.jpg","Attribution-NonCommercial License" +{{< /highlight >}} + +So far, we have only imported standard TFX components and chained them together into a pipeline. Both the Transform and Trainer components have a `module_file` argument defined. That’s where we define the behavior we want from these standard components. + +#### Preprocess + +The Transform component searches the `module_file` for a definition of the function `preprocessing_fn`. This function is the central concept of the `tf.transform` library. As per the [TFX docs](https://www.tensorflow.org/tfx/transform/get_started#define_a_preprocessing_function): + +> The preprocessing function is the most important concept of tf.Transform. The preprocessing function is a logical description of a transformation of the dataset. The preprocessing function accepts and returns a dictionary of tensors, where a tensor means Tensor or SparseTensor. There are two kinds of functions used to define the preprocessing function: +>1. Any function that accepts and returns tensors. These add TensorFlow operations to the graph that transform raw data into transformed data. +>2. Any of the analyzers provided by tf.Transform. Analyzers also accept and return tensors, but unlike TensorFlow functions, they do not add operations to the graph. Instead, analyzers cause tf.Transform to compute a full-pass operation outside of TensorFlow. They use the input tensor values over the entire dataset to generate a constant tensor that is returned as the output. For example, tft.min computes the minimum of a tensor over the dataset. tf.Transform provides a fixed set of analyzers, but this will be extended in future versions. + +So our `preprocesing_fn` can contain all tf operations that accept and return tensors and also specific `tf.transform` operations. In our simple example below we use the former to convert all incoming captions to lowercase letters only, while the latter does a full pass on all the data in our dataset to compute the average length of the captions to be used for a follow-up preprocessing step. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py" tfx_preprocessing_fn >}} +{{< /highlight >}} + +However this function only defines the logical steps that have to be performed during preprocessing and needs a concrete implementation before it can be executed. One such implementation is provided by `tf.Transform` using Apache Beam and provides a PTransform `tft_beam.AnalyzeAndTransformDataset` to process the data. We can test this preproccesing_fn outside of the TFX Transform component using this PTransform explicitly. Calling the `processing_fn` in such a way is not necessary when using `tf.Transform` in combination with the TFX Transform component. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py" tfx_analyze_and_transform >}} +{{< /highlight >}} + +#### Train + +Finally the Trainer component behaves in a similar way as the Transform component, but instead of looking for a `preprocessing_fn` it requires a `run_fn` function to be present in the specified `module_file`. Our simple implementation, creates a stub model using `tf.Keras` and saves the resulting model to a directory. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_utils.py" tfx_run_fn >}} +{{< /highlight >}} + +#### Executing the pipeline + +To launch the pipeline two configurations must be provided: The orchestrator for the TFX pipeline and the pipeline options to run Beam pipelines. In this case we use the `LocalDagRunner` for orchestration to run the pipeline locally without extra setup dependencies. Where the created pipeline can specify Beam’s pipeline options as usual through the `beam_pipeline_args` argument. + +{{< highlight file="sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_local.py" >}} +{{< code_sample "sdks/python/apache_beam/examples/ml-orchestration/tfx/coco_captions_local.py" tfx_execute_pipeline >}} +{{< /highlight >}} diff --git a/website/www/site/content/en/documentation/ml/overview.md b/website/www/site/content/en/documentation/ml/overview.md old mode 100755 new mode 100644 index 0423686329c8..628dd009b75b --- a/website/www/site/content/en/documentation/ml/overview.md +++ b/website/www/site/content/en/documentation/ml/overview.md @@ -47,12 +47,26 @@ Further reading: ## Inference -There are several ways to use and deploy your model: -1. Making it available for online predictions via an API -2. Running it in real-time as new data becomes available in a pipeline -3. Running it in batch on an existing dataset +Beam provides different ways of implementing inference as part of your pipeline. This way you can run your ML model directly in your pipeline and apply it on big scale datasets, both in batch and streaming pipelines. + +### RunInference +The recommended way to implement inference is by using the [RunInference API](https://beam.apache.org/documentation/sdks/python-machine-learning/). RunInference takes advantage of existing Apache Beam concepts, such as the `BatchElements` transform and the `Shared` class, to enable you to use models in your pipelines to create transforms optimized for machine learning inferences. The ability to create arbitrarily complex workflow graphs also allows you to build multi-model pipelines. + +You can easily integrate your model in your pipeline by using the corresponding model handlers. A `ModelHandler` is an object that wraps the underlying model and allows you to configure its parameters. Model handlers are available for PyTorch, Scikit-learn and TensorFlow. Examples of how to use RunInference for PyTorch, Scikit-learn and TensorFlow are shown in this [notebook](https://github.com/apache/beam/blob/master/examples/notebooks/beam-ml/run_inference_pytorch_tensorflow_sklearn.ipynb). + +GPUs are optimized for training artificial intelligence and deep learning models as they can process multiple computations simultaneously. RunInference also allows you to use GPUs for significant inference speedup. An example of how to use RunInference with GPUs is demonstrated [here](/documentation/ml/runinference-metrics). + +### Custom Inference +As of now, RunInference API doesn't support making remote inference calls (e.g. Natural Language API, Cloud Vision API and others). Therefore, in order to use these remote APIs with Beam, one needs to write custom inference call. The [notebook](https://github.com/apache/beam/blob/master/examples/notebooks/beam-ml/custom_remote_inference.ipynb) shows how you can implement such a custom remote inference call using `beam.DoFn`. While implementing such a remote inference for real life projects, you need to think about following: + +* API quotas and the heavy load you might incur on your external API. For optimizing the calls to external API, you can confgure `PipelineOptions` to limit the parallel calls to the external remote API. + +* You must be prepared to encounter, identify, and handle failure as gracefully as possible. We recommend using techniques like `Exponential backoff` and `Dead letter queues`. + +* When running inference with an external API, you should batch your input together to allow for more efficient execution. + +* You should consider monitoring and measuring performance of a pipeline when deploying since monitoring can provide insight into the status and health of the application. -Beam is ideally suitable for the last 2 use cases. In this case your data will run through a pipeline (streaming or batch), and you can obtain predictions by running inference in one of the steps of your pipeline. Beam provides the [RunInference API](https://beam.apache.org/documentation/sdks/python-machine-learning/) to facilitate the integration of your model into a pipeline step. When running your model, a common requirement is to enable GPU execution. Beam also provides support for this. ## Orchestrators @@ -61,6 +75,7 @@ In order to automate and track the AI/ML workflows throughout your project, you ## Examples You can find examples of end-to-end AI/ML pipelines for several use cases: -* [Multi model pipelines in Beam](/documentation/ml/multi-model-pipelines) -* [Online Clustering in Beam](/documentation/ml/online-clustering) -* [Anomaly Detection in Beam](/documentation/ml/anomaly-detection) +* [ML Workflow Orchestration](/documentation/ml/orchestration): illustrates how ML workflows consisting of multiple steps can be orchestrated by using Kubeflow Pipelines and Tensorflow Extended. +* [Multi model pipelines in Beam](/documentation/ml/multi-model-pipelines): explains how multi-model pipelines work and gives an overview of what you need to know to build one using the RunInference API. +* [Online Clustering in Beam](/documentation/ml/online-clustering): demonstrates how to setup a realtime clustering pipeline that can read text from PubSub, convert the text into an embedding using a transformer based language model with the RunInference API, and cluster them using BIRCH with Stateful Processing. +* [Anomaly Detection in Beam](/documentation/ml/anomaly-detection): demonstrates how to setup an anomaly detection pipeline that reads text from PubSub in real-time, and then detects anomaly using a trained HDBSCAN clustering model with the RunInference API. \ No newline at end of file diff --git a/website/www/site/content/en/documentation/ml/runinference-metrics.md b/website/www/site/content/en/documentation/ml/runinference-metrics.md new file mode 100644 index 000000000000..e58cf20c0d42 --- /dev/null +++ b/website/www/site/content/en/documentation/ml/runinference-metrics.md @@ -0,0 +1,102 @@ +--- +title: "RunInference Metrics" +--- + + +# RunInference Metrics Example + +The main purpose of the example is to demonstrate and explain different metrics that are available when using the [RunInference](https://beam.apache.org/documentation/transforms/python/elementwise/runinference/) transform to perform inference using a machine learning model. We use a pipeline that reads a list of sentences, tokeinzes the text, and uses a Transformer based model `distilbert-base-uncased-finetuned-sst-2-english` for classifying the texts into two different classes using `RunInference`. + +We showcase different RunInference metrics when the pipeline is executed using the Dataflow Runner on CPU and GPU. The full example code can be found [here](https://github.com/apache/beam/tree/master/sdks/python/apache_beam/examples/inference/runinference_metrics/). + + +The file structure for entire pipeline is: + + runinference_metrics/ + ├── pipeline/ + │ ├── __init__.py + │ ├── options.py + │ └── transformations.py + ├── __init__.py + ├── config.py + ├── main.py + └── setup.py + +`pipeline/transformations.py` contains the code for `beam.DoFn` and additional functions that are used for pipeline + +`pipeline/options.py` contains the pipeline options to configure the Dataflow pipeline + +`config.py` defines some variables like GCP `PROJECT_ID`, `NUM_WORKERS` that are used multiple times + +`setup.py` defines the packages/requirements for the pipeline to run + +`main.py` contains the pipeline code and some additional functions used for running the pipeline + + +### How to Run the Pipeline +First, make sure you have installed the required packages. One should have access to a Google Cloud Project and then correctly configure the GCP variables like `PROJECT_ID`, `REGION`, and others in `config.py`. To use GPUs, follow the setup instructions [here](https://github.com/GoogleCloudPlatform/python-docs-samples/tree/main/dataflow/gpu-examples/pytorch-minimal). + + +1. Dataflow with CPU: `python main.py --mode cloud --device CPU` +2. Dataflow with GPU: `python main.py --mode cloud --device GPU` + +The pipeline can be broken down into few simple steps: +1. Create a list of texts to use as an input using `beam.Create` +2. Tokenize the text +3. Use RunInference to do inference +4. Postprocess the output of RunInference + +{{< highlight >}} + with beam.Pipeline(options=pipeline_options) as pipeline: + _ = ( + pipeline + | "Create inputs" >> beam.Create(inputs) + | "Tokenize" >> beam.ParDo(Tokenize(cfg.TOKENIZER_NAME)) + | "Inference" >> + RunInference(model_handler=KeyedModelHandler(model_handler)) + | "Decode Predictions" >> beam.ParDo(PostProcessor())) +{{< /highlight >}} + + +## RunInference Metrics + +As mentioned above, we benchmarked the performance of RunInference using Dataflow on both CPU and GPU. These metrics can be seen in the GCP UI and can also be printed using + +{{< highlight >}} +metrics = pipeline.result.metrics().query(beam.metrics.MetricsFilter()) +{{< /highlight >}} + + +A snapshot of different metrics from GCP UI when using Dataflow on GPU: + + ![RunInference GPU metrics rendered on Dataflow](/images/runinference_metrics_snapshot.svg) + +Some metrics commonly used for benchmarking are: + +* `num_inferences`: represents the total number of elements passed to `run_inference()`. + +* `inference_batch_latency_micro_secs_MEAN`: represents the average time taken to perform the inference across all batches of examples, measured in microseconds. + +* `inference_request_batch_size_COUNT`: represents the total number of samples across all batches of examples (created from `beam.BatchElements`) to be passed to run_inference() + +* `inference_request_batch_byte_size_MEAN`: represents the average size of all elements for all samples in all batches of examples (created from `beam.BatchElements`) to be passed to run_inference(). This is measured in bytes. + +* `model_byte_size_MEAN`: represents the average memory consumed to load and initialize the model. This is measured in bytes. + +* `load_model_latency_milli_secs_MEAN`: represents the average time taken to load and initialize the model. This is measured in milliseconds. + +One can derive other relevant metrics like +* `Total time taken for inference` = `num_inferences x inference_batch_latency_micro_secs_MEAN` + diff --git a/website/www/site/content/en/documentation/programming-guide.md b/website/www/site/content/en/documentation/programming-guide.md index 4276e7dc3658..8d242cc60703 100644 --- a/website/www/site/content/en/documentation/programming-guide.md +++ b/website/www/site/content/en/documentation/programming-guide.md @@ -3792,39 +3792,89 @@ the user ids from a `PCollection` of purchases one would write (using the `Selec purchases.apply(Select.fieldNames("userId")); {{< /highlight >}} +{{< highlight py >}} +input_pc = ... # {"user_id": ...,"bank": ..., "purchase_amount": ...} +output_pc = input_pc | beam.Select("user_id") +{{< /highlight >}} + ##### **Nested fields** +{{< paragraph class="language-py" >}} +Support for Nested fields hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for Nested fields hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-java" >}} Individual nested fields can be specified using the dot operator. For example, to select just the postal code from the shipping address one would write +{{< /paragraph >}} {{< highlight java >}} purchases.apply(Select.fieldNames("shippingAddress.postCode")); {{< /highlight >}} + ##### **Wildcards** +{{< paragraph class="language-py" >}} +Support for wildcards hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for wildcards hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-java" >}} The * operator can be specified at any nesting level to represent all fields at that level. For example, to select all shipping-address fields one would write +{{< /paragraph >}} {{< highlight java >}} purchases.apply(Select.fieldNames("shippingAddress.*")); {{< /highlight >}} + ##### **Arrays** +{{< paragraph class="language-java" >}} An array field, where the array element type is a row, can also have subfields of the element type addressed. When selected, the result is an array of the selected subfield type. For example +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +Support for Array fields hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for Array fields hasn't been developed for the Go SDK yet. +{{< /paragraph >}} {{< highlight java >}} purchases.apply(Select.fieldNames("transactions[].bank")); {{< /highlight >}} +{{< paragraph class="language-java" >}} Will result in a row containing an array field with element-type string, containing the list of banks for each transaction. +{{< /paragraph >}} +{{< paragraph class="language-java" >}} While the use of [] brackets in the selector is recommended, to make it clear that array elements are being selected, they can be omitted for brevity. In the future, array slicing will be supported, allowing selection of portions of the array. +{{< /paragraph >}} + ##### **Maps** @@ -3858,6 +3908,14 @@ The following purchasesByType.apply(Select.fieldNames("purchases{}.userId")); {{< /highlight >}} +{{< paragraph class="language-py" >}} +Support for Map fields hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for Map fields hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + Will result in a row containing a map field with key-type string and value-type string. The selected map will contain all of the keys from the original map, and the values will be the userId contained in the purchase record. @@ -3882,6 +3940,14 @@ could select only the userId and streetAddress fields as follows purchases.apply(Select.fieldNames("userId", "shippingAddress.streetAddress")); {{< /highlight >}} +{{< paragraph class="language-py" >}} +Support for Nested fields hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for Nested fields hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + The resulting `PCollection` will have the following schema @@ -3910,6 +3976,14 @@ The same is true for wildcard selections. The following purchases.apply(Select.fieldNames("userId", "shippingAddress.*")); {{< /highlight >}} +{{< paragraph class="language-py" >}} +Support for Wildcards hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for Wildcards hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + Will result in the following schema
    @@ -3956,6 +4030,15 @@ selected field will appear as its own array field. For example purchases.apply(Select.fieldNames( "transactions.bank", "transactions.purchaseAmount")); {{< /highlight >}} +{{< paragraph class="language-py" >}} +Support for nested fields hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for nested fields hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-java" >}} Will result in the following schema
    @@ -3976,6 +4059,7 @@ Will result in the following schema

    +{{< /paragraph >}} Wildcard selections are equivalent to separately selecting each field. @@ -3993,6 +4077,15 @@ Another use of the Select transform is to flatten a nested schema into a single purchases.apply(Select.flattenedSchema()); {{< /highlight >}} +{{< paragraph class="language-py" >}} +Support for nested fields hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for nested fields hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-java" >}} Will result in the following schema @@ -4045,21 +4138,48 @@ Will result in the following schema

    +{{< /paragraph >}} ##### **Grouping aggregations** +{{< paragraph class="language-java" >}} The `Group` transform allows simply grouping data by any number of fields in the input schema, applying aggregations to those groupings, and storing the result of those aggregations in a new schema field. The output of the `Group` transform has a schema with one field corresponding to each aggregation performed. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +The `GroupBy` transform allows simply grouping data by any number of fields in the input schema, applying aggregations to +those groupings, and storing the result of those aggregations in a new schema field. The output of the `GroupBy` transform +has a schema with one field corresponding to each aggregation performed. +{{< /paragraph >}} +{{< paragraph class="language-java" >}} The simplest usage of `Group` specifies no aggregations, in which case all inputs matching the provided set of fields are grouped together into an `ITERABLE` field. For example +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +The simplest usage of `GroupBy` specifies no aggregations, in which case all inputs matching the provided set of fields +are grouped together into an `ITERABLE` field. For example +{{< /paragraph >}} {{< highlight java >}} -purchases.apply(Group.byFieldNames("userId", "shippingAddress.streetAddress")); +purchases.apply(Group.byFieldNames("userId", "bank")); {{< /highlight >}} +{{< highlight py >}} +input_pc = ... # {"user_id": ...,"bank": ..., "purchase_amount": ...} +output_pc = input_pc | beam.GroupBy('user_id','bank') +{{< /highlight >}} + +{{< paragraph class="language-go" >}} +Support for schema-aware grouping hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + +{{< paragraph class="lanuage-java" >}} The output schema of this is: +{{< /paragraph >}} @@ -4071,7 +4191,7 @@ The output schema of this is: - + @@ -4104,6 +4224,18 @@ purchases.apply(Group.byFieldNames("userId") .aggregateField("costCents", Top.largestLongsFn(10), "topPurchases")); {{< /highlight >}} +{{< highlight py >}} +input_pc = ... # {"user_id": ..., "item_Id": ..., "cost_cents": ...} +output_pc = input_pc | beam.GroupBy("user_id") + .aggregate_field("item_id", CountCombineFn, "num_purchases") + .aggregate_field("cost_cents", sum, "total_spendcents") + .aggregate_field("cost_cents", TopCombineFn, "top_purchases") +{{< /highlight >}} + +{{< paragraph class="language-go" >}} +Support for schema-aware grouping hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + The result of this aggregation will have the following schema:
    keyROW{userId:STRING, streetAddress:STRING}ROW{userId:STRING, bank:STRING}
    values
    @@ -4135,6 +4267,14 @@ that are likely associated with that transaction (both the user and product matc "natural join" - one in which the same field names are used on both the left-hand and right-hand sides of the join - and is specified with the `using` keyword: +{{< paragraph class="language-py" >}} +Support for joins hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for joins hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + {{< highlight java >}} PCollection transactions = readTransactions(); PCollection reviews = readReviews(); @@ -4142,6 +4282,7 @@ PCollection joined = transactions.apply( Join.innerJoin(reviews).using("userId", "productId")); {{< /highlight >}} +{{< paragraph class="language-java" >}} The resulting schema is the following:
    @@ -4162,12 +4303,21 @@ The resulting schema is the following:

    +{{< /paragraph >}} Each resulting row contains one Transaction and one Review that matched the join condition. If the fields to match in the two schemas have different names, then the on function can be used. For example, if the Review schema named those fields differently than the Transaction schema, then we could write the following: +{{< paragraph class="language-py" >}} +Support for joins hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for joins hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + {{< highlight java >}} PCollection joined = transactions.apply( Join.innerJoin(reviews).on( @@ -4188,6 +4338,14 @@ can optionally be expanded - providing individual joined records, as in the `Joi processed in unexpanded format - providing the join key along with Iterables of all records from each input that matched that key. +{{< paragraph class="language-py" >}} +Support for joins hasn't been developed for the Python SDK yet. +{{< /paragraph >}} + +{{< paragraph class="language-go" >}} +Support for joins hasn't been developed for the Go SDK yet. +{{< /paragraph >}} + ##### **Filtering events** The `Filter` transform can be configured with a set of predicates, each one based one specified fields. Only records for @@ -7282,7 +7440,18 @@ To create an SDK wrapper for use in a Python pipeline, do the following: #### 13.1.2. Creating cross-language Python transforms -To make your Python transform usable with different SDK languages, you must create a Python module that registers an existing Python transform as a cross-language transform for use with the Python expansion service and calls into that existing transform to perform its intended operation. +Any Python transforms defined in the scope of the expansion service should be accessible by specifying their fully qualified names. For example, you could use Python's `ReadFromText` transform in a Java pipeline with its fully qualified name `apache_beam.io.ReadFromText`: + +```java +p.apply("Read", + PythonExternalTransform.>from("apache_beam.io.ReadFromText") + .withKwarg("file_pattern", options.getInputFile()) + .withKwarg("validate", false)) +``` + + > **Note:** `PythonExternalTransform` has other useful methods such as `withExtraPackages` for staging PyPI package dependencies and `withOutputCoder` for setting an output coder. + +Alternatively, you may want to create a Python module that registers an existing Python transform as a cross-language transform for use with the Python expansion service and calls into that existing transform to perform its intended operation. A registered URN can be used later in an expansion request for indicating an expansion target. **Defining the Python module** @@ -7336,7 +7505,7 @@ $ export PORT_FOR_EXPANSION_SERVICE=12345 3. Import any modules that contain transforms to be made available using the expansion service. {{< highlight >}} -$ python -m apache_beam.runners.portability.expansion_service_test -p $PORT_FOR_EXPANSION_SERVICE +$ python -m apache_beam.runners.portability.expansion_service_test -p $PORT_FOR_EXPANSION_SERVICE --pickle_library=cloudpickle {{< /highlight >}} 4. This expansion service is now ready to serve up transforms on the address `localhost:$PORT_FOR_EXPANSION_SERVICE`. @@ -7393,7 +7562,29 @@ Depending on the SDK language of the pipeline, you can use a high-level SDK-wrap #### 13.2.1. Using cross-language transforms in a Java pipeline -Currently, to access cross-language transforms from the Java SDK, you have to use the lower-level [External](https://github.com/apache/beam/blob/master/runners/core-construction-java/src/main/java/org/apache/beam/runners/core/construction/External.java) class. +Users have three options to use cross-language transforms in a Java pipeline. At the highest level of abstraction, some popular Python transforms are accessible through dedicated Java wrapper transforms. For example, the Java SDK has the `DataframeTransform` class, which uses the Python SDK's `DataframeTransform`, and it has the `RunInference` class, which uses the Python SDK's `RunInference`, and so on. When an SDK-specific wrapper transform is not available for a target Python transform, you can use the lower-level [PythonExternalTransform](https://github.com/apache/beam/blob/master/sdks/java/extensions/python/src/main/java/org/apache/beam/sdk/extensions/python/PythonExternalTransform.java) class instead by specifying the fully qualified name of the Python transform. If you want to try external transforms from SDKs other than Python (including Java SDK itself), you can also use the lowest-level [External](https://github.com/apache/beam/blob/master/runners/core-construction-java/src/main/java/org/apache/beam/runners/core/construction/External.java) class. + +**Using an SDK wrapper** + +To use a cross-language transform through an SDK wrapper, import the module for the SDK wrapper and call it from your pipeline, as shown in the example: + +```java +import org.apache.beam.sdk.extensions.python.transforms.DataframeTransform; + +input.apply(DataframeTransform.of("lambda df: df.groupby('a').sum()").withIndexes()) +``` + +**Using the PythonExternalTransform class** + +When an SDK-specific wrapper is not available, you can access the Python cross-language transform through the `PythonExternalTransform` class by specifying the fully qualified name and the constructor arguments of the target Python transform. + +```java +input.apply( + PythonExternalTransform., PCollection>from( + "apache_beam.dataframe.transforms.DataframeTransform") + .withKwarg("func", PythonCallableSource.of("lambda df: df.groupby('a').sum()")) + .withKwarg("include_indexes", true)) +``` **Using the External class** @@ -7608,3 +7799,238 @@ Dataflow supports multi-language pipelines through the Dataflow Runner v2 backen ### 13.4 Tips and Troubleshooting {#x-lang-transform-tips-troubleshooting} For additional tips and troubleshooting information, see [here](https://cwiki.apache.org/confluence/display/BEAM/Multi-language+Pipelines+Tips). + +## 14 Batched DoFns {#batched-dofns} +{{< language-switcher java py go typescript >}} + +{{< paragraph class="language-go language-java language-typescript" >}} +Batched DoFns are currently a Python-only feature. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +Batched DoFns enable users to create modular, composable components that +operate on batches of multiple logical elements. These DoFns can leverage +vectorized Python libraries, like numpy, scipy, and pandas, which operate on +batches of data for efficiency. +{{< /paragraph >}} + +### 14.1 Basics {#batched-dofn-basics} +{{< paragraph class="language-go language-java language-typescript" >}} +Batched DoFns are currently a Python-only feature. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +A trivial Batched DoFn might look like this: +{{< /paragraph >}} + +{{< highlight py >}} +class MultiplyByTwo(beam.DoFn): +  # Type +  def process_batch(self, batch: np.ndarray) -> Iterator[np.ndarray]: +    yield batch * 2 + +  # Declare what the element-wise output type is +  def infer_output_type(self, input_element_type): +    return input_element_type +{{< /highlight >}} + +{{< paragraph class="language-py" >}} +This DoFn can be used in a Beam pipeline that otherwise operates on individual +elements. Beam will implicitly buffer elements and create numpy arrays on the +input side, and on the output side it will explode the numpy arrays back into +individual elements: +{{< /paragraph >}} + +{{< highlight py >}} +(p | beam.Create([1, 2, 3, 4]).with_output_types(np.int64) +   | beam.ParDo(MultiplyByTwo()) # Implicit buffering and batch creation +   | beam.Map(lambda x: x/3))  # Implicit batch explosion +{{< /highlight >}} + +{{< paragraph class="language-py" >}} +Note that we use +[`PTransform.with_output_types`](https://beam.apache.org/releases/pydoc/current/apache_beam.transforms.ptransform.html#apache_beam.transforms.ptransform.PTransform.with_output_types) to +set the _element-wise_ typehint for the output of `beam.Create`. Then, when +`MultiplyByTwo` is applied to this `PCollection`, Beam recognizes that +`np.ndarray` is an acceptable batch type to use in conjunction with `np.int64` +elements. We will use numpy typehints like these throughout this guide, but +Beam supports typehints from other libraries as well, see [Supported Batch +Types](#batched-dofn-types). +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +In the previous case, Beam will implicitly create and explode batches at the +input and output boundaries. However, if Batched DoFns with equivalent types are +chained together, this batch creation and explosion will be elided. The batches +will be passed straight through! This makes it much simpler to efficiently +compose transforms that operate on batches. +{{< /paragraph >}} + +{{< highlight py >}} +(p | beam.Create([1, 2, 3, 4]).with_output_types(np.int64) +   | beam.ParDo(MultiplyByTwo()) # Implicit buffering and batch creation +   | beam.ParDo(MultiplyByTwo()) # Batches passed through +   | beam.ParDo(MultiplyByTwo())) +{{< /highlight >}} + +### 14.2 Element-wise Fallback {#batched-dofn-elementwise} +{{< paragraph class="language-go language-java language-typescript" >}} +Batched DoFns are currently a Python-only feature. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +For some DoFns you may be able to provide both a batched and an element-wise +implementation of your desired logic. You can do this by simply defining both +`process` and `process_batch`: +{{< /paragraph >}} + +{{< highlight py >}} +class MultiplyByTwo(beam.DoFn): +  def process(self, element: np.int64) -> Iterator[np.int64]: + # Multiply an individual int64 by 2 +    yield batch * 2 + +  def process_batch(self, batch: np.ndarray) -> Iterator[np.ndarray]: + # Multiply a _batch_ of int64s by 2 +    yield batch * 2 +{{< /highlight >}} + +{{< paragraph class="language-py" >}} +When executing this DoFn, Beam will select the best implementation to use given +the context. Generally, if the inputs to a DoFn are already batched Beam will +use the batched implementation; otherwise it will use the element-wise +implementation defined in the `process` method. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +Note that, in this case, there is no need to define `infer_output_type`. This is +because Beam can get the output type from the typehint on `process`. +{{< /paragraph >}} + + + +### 14.3 Batch Production vs. Batch Consumption {#batched-dofn-batch-production} +{{< paragraph class="language-go language-java language-typescript" >}} +Batched DoFns are currently a Python-only feature. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +By convention, Beam assumes that the `process_batch` method, which consumes +batched inputs, will also produce batched outputs. Similarly, Beam assumes the +`process` method will produce individual elements. This can be overridden with +the [`@beam.DoFn.yields_elements`](https://beam.apache.org/releases/pydoc/current/apache_beam.transforms.core.html#apache_beam.transforms.core.DoFn.yields_elements) and +[`@beam.DoFn.yields_batches`](https://beam.apache.org/releases/pydoc/current/apache_beam.transforms.core.html#apache_beam.transforms.core.DoFn.yields_batches) decorators. For example: +{{< /paragraph >}} + +{{< highlight py >}} +# Consumes elements, produces batches +class ReadFromFile(beam.DoFn): + +  @beam.DoFn.yields_batches +  def process(self, path: str) -> Iterator[np.ndarray]: +    ... +    yield array +   + +  # Declare what the element-wise output type is +  def infer_output_type(self): +    return np.int64 + +# Consumes batches, produces elements +class WriteToFile(beam.DoFn): +  @beam.DoFn.yields_elements +  def process_batch(self, batch: np.ndarray) -> Iterator[str]: +    ... +    yield output_path +{{< /highlight >}} + +### 14.4 Supported Batch Types {#batched-dofn-types} +{{< paragraph class="language-go language-java language-typescript" >}} +Batched DoFns are currently a Python-only feature. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +We’ve used numpy types in the Batched DoFn implementations in this guide – +`np.int64 ` as the element typehint and `np.ndarray` as the corresponding +batch typehint – but Beam supports typehints from other libraries as well. +{{< /paragraph >}} + +#### [numpy](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/typehints/batch.py) +| Element Typehint | Batch Typehint | +| ---------------- | -------------- | +| Numeric types (`int`, `np.int32`, `bool`, ...) | np.ndarray (or NumpyArray) | + +#### [pandas](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/typehints/pandas_type_compatibility.py) +| Element Typehint | Batch Typehint | +| ---------------- | -------------- | +| Numeric types (`int`, `np.int32`, `bool`, ...) | `pd.Series` | +| `bytes` | | +| `Any` | | +| [Beam Schema Types](#schemas) | `pd.DataFrame` | + +#### Other types? +If there are other batch types you would like to use with Batched DoFns, please +[file an issue](https://github.com/apache/beam/issues/new/choose). + +### 14.5 Dynamic Batch Input and Output Types {#batched-dofn-dynamic-types} +{{< paragraph class="language-go language-java language-typescript" >}} +Batched DoFns are currently a Python-only feature. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +For some Batched DoFns, it may not be sufficient to declare batch types +statically, with typehints on `process` and/or `process_batch`. You may need to +declare these types dynamically. You can do this by overriding the +[`get_input_batch_type`](https://beam.apache.org/releases/pydoc/current/apache_beam.transforms.core.html#apache_beam.transforms.core.DoFn.get_input_batch_type) +and +[`get_output_batch_type`](https://beam.apache.org/releases/pydoc/current/apache_beam.transforms.core.html#apache_beam.transforms.core.DoFn.get_output_batch_type) +methods on your DoFn: +{{< /paragraph >}} + +{{< highlight py >}} +# Utilize Beam's parameterized NumpyArray typehint +from apache_beam.typehints.batch import NumpyArray + +class MultipyByTwo(beam.DoFn): + # No typehints needed +  def process_batch(self, batch): +    yield batch * 2 + +  def get_input_batch_type(self, input_element_type): +    return NumpyArray[input_element_type] + +  def get_output_batch_type(self, input_element_type): +    return NumpyArray[input_element_type] + +  def infer_output_type(self, input_element_type): +    return input_element_type +{{< /highlight >}} + +### 14.6 Batches and Event-time Semantics {#batched-dofn-event-time} +{{< paragraph class="language-go language-java language-typescript" >}} +Batched DoFns are currently a Python-only feature. +{{< /paragraph >}} + +{{< paragraph class="language-py" >}} +Currently, batches must have a single set of timing information (event time, +windows, etc...) that applies to every logical element in the batch. There is +currently no mechanism to create batches that span multiple timestamps. However, +it is possible to retrieve this timing information in Batched DoFn +implementations. This information can be accessed by using the conventional +`DoFn.*Param` attributes: +{{< /paragraph >}} + +{{< highlight py >}} +class RetrieveTimingDoFn(beam.DoFn): + +  def process_batch( +    self, +    batch: np.ndarray, +    timestamp=beam.DoFn.TimestampParam, +    pane_info=beam.DoFn.PaneInfoParam, +   ) -> Iterator[np.ndarray]: +     ... + +  def infer_output_type(self, input_type): +    return input_type +{{< /highlight >}} diff --git a/website/www/site/content/en/documentation/runners/spark.md b/website/www/site/content/en/documentation/runners/spark.md index ff6fa3cc47a8..b7283f0cbe1b 100644 --- a/website/www/site/content/en/documentation/runners/spark.md +++ b/website/www/site/content/en/documentation/runners/spark.md @@ -293,7 +293,7 @@ python -m apache_beam.examples.wordcount \ - `--runner`(required): `SparkRunner`. - `--output_executable_path`(required): path for the bundle jar to be created. - `--output`(required): where output shall be written. -- `--spark_version`(optional): select spark version 2 (default) or 3. +- `--spark_version`(optional): select spark version 3 (default) or 2 (deprecated!). 5. Submit spark job to Dataproc cluster's master node. diff --git a/website/www/site/content/en/documentation/sdks/java-multi-language-pipelines.md b/website/www/site/content/en/documentation/sdks/java-multi-language-pipelines.md index 5f1b971f2046..fe1fba52d17f 100644 --- a/website/www/site/content/en/documentation/sdks/java-multi-language-pipelines.md +++ b/website/www/site/content/en/documentation/sdks/java-multi-language-pipelines.md @@ -138,26 +138,27 @@ default Beam SDK, you might need to run your own expansion service. In such cases, [start the expansion service](#advanced-start-an-expansion-service) before running your pipeline. -Here we've provided commands for running the example pipeline using -Gradle on a [Beam HEAD Git clone](https://github.com/apache/beam). -If you need a more stable environment, please -[setup a Java project](/get-started/quickstart-java/) that uses the latest -released Beam version and include the necessary dependencies. +### Run with Dataflow runner at HEAD (Beam 2.41.0 and later) -### Run with Dataflow runner +> **Note:** Due to [issue#23717](https://github.com/apache/beam/issues/23717), +> Beam 2.42.0 requires manually starting up an expansion service (see +> [these instructions](https://beam.apache.org/documentation/sdks/java-multi-language-pipelines/#advanced-start-an-expansion-service)) +> and using the additional pipeline option `--expansionService=localhost:` +> when executing the pipeline. The following script runs the example multi-language pipeline on Dataflow, using example text from a Cloud Storage bucket. You’ll need to adapt the script to your environment. ``` +export GCP_PROJECT= export OUTPUT_BUCKET= export GCP_REGION= export TEMP_LOCATION=gs://$OUTPUT_BUCKET/tmp -export PYTHON_VERSION= ./gradlew :examples:multi-language:pythonDataframeWordCount --args=" \ --runner=DataflowRunner \ +--project=$GCP_PROJECT \ --output=gs://${OUTPUT_BUCKET}/count \ --region=${GCP_REGION}" ``` @@ -187,15 +188,20 @@ python -m apache_beam.runners.portability.local_job_service_main -p $JOB_SERVER_ (this guide requires that your JAVA_HOME is set to Java 11). ``` -./gradlew :sdks:java:container:java11:docker +./gradlew :sdks:java:container:java11:docker -Pjava11Home=$JAVA_HOME ``` 5. Run the pipeline. +> **Note:** Due to [issue#23717](https://github.com/apache/beam/issues/23717), +> Beam 2.42.0 requires manually starting up an expansion service (see +> [these instructions](https://beam.apache.org/documentation/sdks/java-multi-language-pipelines/#advanced-start-an-expansion-service)) +> and using the additional pipeline option `--expansionService=localhost:` +> when executing the pipeline. + ``` export JOB_SERVER_PORT= # Same port as before export OUTPUT_FILE= -export PYTHON_VERSION= ./gradlew :examples:multi-language:pythonDataframeWordCount --args=" \ --runner=PortableRunner \ @@ -226,19 +232,64 @@ For example, to start the standard expansion service for a Python transform, [ExpansionServiceServicer](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/runners/portability/expansion_service.py), follow these steps: -1. Activate a Python virtual environment and install Apache Beam, as described - in the [Python quick start](/get-started/quickstart-py/). -2. In the **beam/sdks/python** directory of the Beam source code, run the - following command: +1. Activate a new virtual environment following +[these instructions](https://beam.apache.org/get-started/quickstart-py/#create-and-activate-a-virtual-environment). + +2. Install Apache Beam with `gcp` and `dataframe` packages. + +``` +pip install apache-beam[gcp,dataframe] +``` - ``` - python apache_beam/runners/portability/expansion_service_main.py -p 18089 --fully_qualified_name_glob "*" - ``` +4. Run the following command + +``` +python -m apache_beam.runners.portability.expansion_service_main -p --fully_qualified_name_glob "*" +``` The command runs [expansion_service_main.py](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/runners/portability/expansion_service_main.py), which starts the standard expansion service. When you use Gradle to run your Java pipeline, you can specify the expansion service with the -`expansionService` option. For example: `--expansionService=localhost:18089`. +`expansionService` option. For example: `--expansionService=localhost:`. + +### Run with Dataflow runner using a Beam release (Beam 2.43.0 and later) + +> **Note:** Due to [issue#23717](https://github.com/apache/beam/issues/23717), +> Beam 2.42.0 requires manually starting up an expansion service (see +> [these instructions](https://beam.apache.org/documentation/sdks/java-multi-language-pipelines/#advanced-start-an-expansion-service)) +> and using the additional pipeline option `--expansionService=localhost:` +> when executing the pipeline. + +* Check out the Beam examples Maven archetype for the relevant Beam version. + +``` +export BEAM_VERSION= + +mvn archetype:generate \ + -DarchetypeGroupId=org.apache.beam \ + -DarchetypeArtifactId=beam-sdks-java-maven-archetypes-examples \ + -DarchetypeVersion=$BEAM_VERSION \ + -DgroupId=org.example \ + -DartifactId=multi-language-beam \ + -Dversion="0.1" \ + -Dpackage=org.apache.beam.examples \ + -DinteractiveMode=false +``` + +* Run the pipeline. + +``` +export GCP_PROJECT= +export GCP_BUCKET= +export GCP_REGION= + +mvn compile exec:java -Dexec.mainClass=org.apache.beam.examples.multilanguage.PythonDataframeWordCount \ + -Dexec.args="--runner=DataflowRunner --project=$GCP_PROJECT \ + --region=us-central1 \ + --gcpTempLocation=gs://$GCP_BUCKET/multi-language-beam/tmp \ + --output=gs://$GCP_BUCKET/multi-language-beam/output" \ + -Pdataflow-runner +``` ## Next steps diff --git a/website/www/site/content/en/documentation/sdks/java/testing/nexmark.md b/website/www/site/content/en/documentation/sdks/java/testing/nexmark.md index da5378034d8e..74ca4f2caaaa 100644 --- a/website/www/site/content/en/documentation/sdks/java/testing/nexmark.md +++ b/website/www/site/content/en/documentation/sdks/java/testing/nexmark.md @@ -494,7 +494,7 @@ configure logging. Batch Mode: ./gradlew :sdks:java:testing:nexmark:run \ - -Pnexmark.runner=":runners:spark:2" \ + -Pnexmark.runner=":runners:spark:3" \ -Pnexmark.args=" --runner=SparkRunner --suite=SMOKE @@ -506,7 +506,7 @@ Batch Mode: Streaming Mode: ./gradlew :sdks:java:testing:nexmark:run \ - -Pnexmark.runner=":runners:spark:2" \ + -Pnexmark.runner=":runners:spark:3" \ -Pnexmark.args=" --runner=SparkRunner --suite=SMOKE diff --git a/website/www/site/content/en/documentation/sdks/python-machine-learning.md b/website/www/site/content/en/documentation/sdks/python-machine-learning.md index b899e5149642..b35ab347a8b9 100644 --- a/website/www/site/content/en/documentation/sdks/python-machine-learning.md +++ b/website/www/site/content/en/documentation/sdks/python-machine-learning.md @@ -77,6 +77,9 @@ You need to provide a path to a file that contains the model's saved weights. Th 1. Download the pre-trained weights and host them in a location that the pipeline can access. 2. Pass the path of the model weights to the PyTorch `ModelHandler` by using the following code: `state_dict_path=`. +See [this notebook](https://github.com/apache/beam/blob/master/examples/notebooks/beam-ml/run_inference_pytorch.ipynb) +that illustrates running PyTorch models with Apache Beam. + #### Scikit-learn You need to provide a path to a file that contains the pickled Scikit-learn model. This path must be accessible by the pipeline. To use pre-trained models with the RunInference API and the Scikit-learn framework, complete the following steps: @@ -86,6 +89,9 @@ You need to provide a path to a file that contains the pickled Scikit-learn mode `model_uri=` and `model_file_type: `, where you can specify `ModelFileType.PICKLE` or `ModelFileType.JOBLIB`, depending on how the model was serialized. +See [this notebook](https://github.com/apache/beam/blob/master/examples/notebooks/beam-ml/run_inference_sklearn.ipynb) +that illustrates running Scikit-learn models with Apache Beam. + #### TensorFlow To use TensorFlow with the RunInference API, you need to do the following: @@ -94,48 +100,8 @@ To use TensorFlow with the RunInference API, you need to do the following: * Create a model handler using `tfx_bsl.public.beam.run_inference.CreateModelHandler()`. * Use the model handler with the [`apache_beam.ml.inference.base.RunInference`](/releases/pydoc/current/apache_beam.ml.inference.base.html) transform. -A sample pipeline might look like the following example: - -``` -import apache_beam as beam -from apache_beam.ml.inference.base import RunInference -from tensorflow_serving.apis import prediction_log_pb2 -from tfx_bsl.public.proto import model_spec_pb2 -from tfx_bsl.public.tfxio import TFExampleRecord -from tfx_bsl.public.beam.run_inference import CreateModelHandler - -pipeline = beam.Pipeline() -tfexample_beam_record = TFExampleRecord(file_pattern='/path/to/examples') -saved_model_spec = model_spec_pb2.SavedModelSpec(model_path='/path/to/model') -inference_spec_type = model_spec_pb2.InferenceSpecType(saved_model_spec=saved_model_spec) -model_handler = CreateModelHandler(inference_spec_type) -with pipeline as p: - _ = (p | tfexample_beam_record.RawRecordBeamSource() - | RunInference(model_handler) - | beam.Map(print) - ) -``` - -Note: A model handler that is created with `CreateModelHander()` is always unkeyed. - -### Keyed Model Handlers -To make a keyed model handler, wrap any unkeyed model handler in the keyed model handler. For example: - -``` -from apache_beam.ml.inference.base import RunInference -from apache_beam.ml.inference.base import KeyedModelHandler -model_handler = -keyed_model_handler = KeyedModelHandler(model_handler) - -with pipeline as p: - p | ( - RunInference(keyed_model_handler) - ) -``` - -If you are unsure if your data is keyed, you can also use `MaybeKeyedModelHandler`. - -For more information, see [`KeyedModelHander`](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.base.html#apache_beam.ml.inference.base.KeyedModelHandler). +See [this notebook](https://github.com/apache/beam/blob/master/examples/notebooks/beam-ml/run_inference_tensorflow.ipynb) +that illustrates running TensorFlow models with Apache Beam and tfx-bsl. ### Use custom models @@ -209,6 +175,10 @@ with pipeline as p: predictions = data | RunInference(keyed_model_handler) ``` +If you are unsure if your data is keyed, you can also use `MaybeKeyedModelHandler`. + +For more information, see [`KeyedModelHander`](https://beam.apache.org/releases/pydoc/current/apache_beam.ml.inference.base.html#apache_beam.ml.inference.base.KeyedModelHandler). + ### Use the PredictionResults object When doing a prediction in Apache Beam, the output `PCollection` includes both the keys of the input examples and the inferences. Including both these items in the output allows you to find the input that determined the predictions. diff --git a/website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md b/website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md index bf2e44e55866..330a8af8e449 100644 --- a/website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md +++ b/website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md @@ -36,7 +36,7 @@ If your pipeline uses public packages from the [Python Package Index](https://py This command creates a `requirements.txt` file that lists all packages that are installed on your machine, regardless of where they were installed from. -2. Edit the `requirements.txt` file and leave only the packages that were installed from PyPI and are used in the workflow source. Delete all packages that are not relevant to your code. +2. Edit the `requirements.txt` file and delete all packages that are not relevant to your code. 3. Run your pipeline with the following command-line option: @@ -44,7 +44,6 @@ If your pipeline uses public packages from the [Python Package Index](https://py The runner will use the `requirements.txt` file to install your additional dependencies onto the remote workers. -**Important:** Remote workers will install all packages listed in the `requirements.txt` file. Because of this, it's very important that you delete non-PyPI packages from the `requirements.txt` file, as stated in step 2. If you don't remove non-PyPI packages, the remote workers will fail when attempting to install packages from sources that are unknown to them. > **NOTE**: An alternative to `pip freeze` is to use a library like [pip-tools](https://github.com/jazzband/pip-tools) to compile all the dependencies required for the pipeline from a `--requirements_file`, where only top-level dependencies are mentioned. ## Custom Containers {#custom-containers} diff --git a/website/www/site/content/en/get-started/downloads.md b/website/www/site/content/en/get-started/downloads.md index 522ad829a07f..20eb6d6d1b03 100644 --- a/website/www/site/content/en/get-started/downloads.md +++ b/website/www/site/content/en/get-started/downloads.md @@ -53,6 +53,14 @@ Additionally, you may want to depend on additional SDK modules, such as IO connectors or other extensions, and additional runners to execute your pipeline at scale. +The Go SDK is accessible via Go Modules and calling `go get` from a module subdirectory: + + go get github.com/apache/beam/sdks/v2/go/pkg/beam + +Specific versions can be depended on similarly: + + go get github.com/apache/beam/sdks/v2@v{{< param release_latest >}}/go/pkg/beam + ## Downloading source code You can download the source code package for a release from the links in the diff --git a/website/www/site/layouts/case-studies/list.html b/website/www/site/layouts/case-studies/list.html index c1957847a3ca..a67079bd06ad 100644 --- a/website/www/site/layouts/case-studies/list.html +++ b/website/www/site/layouts/case-studies/list.html @@ -54,14 +54,22 @@

    {{ .Params.cardTitle }}

    Also used by

    {{ range where $pages "Params.category" "ne" "study" }} -
    -
    - -
    -
    - {{ .Params.cardDescription | safeHTML }} + {{ if .Params.hasLink }} + +
    + +
    +
    + {{ else }} +
    +
    + +
    +
    + {{ .Params.cardDescription | safeHTML }} +
    -
    + {{ end }} {{ end }}
    @@ -71,4 +79,8 @@

    Also used by

    + +{{ $shuffle := resources.Get "js/shuffle-elements.js" | minify | fingerprint }} + + {{ end }} diff --git a/website/www/site/layouts/index.html b/website/www/site/layouts/index.html index 396b0dbac839..6ada1652bdfd 100644 --- a/website/www/site/layouts/index.html +++ b/website/www/site/layouts/index.html @@ -104,7 +104,7 @@

    - @@ -148,8 +148,8 @@

    -
    - +
    +
    diff --git a/website/www/site/layouts/partials/header.html b/website/www/site/layouts/partials/header.html index 328396b35750..76735af60da9 100644 --- a/website/www/site/layouts/partials/header.html +++ b/website/www/site/layouts/partials/header.html @@ -16,7 +16,7 @@ Brand - {{ T "nav-get-started" }} + {{ T "nav-get-started" }} {{ T "nav-documentation" }}

  • - {{ T "nav-get-started" }} + {{ T "nav-get-started" }}
  • Documentation @@ -125,7 +125,7 @@
  • Runner Support
  • +
  • Batched DoFns
  • @@ -214,9 +215,11 @@
  • diff --git a/website/www/site/static/images/logos/powered-by/Amazon.png b/website/www/site/static/images/logos/powered-by/Amazon.png new file mode 100644 index 000000000000..7ff122bf2f5e Binary files /dev/null and b/website/www/site/static/images/logos/powered-by/Amazon.png differ diff --git a/website/www/site/static/images/logos/powered-by/ML6.jpg b/website/www/site/static/images/logos/powered-by/ML6.jpg new file mode 100644 index 000000000000..49062000537e Binary files /dev/null and b/website/www/site/static/images/logos/powered-by/ML6.jpg differ diff --git a/website/www/site/static/images/logos/powered-by/Strivr.png b/website/www/site/static/images/logos/powered-by/Strivr.png new file mode 100644 index 000000000000..cd9c99347564 Binary files /dev/null and b/website/www/site/static/images/logos/powered-by/Strivr.png differ diff --git a/website/www/site/static/images/logos/powered-by/Trustpilot.png b/website/www/site/static/images/logos/powered-by/Trustpilot.png new file mode 100644 index 000000000000..62703ac96202 Binary files /dev/null and b/website/www/site/static/images/logos/powered-by/Trustpilot.png differ diff --git a/website/www/site/static/images/logos/powered-by/Twitter.png b/website/www/site/static/images/logos/powered-by/Twitter.png new file mode 100644 index 000000000000..4ca58962cfeb Binary files /dev/null and b/website/www/site/static/images/logos/powered-by/Twitter.png differ diff --git a/website/www/site/static/images/logos/powered-by/Wayfair.png b/website/www/site/static/images/logos/powered-by/Wayfair.png new file mode 100644 index 000000000000..62745a08b75c Binary files /dev/null and b/website/www/site/static/images/logos/powered-by/Wayfair.png differ diff --git a/website/www/site/static/images/logos/powered-by/Wizeline.png b/website/www/site/static/images/logos/powered-by/Wizeline.png new file mode 100644 index 000000000000..16045d207da4 Binary files /dev/null and b/website/www/site/static/images/logos/powered-by/Wizeline.png differ diff --git a/website/www/site/static/images/orchestrated-beam-pipeline.svg b/website/www/site/static/images/orchestrated-beam-pipeline.svg new file mode 100644 index 000000000000..7270c6df081b --- /dev/null +++ b/website/www/site/static/images/orchestrated-beam-pipeline.svg @@ -0,0 +1,35 @@ + + + + + + + + + Beam DAGLoad DataTransformTransformTransformTransformTransformTransformData sourcesTrain/Val DatasetTest DatasetScrape DataTrain ML ModelEvaluate ML ModelOrchestrating DAG \ No newline at end of file diff --git a/website/www/site/static/images/runinference_metrics_snapshot.svg b/website/www/site/static/images/runinference_metrics_snapshot.svg new file mode 100644 index 000000000000..a1b41b2c2084 --- /dev/null +++ b/website/www/site/static/images/runinference_metrics_snapshot.svg @@ -0,0 +1,4751 @@ + + + + + diff --git a/website/www/site/static/images/standalone-beam-pipeline.svg b/website/www/site/static/images/standalone-beam-pipeline.svg new file mode 100644 index 000000000000..325b5be3d3e0 --- /dev/null +++ b/website/www/site/static/images/standalone-beam-pipeline.svg @@ -0,0 +1,35 @@ + + + + + + + + + Beam DAGLoad DataTransformTransformTransformTransformTransformTransformData sourcesOutputOutput \ No newline at end of file