diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 0000000000..8f5136657b --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,44 @@ +name: "Dry-Run Cleanup" +run-name: "Dry Run Cleanup for ${{ github.ref }}" + +on: + workflow_dispatch: + inputs: + confirm: + description: Indicate whether you want this workflow to run (must be "true") + required: true + type: string + tag: + description: The name of the tag (and release) to clean up + required: true + type: string + +jobs: + release: + name: "Dry-Run Cleanup" + environment: release + runs-on: 'ubuntu-latest' + if: ${{ inputs.confirm == 'true' }} + + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: write + + # required by the mongodb-labs/drivers-github-tools/setup@v2 step + # also required by `rubygems/release-gem` + id-token: write + + steps: + - name: "Run the cleanup action" + uses: mongodb-labs/drivers-github-tools/ruby/cleanup@v2 + with: + app_id: ${{ vars.APP_ID }} + app_private_key: ${{ secrets.APP_PRIVATE_KEY }} + tag: ${{ inputs.tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13bb0c93f5..1232eb9e5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,17 @@ name: "Driver Release" -run-name: "Ruby Driver Release ${{ github.ref_name }}" +run-name: "Driver Release for ${{ github.ref }}" -on: workflow_dispatch +on: + workflow_dispatch: + inputs: + dry_run: + description: Whether this is a dry run or not + required: true + default: true + type: boolean env: + SILK_ASSET_GROUP: mongodb-ruby-driver RELEASE_MESSAGE_TEMPLATE: | Version {0} of the [MongoDB Ruby Driver](https://rubygems.org/gems/mongo) is now available. @@ -43,88 +51,17 @@ jobs: id-token: write steps: - - name: "Create temporary app token" - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: "Store GitHub token in environment" - run: echo "GH_TOKEN=${{ steps.app-token.outputs.token }}" >> "$GITHUB_ENV" - shell: bash - - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ env.GH_TOKEN }} - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.2' - bundler-cache: true - - - name: Setup GitHub tooling for DBX Drivers - uses: mongodb-labs/drivers-github-tools/setup@v2 + - name: "Run the publish action" + uses: mongodb-labs/drivers-github-tools/ruby/publish@v2 with: + app_id: ${{ vars.APP_ID }} + app_private_key: ${{ secrets.APP_PRIVATE_KEY }} aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} aws_region_name: ${{ vars.AWS_REGION_NAME }} aws_secret_id: ${{ secrets.AWS_SECRET_ID }} - - - name: Get the driver version - shell: bash - run: | - echo "DRIVER_VERSION=$(ruby -Ilib -rmongo/version -e 'puts Mongo::VERSION')" >> "$GITHUB_ENV" - - - name: Set output gem file name - shell: bash - run: | - echo "GEM_FILE_NAME=mongo-${{ env.DRIVER_VERSION }}.gem" >> "$GITHUB_ENV" - - - name: Build the gem - shell: bash - run: | - gem build --output=${{ env.GEM_FILE_NAME }} mongo.gemspec - - - name: Sign the gem - uses: mongodb-labs/drivers-github-tools/gpg-sign@v2 - with: - filenames: '${{ env.GEM_FILE_NAME }}' - - - name: Create and sign the tag - uses: mongodb-labs/drivers-github-tools/git-sign@v2 - with: - command: "git tag -u ${{ env.GPG_KEY_ID }} -m 'Release tag for v${{ env.DRIVER_VERSION }}' v${{ env.DRIVER_VERSION }}" - - - name: Push the tag to the repository - shell: bash - run: | - git push origin v${{ env.DRIVER_VERSION }} - - - name: Create a new release - shell: bash - run: gh release create v${{ env.DRIVER_VERSION }} --title ${{ env.DRIVER_VERSION }} --generate-notes --draft - - - name: Capture the changelog - shell: bash - run: gh release view v${{ env.DRIVER_VERSION }} --json body --template '{{ .body }}' >> changelog - - - name: Prepare release message - shell: bash - run: | - echo "${{ format(env.RELEASE_MESSAGE_TEMPLATE, env.DRIVER_VERSION) }}" > release-message - cat changelog >> release-message - - - name: Update release information - shell: bash - run: | - echo "RELEASE_URL=$(gh release edit v${{ env.DRIVER_VERSION }} --notes-file release-message)" >> "$GITHUB_ENV" - - - name: Upload release artifacts - run: gh release upload v${{ env.DRIVER_VERSION }} ${{ env.GEM_FILE_NAME }} ${{ env.RELEASE_ASSETS }}/${{ env.GEM_FILE_NAME }}.sig - - - name: Publish the gem - uses: rubygems/release-gem@v1 - with: - await-release: false + dry_run: ${{ inputs.dry_run }} + gem_name: mongo + product_name: Ruby Driver + product_id: mongodb-ruby-driver + release_message_template: ${{ env.RELEASE_MESSAGE_TEMPLATE }} + silk_asset_group: ${{ env.SILK_ASSET_GROUP }} diff --git a/Rakefile b/Rakefile index f9b1d89ad0..5b4bcc744f 100644 --- a/Rakefile +++ b/Rakefile @@ -46,6 +46,18 @@ task :build do WARNING end +# `rake version` is used by the deployment system so get the release version +# of the product beng deployed. It must do nothing more than just print the +# product version number. +# +# See the mongodb-labs/driver-github-tools/ruby/publish Github action. +desc "Print the current value of Mongo::VERSION" +task :version do + require 'mongo/version' + + puts Mongo::VERSION +end + # overrides the default Bundler-provided `release` task, which also # builds the gem. Our release process assumes the gem has already # been built (and signed via GPG), so we just need `rake release` to diff --git a/lib/mongo/retryable/base_worker.rb b/lib/mongo/retryable/base_worker.rb index d3f775abe8..d9ba821337 100644 --- a/lib/mongo/retryable/base_worker.rb +++ b/lib/mongo/retryable/base_worker.rb @@ -49,7 +49,8 @@ def initialize(retryable) private - # Indicate which exception classes that are generally retryable. + # Indicate which exception classes that are generally retryable + # when using modern retries mechanism. # # @return [ Array ] Array of exception classes that are # considered retryable. @@ -58,18 +59,42 @@ def retryable_exceptions Error::ConnectionPerished, Error::ServerNotUsable, Error::SocketError, - Error::SocketTimeoutError + Error::SocketTimeoutError, ].freeze end + # Indicate which exception classes that are generally retryable + # when using legacy retries mechanism. + # + # @return [ Array ] Array of exception classes that are + # considered retryable. + def legacy_retryable_exceptions + [ + Error::ConnectionPerished, + Error::ServerNotUsable, + Error::SocketError, + Error::SocketTimeoutError, + Error::PoolClearedError, + Error::PoolPausedError, + ].freeze + end + + # Tests to see if the given exception instance is of a type that can - # be retried. + # be retried with modern retry mechanism. # # @return [ true | false ] true if the exception is retryable. def is_retryable_exception?(e) retryable_exceptions.any? { |klass| klass === e } end + # Tests to see if the given exception instance is of a type that can + # be retried with legacy retry mechanism. + # + # @return [ true | false ] true if the exception is retryable. + def is_legacy_retryable_exception?(e) + legacy_retryable_exceptions.any? { |klass| klass === e } + end # Logs the given deprecation warning the first time it is called for a # given key; after that, it does nothing when given the same key. def deprecation_warning(key, warning) diff --git a/lib/mongo/retryable/read_worker.rb b/lib/mongo/retryable/read_worker.rb index 0a1f437901..eb3272ca34 100644 --- a/lib/mongo/retryable/read_worker.rb +++ b/lib/mongo/retryable/read_worker.rb @@ -227,10 +227,10 @@ def legacy_read_with_retry(session, server_selector, context = nil, &block) context&.check_timeout! attempt = attempt ? attempt + 1 : 1 yield select_server(cluster, server_selector, session) - rescue *retryable_exceptions, Error::OperationFailure::Family, Error::PoolError => e + rescue *legacy_retryable_exceptions, Error::OperationFailure::Family => e e.add_notes('legacy retry', "attempt #{attempt}") - if is_retryable_exception?(e) + if is_legacy_retryable_exception?(e) raise e if attempt > client.max_read_retries || session&.in_transaction? elsif e.retryable? && !session&.in_transaction? raise e if attempt > client.max_read_retries diff --git a/spec/integration/retryable_reads_errors_spec.rb b/spec/integration/retryable_reads_errors_spec.rb index a378eecfd0..f767bb46fb 100644 --- a/spec/integration/retryable_reads_errors_spec.rb +++ b/spec/integration/retryable_reads_errors_spec.rb @@ -74,31 +74,42 @@ client.subscribe(Mongo::Monitoring::CONNECTION_POOL, subscriber) end - it "retries on PoolClearedError" do - # After the first find fails, the pool is paused and retry is triggered. - # Now, a race is started between the second find acquiring a connection, - # and the first retrying the read. Now, retry reads cause the cluster to - # be rescanned and the pool to be unpaused, allowing the second checkout - # to succeed (when it should fail). Therefore we want the second find's - # check out to win the race. This gives the check out a little head start. - allow_any_instance_of(Mongo::Server::ConnectionPool).to receive(:ready).and_wrap_original do |m, *args, &block| - ::Utils.wait_for_condition(5) do - # check_out_results should contain: - # - find1 connection check out successful - # - pool cleared - # - find2 connection check out failed - # We wait here for the third event to happen before we ready the pool. - cmap_events.select do |e| - event_types.include?(e.class) - end.length >= 3 + shared_examples_for 'retries on PoolClearedError' do + it "retries on PoolClearedError" do + # After the first find fails, the pool is paused and retry is triggered. + # Now, a race is started between the second find acquiring a connection, + # and the first retrying the read. Now, retry reads cause the cluster to + # be rescanned and the pool to be unpaused, allowing the second checkout + # to succeed (when it should fail). Therefore we want the second find's + # check out to win the race. This gives the check out a little head start. + allow_any_instance_of(Mongo::Server::ConnectionPool).to receive(:ready).and_wrap_original do |m, *args, &block| + ::Utils.wait_for_condition(5) do + # check_out_results should contain: + # - find1 connection check out successful + # - pool cleared + # - find2 connection check out failed + # We wait here for the third event to happen before we ready the pool. + cmap_events.select do |e| + event_types.include?(e.class) + end.length >= 3 + end + m.call(*args, &block) end - m.call(*args, &block) + threads.map(&:join) + expect(check_out_results[0]).to be_a(Mongo::Monitoring::Event::Cmap::ConnectionCheckedOut) + expect(check_out_results[1]).to be_a(Mongo::Monitoring::Event::Cmap::PoolCleared) + expect(check_out_results[2]).to be_a(Mongo::Monitoring::Event::Cmap::ConnectionCheckOutFailed) + expect(find_events.length).to eq(3) end - threads.map(&:join) - expect(check_out_results[0]).to be_a(Mongo::Monitoring::Event::Cmap::ConnectionCheckedOut) - expect(check_out_results[1]).to be_a(Mongo::Monitoring::Event::Cmap::PoolCleared) - expect(check_out_results[2]).to be_a(Mongo::Monitoring::Event::Cmap::ConnectionCheckOutFailed) - expect(find_events.length).to eq(3) + end + + it_behaves_like 'retries on PoolClearedError' + + context 'legacy read retries' do + + let(:client) { authorized_client.with(options.merge(retry_reads: false, max_read_retries: 1)) } + + it_behaves_like 'retries on PoolClearedError' end after do