-
Notifications
You must be signed in to change notification settings - Fork 375
chore: Improve network retry configuration for maven and artifact upload #5782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
58aa2bf
chore: Improve network retry configuration for maven and artifact upload
comphead ce5ff44
chore: tighten CI retry comments and note the unwrapped upload sites
comphead 8664b42
chore: pin java-test artifact uploads to upload-artifact@v7
comphead ed84fee
chore: scope shared CI artifact names per producer, register their ch…
comphead 94b87ad
chore: pin the last three jobs to ubuntu-24.04 instead of ubuntu-latest
comphead File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
|
||
| name: "Upload Artifact (with retry)" | ||
| description: > | ||
| Drop-in replacement for actions/upload-artifact that retries the upload three | ||
| times. The artifact client only retries 429/500/502/503/504, so a | ||
| FinalizeArtifact answered "(403) Forbidden: Error from intermediary" fails the | ||
| step even though the content uploaded fine. There is no input to widen that | ||
| list, Actions has no built-in step retry, and third-party retry wrappers are | ||
| not on the Apache allowed-actions list. See .github/workflows/README.md. | ||
|
|
||
| Retries force overwrite, which deletes the newest artifact carrying the name | ||
| before re-uploading. `name` must therefore identify exactly one producer in | ||
| the run: a reusable workflow ci.yml calls more than once has to qualify the | ||
| name with its version inputs. dev/ci/check-ci-config.py enforces that. | ||
|
|
||
| # Inputs mirror actions/upload-artifact@v7 one for one. The boolean defaults | ||
| # have to be real 'true'/'false' strings because core.getBooleanInput() throws | ||
| # on an empty value; the numeric ones default to '' so "unset" round-trips. | ||
| inputs: | ||
| name: | ||
| description: 'Artifact name' | ||
| required: false | ||
| default: 'artifact' | ||
| path: | ||
| description: 'A file, directory or wildcard pattern that describes what to upload' | ||
| required: true | ||
| if-no-files-found: | ||
| description: "Behavior if no files are found: warn, error or ignore" | ||
| required: false | ||
| default: 'warn' | ||
| retention-days: | ||
| description: 'Days before the artifact expires (empty means repository default)' | ||
| required: false | ||
| default: '' | ||
| compression-level: | ||
| description: 'Zlib compression level 0-9 (empty means the action default)' | ||
| required: false | ||
| default: '' | ||
| overwrite: | ||
| description: 'Delete an existing artifact with the same name before uploading' | ||
| required: false | ||
| default: 'false' | ||
| include-hidden-files: | ||
| description: 'Include hidden files in the artifact' | ||
| required: false | ||
| default: 'false' | ||
| archive: | ||
| description: 'Zip the content before uploading' | ||
| required: false | ||
| default: 'true' | ||
|
|
||
| # `||` yields the first non-empty operand, so this picks whichever attempt ran. | ||
| outputs: | ||
| artifact-id: | ||
| description: 'ID of the uploaded artifact' | ||
| value: ${{ steps.attempt-1.outputs.artifact-id || steps.attempt-2.outputs.artifact-id || steps.attempt-3.outputs.artifact-id }} | ||
| artifact-url: | ||
| description: 'Download URL of the uploaded artifact' | ||
| value: ${{ steps.attempt-1.outputs.artifact-url || steps.attempt-2.outputs.artifact-url || steps.attempt-3.outputs.artifact-url }} | ||
| artifact-digest: | ||
| description: 'SHA-256 digest of the uploaded artifact' | ||
| value: ${{ steps.attempt-1.outputs.artifact-digest || steps.attempt-2.outputs.artifact-digest || steps.attempt-3.outputs.artifact-digest }} | ||
|
|
||
| runs: | ||
| using: "composite" | ||
| steps: | ||
| # continue-on-error keeps a failed attempt from failing the job while still | ||
| # recording outcome == 'failure', which the later attempts gate on. The last | ||
| # attempt omits it so a genuinely broken upload still fails loudly. | ||
| - name: Upload ${{ inputs.name }} (attempt 1 of 3) | ||
| id: attempt-1 | ||
| uses: actions/upload-artifact@v7 | ||
| continue-on-error: true | ||
| with: | ||
| name: ${{ inputs.name }} | ||
| path: ${{ inputs.path }} | ||
| if-no-files-found: ${{ inputs.if-no-files-found }} | ||
| retention-days: ${{ inputs.retention-days }} | ||
| compression-level: ${{ inputs.compression-level }} | ||
| overwrite: ${{ inputs.overwrite }} | ||
| include-hidden-files: ${{ inputs.include-hidden-files }} | ||
| archive: ${{ inputs.archive }} | ||
|
|
||
| - name: Wait before retrying ${{ inputs.name }} | ||
| if: ${{ steps.attempt-1.outcome == 'failure' }} | ||
| shell: bash | ||
| run: | | ||
| echo "::warning::Upload of '${{ inputs.name }}' failed; retrying in 15s (attempt 2 of 3)." | ||
| sleep 15 | ||
|
|
||
| # Retries force overwrite: attempt 1 may have created the server-side record | ||
| # before failing, and CreateArtifact rejects a duplicate name. The delete is | ||
| # best effort inside upload-artifact, so it no-ops when nothing exists, and | ||
| # the per-producer naming rule keeps the record it does find our own. | ||
| - name: Upload ${{ inputs.name }} (attempt 2 of 3) | ||
| id: attempt-2 | ||
| if: ${{ steps.attempt-1.outcome == 'failure' }} | ||
| uses: actions/upload-artifact@v7 | ||
| continue-on-error: true | ||
| with: | ||
| name: ${{ inputs.name }} | ||
| path: ${{ inputs.path }} | ||
| if-no-files-found: ${{ inputs.if-no-files-found }} | ||
| retention-days: ${{ inputs.retention-days }} | ||
| compression-level: ${{ inputs.compression-level }} | ||
| overwrite: 'true' | ||
| include-hidden-files: ${{ inputs.include-hidden-files }} | ||
| archive: ${{ inputs.archive }} | ||
|
|
||
| - name: Wait before final retry of ${{ inputs.name }} | ||
| if: ${{ steps.attempt-1.outcome == 'failure' && steps.attempt-2.outcome == 'failure' }} | ||
| shell: bash | ||
| run: | | ||
| echo "::warning::Upload of '${{ inputs.name }}' failed again; retrying in 45s (attempt 3 of 3)." | ||
| sleep 45 | ||
|
|
||
| - name: Upload ${{ inputs.name }} (attempt 3 of 3) | ||
| id: attempt-3 | ||
| if: ${{ steps.attempt-1.outcome == 'failure' && steps.attempt-2.outcome == 'failure' }} | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: ${{ inputs.name }} | ||
| path: ${{ inputs.path }} | ||
| if-no-files-found: ${{ inputs.if-no-files-found }} | ||
| retention-days: ${{ inputs.retention-days }} | ||
| compression-level: ${{ inputs.compression-level }} | ||
| overwrite: 'true' | ||
| include-hidden-files: ${{ inputs.include-hidden-files }} | ||
| archive: ${{ inputs.archive }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correctness
[P2] Give parallel producers distinct artifact names before retrying with overwrite
ci.ymlruns the Linux native build and the Spark 3.5/4.1 builds in parallel, and all three uploadnative-lib-linux. The v7 delete selector selects the newest artifact with that name and sends its producer's job ID toDeleteArtifact. The pinned action documentation also explicitly supports overwriting an artifact from a different job. If one producer finishes while another fails before creating its artifact, this retry can delete the completed producer's artifact. Consumers depend only on their own producer and can then fail lookup during the replacement upload, or if that upload also fails. Please make the producer and download names unique per workflow/version before enabling forced overwrite. The parallel Iceberg versions also sharenative-lib-icebergwhen those jobs are enabled.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 5db3fc7. Each producer now qualifies the name with its version inputs, and the consumers follow:
spark_sql_test_reusable.yml:native-lib-spark-${{ inputs.spark-full }}-jdk${{ inputs.java }}, matching thejvm-compiled-spark-<full>-jdk<N>the same job already publishes.iceberg_spark_test_reusable.yml:native-lib-iceberg-${{ inputs.iceberg-full }}-spark-${{ inputs.spark-full }}-jdk${{ inputs.java }}.native-lib-linuxinpr_build_linux.ymlandnative-lib-macosinpr_build_macos.ymlkeep their bare names:ci.ymlcalls each of those workflows exactly once, so after the rename each has a single producer in the run.Since a shared name is invisible until a retry lands on it, the invariant is now enforced rather than just fixed.
dev/ci/check-ci-config.py(new, run frompreflight) fails when a reusable workflow thatci.ymlcalls more than once uploads an artifact whose name carries noinputs.reference, and when adownload-artifactname is not produced by an upload in the same workflow. I checked it by fault injection: reverting just the Spark producer back tonative-lib-linuxreports both the shared name and the now-orphaned consumer.