[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"bundler\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    reviewers:\n      - \"andrcuns\"\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    reviewers:\n      - andrcuns\n    labels:\n      - \"ci\"\n"
  },
  {
    "path": ".github/release.yml",
    "content": "changelog:\n  categories:\n    - title: '🚀 New feature or request'\n      labels: \n        - 'enhancement'\n    - title: '🐞 Bug Fixes'\n      labels: \n        - 'bug'\n    - title: '📦 Dependency updates'\n      labels: \n        - 'dependencies'\n    - title: '🧰 Maintenance'\n      labels: \n        - 'maintenance'\n"
  },
  {
    "path": ".github/workflows/changelog.yml",
    "content": "name: Changelog\n\non:\n  push:\n    tags:\n      - v[0-9]+.[0-9]+.[0-9]+\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v4\n      -\n        name: Create GitHub release\n        uses: softprops/action-gh-release@v2\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          generate_release_notes: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      semver:\n        description: Bump\n        required: true\n        type: choice\n        options:\n          - major\n          - minor\n          - patch\n\njobs:\n  release:\n    name: Ruby gem\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n          token: ${{ secrets.RELEASE_GITHUB_TOKEN }}\n      -\n        name: Set up Ruby 3.3\n        uses: ruby/setup-ruby@v1\n        with:\n          bundler-cache: true\n      -\n        name: Update version\n        run: |\n          git config user.name github-actions\n          git config user.email github-actions@github.com\n          bundle config unset deployment\n          bundle exec rake \"version[${{ inputs.semver }}]\" && git push\n      -\n        name: Create tag and push to rubygems\n        run: bundle exec rake release\n        env:\n          GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\njobs:\n  rubocop:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Ruby\n        uses: ruby/setup-ruby@v1\n        with:\n          bundler-cache: true\n          cache-version: 1\n      - name: Run lint\n        run: bundle exec rubocop --color\n  test:\n    runs-on: ubuntu-latest\n    needs: rubocop\n    strategy:\n      fail-fast: false\n      matrix:\n        ruby-version: [\"3.4\", \"3.3\", \"3.2\"]\n        sidekiq-version: [\"~> 6.5\", \"~> 7\", \"~> 8\"]\n        exclude:\n          - sidekiq-version: \"~> 8\"\n            ruby-version: \"3.1\"\n    # Service containers to run with `runner-job`\n    services:\n      # Label used to access the service container\n      redis:\n        # Docker Hub image\n        image: redis\n        # Set health checks to wait until redis has started\n        options: >-\n          --health-cmd \"redis-cli ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          # Maps port 6379 on service container to the host\n          - 6379:6379\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Ruby ${{ matrix.ruby-version }} with Sidekiq ${{ matrix.sidekiq-version }}\n        uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ matrix.ruby-version }}\n          bundler-cache: true\n          cache-version: 1\n        env:\n          SIDEKIQ_VERSION_RANGE: ${{ matrix.sidekiq-version }}\n      - name: Run tests\n        run: bundle exec rspec --force-color\n        env:\n          SIDEKIQ_VERSION_RANGE: ${{ matrix.sidekiq-version }}\n      - name: Add coverage report\n        uses: insightsengineering/coverage-action@v2\n        # TODO: Add coverage merging from different test runs\n        if: ${{ matrix.ruby-version == '3.4' && matrix.sidekiq-version == '~> 7' }}\n        with:\n          path: coverage/coverage.xml\n          publish: true\n          threshold: 90\n          pycobertura-exception-failure: false\n          diff: true\n          diff-branch: master\n          coverage-reduction-failure: true\n"
  },
  {
    "path": ".gitignore",
    "content": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\nGemfile.lock\n\n# rspec failure tracking\n.rspec_status\nvendor\n"
  },
  {
    "path": ".rspec",
    "content": "--format documentation\n--color\n--require spec_helper\n--order random\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "inherit_gem:\n  rubocop-shopify: rubocop.yml\n\nAllCops:\n  TargetRubyVersion: 3.1\n  SuggestExtensions: false\n  NewCops: enable\n\nLayout/ArgumentAlignment:\n  Enabled: true\n  EnforcedStyle: with_first_argument\n\nStyle/InvertibleUnlessCondition:\n  Exclude:\n    - \"lib/sidekiq_alive.rb\"\n"
  },
  {
    "path": ".ruby-version",
    "content": "3.4.4\n"
  },
  {
    "path": ".tool-versions",
    "content": "ruby 3.4.4\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, gender identity and expression, level of experience,\nnationality, personal appearance, race, religion, or sexual identity and\norientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\nadvances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at arturictus@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "Gemfile",
    "content": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngit_source(:github) { |repo_name| \"https://github.com/#{repo_name}\" }\n\n# Specify your gem's dependencies in sidekiq_alive.gemspec\ngemspec\n\ngem \"sidekiq\", ENV[\"SIDEKIQ_VERSION_RANGE\"] || \"< 9\"\n\ngem \"ruby-lsp\", \"~> 0.23.11\", group: :development\n\ngroup :test do\n  gem \"simplecov\", require: false\n  gem \"simplecov-cobertura\", require: false\n\n  # used for testing rack based server\n  gem \"rack-test\", \"~> 2.2.0\"\n  # rackup is not compatible with sidekiq < 7 due to rack version requirement\n  if [\"7\", \"8\"].any? { |range| ENV[\"SIDEKIQ_VERSION_RANGE\"]&.include?(range) }\n    gem \"rackup\", \"~> 2.2.0\"\n  else\n    gem \"rack\", \"< 3\"\n    gem \"webrick\", \"< 2\"\n  end\nend\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2018 Artur Pañach\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# SidekiqAlive\n\n[![Gem Version](https://badge.fury.io/rb/sidekiq_alive.svg)](https://rubygems.org/gems/sidekiq_alive)\n[![Total Downloads](https://img.shields.io/gem/dt/sidekiq_alive?color=blue)](https://rubygems.org/gems/https://rubygems.org/gems/sidekiq_alive)\n![Workflow status](https://github.com/allure-framework/allure-ruby/workflows/Test/badge.svg)\n\n---\n\nSidekiqAlive offers a solution to add liveness probe for a Sidekiq instance deployed in Kubernetes.\nThis library can be used to check sidekiq health outside kubernetes.\n\n**How?**\n\nA http server is started and on each requests validates that a liveness key is stored in Redis. If it is there means is working.\n\nA Sidekiq worker is the responsible to storing this key. If Sidekiq stops processing workers\nthis key gets expired by Redis an consequently the http server will return a 500 error.\n\nThis worker is responsible to requeue itself for the next liveness probe.\n\nEach instance in kubernetes will be checked based on `ENV` variable `HOSTNAME` (kubernetes sets this for each replica/pod).\n\nOn initialization SidekiqAlive will asign to Sidekiq::Worker a queue with the current host and add this queue to the current instance queues to process.\n\nexample:\n\n```\nhostname: foo\n  Worker queue: sidekiq_alive-foo\n  instance queues:\n   - sidekiq_alive-foo\n   *- your queues\n\nhostname: bar\n  Worker queue: sidekiq_alive-bar\n  instance queues:\n   - sidekiq_alive-bar\n   *- your queues\n```\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'sidekiq_alive'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install sidekiq_alive\n\n## Usage\n\nSidekiqAlive will start when running `sidekiq` command.\n\nRun `Sidekiq`\n\n```\nbundle exec sidekiq\n```\n\n```\ncurl localhost:7433\n#=> Alive!\n```\n\n**how to disable?**\nYou can disabled by setting `ENV` variable `DISABLE_SIDEKIQ_ALIVE`\nexample:\n\n```\nDISABLE_SIDEKIQ_ALIVE=true bundle exec sidekiq\n```\n\n### Kubernetes setup\n\nSet `livenessProbe` in your Kubernetes deployment\n\nexample with recommended setup:\n\n#### Sidekiq < 6\n\n```yaml\nspec:\n  containers:\n    - name: my_app\n      image: my_app:latest\n      env:\n        - name: RAILS_ENV\n          value: production\n      command:\n        - bundle\n        - exec\n        - sidekiq\n      ports:\n        - containerPort: 7433\n      livenessProbe:\n        httpGet:\n          path: /\n          port: 7433\n        initialDelaySeconds: 80 # app specific. Time your sidekiq takes to start processing.\n        timeoutSeconds: 5 # can be much less\n      readinessProbe:\n        httpGet:\n          path: /\n          port: 7433\n        initialDelaySeconds: 80 # app specific\n        timeoutSeconds: 5 # can be much less\n      lifecycle:\n        preStop:\n          exec:\n            # SIGTERM triggers a quick exit; gracefully terminate instead\n            command: ['bundle', 'exec', 'sidekiqctl', 'quiet']\n  terminationGracePeriodSeconds: 60 # put your longest Job time here plus security time.\n```\n\n#### Sidekiq >= 6\n\nCreate file:\n\n_kube/sidekiq_quiet_\n\n```bash\n#!/bin/bash\n\n# Find Pid\nSIDEKIQ_PID=$(ps aux | grep sidekiq | grep busy | awk '{ print $2 }')\n# Send TSTP signal. Note: Alpine Linux needs to use `kill -s SIGTSTP` instead of `kill -TSTP`\nkill -SIGTSTP $SIDEKIQ_PID\n```\n\nMake it executable:\n\n```\n$ chmod +x kube/sidekiq_quiet\n```\n\nExecute it in your deployment preStop:\n\n```yaml\nspec:\n  containers:\n    - name: my_app\n      image: my_app:latest\n      env:\n        - name: RAILS_ENV\n          value: production\n      command:\n        - bundle\n        - exec\n        - sidekiq\n      ports:\n        - containerPort: 7433\n      livenessProbe:\n        httpGet:\n          path: /\n          port: 7433\n        initialDelaySeconds: 80 # app specific. Time your sidekiq takes to start processing.\n        timeoutSeconds: 5 # can be much less\n      readinessProbe:\n        httpGet:\n          path: /\n          port: 7433\n        initialDelaySeconds: 80 # app specific\n        timeoutSeconds: 5 # can be much less\n      lifecycle:\n        preStop:\n          exec:\n            # SIGTERM triggers a quick exit; gracefully terminate instead\n            command: ['kube/sidekiq_quiet']\n  terminationGracePeriodSeconds: 60 # put your longest Job time here plus security time.\n```\n\n### Outside kubernetes\n\nIt's just up to you how you want to use it.\n\nAn example in local would be:\n\n```\nbundle exec sidekiq\n# let it initialize ...\n```\n\n```\ncurl localhost:7433\n#=> Alive!\n```\n\n## Options\n\n```ruby\nSidekiqAlive.setup do |config|\n  # ==> Server host\n  # Host to bind the server.\n  # Can also be set with the environment variable SIDEKIQ_ALIVE_HOST.\n  # default: 0.0.0.0\n  #\n  #   config.host = 0.0.0.0\n\n  # ==> Server port\n  # Port to bind the server.\n  # Can also be set with the environment variable SIDEKIQ_ALIVE_PORT.\n  # default: 7433\n  #\n  #   config.port = 7433\n\n  # ==> Server path\n  # HTTP path to respond to.\n  # Can also be set with the environment variable SIDEKIQ_ALIVE_PATH.\n  # default: '/'\n  #\n  #   config.path = '/'\n\n  # ==> Custom Liveness Probe\n  # Extra check to decide if restart the pod or not for example connection to DB.\n  # `false`, `nil` or `raise` will not write the liveness probe\n  # default: proc { true }\n  #\n  #     config.custom_liveness_probe = proc { db_running? }\n\n  # ==> Liveness key\n  # Key to be stored in Redis as probe of liveness\n  # default: \"SIDEKIQ::LIVENESS_PROBE_TIMESTAMP\"\n  #\n  #   config.liveness_key = \"SIDEKIQ::LIVENESS_PROBE_TIMESTAMP\"\n\n  # ==> Time to live\n  # Time for the key to be kept by Redis.\n  # Here is where you can set de periodicity that the Sidekiq has to probe it is working\n  # Time unit: seconds\n  # default: 10 * 60 # 10 minutes\n  #\n  #   config.time_to_live = 10 * 60\n\n  # ==> Callback\n  # After the key is stored in redis you can perform anything.\n  # For example a webhook or email to notify the team\n  # default: proc {}\n  #\n  #    require 'net/http'\n  #    config.callback = proc { Net::HTTP.get(\"https://status.com/ping\") }\n\n  # ==> Shutdown callback\n  # When sidekiq process is shutting down, you can perform some arbitrary action.\n  # default: proc {}\n  #\n  #    config.shutdown_callback = proc { puts \"Sidekiq is shutting down\" }\n\n  # ==> Queue Prefix\n  # SidekiqAlive will run in a independent queue for each instance/replica\n  # This queue name will be generated with: \"#{queue_prefix}-#{hostname}.\n  # You can customize the prefix here.\n  # default: :sidekiq-alive\n  #\n  #    config.queue_prefix = :other\n\n  # ==> Concurrency\n  # The maximum number of Redis connections requested for the SidekiqAlive pool.\n  # Can also be set with the environment variable SIDEKIQ_ALIVE_CONCURRENCY.\n  # NOTE: only effects Sidekiq 7 or greater.\n  # default: 2\n  #\n  #    config.concurrency = 3\n\n  # ==> Rack server\n  # Web server used to serve an HTTP response. By default simple GServer based http server is used.\n  # To use specific server, rack gem version > 2 is required. For rack version >= 3, rackup gem is required.\n  # Can also be set with the environment variable SIDEKIQ_ALIVE_SERVER.\n  # default: nil\n  #\n  #    config.server = 'puma'\n\n  # ==> Quiet mode timeout in seconds\n  # When sidekiq is shutting down, the Sidekiq process stops pulling jobs from the queue. This includes alive key update job. In case of\n  # long running jobs, alive key can expire before the job is finished. To avoid this, web server is set in to quiet mode\n  # and is returning 200 OK for healthcheck requests. To avoid infinite quiet mode in case sidekiq process is stuck in shutdown,\n  # timeout can be set. After timeout is reached, web server resumes normal operations and will return unhealthy status in case\n  # alive key is expired or purged from redis.\n  # default: 180\n  #\n  #    config.quiet_timeout = 300\n\nend\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`.\n\nHere is an example [rails app](https://github.com/arturictus/sidekiq_alive_example)\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/arturictus/sidekiq_alive. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n"
  },
  {
    "path": "Rakefile",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\n\nRSpec::Core::RakeTask.new(:spec)\ntask default: :spec\n\nload \"tasks/version.rake\"\nSidekiqAlive::VersionTask.new\n"
  },
  {
    "path": "bin/console",
    "content": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"sidekiq_alive\"\n\n# You can add fixtures and/or initialization code here to make experimenting\n# with your gem easier. You can also use a different console, if you like.\n\n# (If you use this, don't forget to add pry to your Gemfile!)\n# require \"pry\"\n# Pry.start\n\nrequire \"irb\"\nIRB.start(__FILE__)\n"
  },
  {
    "path": "bin/setup",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\n\n# Do any other automated setup that you need to do here\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.6'\nservices:\n  redis:\n    image: redis\n    ports:\n      - 6379:6379\n"
  },
  {
    "path": "lib/sidekiq_alive/config.rb",
    "content": "# frozen_string_literal: true\n\nmodule SidekiqAlive\n  class Config\n    include Singleton\n\n    attr_accessor :host,\n                  :port,\n                  :path,\n                  :liveness_key,\n                  :time_to_live,\n                  :callback,\n                  :registered_instance_key,\n                  :queue_prefix,\n                  :custom_liveness_probe,\n                  :logger,\n                  :shutdown_callback,\n                  :concurrency,\n                  :server,\n                  :quiet_timeout\n\n    def initialize\n      set_defaults\n    end\n\n    def set_defaults\n      @host = ENV.fetch(\"SIDEKIQ_ALIVE_HOST\", \"0.0.0.0\")\n      @port = ENV.fetch(\"SIDEKIQ_ALIVE_PORT\", 7433)\n      @path = ENV.fetch(\"SIDEKIQ_ALIVE_PATH\", \"/\")\n      @liveness_key = \"SIDEKIQ::LIVENESS_PROBE_TIMESTAMP\"\n      @time_to_live = 10 * 60\n      @callback = proc {}\n      @registered_instance_key = \"SIDEKIQ_REGISTERED_INSTANCE\"\n      @queue_prefix = :\"sidekiq-alive\"\n      @custom_liveness_probe = proc { true }\n      @shutdown_callback = proc {}\n      @concurrency = Integer(ENV.fetch(\"SIDEKIQ_ALIVE_CONCURRENCY\", 2), exception: false) || 2\n      @server = ENV.fetch(\"SIDEKIQ_ALIVE_SERVER\", nil)\n      @quiet_timeout = Integer(ENV.fetch(\"SIDEKIQ_ALIVE_QUIET_TIMEOUT\", 180), exception: false) || 180\n    end\n\n    def registration_ttl\n      @registration_ttl || time_to_live * 3\n    end\n\n    def worker_interval\n      time_to_live / 2\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/helpers.rb",
    "content": "# frozen_string_literal: true\n\nmodule SidekiqAlive\n  module Helpers\n    class << self\n      def sidekiq_7?\n        current_sidekiq_version >= Gem::Version.new(\"7\")\n      end\n\n      def sidekiq_6?\n        current_sidekiq_version >= Gem::Version.new(\"6\") &&\n          current_sidekiq_version < Gem::Version.new(\"7\")\n      end\n\n      def sidekiq_5?\n        current_sidekiq_version >= Gem::Version.new(\"5\") &&\n          current_sidekiq_version < Gem::Version.new(\"6\")\n      end\n\n      def use_rack?\n        return @use_rack if defined?(@use_rack)\n\n        require \"rack\"\n        @use_rack = current_rack_version < Gem::Version.new(\"3\")\n      rescue LoadError\n        # currently this won't happen because rack is a dependency of sidekiq\n        @use_rack = false\n      end\n\n      def use_rackup?\n        return @use_rackup if defined?(@use_rackup)\n\n        require \"rackup\"\n        @use_rackup = current_rack_version >= Gem::Version.new(\"3\")\n      rescue LoadError\n        if current_rack_version >= Gem::Version.new(\"3\")\n          SidekiqAlive.logger.warn(\"rackup gem required with rack >= 3, defaulting to default server\")\n        end\n        @use_rackup = false\n      end\n\n      private\n\n      def current_sidekiq_version\n        Gem.loaded_specs[\"sidekiq\"].version\n      end\n\n      def current_rack_version\n        Gem.loaded_specs[\"rack\"].version\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/redis/base.rb",
    "content": "# frozen_string_literal: true\n\nmodule SidekiqAlive\n  module Redis\n    class Base\n      def set(...)\n        raise(NotImplementedError)\n      end\n\n      def zadd(set_key, ex, key)\n        raise(NotImplementedError)\n      end\n\n      def zrange(set_key, start, stop)\n        raise(NotImplementedError)\n      end\n\n      def zrangebyscore(set_key, min, max)\n        raise(NotImplementedError)\n      end\n\n      def zrem(set_key, key)\n        raise(NotImplementedError)\n      end\n\n      def delete(key)\n        raise(NotImplementedError)\n      end\n\n      def ttl(...)\n        redis { |r| r.ttl(...) }\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/redis/redis_client_gem.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"base\"\n\nmodule SidekiqAlive\n  module Redis\n    # Wrapper for `redis-client` gem used by `sidekiq` > 7\n    # https://github.com/redis-rb/redis-client\n    class RedisClientGem < Base\n      def initialize(capsule = nil)\n        super()\n\n        @capsule = Sidekiq.default_configuration.capsules[capsule || CAPSULE_NAME]\n      end\n\n      def set(key, time:, ex:)\n        redis { |r| r.call(\"SET\", key, time, ex: ex) }\n      end\n\n      def get(key)\n        redis { |r| r.call(\"GET\", key) }\n      end\n\n      def zadd(set_key, ex, key)\n        redis { |r| r.call(\"ZADD\", set_key, ex, key) }\n      end\n\n      def zrange(set_key, start, stop)\n        redis { |r| r.call(\"ZRANGE\", set_key, start, stop) }\n      end\n\n      def zrangebyscore(set_key, min, max)\n        redis { |r| r.call(\"ZRANGEBYSCORE\", set_key, min, max) }\n      end\n\n      def zrem(set_key, key)\n        redis { |r| r.call(\"ZREM\", set_key, key) }\n      end\n\n      def delete(key)\n        redis { |r| r.call(\"DEL\", key) }\n      end\n\n      private\n\n      def redis(&block)\n        # Default to Sidekiq.redis if capsule is not configured yet but redis adapter is accessed\n        (@capsule || Sidekiq).redis(&block)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/redis/redis_gem.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"base\"\n\nmodule SidekiqAlive\n  module Redis\n    # Wrapper for `redis` gem used by sidekiq < 7\n    # https://github.com/redis/redis-rb\n    class RedisGem < Base\n      def set(key, time:, ex:)\n        redis { |r| r.set(key, time, ex: ex) }\n      end\n\n      def get(key)\n        redis { |r| r.get(key) }\n      end\n\n      def zadd(set_key, ex, key)\n        redis { |r| r.zadd(set_key, ex, key) }\n      end\n\n      def zrange(set_key, start, stop)\n        redis { |r| r.zrange(set_key, start, stop) }\n      end\n\n      def zrangebyscore(set_key, min, max)\n        redis { |r| r.zrangebyscore(set_key, min, max) }\n      end\n\n      def zrem(set_key, key)\n        redis { |r| r.zrem(set_key, key) }\n      end\n\n      def delete(key)\n        redis { |r| r.del(key) }\n      end\n\n      private\n\n      def redis(&block)\n        Sidekiq.redis(&block)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/redis.rb",
    "content": "# frozen_string_literal: true\n\nmodule SidekiqAlive\n  module Redis\n    class << self\n      def adapter(capsule = nil)\n        Helpers.sidekiq_7? ? Redis::RedisClientGem.new(capsule) : Redis::RedisGem.new\n      end\n    end\n  end\nend\n\nrequire_relative \"redis/base\"\nrequire_relative \"redis/redis_client_gem\"\nrequire_relative \"redis/redis_gem\"\n"
  },
  {
    "path": "lib/sidekiq_alive/server/base.rb",
    "content": "# frozen_string_literal: true\n\nmodule SidekiqAlive\n  module Server\n    module Base\n      SHUTDOWN_SIGNAL = \"TERM\"\n      QUIET_SIGNAL = \"TSTP\"\n\n      # set web server to quiet mode\n      def quiet!\n        logger.info(\"[SidekiqAlive] Setting web server to quiet mode\")\n        Process.kill(QUIET_SIGNAL, @server_pid) unless @server_pid.nil?\n      end\n\n      private\n\n      def configure_shutdown\n        Kernel.at_exit do\n          next if @server_pid.nil?\n\n          logger.info(\"Shutting down SidekiqAlive web server\")\n          Process.kill(SHUTDOWN_SIGNAL, @server_pid)\n          Process.wait(@server_pid)\n        end\n      end\n\n      def configure_shutdown_signal(&block)\n        Signal.trap(SHUTDOWN_SIGNAL, &block)\n      end\n\n      def configure_quiet_signal(&block)\n        Signal.trap(QUIET_SIGNAL, &block)\n      end\n\n      def host\n        SidekiqAlive.config.host\n      end\n\n      def port\n        SidekiqAlive.config.port.to_i\n      end\n\n      def path\n        SidekiqAlive.config.path\n      end\n\n      def logger\n        SidekiqAlive.logger\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/server/default.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"http_server\"\nrequire_relative \"base\"\n\nmodule SidekiqAlive\n  module Server\n    class Default < HttpServer\n      extend Base\n\n      class << self\n        def run!\n          logger.info(\"[SidekiqAlive] Starting default healthcheck server on #{host}:#{port}\")\n          @server_pid = ::Process.fork do\n            @server = new(port, host, path)\n            # stop is wrapped in a thread because gserver calls synchrnonize which raises an error when in trap context\n            configure_shutdown_signal { Thread.new { @server.stop } }\n            configure_quiet_signal { @server.quiet! }\n\n            @server.start\n            @server.join\n          end\n          configure_shutdown\n          logger.info(\"[SidekiqAlive] Web server started in subprocess with pid #{@server_pid}\")\n\n          self\n        end\n      end\n\n      def initialize(port, host, path, logger = SidekiqAlive.logger)\n        super(self, port, host, logger)\n\n        @path = path\n      end\n\n      def request_handler(req, res)\n        if req.path != path\n          res.status = 404\n          res.body = \"Not found\"\n          return logger.warn(\"[SidekiqAlive] Path '#{req.path}' not found\")\n        end\n\n        if quiet?\n          res.status = 200\n          res.body = \"Server is shutting down\"\n          return logger.debug(\"[SidekiqAlive] Server in quiet mode, skipping alive key lookup!\")\n        end\n\n        if SidekiqAlive.alive?\n          res.status = 200\n          res.body = \"Alive!\"\n          return logger.debug(\"[SidekiqAlive] Found alive key!\")\n        end\n\n        response = \"Can't find the alive key\"\n        res.status = 404\n        res.body = response\n        logger.error(\"[SidekiqAlive] #{response}\")\n      rescue StandardError => e\n        response = \"Internal Server Error\"\n        res.status = 500\n        res.body = response\n        logger.error(\"[SidekiqAlive] #{response} looking for alive key. Error: #{e.message}\")\n      end\n\n      def quiet!\n        @quiet = Time.now\n      end\n\n      private\n\n      attr_reader :path\n\n      def quiet?\n        @quiet && (Time.now - @quiet) < SidekiqAlive.config.quiet_timeout\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/server/http_server.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"gserver\"\n\nmodule SidekiqAlive\n  module Server\n    # Simple HTTP server implementation\n    #\n    class HttpServer < GServer\n      # Request class for HTTP server\n      #\n      class Request\n        attr_reader :data, :header, :method, :path, :proto\n\n        def initialize(data, method = nil, path = nil, proto = nil)\n          @header = {}\n          @data = data\n          @method = method\n          @path = path\n          @proto = proto\n        end\n\n        def content_length\n          len = @header[\"Content-Length\"]\n          return if len.nil?\n\n          len.to_i\n        end\n      end\n\n      # Response class for HTTP server\n      #\n      class Response\n        attr_reader   :header\n        attr_accessor :body, :status, :status_message\n\n        def initialize(status = 200)\n          @status = status\n          @status_message = nil\n          @header = {}\n        end\n      end\n\n      def initialize(handle_obj, port, host, logger = Logger.new($stdout))\n        @handler = handle_obj\n        @logger = logger\n\n        super(port, host, 1, nil, logger.debug?, logger.debug?)\n      end\n\n      private\n\n      attr_reader :handler, :logger\n\n      CRLF        = \"\\r\\n\"\n      HTTP_PROTO  = \"HTTP/1.1\"\n      SERVER_NAME = \"SidekiqAlive/#{SidekiqAlive::VERSION} (Ruby/#{RUBY_VERSION})\"\n\n      # Default header for the server name\n      DEFAULT_HEADER = {\n        \"Server\" => SERVER_NAME,\n      }\n\n      # Mapping of status codes and error messages\n      STATUS_CODE_MAPPING = {\n        200 => \"OK\",\n        400 => \"Bad Request\",\n        403 => \"Forbidden\",\n        404 => \"Not Found\",\n        405 => \"Method Not Allowed\",\n        411 => \"Length Required\",\n        500 => \"Internal Server Error\",\n      }\n\n      def serve(io)\n        # parse first line\n        if io.gets =~ /^(\\S+)\\s+(\\S+)\\s+(\\S+)/\n          request = Request.new(io, ::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3))\n        else\n          io << http_resp(status_code: 400)\n          return\n        end\n\n        # parse HTTP headers\n        while (line = io.gets) !~ /^(\\n|\\r)/\n          if line =~ /^([\\w-]+):\\s*(.*)$/\n            request.header[::Regexp.last_match(1)] = ::Regexp.last_match(2).strip\n          end\n        end\n\n        io.binmode\n        response = Response.new\n\n        # execute request handler\n        handler.request_handler(request, response)\n\n        http_response = http_resp(\n          status_code: response.status,\n          status_message: response.status_message,\n          header: response.header,\n          body: response.body,\n        )\n\n        # write response back to the client\n        io << http_response\n      rescue StandardError\n        io << http_resp(status_code: 500)\n      end\n\n      def http_header(header = nil)\n        new_header = DEFAULT_HEADER.dup\n        new_header.merge(header) unless header.nil?\n\n        new_header[\"Connection\"] = \"Keep-Alive\"\n        new_header[\"Date\"] = http_date(Time.now)\n\n        new_header\n      end\n\n      def http_resp(status_code:, status_message: nil, header: nil, body: nil)\n        status_message ||= STATUS_CODE_MAPPING[status_code]\n        status_line = \"#{HTTP_PROTO} #{status_code} #{status_message}\".rstrip + CRLF\n\n        resp_header = http_header(header)\n        resp_header[\"Content-Length\"] = body.bytesize.to_s unless body.nil?\n        header_lines = resp_header.map { |k, v| \"#{k}: #{v}#{CRLF}\" }.join\n\n        [status_line, header_lines, CRLF, body].compact.join\n      end\n\n      def http_date(a_time)\n        a_time.gmtime.strftime(\"%a, %d %b %Y %H:%M:%S GMT\")\n      end\n\n      def log(msg)\n        logger.debug(msg)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/server/rack.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"base\"\n\nmodule SidekiqAlive\n  module Server\n    class Rack\n      extend Base\n\n      class << self\n        def run!\n          logger.info(\"[SidekiqAlive] Starting healthcheck '#{server}' server\")\n          @server_pid = ::Process.fork do\n            @handler = handler\n            configure_shutdown_signal { @handler.shutdown }\n            configure_quiet_signal { @quiet = Time.now }\n\n            @handler.run(self, Port: port, Host: host, AccessLog: [], Logger: logger)\n          end\n          configure_shutdown\n\n          self\n        end\n\n        def call(env)\n          req = ::Rack::Request.new(env)\n\n          if req.path != path\n            logger.warn(\"[SidekiqAlive] Path '#{req.path}' not found\")\n            return [404, {}, [\"Not found\"]]\n          end\n\n          if quiet?\n            logger.debug(\"[SidekiqAlive] [SidekiqAlive] Server in quiet mode, skipping alive key lookup!\")\n            return [200, {}, [\"Server is shutting down\"]]\n          end\n\n          if SidekiqAlive.alive?\n            logger.debug(\"[SidekiqAlive] Found alive key!\")\n            return [200, {}, [\"Alive!\"]]\n          end\n\n          response = \"Can't find the alive key\"\n          logger.error(\"[SidekiqAlive] #{response}\")\n          [404, {}, [response]]\n        rescue StandardError => e\n          logger.error(\"[SidekiqAlive] #{response} looking for alive key. Error: #{e.message}\")\n          [500, {}, [\"Internal Server Error\"]]\n        end\n\n        private\n\n        def quiet?\n          @quiet && (Time.now - @quiet) < SidekiqAlive.config.quiet_timeout\n        end\n\n        def handler\n          Helpers.use_rackup? ? ::Rackup::Handler.get(server) : ::Rack::Handler.get(server)\n        end\n\n        def server\n          SidekiqAlive.config.server\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/server.rb",
    "content": "# frozen_string_literal: true\n\nmodule SidekiqAlive\n  module Server\n    class << self\n      def run!\n        server.run!\n      end\n\n      private\n\n      def server\n        use_rack? ? Rack : Default\n      end\n\n      def use_rack?\n        return false unless SidekiqAlive.config.server\n\n        Helpers.use_rackup? || Helpers.use_rack?\n      end\n\n      def logger\n        SidekiqAlive.logger\n      end\n    end\n  end\nend\n\nrequire_relative \"server/default\"\nrequire_relative \"server/rack\"\n"
  },
  {
    "path": "lib/sidekiq_alive/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule SidekiqAlive\n  VERSION = \"2.5.0\"\nend\n"
  },
  {
    "path": "lib/sidekiq_alive/worker.rb",
    "content": "# frozen_string_literal: true\n\nmodule SidekiqAlive\n  class Worker\n    include Sidekiq::Worker\n\n    sidekiq_options retry: false\n\n    # Passing the hostname argument it's only for debugging enqueued jobs\n    def perform(_hostname = SidekiqAlive.hostname)\n      # Checks if custom liveness probe passes should fail or return false\n      return unless config.custom_liveness_probe.call\n\n      # Writes the liveness in Redis\n      write_living_probe\n      remove_orphaned_queues\n      # schedules next living probe\n      self.class.perform_in(config.worker_interval, current_hostname)\n    end\n\n    def write_living_probe\n      # Write liveness probe\n      SidekiqAlive.store_alive_key\n      # Increment ttl for current registered instance\n      SidekiqAlive.register_current_instance\n      # after callbacks\n      begin\n        config.callback.call\n      rescue StandardError\n        nil\n      end\n    end\n\n    # Removes orphaned Sidekiq queues left behind by unexpected instance shutdowns (e.g., due to OOM)\n    def remove_orphaned_queues\n      # If the worker isn't executed within this window, the lifeness key expires\n      latency_threshold = config.time_to_live - config.worker_interval\n      Sidekiq::Queue.all\n        .filter { |q| q.name.start_with?(config.queue_prefix.to_s) }\n        .filter { |q| q.latency > latency_threshold }\n        .filter { |q| q.size == 1 && q.all? { |job| job.klass == self.class.name } }\n        .each(&:clear)\n    end\n\n    def current_hostname\n      SidekiqAlive.hostname\n    end\n\n    def config\n      SidekiqAlive.config\n    end\n  end\nend\n"
  },
  {
    "path": "lib/sidekiq_alive.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"sidekiq\"\nrequire \"sidekiq/api\"\nrequire \"singleton\"\nrequire \"sidekiq_alive/version\"\nrequire \"sidekiq_alive/config\"\nrequire \"sidekiq_alive/helpers\"\nrequire \"sidekiq_alive/redis\"\n\nmodule SidekiqAlive\n  HOSTNAME_REGISTRY = \"sidekiq-alive-hostnames\"\n  CAPSULE_NAME = \"sidekiq-alive\"\n\n  class << self\n    def start\n      Sidekiq.configure_server do |sq_config|\n        sq_config.on(:startup) do\n          SidekiqAlive::Worker.sidekiq_options(queue: current_queue)\n\n          if Helpers.sidekiq_7?\n            sq_config.capsule(CAPSULE_NAME) do |cap|\n              cap.concurrency = config.concurrency\n              cap.queues = [current_queue]\n            end\n          else\n            (sq_config.respond_to?(:[]) ? sq_config[:queues] : sq_config.options[:queues]).unshift(current_queue)\n          end\n\n          logger.info(\"[SidekiqAlive] #{startup_info}\")\n          register_current_instance\n          store_alive_key\n          # Passing the hostname argument it's only for debugging enqueued jobs\n          SidekiqAlive::Worker.perform_async(hostname)\n          @server = SidekiqAlive::Server.run!\n\n          logger.info(\"[SidekiqAlive] #{successful_startup_text}\")\n        end\n\n        sq_config.on(:quiet) do\n          logger.info(\"[SidekiqAlive] #{shutdown_info}\")\n          purge_pending_jobs\n          # set web server to quiet mode\n          @server&.quiet!\n        end\n\n        sq_config.on(:shutdown) do\n          remove_queue\n          # make sure correct redis connection pool is used\n          # sidekiq will terminate non internal capsules\n          Redis.adapter(\"internal\").zrem(HOSTNAME_REGISTRY, current_instance_register_key)\n          config.shutdown_callback.call\n        end\n      end\n    end\n\n    def current_queue\n      \"#{config.queue_prefix}-#{hostname}\"\n    end\n\n    def register_current_instance\n      register_instance(current_instance_register_key)\n    end\n\n    def registered_instances\n      # before we return we make sure we expire old keys\n      expire_old_keys\n      redis.zrange(HOSTNAME_REGISTRY, 0, -1)\n    end\n\n    def purge_pending_jobs\n      schedule_set = Sidekiq::ScheduledSet.new\n      jobs = if Helpers.sidekiq_5?\n        schedule_set.select { |job| job.klass == \"SidekiqAlive::Worker\" && job.queue == current_queue }\n      else\n        schedule_set.scan('\"class\":\"SidekiqAlive::Worker\"').select { |job| job.queue == current_queue }\n      end\n\n      unless jobs.empty?\n        logger.info(\"[SidekiqAlive] Purging #{jobs.count} pending jobs for #{hostname}\")\n        jobs.each(&:delete)\n      end\n    end\n\n    def remove_queue\n      logger.info(\"[SidekiqAlive] Removing queue #{current_queue}\")\n      Sidekiq::Queue.new(current_queue).clear\n    end\n\n    def current_instance_register_key\n      \"#{config.registered_instance_key}::#{hostname}\"\n    end\n\n    def current_instance_registered?\n      redis.get(current_instance_register_key)\n    end\n\n    def store_alive_key\n      redis.set(current_lifeness_key, time: Time.now.to_i, ex: config.time_to_live.to_i)\n    end\n\n    def redis\n      @redis ||= Redis.adapter\n    end\n\n    def alive?\n      redis.ttl(current_lifeness_key) != -2\n    end\n\n    # CONFIG ---------------------------------------\n\n    def setup\n      yield(config)\n    end\n\n    def logger\n      config.logger || Sidekiq.logger\n    end\n\n    def config\n      @config ||= SidekiqAlive::Config.instance\n    end\n\n    def current_lifeness_key\n      \"#{config.liveness_key}::#{hostname}\"\n    end\n\n    def hostname\n      ENV[\"HOSTNAME\"] || \"HOSTNAME_NOT_SET\"\n    end\n\n    def shutdown_info\n      \"Shutting down sidekiq-alive!\"\n    end\n\n    def startup_info\n      info = {\n        hostname: hostname,\n        port: config.port,\n        ttl: config.time_to_live,\n        queue: current_queue,\n        register_set: HOSTNAME_REGISTRY,\n        liveness_key: current_lifeness_key,\n        register_key: current_instance_register_key,\n      }\n\n      \"Starting sidekiq-alive: #{info}\"\n    end\n\n    def successful_startup_text\n      \"Successfully started sidekiq-alive, registered with key: \" \\\n        \"#{current_instance_register_key} on set #{HOSTNAME_REGISTRY}\"\n    end\n\n    def expire_old_keys\n      # we get every key that should be expired by now\n      keys_to_expire = redis.zrangebyscore(HOSTNAME_REGISTRY, 0, Time.now.to_i)\n      # then we remove it\n      keys_to_expire.each { |key| redis.zrem(HOSTNAME_REGISTRY, key) }\n    end\n\n    def register_instance(instance_name)\n      expiration = Time.now.to_i + config.registration_ttl.to_i\n      redis.zadd(HOSTNAME_REGISTRY, expiration, instance_name)\n      expire_old_keys\n    end\n  end\nend\n\nrequire \"sidekiq_alive/worker\"\nrequire \"sidekiq_alive/server\"\n\nSidekiqAlive.start unless ENV.fetch(\"DISABLE_SIDEKIQ_ALIVE\", \"\").casecmp(\"true\").zero?\n"
  },
  {
    "path": "sidekiq_alive.gemspec",
    "content": "# frozen_string_literal: true\n\nlib = File.expand_path(\"lib\", __dir__)\n$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)\n\nrequire \"sidekiq_alive/version\"\n\nGem::Specification.new do |spec|\n  spec.name          = \"sidekiq_alive\"\n  spec.authors       = [\"Andrejs Cunskis\", \"Artur Pañach\"]\n  spec.email         = [\"andrejs.cunskis@gmail.com\", \"arturictus@gmail.com\"]\n\n  spec.version       = SidekiqAlive::VERSION\n\n  spec.required_ruby_version = Gem::Requirement.new(\">= 3.1\")\n\n  spec.homepage      = \"https://github.com/arturictus/sidekiq_alive\"\n  spec.summary       = \"Liveness probe for sidekiq on Kubernetes deployments.\"\n  spec.license       = \"MIT\"\n  spec.description   = <<~DSC\n    SidekiqAlive offers a solution to add liveness probe of a Sidekiq instance.\n\n    How?\n\n    A http server is started and on each requests validates that a liveness key is stored in Redis. If it is there means is working.\n\n    A Sidekiq job is the responsable to storing this key. If Sidekiq stops processing jobs\n    this key gets expired by Redis an consequently the http server will return a 500 error.\n\n    This Job is responsible to requeue itself for the next liveness probe.\n  DSC\n\n  spec.metadata = {\n    \"homepage_uri\" => spec.homepage,\n    \"source_code_uri\" => spec.homepage,\n    \"changelog_uri\" => \"#{spec.homepage}/releases\",\n    \"documentation_uri\" => \"#{spec.homepage}/blob/v#{spec.version}/README.md\",\n    \"bug_tracker_uri\" => \"#{spec.homepage}/issues\",\n  }\n\n  spec.files         = Dir[\"README.md\", \"lib/**/*\"]\n  spec.require_paths = [\"lib\"]\n\n  spec.add_development_dependency(\"bundler\", \"> 1.16\")\n  spec.add_development_dependency(\"debug\", \"~> 1.6\")\n  spec.add_development_dependency(\"rake\", \"~> 13.0\")\n  spec.add_development_dependency(\"rspec\", \"~> 3.0\")\n  spec.add_development_dependency(\"rspec-sidekiq\", \"~> 5.0\")\n  spec.add_development_dependency(\"rubocop-shopify\", \"~> 2.10\")\n  spec.add_development_dependency(\"semver2\", \"~> 3.4\")\n  spec.add_development_dependency(\"solargraph\", \"~> 0.54.0\")\n\n  spec.add_dependency(\"base64\", \">= 0\", \"< 1\") # sidekiq 6 requires base64 which is not part of stdlib in Ruby 3.4+\n  spec.add_dependency(\"gserver\", \"~> 0.0.1\")\n  spec.add_dependency(\"sidekiq\", \">= 5\", \"< 9\")\nend\n"
  },
  {
    "path": "spec/config_spec.rb",
    "content": "# frozen_string_literal: true\n\nRSpec.describe(SidekiqAlive::Config) do\n  subject(:config) { described_class.instance }\n\n  describe \"#worker_interval\" do\n    it \"less than ttl\" do\n      expect(config.worker_interval).to(satisfy { |i| i < config.time_to_live })\n    end\n  end\nend\n"
  },
  {
    "path": "spec/redis_spec.rb",
    "content": "# frozen_string_literal: true\n\nRSpec.describe(SidekiqAlive::Redis) do\n  let(:redis) { SidekiqAlive::Redis.adapter }\n\n  it \"Works\" do\n    time = Time.now.to_s\n    redis.set(\"hello\", time: time, ex: 60)\n    expect(redis.ttl(\"hello\") > 1).to(be(true))\n    expect(redis.get(\"hello\")).to(eq(time))\n    redis.zadd(\"test_set\", Time.now.to_i, \"test-key-1\")\n    redis.zadd(\"test_set\", Time.now.to_i, \"test-key-2\")\n    expect(redis.zrange(\"test_set\", 0, -1)).to(eq([\"test-key-1\", \"test-key-2\"]))\n    expect(redis.zrem(\"test_set\", \"test-key-1\"))\n    expect(redis.zrange(\"test_set\", 0, -1)).to(eq([\"test-key-2\"]))\n  end\nend\n"
  },
  {
    "path": "spec/server/default_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"net/http\"\n\nRSpec.describe(SidekiqAlive::Server::Default) do\n  let(:port) { 7433 }\n  let(:path) { \"/\" }\n  let(:server) { SidekiqAlive::Server::Default.new(port, \"0.0.0.0\", path) }\n\n  before do\n    server.start\n  end\n\n  after do\n    server.stop\n  end\n\n  def get(uri)\n    @last_response = Net::HTTP.get_response(URI(\"http://localhost:#{port}#{uri}\"))\n  end\n\n  context \"with default configuration\" do\n    it \"responds with success when the service is alive\" do\n      allow(SidekiqAlive).to(receive(:alive?) { true })\n\n      get \"/\"\n      expect(@last_response.code).to(eq(\"200\"))\n      expect(@last_response.body).to(eq(\"Alive!\"))\n    end\n\n    it \"responds with an error when the service is not alive\" do\n      allow(SidekiqAlive).to(receive(:alive?) { false })\n\n      get \"/\"\n      expect(@last_response.code).to(eq(\"404\"))\n      expect(@last_response.body).to(eq(\"Can't find the alive key\"))\n    end\n\n    it \"responds not found on an unknown path\" do\n      get \"/unknown-path\"\n      expect(@last_response.code).to(eq(\"404\"))\n      expect(@last_response.body).to(eq(\"Not found\"))\n    end\n  end\n\n  context \"with custom path\" do\n    let(:path) { \"/sidekiq-probe\" }\n\n    it \"responds ok to the given path\" do\n      allow(SidekiqAlive).to(receive(:alive?) { true })\n\n      get \"/sidekiq-probe\"\n      expect(@last_response.code).to(eq(\"200\"))\n    end\n  end\n\n  context \"with quiet mode\" do\n    before do\n      server.quiet!\n    end\n\n    it \"responds with success and server is shutting down message\" do\n      get \"/\"\n      expect(@last_response.code).to(eq(\"200\"))\n      expect(@last_response.body).to(eq(\"Server is shutting down\"))\n    end\n  end\nend\n"
  },
  {
    "path": "spec/server/rack_spec.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"rack/test\"\n\nENV[\"RACK_ENV\"] = \"test\"\n\nRSpec.describe(SidekiqAlive::Server::Rack) do\n  include Rack::Test::Methods\n\n  subject(:app) { described_class }\n\n  before do\n    described_class.instance_variable_set(:@quiet, nil)\n  end\n\n  context \"with default configuration\" do\n    it \"responds with success when the service is alive\" do\n      allow(SidekiqAlive).to(receive(:alive?) { true })\n\n      get \"/\"\n      expect(last_response).to(be_ok)\n      expect(last_response.body).to(eq(\"Alive!\"))\n    end\n\n    it \"responds with an error when the service is not alive\" do\n      allow(SidekiqAlive).to(receive(:alive?) { false })\n\n      get \"/\"\n      expect(last_response).not_to(be_ok)\n      expect(last_response.body).to(eq(\"Can't find the alive key\"))\n    end\n\n    it \"responds not found on an unknown path\" do\n      get \"/unknown-path\"\n      expect(last_response).not_to(be_ok)\n      expect(last_response.body).to(eq(\"Not found\"))\n    end\n  end\n\n  context \"with custom path\" do\n    let(:path) { \"/sidekiq-probe\" }\n\n    before do\n      ENV[\"SIDEKIQ_ALIVE_PATH\"] = path\n      SidekiqAlive.config.set_defaults\n    end\n\n    after do\n      ENV[\"SIDEKIQ_ALIVE_PATH\"] = nil\n    end\n\n    it \"responds ok to the given path\" do\n      allow(SidekiqAlive).to(receive(:alive?) { true })\n\n      get \"/sidekiq-probe\"\n      expect(last_response).to(be_ok)\n    end\n  end\n\n  context \"with quiet mode\" do\n    before do\n      described_class.instance_variable_set(:@quiet, Time.now)\n    end\n\n    it \"responds with success and server is shutting down message\" do\n      get \"/\"\n      expect(last_response).to(be_ok)\n      expect(last_response.body).to(eq(\"Server is shutting down\"))\n    end\n  end\nend\n"
  },
  {
    "path": "spec/server_spec.rb",
    "content": "# frozen_string_literal: true\n\naround_config = proc do |example|\n  ENV[\"SIDEKIQ_ALIVE_HOST\"] = \"1.2.3.4\"\n  ENV[\"SIDEKIQ_ALIVE_PORT\"] = \"4567\"\n  ENV[\"SIDEKIQ_ALIVE_PATH\"] = \"/health\"\n  SidekiqAlive.config.set_defaults\n\n  example.run\n\n  ENV[\"SIDEKIQ_ALIVE_HOST\"] = nil\n  ENV[\"SIDEKIQ_ALIVE_PORT\"] = nil\n  ENV[\"SIDEKIQ_ALIVE_PATH\"] = nil\nend\n\nRSpec.describe(SidekiqAlive::Server, :aggregate_failures) do\n  subject(:app) { described_class }\n\n  let(:pid) { Random.rand(1000) }\n\n  before do\n    allow(Process).to(receive(:fork).and_yield.and_return(pid))\n    allow(Process).to(receive(:kill))\n    allow(Process).to(receive(:wait))\n    allow(Signal).to(receive(:trap))\n    allow(Kernel).to(receive(:at_exit))\n  end\n\n  context \"with default server\" do\n    let(:fake_server) do\n      instance_double(\n        SidekiqAlive::Server::Default,\n        start: nil,\n        stop: nil,\n        join: nil,\n        quiet!: nil,\n      )\n    end\n\n    before do\n      allow(SidekiqAlive::Server::Default).to(receive(:new).and_return(fake_server))\n      allow(Thread).to(receive(:new).and_yield)\n\n      app.run!\n    end\n\n    context \"with default config\" do\n      it \"starts server with default arguments and configures lifecycle\" do\n        expect(SidekiqAlive::Server::Default).to(have_received(:new).with(7433, \"0.0.0.0\", \"/\"))\n        expect(fake_server).to(have_received(:start))\n        expect(fake_server).to(have_received(:join))\n      end\n\n      it \"configures signals\" do\n        expect(Signal).to(have_received(:trap).with(\"TERM\")) do |&arg|\n          arg.call\n\n          expect(fake_server).to(have_received(:stop))\n        end\n        expect(Signal).to(have_received(:trap).with(\"TSTP\")) do |&arg|\n          arg.call\n\n          expect(fake_server).to(have_received(:quiet!))\n        end\n      end\n\n      it \"configures shutdown\" do\n        allow(Kernel).to(receive(:at_exit)) do |&arg|\n          arg.call\n\n          expect(Process).to(have_received(:kill).with(\"TERM\", pid))\n          expect(Process).to(have_received(:wait).with(pid))\n        end\n      end\n    end\n\n    context \"with changed host, port and path configuration\" do\n      around(&around_config)\n\n      it \"starts with updated configuration\" do\n        expect(SidekiqAlive::Server::Default).to(have_received(:new).with(4567, \"1.2.3.4\", \"/health\"))\n      end\n    end\n  end\n\n  context \"rack based server\" do\n    let(:fake_server) { double(\"rack server\", run: nil, shutdown: nil) }\n    let(:handler) { SidekiqAlive::Helpers.use_rackup? ? Rackup::Handler : Rack::Handler }\n\n    before do\n      ENV[\"SIDEKIQ_ALIVE_SERVER\"] = \"webrick\"\n      SidekiqAlive.config.set_defaults\n\n      allow(handler).to(receive(:get).and_return(fake_server))\n      SidekiqAlive::Server::Rack.instance_variable_set(:@quiet, nil)\n\n      app.run!\n    end\n\n    after { ENV[\"SIDEKIQ_ALIVE_SERVER\"] = nil }\n\n    context \"with default config\" do\n      it \"starts server with default arguments and traps shutdown\", :aggregate_failures do\n        expect(handler).to(have_received(:get).with(\"webrick\"))\n        expect(fake_server).to(have_received(:run).with(\n          SidekiqAlive::Server::Rack, Port: 7433, Host: \"0.0.0.0\", AccessLog: [], Logger: SidekiqAlive.logger\n        ))\n      end\n\n      it \"configures signals\" do\n        expect(Signal).to(have_received(:trap).with(\"TERM\")) do |&arg|\n          arg.call\n\n          expect(fake_server).to(have_received(:shutdown))\n        end\n        expect(Signal).to(have_received(:trap).with(\"TSTP\")) do |&arg|\n          arg.call\n\n          expect(SidekiqAlive::Server::Rack.instance_variable_get(:@quiet)).to(be_instance_of(Time))\n        end\n      end\n\n      it \"configures shutdown\" do\n        allow(Kernel).to(receive(:at_exit)) do |&arg|\n          arg.call\n\n          expect(Process).to(have_received(:kill).with(\"TERM\", pid))\n          expect(Process).to(have_received(:wait).with(pid))\n        end\n      end\n    end\n\n    context \"with changed host, port and path configuration\" do\n      around(&around_config)\n\n      it \"starts with updated configuration\" do\n        expect(fake_server).to(have_received(:run).with(\n          SidekiqAlive::Server::Rack, Port: 4567, Host: \"1.2.3.4\", AccessLog: [], Logger: SidekiqAlive.logger\n        ))\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/sidekiq_alive_spec.rb",
    "content": "# frozen_string_literal: true\n\nbegin\n  # this is needed for spec to work with sidekiq >7\n  require \"sidekiq/capsule\"\nrescue LoadError # rubocop:disable Lint/SuppressedException\nend\n\nRSpec.describe(SidekiqAlive) do\n  context \"with configuration\" do\n    it \"has a version number\" do\n      expect(SidekiqAlive::VERSION).not_to(be(nil))\n    end\n\n    it \"configures the host from the #setup\" do\n      described_class.setup do |config|\n        config.host = \"1.2.3.4\"\n      end\n\n      expect(described_class.config.host).to(eq(\"1.2.3.4\"))\n    end\n\n    it \"configures the host from the SIDEKIQ_ALIVE_HOST ENV var\" do\n      ENV[\"SIDEKIQ_ALIVE_HOST\"] = \"1.2.3.4\"\n\n      SidekiqAlive.config.set_defaults\n\n      expect(described_class.config.host).to(eq(\"1.2.3.4\"))\n\n      ENV[\"SIDEKIQ_ALIVE_HOST\"] = nil\n    end\n\n    it \"configures the port from the #setup\" do\n      described_class.setup do |config|\n        config.port = 4567\n      end\n\n      expect(described_class.config.port).to(eq(4567))\n    end\n\n    it \"configures the port from the SIDEKIQ_ALIVE_PORT ENV var\" do\n      ENV[\"SIDEKIQ_ALIVE_PORT\"] = \"4567\"\n\n      SidekiqAlive.config.set_defaults\n\n      expect(described_class.config.port).to(eq(\"4567\"))\n\n      ENV[\"SIDEKIQ_ALIVE_PORT\"] = nil\n    end\n\n    it \"configures the concurrency from the SIDEKIQ_ALIVE_CONCURRENCY ENV var\" do\n      ENV[\"SIDEKIQ_ALIVE_CONCURRENCY\"] = \"3\"\n\n      SidekiqAlive.config.set_defaults\n\n      expect(described_class.config.concurrency).to(eq(3))\n\n      ENV[\"SIDEKIQ_ALIVE_CONCURRENCY\"] = nil\n    end\n\n    it \"configurations behave as expected\" do\n      k = described_class.config\n\n      expect(k.host).to(eq(\"0.0.0.0\"))\n      k.host = \"1.2.3.4\"\n      expect(k.host).to(eq(\"1.2.3.4\"))\n\n      expect(k.port).to(eq(7433))\n      k.port = 4567\n      expect(k.port).to(eq(4567))\n\n      expect(k.liveness_key).to(eq(\"SIDEKIQ::LIVENESS_PROBE_TIMESTAMP\"))\n      k.liveness_key = \"key\"\n      expect(k.liveness_key).to(eq(\"key\"))\n\n      expect(k.time_to_live).to(eq(10 * 60))\n      k.time_to_live = 2 * 60\n      expect(k.time_to_live).to(eq(2 * 60))\n\n      expect(k.callback.call).to(eq(nil))\n      k.callback = proc { \"hello\" }\n      expect(k.callback.call).to(eq(\"hello\"))\n\n      expect(k.queue_prefix).to(eq(:\"sidekiq-alive\"))\n      k.queue_prefix = :other\n      expect(k.queue_prefix).to(eq(:other))\n\n      expect(k.shutdown_callback.call).to(eq(nil))\n      k.shutdown_callback = proc { \"hello\" }\n      expect(k.shutdown_callback.call).to(eq(\"hello\"))\n    end\n  end\n\n  context \"with redis\" do\n    let(:sidekiq_7) { SidekiqAlive::Helpers.sidekiq_7? }\n    # Older versions of sidekiq yielded Sidekiq module as configuration object\n    # With sidekiq > 7, configuration is a separate class\n    let(:sq_config) { sidekiq_7 ? Sidekiq.default_configuration : Sidekiq }\n\n    before do\n      allow(Sidekiq).to(receive(:server?) { true })\n      allow(sq_config).to(receive(:on))\n\n      if sidekiq_7\n        allow(sq_config).to(receive(:capsule).and_call_original)\n      elsif sq_config.respond_to?(:[])\n        allow(sq_config).to(receive(:[]).and_call_original)\n      else\n        allow(sq_config).to(receive(:options).and_call_original)\n      end\n    end\n\n    it '::store_alive_key\" stores key with the expected ttl' do\n      redis = SidekiqAlive.redis\n\n      expect(redis.ttl(SidekiqAlive.current_lifeness_key)).to(eq(-2))\n      SidekiqAlive.store_alive_key\n      expect(redis.ttl(SidekiqAlive.current_lifeness_key)).to(eq(SidekiqAlive.config.time_to_live))\n    end\n\n    it \"::current_lifeness_key\" do\n      expect(SidekiqAlive.current_lifeness_key).to(include(\"::test-hostname\"))\n    end\n\n    it \"::hostname\" do\n      expect(SidekiqAlive.hostname).to(eq(\"test-hostname\"))\n    end\n\n    it \"::alive?\" do\n      expect(SidekiqAlive.alive?).to(be(false))\n      SidekiqAlive.store_alive_key\n      expect(SidekiqAlive.alive?).to(be(true))\n    end\n\n    context \"::start\" do\n      let(:server) { double(\"Server\", quiet!: nil) }\n      let(:queue_prefix) { :heathcheck }\n      let(:queues) do\n        next Sidekiq.default_configuration.capsules[SidekiqAlive::CAPSULE_NAME].queues if sidekiq_7\n\n        sq_config.options[:queues]\n      end\n\n      before do\n        allow(SidekiqAlive::Server).to(receive(:run!) { server })\n        allow(sq_config).to(receive(:on).with(:startup).and_yield)\n\n        SidekiqAlive.instance_variable_set(:@redis, nil)\n      end\n\n      it \"::registered_instances\" do\n        SidekiqAlive.start\n        expect(SidekiqAlive.registered_instances.count).to(eq(1))\n        expect(SidekiqAlive.registered_instances.first).to(include(\"test-hostname\"))\n      end\n\n      it \"::on(:quiet)\" do\n        SidekiqAlive.start\n\n        expect(sq_config).to(have_received(:on).with(:quiet)) do |&arg|\n          arg.call\n\n          expect(server).to(have_received(:quiet!))\n        end\n      end\n\n      it \"::on(:shutdown)\" do\n        callback = double(\"callback\", call: nil)\n        SidekiqAlive.config.shutdown_callback = callback\n\n        SidekiqAlive.start\n\n        expect(sq_config).to(have_received(:on).with(:shutdown)) do |&arg|\n          arg.call\n\n          expect(SidekiqAlive.registered_instances.count).to(eq(0))\n          expect(callback).to(have_received(:call))\n        end\n      end\n\n      it \"::queues\" do\n        SidekiqAlive.config.queue_prefix = queue_prefix\n\n        SidekiqAlive.start\n\n        expect(queues.first).to(eq(\"#{queue_prefix}-test-hostname\"))\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"simplecov\"\nSimpleCov.start\nrequire \"simplecov-cobertura\"\nSimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter\n\nrequire \"bundler/setup\"\nrequire \"sidekiq_alive\"\nrequire \"rspec-sidekiq\"\nrequire \"debug\"\nrequire \"rack\"\n\nENV[\"HOSTNAME\"] = \"test-hostname\"\n\nSidekiq.logger.level = Logger::FATAL\n\nRSpec.configure do |config|\n  # Enable flags like --only-failures and --next-failure\n  config.example_status_persistence_file_path = \".rspec_status\"\n\n  # Disable RSpec exposing methods globally on `Module` and `main`\n  config.disable_monkey_patching!\n\n  config.expect_with(:rspec) do |c|\n    c.syntax = :expect\n  end\n\n  config.prepend_before do\n    Sidekiq.redis(&:flushall)\n    SidekiqAlive.config.set_defaults\n  end\nend\n"
  },
  {
    "path": "spec/worker_spec.rb",
    "content": "# frozen_string_literal: true\n\nRSpec.describe(SidekiqAlive::Worker) do\n  subject(:perform) do\n    described_class.new.perform\n  end\n\n  context \"When being executed in the same instance\" do\n    it \"stores alive key and requeues it self\" do\n      SidekiqAlive.register_current_instance\n      expect(described_class).to(receive(:perform_in))\n      n = 0\n      SidekiqAlive.config.callback = proc { n = 2 }\n      perform\n      expect(n).to(eq(2))\n      expect(SidekiqAlive.alive?).to(be(true))\n    end\n  end\n\n  context \"custom liveness probe\" do\n    it \"on error\" do\n      expect(described_class).not_to(receive(:perform_in))\n      n = 0\n      SidekiqAlive.config.custom_liveness_probe = proc do\n        n = 2\n        raise \"Nop\"\n      end\n      begin\n        perform\n      rescue StandardError\n        nil\n      end\n      expect(n).to(eq(2))\n      expect(SidekiqAlive.alive?).to(be(false))\n    end\n\n    it \"on success\" do\n      expect(described_class).to(receive(:perform_in))\n      n = 0\n      SidekiqAlive.config.custom_liveness_probe = proc { n = 2 }\n      perform\n\n      expect(n).to(eq(2))\n      expect(SidekiqAlive.alive?).to(be(true))\n    end\n  end\n\n  describe \"orphaned queues removal\" do\n    it \"removes orphaned queues\" do\n      queue = instance_double(Sidekiq::Queue, name: \"notifications\", latency: 10_000, size: 1, clear: nil)\n      orphaning_queue = instance_double(Sidekiq::Queue, name: \"sidekiq-alive-bar\", latency: 200, size: 1, clear: nil)\n\n      orphaned_queue = instance_double(Sidekiq::Queue, name: \"sidekiq-alive-foo\", latency: 350, size: 1, clear: nil)\n      alive_job = instance_double(Sidekiq::JobRecord, klass: \"SidekiqAlive::Worker\")\n      allow(orphaned_queue).to(receive(:all?).and_yield(alive_job))\n\n      imposter_queue = instance_double(Sidekiq::Queue, name: \"sidekiq-aliveness\", latency: 10_000, size: 1, clear: nil)\n      job = instance_double(Sidekiq::JobRecord, klass: \"AlivenessWorker\")\n      allow(imposter_queue).to(receive(:all?).and_yield(job))\n\n      allow(Sidekiq::Queue).to(receive(:all).and_return([queue, imposter_queue, orphaned_queue, orphaning_queue]))\n\n      perform\n\n      expect(queue).not_to(have_received(:clear))\n      expect(imposter_queue).not_to(have_received(:clear))\n      expect(orphaned_queue).to(have_received(:clear))\n      expect(orphaning_queue).not_to(have_received(:clear))\n    end\n  end\nend\n"
  },
  {
    "path": "tasks/version.rake",
    "content": "# frozen_string_literal: true\n\nrequire \"semver\"\n\nmodule SidekiqAlive\n  # Update app version\n  #\n  class VersionTask\n    include Rake::DSL\n\n    VERSION_FILE = \"lib/sidekiq_alive/version.rb\"\n\n    def initialize\n      add_version_task\n    end\n\n    # Add version bump task\n    #\n    def add_version_task\n      desc(\"Bump application version [major, minor, patch]\")\n      task(:version, [:semver]) do |_task, args|\n        new_version = send(args[:semver]).format(\"%M.%m.%p\").to_s\n\n        update_version(new_version)\n        commit_and_tag(new_version)\n      end\n    end\n\n    private\n\n    # Update version file\n    #\n    # @param [SemVer] new_version\n    # @return [void]\n    def update_version(new_version)\n      u_version = File.read(VERSION_FILE).gsub(SidekiqAlive::VERSION, new_version)\n      File.write(VERSION_FILE, u_version)\n    end\n\n    # Commit updated version file and Gemfile.lock\n    #\n    # @return [void]\n    def commit_and_tag(new_version)\n      sh(\"git add #{VERSION_FILE}\")\n      sh(\"git commit -m 'Update version to #{new_version}'\")\n    end\n\n    # Semver of ref from\n    #\n    # @return [SemVer]\n    def semver\n      @semver ||= SemVer.parse(SidekiqAlive::VERSION)\n    end\n\n    # Increase patch version\n    #\n    # @return [SemVer]\n    def patch\n      semver.tap { |ver| ver.patch += 1 }\n    end\n\n    # Increase minor version\n    #\n    # @return [SemVer]\n    def minor\n      semver.tap do |ver|\n        ver.minor += 1\n        ver.patch = 0\n      end\n    end\n\n    # Increase major version\n    #\n    # @return [SemVer]\n    def major\n      semver.tap do |ver|\n        ver.major += 1\n        ver.minor = 0\n        ver.patch = 0\n      end\n    end\n  end\nend\n"
  }
]