From 97de556ccf6996adf27cc0dc05b84828cc0c7ffb Mon Sep 17 00:00:00 2001 From: Adam McCrea Date: Wed, 8 Feb 2023 07:57:49 -0500 Subject: [PATCH] feature: Add GoodJob adapter (#116) * Copy delayed_job and rename * rename files and folders * Add GoodJob sample app and implement collector logic * Update README.md * Rename variables * Find all known queues * Fix CI * Fix CI * Query good_jobs table directly instead of using JobsFilter * Rename variable * Add finished_at for index preference * Fix syntax for Ruby 2.6 * Add busy jobs reporting for GoodJob * Silence query logs for GoodJob collector * Move query for "known queue names" to initializer * Change mock API endpoint for sample apps --- .github/workflows/judoscale-good_job-test.yml | 46 ++++ README.md | 9 +- judoscale-good_job/Gemfile | 10 + judoscale-good_job/Gemfile-activerecord-6-1 | 10 + judoscale-good_job/Gemfile.lock | 113 +++++++++ judoscale-good_job/Rakefile | 12 + judoscale-good_job/judoscale-good_job.gemspec | 30 +++ judoscale-good_job/lib/judoscale-good_job.rb | 3 + judoscale-good_job/lib/judoscale/good_job.rb | 19 ++ .../judoscale/good_job/metrics_collector.rb | 66 ++++++ .../lib/judoscale/good_job/version.rb | 7 + .../lib/rails-autoscale-good_job.rb | 3 + .../rails-autoscale-good_job.gemspec | 30 +++ judoscale-good_job/test/adapter_test.rb | 36 +++ .../test/metrics_collector_test.rb | 218 ++++++++++++++++++ judoscale-good_job/test/test_helper.rb | 75 ++++++ .../active_record_helper.rb | 10 +- sample-apps/delayed_job-sample/Gemfile.lock | 24 +- sample-apps/delayed_job-sample/README.md | 2 +- .../app/views/jobs/index.html.erb | 2 +- .../config/initializers/rails_autoscale.rb | 4 +- sample-apps/good_job-sample/Gemfile | 18 ++ sample-apps/good_job-sample/Gemfile.lock | 130 +++++++++++ sample-apps/good_job-sample/Procfile | 4 + sample-apps/good_job-sample/README.md | 53 +++++ sample-apps/good_job-sample/Rakefile | 3 + .../app/controllers/application_controller.rb | 2 + .../app/controllers/jobs_controller.rb | 27 +++ .../app/jobs/application_job.rb | 7 + .../good_job-sample/app/jobs/sample_job.rb | 7 + .../app/views/jobs/index.html.erb | 44 ++++ .../app/views/layouts/application.html.erb | 15 ++ sample-apps/good_job-sample/bin/bundle | 114 +++++++++ sample-apps/good_job-sample/bin/dev | 5 + sample-apps/good_job-sample/bin/rails | 4 + sample-apps/good_job-sample/bin/rake | 4 + sample-apps/good_job-sample/bin/setup | 33 +++ sample-apps/good_job-sample/config.ru | 6 + .../good_job-sample/config/application.rb | 45 ++++ sample-apps/good_job-sample/config/boot.rb | 3 + .../config/credentials.yml.enc | 1 + .../good_job-sample/config/database.yml | 14 ++ .../good_job-sample/config/environment.rb | 5 + .../config/environments/development.rb | 64 +++++ .../config/initializers/rails_autoscale.rb | 9 + sample-apps/good_job-sample/config/puma.rb | 43 ++++ sample-apps/good_job-sample/config/routes.rb | 5 + .../20230117185228_create_good_jobs.rb | 47 ++++ sample-apps/good_job-sample/db/schema.rb | 60 +++++ sample-apps/good_job-sample/log/.keep | 0 sample-apps/que-2-sample/README.md | 2 +- .../app/views/jobs/index.html.erb | 2 +- .../config/initializers/rails_autoscale.rb | 4 +- sample-apps/rails-sample/README.md | 2 +- .../app/views/home/index.html.erb | 2 +- .../config/initializers/rails_autoscale.rb | 4 +- sample-apps/resque-sample/README.md | 2 +- .../app/views/jobs/index.html.erb | 2 +- .../config/initializers/rails_autoscale.rb | 4 +- sample-apps/sidekiq-sample/README.md | 2 +- .../app/views/jobs/index.html.erb | 2 +- .../config/initializers/rails_autoscale.rb | 4 +- sample-apps/sinatra-sample/config.ru | 4 +- 63 files changed, 1497 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/judoscale-good_job-test.yml create mode 100644 judoscale-good_job/Gemfile create mode 100644 judoscale-good_job/Gemfile-activerecord-6-1 create mode 100644 judoscale-good_job/Gemfile.lock create mode 100644 judoscale-good_job/Rakefile create mode 100644 judoscale-good_job/judoscale-good_job.gemspec create mode 100644 judoscale-good_job/lib/judoscale-good_job.rb create mode 100644 judoscale-good_job/lib/judoscale/good_job.rb create mode 100644 judoscale-good_job/lib/judoscale/good_job/metrics_collector.rb create mode 100644 judoscale-good_job/lib/judoscale/good_job/version.rb create mode 100644 judoscale-good_job/lib/rails-autoscale-good_job.rb create mode 100644 judoscale-good_job/rails-autoscale-good_job.gemspec create mode 100644 judoscale-good_job/test/adapter_test.rb create mode 100644 judoscale-good_job/test/metrics_collector_test.rb create mode 100644 judoscale-good_job/test/test_helper.rb create mode 100644 sample-apps/good_job-sample/Gemfile create mode 100644 sample-apps/good_job-sample/Gemfile.lock create mode 100644 sample-apps/good_job-sample/Procfile create mode 100644 sample-apps/good_job-sample/README.md create mode 100644 sample-apps/good_job-sample/Rakefile create mode 100644 sample-apps/good_job-sample/app/controllers/application_controller.rb create mode 100644 sample-apps/good_job-sample/app/controllers/jobs_controller.rb create mode 100644 sample-apps/good_job-sample/app/jobs/application_job.rb create mode 100644 sample-apps/good_job-sample/app/jobs/sample_job.rb create mode 100644 sample-apps/good_job-sample/app/views/jobs/index.html.erb create mode 100644 sample-apps/good_job-sample/app/views/layouts/application.html.erb create mode 100755 sample-apps/good_job-sample/bin/bundle create mode 100755 sample-apps/good_job-sample/bin/dev create mode 100755 sample-apps/good_job-sample/bin/rails create mode 100755 sample-apps/good_job-sample/bin/rake create mode 100755 sample-apps/good_job-sample/bin/setup create mode 100644 sample-apps/good_job-sample/config.ru create mode 100644 sample-apps/good_job-sample/config/application.rb create mode 100644 sample-apps/good_job-sample/config/boot.rb create mode 100644 sample-apps/good_job-sample/config/credentials.yml.enc create mode 100644 sample-apps/good_job-sample/config/database.yml create mode 100644 sample-apps/good_job-sample/config/environment.rb create mode 100644 sample-apps/good_job-sample/config/environments/development.rb create mode 100644 sample-apps/good_job-sample/config/initializers/rails_autoscale.rb create mode 100644 sample-apps/good_job-sample/config/puma.rb create mode 100644 sample-apps/good_job-sample/config/routes.rb create mode 100644 sample-apps/good_job-sample/db/migrate/20230117185228_create_good_jobs.rb create mode 100644 sample-apps/good_job-sample/db/schema.rb create mode 100644 sample-apps/good_job-sample/log/.keep diff --git a/.github/workflows/judoscale-good_job-test.yml b/.github/workflows/judoscale-good_job-test.yml new file mode 100644 index 00000000..7499d015 --- /dev/null +++ b/.github/workflows/judoscale-good_job-test.yml @@ -0,0 +1,46 @@ +name: judoscale-good_job tests +defaults: + run: + working-directory: judoscale-good_job +on: + push: + branches: + - main + pull_request: +jobs: + test: + strategy: + fail-fast: false + matrix: + gemfile: + - Gemfile + - Gemfile-activerecord-6-1 + ruby: + - "2.6" + - "2.7" + - "3.0" + - "3.1" + exclude: + - gemfile: Gemfile + ruby: "2.6" + runs-on: ubuntu-latest + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/judoscale-good_job/${{ matrix.gemfile }} + services: + db: + image: postgres:latest + env: + POSTGRES_HOST_AUTH_METHOD: trust + ports: ["5432:5432"] + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true # runs bundle install and caches installed gems automatically + - run: bundle exec rake diff --git a/README.md b/README.md index e212b42d..d8a32d0d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Build Status: judoscale-ruby](https://github.com/judoscale/judoscale-ruby/actions/workflows/judoscale-ruby-test.yml/badge.svg)](https://github.com/judoscale/judoscale-ruby/actions) [![Build Status: judoscale-rails](https://github.com/judoscale/judoscale-ruby/actions/workflows/judoscale-rails-test.yml/badge.svg)](https://github.com/judoscale/judoscale-ruby/actions) [![Build Status: judoscale-delayed_job](https://github.com/judoscale/judoscale-ruby/actions/workflows/judoscale-delayed_job-test.yml/badge.svg)](https://github.com/judoscale/judoscale-ruby/actions) +[![Build Status: judoscale-good_job](https://github.com/judoscale/judoscale-ruby/actions/workflows/judoscale-good_job-test.yml/badge.svg)](https://github.com/judoscale/judoscale-ruby/actions) [![Build Status: judoscale-que](https://github.com/judoscale/judoscale-ruby/actions/workflows/judoscale-que-test.yml/badge.svg)](https://github.com/judoscale/judoscale-ruby/actions) [![Build Status: judoscale-sidekiq](https://github.com/judoscale/judoscale-ruby/actions/workflows/judoscale-sidekiq-test.yml/badge.svg)](https://github.com/judoscale/judoscale-ruby/actions) @@ -24,10 +25,11 @@ gem "judoscale-rails" # gem "judoscale-sidekiq" # gem "judoscale-resque" # gem "judoscale-delayed_job" +# gem "judoscale-good_job" # gem "judoscale-que" ``` -_If you're using a background job queue like Sidekiq or Delayed Job, make sure you include the corresponding judoscale-\* gem as well._ +_If you're using a background job queue, make sure you include the corresponding judoscale-\* gem as well._ The adapters report queue metrics to Judoscale every 10 seconds. The reporter will not run in development, or any other environment missing the `JUDOSCALE_URL` environment variable. (This environment variable is set for you automatically when provisioning the add-on.) @@ -50,12 +52,13 @@ The middleware will start the async reporter when it processes the first request ## Worker adapters -Judoscale will autoscale your worker dynos! Four job backends are supported: Sidekiq, Delayed Job, and Que. Be sure to install the gem specific to your job backend: +Judoscale will autoscale your worker dynos! Four job backends are supported: Sidekiq, Delayed Job, Good Job, and Que. Be sure to install the gem specific to your job backend: ```ruby gem "judoscale-sidekiq" gem "judoscale-resque" gem "judoscale-delayed_job" +gem "judoscale-good_job" gem "judoscale-que" ``` @@ -86,7 +89,7 @@ Judoscale aggregates and stores this information to power the autoscaler algorit ## Migrating from `rails_autoscale_agent` -The migration from `rails_autoscale_agent` to `judoscale-rails` (+ your job framework gem) is typically a single step: replace the `gem "rails_autoscale_agent"` in your Gemfile with `gem "judoscale-rails"` _and_ the appropriate `judoscale-*` package for your back-end job framework (`sidekiq`, `resque`, `delayed_job`, or `que`) or see the [Installation](#installation) section above for further specifics. If you previously had any custom configuration for the `rails_autoscale_agent`, please note that we now use a `configure` block as shown below. +The migration from `rails_autoscale_agent` to `judoscale-rails` (+ your job framework gem) is typically a single step: replace the `gem "rails_autoscale_agent"` in your Gemfile with `gem "judoscale-rails"` _and_ the appropriate `judoscale-*` package for your back-end job framework (`sidekiq`, `resque`, `delayed_job`, `good_job`, or `que`) or see the [Installation](#installation) section above for further specifics. If you previously had any custom configuration for the `rails_autoscale_agent`, please note that we now use a `configure` block as shown below. Looking for the old `rails_autoscale_agent` docs? They're available on [this branch](https://github.com/judoscale/judoscale-ruby/tree/rails_autoscale_agent). diff --git a/judoscale-good_job/Gemfile b/judoscale-good_job/Gemfile new file mode 100644 index 00000000..909150cb --- /dev/null +++ b/judoscale-good_job/Gemfile @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +gemspec name: "judoscale-good_job" + +gem "judoscale-ruby", path: "../judoscale-ruby" +gem "activerecord" +gem "pg" +gem "minitest" +gem "rake" +gem "rake-release" diff --git a/judoscale-good_job/Gemfile-activerecord-6-1 b/judoscale-good_job/Gemfile-activerecord-6-1 new file mode 100644 index 00000000..858e6b5e --- /dev/null +++ b/judoscale-good_job/Gemfile-activerecord-6-1 @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +gemspec name: "judoscale-good_job" + +gem "judoscale-ruby", path: "../judoscale-ruby" +gem "activerecord", "~> 6.1" +gem "pg" +gem "minitest" +gem "rake" +gem "rake-release" diff --git a/judoscale-good_job/Gemfile.lock b/judoscale-good_job/Gemfile.lock new file mode 100644 index 00000000..3a46e03c --- /dev/null +++ b/judoscale-good_job/Gemfile.lock @@ -0,0 +1,113 @@ +PATH + remote: ../judoscale-ruby + specs: + judoscale-ruby (1.2.3) + +PATH + remote: . + specs: + judoscale-good_job (1.2.3) + good_job (>= 3.0) + judoscale-ruby (= 1.2.3) + +GEM + remote: https://rubygems.org/ + specs: + actionpack (7.0.4.1) + actionview (= 7.0.4.1) + activesupport (= 7.0.4.1) + rack (~> 2.0, >= 2.2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actionview (7.0.4.1) + activesupport (= 7.0.4.1) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.4.1) + activesupport (= 7.0.4.1) + globalid (>= 0.3.6) + activemodel (7.0.4.1) + activesupport (= 7.0.4.1) + activerecord (7.0.4.1) + activemodel (= 7.0.4.1) + activesupport (= 7.0.4.1) + activesupport (7.0.4.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + builder (3.2.4) + concurrent-ruby (1.1.10) + crass (1.0.6) + erubi (1.12.0) + et-orbi (1.2.7) + tzinfo + fugit (1.8.0) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) + globalid (1.0.0) + activesupport (>= 5.0) + good_job (3.7.4) + activejob (>= 6.0.0) + activerecord (>= 6.0.0) + concurrent-ruby (>= 1.0.2) + fugit (>= 1.1) + railties (>= 6.0.0) + thor (>= 0.14.1) + webrick (>= 1.3) + i18n (1.12.0) + concurrent-ruby (~> 1.0) + loofah (2.19.1) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + method_source (1.0.0) + minitest (5.17.0) + nokogiri (1.14.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.14.0-x86_64-linux) + racc (~> 1.4) + pg (1.4.5) + raabro (1.4.0) + racc (1.6.2) + rack (2.2.6) + rack-test (2.0.2) + rack (>= 1.3) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.4) + loofah (~> 2.19, >= 2.19.1) + railties (7.0.4.1) + actionpack (= 7.0.4.1) + activesupport (= 7.0.4.1) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rake (13.0.6) + rake-release (1.3.0) + bundler (>= 1.11, < 3) + thor (1.2.1) + tzinfo (2.0.5) + concurrent-ruby (~> 1.0) + webrick (1.7.0) + zeitwerk (2.6.6) + +PLATFORMS + arm64-darwin-21 + x86_64-linux + +DEPENDENCIES + activerecord + judoscale-good_job! + judoscale-ruby! + minitest + pg + rake + rake-release + +BUNDLED WITH + 2.3.9 diff --git a/judoscale-good_job/Rakefile b/judoscale-good_job/Rakefile new file mode 100644 index 00000000..30388389 --- /dev/null +++ b/judoscale-good_job/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "rake/release" +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "lib" + t.libs << "test" + t.test_files = FileList["test/**/*_test.rb"] +end + +task default: :test diff --git a/judoscale-good_job/judoscale-good_job.gemspec b/judoscale-good_job/judoscale-good_job.gemspec new file mode 100644 index 00000000..e7a40ba4 --- /dev/null +++ b/judoscale-good_job/judoscale-good_job.gemspec @@ -0,0 +1,30 @@ +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "judoscale/good_job/version" + +Gem::Specification.new do |spec| + spec.name = "judoscale-good_job" + spec.version = Judoscale::GoodJob::VERSION + spec.authors = ["Adam McCrea", "Carlos Antonio da Silva"] + spec.email = ["adam@adamlogic.com"] + + spec.summary = "This gem provides GoodJob integration with the Judoscale autoscaling add-on for Heroku." + spec.homepage = "https://judoscale.com" + spec.license = "MIT" + + spec.metadata = { + "homepage_uri" => "https://judoscale.com", + "bug_tracker_uri" => "https://github.com/judoscale/judoscale-ruby/issues", + "documentation_uri" => "https://judoscale.com/docs", + "changelog_uri" => "https://github.com/judoscale/judoscale-ruby/blob/main/CHANGELOG.md", + "source_code_uri" => "https://github.com/judoscale/judoscale-ruby" + } + + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.require_paths = ["lib"] + + spec.required_ruby_version = ">= 2.6.0" + + spec.add_dependency "judoscale-ruby", Judoscale::GoodJob::VERSION + spec.add_dependency "good_job", ">= 3.0" +end diff --git a/judoscale-good_job/lib/judoscale-good_job.rb b/judoscale-good_job/lib/judoscale-good_job.rb new file mode 100644 index 00000000..6db9ef36 --- /dev/null +++ b/judoscale-good_job/lib/judoscale-good_job.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "judoscale/good_job" diff --git a/judoscale-good_job/lib/judoscale/good_job.rb b/judoscale-good_job/lib/judoscale/good_job.rb new file mode 100644 index 00000000..1dfe7b20 --- /dev/null +++ b/judoscale-good_job/lib/judoscale/good_job.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# require "good_job" fails unless we require these first +require "rails" +require "active_support/core_ext/numeric/time" +require "active_job/railtie" +require "good_job" +require "judoscale-ruby" +require "judoscale/config" +require "judoscale/good_job/version" +require "judoscale/good_job/metrics_collector" + +Judoscale.add_adapter :"judoscale-good_job", + { + adapter_version: Judoscale::GoodJob::VERSION, + framework_version: ::GoodJob::VERSION + }, + metrics_collector: Judoscale::GoodJob::MetricsCollector, + expose_config: Judoscale::Config::JobAdapterConfig.new(:good_job) diff --git a/judoscale-good_job/lib/judoscale/good_job/metrics_collector.rb b/judoscale-good_job/lib/judoscale/good_job/metrics_collector.rb new file mode 100644 index 00000000..cb68ca76 --- /dev/null +++ b/judoscale-good_job/lib/judoscale/good_job/metrics_collector.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "judoscale/job_metrics_collector" +require "judoscale/job_metrics_collector/active_record_helper" +require "judoscale/metric" + +module Judoscale + module GoodJob + class MetricsCollector < Judoscale::JobMetricsCollector + include ActiveRecordHelper + + def self.adapter_config + Judoscale::Config.instance.good_job + end + + def initialize + super + + queue_names = run_silently do + ::GoodJob::Execution.select("distinct queue_name").map(&:queue_name) + end + self.queues |= queue_names + end + + def collect + metrics = [] + time = Time.now.utc + + # logically we don't need the finished_at condition, but it lets postgres use the indexes + oldest_execution_time_by_queue = run_silently do + ::GoodJob::Execution + .where(performed_at: nil, finished_at: nil) + .group(:queue_name) + .pluck(:queue_name, Arel.sql("min(coalesce(scheduled_at, created_at))")) + .to_h + end + self.queues |= oldest_execution_time_by_queue.keys + + if track_busy_jobs? + busy_count_by_queue = run_silently do + ::GoodJob::Execution.running.group(:queue_name).count + end + self.queues |= busy_count_by_queue.keys + end + + queues.each do |queue| + run_at = oldest_execution_time_by_queue[queue] + # DateTime.parse assumes a UTC string + run_at = DateTime.parse(run_at) if run_at.is_a?(String) + latency_ms = run_at ? ((time - run_at) * 1000).ceil : 0 + latency_ms = 0 if latency_ms < 0 + + metrics.push Metric.new(:qt, latency_ms, time, queue) + + if track_busy_jobs? + busy_count = busy_count_by_queue[queue] || 0 + metrics.push Metric.new(:busy, busy_count, Time.now, queue) + end + end + + log_collection(metrics) + metrics + end + end + end +end diff --git a/judoscale-good_job/lib/judoscale/good_job/version.rb b/judoscale-good_job/lib/judoscale/good_job/version.rb new file mode 100644 index 00000000..d22c15d5 --- /dev/null +++ b/judoscale-good_job/lib/judoscale/good_job/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Judoscale + module GoodJob + VERSION = "1.2.3" + end +end diff --git a/judoscale-good_job/lib/rails-autoscale-good_job.rb b/judoscale-good_job/lib/rails-autoscale-good_job.rb new file mode 100644 index 00000000..6db9ef36 --- /dev/null +++ b/judoscale-good_job/lib/rails-autoscale-good_job.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "judoscale/good_job" diff --git a/judoscale-good_job/rails-autoscale-good_job.gemspec b/judoscale-good_job/rails-autoscale-good_job.gemspec new file mode 100644 index 00000000..2f3c71fc --- /dev/null +++ b/judoscale-good_job/rails-autoscale-good_job.gemspec @@ -0,0 +1,30 @@ +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "judoscale/good_job/version" + +Gem::Specification.new do |spec| + spec.name = "rails-autoscale-good_job" + spec.version = Judoscale::GoodJob::VERSION + spec.authors = ["Adam McCrea", "Carlos Antonio da Silva"] + spec.email = ["adam@adamlogic.com"] + + spec.summary = "This gem provides GoodJob integration with the Judoscale autoscaling add-on for Heroku." + spec.homepage = "https://judoscale.com" + spec.license = "MIT" + + spec.metadata = { + "homepage_uri" => "https://judoscale.com", + "bug_tracker_uri" => "https://github.com/judoscale/judoscale-ruby/issues", + "documentation_uri" => "https://judoscale.com/docs", + "changelog_uri" => "https://github.com/judoscale/judoscale-ruby/blob/main/CHANGELOG.md", + "source_code_uri" => "https://github.com/judoscale/judoscale-ruby" + } + + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.require_paths = ["lib"] + + spec.required_ruby_version = ">= 2.6.0" + + spec.add_dependency "rails-autoscale-core", Judoscale::GoodJob::VERSION + spec.add_dependency "good_job_active_record", ">= 4.0" +end diff --git a/judoscale-good_job/test/adapter_test.rb b/judoscale-good_job/test/adapter_test.rb new file mode 100644 index 00000000..5d57193f --- /dev/null +++ b/judoscale-good_job/test/adapter_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "test_helper" +require "judoscale/report" + +module Judoscale + describe GoodJob do + it "adds itself as an adapter with information to be reported to the Rails Autoscale API" do + adapter = Judoscale.adapters.detect { |adapter| adapter.identifier == :"judoscale-good_job" } + _(adapter).wont_be_nil + _(adapter.metrics_collector).must_equal Judoscale::GoodJob::MetricsCollector + + report = ::Judoscale::Report.new(Judoscale.adapters, Judoscale::Config.instance, []) + _(report.as_json[:adapters]).must_include(:"judoscale-good_job") + end + + it "sets up a config property for the library" do + config = Config.instance + _(config.good_job.enabled).must_equal true + _(config.good_job.max_queues).must_equal 20 + _(config.good_job.queues).must_equal [] + _(config.good_job.track_busy_jobs).must_equal false + + Judoscale.configure do |config| + config.good_job.queues = %w[test drive] + config.good_job.track_busy_jobs = true + end + + _(config.good_job.queues).must_equal %w[test drive] + _(config.good_job.track_busy_jobs).must_equal true + + report = ::Judoscale::Report.new(Judoscale.adapters, Judoscale::Config.instance, []) + _(report.as_json[:config]).must_include(:good_job) + end + end +end diff --git a/judoscale-good_job/test/metrics_collector_test.rb b/judoscale-good_job/test/metrics_collector_test.rb new file mode 100644 index 00000000..38dab552 --- /dev/null +++ b/judoscale-good_job/test/metrics_collector_test.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +require "test_helper" +require "judoscale/good_job/metrics_collector" + +class Delayable < ActiveJob::Base + retry_on StandardError + + def perform(succeed = true) + raise "boom" unless succeed + end +end + +module Judoscale + describe GoodJob::MetricsCollector do + subject { GoodJob::MetricsCollector.new } + + def clear_enqueued_jobs + ActiveRecord::Base.connection.execute("DELETE FROM good_jobs") + end + + describe "#collect" do + after { + clear_enqueued_jobs + subject.clear_queues + } + + it "collects latency for each queue, using the oldest enqueued job" do + now = Time.now.utc + + freeze_time now - 0.15 do + Delayable.set(queue: "default").perform_later + end + + metrics = freeze_time now do + Delayable.set(queue: "default").perform_later + Delayable.set(queue: "high").perform_later + + subject.collect + end + + _(metrics.size).must_equal 2 + _(metrics.map(&:queue_name).sort).must_equal %w[default high] + + metrics_hash = metrics.map { |m| [m.queue_name, m] }.to_h + + _(metrics_hash["default"].queue_name).must_equal "default" + _(metrics_hash["default"].value).must_be_within_delta 150, 1 + _(metrics_hash["default"].identifier).must_equal :qt + _(metrics_hash["high"].queue_name).must_equal "high" + _(metrics_hash["high"].value).must_be_within_delta 0, 1 + _(metrics_hash["high"].identifier).must_equal :qt + end + + it "always collects for known queues" do + Delayable.set(queue: "high").perform_later + ::GoodJob::JobPerformer.new("high").next + + metrics = subject.collect + + _(metrics.size).must_equal 1 + _(metrics[0].queue_name).must_equal "high" + _(metrics[0].value).must_equal 0 + + Delayable.set(queue: "default").perform_later + + metrics = subject.collect + + _(metrics.map(&:queue_name).sort).must_equal %w[default high] + + clear_enqueued_jobs + metrics = subject.collect + + _(metrics.size).must_equal 2 + _(metrics.map(&:queue_name).sort).must_equal %w[default high] + end + + it "always collects for queues with completed jobs" do + metrics = subject.collect + + _(metrics).must_be :empty? + + now = Time.now.utc + freeze_time(now - 0.15) { Delayable.set(queue: "default").perform_later } + metrics = freeze_time(now) { subject.collect } + + _(metrics.size).must_equal 1 + _(metrics[0].queue_name).must_equal "default" + _(metrics[0].value).must_be_within_delta 150, 1 + + ::GoodJob::JobPerformer.new("default").next + metrics = subject.collect + + _(metrics.size).must_equal 1 + _(metrics[0].queue_name).must_equal "default" + _(metrics[0].value).must_equal 0 + end + + it "ignores future jobs" do + Delayable.set(queue: "default", wait: 10.seconds).perform_later + + metrics = subject.collect + + _(metrics.size).must_equal 1 + _(metrics[0].queue_name).must_equal "default" + _(metrics[0].value).must_equal 0 + end + + it "ignores failed jobs waiting on retry" do + Delayable.set(queue: "default").perform_later(false) + ::GoodJob::JobPerformer.new("default").next + + metrics = subject.collect + + _(metrics.size).must_equal 1 + _(metrics[0].queue_name).must_equal "default" + _(metrics[0].value).must_equal 0 + end + + it "collects metrics for jobs without a queue name" do + metrics = freeze_time do + Delayable.perform_later + + subject.collect + end + + _(metrics.size).must_equal 1 + _(metrics[0].queue_name).must_equal "default" + _(metrics[0].value).must_be_within_delta 0, 1 + end + + it "logs debug information for each queue being collected" do + use_config log_level: :debug do + Delayable.set(queue: "default").perform_later + + subject.collect + + _(log_string).must_match %r{good_job-qt.default=\d+ms} + _(log_string).wont_match %r{good_job-busy} + end + end + + it "tracks busy jobs when the configuration is enabled" do + use_adapter_config :good_job, track_busy_jobs: true do + Delayable.set(queue: "default").perform_later + ::GoodJob::Execution.last.update!(performed_at: Time.now.utc) + + metrics = subject.collect + + _(metrics.size).must_equal 2 + _(metrics[1].value).must_equal 1 + _(metrics[1].queue_name).must_equal "default" + _(metrics[1].identifier).must_equal :busy + end + end + + it "logs debug information about busy jobs being collected" do + use_config log_level: :debug do + use_adapter_config :good_job, track_busy_jobs: true do + Delayable.set(queue: "default").perform_later + ::GoodJob::Execution.last.update!(performed_at: Time.now.utc) + + subject.collect + + _(log_string).must_match %r{good_job-qt.default=.+ good_job-busy.default=1} + end + end + end + + it "filters queues matching UUID format by default, to prevent reporting for dynamically generated queues" do + %W[low-#{SecureRandom.uuid} default #{SecureRandom.uuid}-high].each { |queue| + Delayable.set(queue: queue).perform_later + } + + metrics = subject.collect + + _(metrics.size).must_equal 1 + _(metrics[0].queue_name).must_equal "default" + end + + it "filters queues to collect metrics from based on the configured queue filter proc, overriding the default UUID filter" do + use_adapter_config :good_job, queue_filter: ->(queue_name) { queue_name.start_with? "low" } do + %W[low default high low-#{SecureRandom.uuid}].each { |queue| + Delayable.set(queue: queue).perform_later + } + + metrics = subject.collect + + queue_names = metrics.map(&:queue_name).sort + _(queue_names.size).must_equal 2 + _(queue_names[0]).must_equal "low" + _(queue_names[1]).must_be :start_with?, "low-" + end + end + + it "collects metrics only from the configured queues if the configuration is present, ignoring the queue filter" do + use_adapter_config :good_job, queues: %w[low ultra], queue_filter: ->(queue_name) { queue_name != "low" } do + %w[low default high].each { |queue| Delayable.set(queue: queue).perform_later } + + metrics = subject.collect + + _(metrics.map(&:queue_name)).must_equal %w[low ultra] + end + end + + it "collects metrics up to the configured number of max queues, sorting by length of the queue name" do + use_adapter_config :good_job, max_queues: 2 do + %w[low default high].each { |queue| Delayable.set(queue: queue).perform_later } + + metrics = subject.collect + + _(metrics.map(&:queue_name)).must_equal %w[low high] + _(log_string).must_match %r{GoodJob metrics reporting only 2 queues max, skipping the rest \(1\)} + end + end + end + end +end diff --git a/judoscale-good_job/test/test_helper.rb b/judoscale-good_job/test/test_helper.rb new file mode 100644 index 00000000..ba7bf7c8 --- /dev/null +++ b/judoscale-good_job/test/test_helper.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "judoscale-good_job" + +require "minitest/autorun" +require "minitest/spec" + +ENV["RACK_ENV"] ||= "test" +require "action_controller" + +class TestRailsApp < Rails::Application + config.secret_key_base = "test-secret" + config.eager_load = false + config.logger = ::Logger.new(StringIO.new, progname: "rails-app") + config.active_job.queue_adapter = :good_job + # Don't execute the jobs in-process. Our specs need to assert that jobs are in the queue. + config.good_job.execution_mode = :external + routes.append do + root to: proc { + [200, {"Content-Type" => "text/plain"}, ["Hello World"]] + } + end + initialize! +end + +require "active_record" + +DATABASE_NAME = "rails_autoscale_good_job_test" +DATABASE_USERNAME = "postgres" +DATABASE_URL = "postgres://#{DATABASE_USERNAME}:@localhost/#{DATABASE_NAME}" + +ActiveRecord::Tasks::DatabaseTasks.create(DATABASE_URL) +Minitest.after_run { + ActiveRecord::Tasks::DatabaseTasks.drop(DATABASE_URL) +} +ActiveRecord::Base.establish_connection(DATABASE_URL) + +ActiveRecord::Schema.define do + # https://github.com/collectiveidea/good_job_active_record/blob/master/lib/generators/good_job/templates/migration.rb#L3 + # standard:disable all + create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.text "queue_name" + t.integer "priority" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "performed_at" + t.datetime "finished_at" + t.text "error" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id" + t.text "concurrency_key" + t.text "cron_key" + t.uuid "retried_good_job_id" + t.datetime "cron_at" + t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at" + t.index ["active_job_id"], name: "index_good_jobs_on_active_job_id" + t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)" + t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at" + t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at", unique: true + t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))" + t.index ["priority", "created_at"], name: "index_good_jobs_jobs_on_priority_created_at_when_unfinished", order: { priority: "DESC NULLS LAST" }, where: "(finished_at IS NULL)" + t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)" + t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)" + end + # standard:enable all +end + +module Judoscale::Test +end + +Dir[File.expand_path("../../judoscale-ruby/test/support/*.rb", __dir__)].sort.each { |file| require file } + +Minitest::Test.include(Judoscale::Test) diff --git a/judoscale-ruby/lib/judoscale/job_metrics_collector/active_record_helper.rb b/judoscale-ruby/lib/judoscale/job_metrics_collector/active_record_helper.rb index 50abf777..42112c50 100644 --- a/judoscale-ruby/lib/judoscale/job_metrics_collector/active_record_helper.rb +++ b/judoscale-ruby/lib/judoscale/job_metrics_collector/active_record_helper.rb @@ -14,14 +14,18 @@ def self.cleanse_sql(sql) private - def select_rows_silently(sql) + def run_silently(&block) if Config.instance.log_level && ::ActiveRecord::Base.logger.respond_to?(:silence) - ::ActiveRecord::Base.logger.silence(Config.instance.log_level) { select_rows_tagged(sql) } + ::ActiveRecord::Base.logger.silence(Config.instance.log_level) { yield } else - select_rows_tagged(sql) + yield end end + def select_rows_silently(sql) + run_silently { select_rows_tagged(sql) } + end + def select_rows_tagged(sql) if ActiveRecord::Base.logger.respond_to?(:tagged) ActiveRecord::Base.logger.tagged(Config.instance.log_tag) { select_rows(sql) } diff --git a/sample-apps/delayed_job-sample/Gemfile.lock b/sample-apps/delayed_job-sample/Gemfile.lock index 8d3de9ea..271fe1bc 100644 --- a/sample-apps/delayed_job-sample/Gemfile.lock +++ b/sample-apps/delayed_job-sample/Gemfile.lock @@ -1,22 +1,22 @@ -PATH - remote: ../../judoscale-ruby - specs: - judoscale-ruby (0.0.1) - PATH remote: ../../judoscale-delayed_job specs: - judoscale-delayed_job (0.0.1) + judoscale-delayed_job (1.2.3) delayed_job_active_record (>= 4.0) - judoscale-ruby + judoscale-ruby (= 1.2.3) PATH remote: ../../judoscale-rails specs: - judoscale-rails (0.0.1) - judoscale-ruby + judoscale-rails (1.2.3) + judoscale-ruby (= 1.2.3) railties +PATH + remote: ../../judoscale-ruby + specs: + judoscale-ruby (1.2.3) + GEM remote: https://rubygems.org/ specs: @@ -119,11 +119,11 @@ DEPENDENCIES activerecord (~> 7.0.1) delayed_job_active_record delayed_job_web - pg (~> 1.1) - puma (~> 5.0) - judoscale-ruby! judoscale-delayed_job! judoscale-rails! + judoscale-ruby! + pg (~> 1.1) + puma (~> 5.0) railties (~> 7.0.1) BUNDLED WITH diff --git a/sample-apps/delayed_job-sample/README.md b/sample-apps/delayed_job-sample/README.md index 856509c7..f7492a39 100644 --- a/sample-apps/delayed_job-sample/README.md +++ b/sample-apps/delayed_job-sample/README.md @@ -26,7 +26,7 @@ Run `./bin/dev` to run the app in development mode. This will... ## How to use this sample app -Open https://judoscale-adapter-mock.requestcatcher.com in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. +Open https://requestinspector.com/p/judoscale-ruby in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. Run the app. Both the Rails and DelayedJob processes will send an initial request to the API once the app boots up. These can be inspected via request catcher. diff --git a/sample-apps/delayed_job-sample/app/views/jobs/index.html.erb b/sample-apps/delayed_job-sample/app/views/jobs/index.html.erb index 00a18210..ac6973c2 100644 --- a/sample-apps/delayed_job-sample/app/views/jobs/index.html.erb +++ b/sample-apps/delayed_job-sample/app/views/jobs/index.html.erb @@ -1,7 +1,7 @@

Rails Autoscale: DelayedJob Sample

- Rails Autoscale is reporting metrics to https://judoscale-adapter-mock.requestcatcher.com. + Rails Autoscale is reporting metrics to https://requestinspector.com/p/judoscale-ruby.

Open that page to watch as metrics are being reported, and reload this page multiple times to collect and report web metrics. diff --git a/sample-apps/delayed_job-sample/config/initializers/rails_autoscale.rb b/sample-apps/delayed_job-sample/config/initializers/rails_autoscale.rb index 0407511f..c74d26d3 100644 --- a/sample-apps/delayed_job-sample/config/initializers/rails_autoscale.rb +++ b/sample-apps/delayed_job-sample/config/initializers/rails_autoscale.rb @@ -1,8 +1,8 @@ return unless defined?(Judoscale) Judoscale.configure do |config| - # Open https://judoscale-adapter-mock.requestcatcher.com in a browser to monitor requests - config.api_base_url = ENV["JUDOSCALE_URL"] || "https://judoscale-adapter-mock.requestcatcher.com" + # Open https://requestinspector.com/p/judoscale-ruby in a browser to monitor requests + config.api_base_url = ENV["JUDOSCALE_URL"] || "https://requestinspector.com/inspect/judoscale-ruby" # Enable busy jobs tracking for testing with the sample app. config.delayed_job.track_busy_jobs = true diff --git a/sample-apps/good_job-sample/Gemfile b/sample-apps/good_job-sample/Gemfile new file mode 100644 index 00000000..ed11b95e --- /dev/null +++ b/sample-apps/good_job-sample/Gemfile @@ -0,0 +1,18 @@ +source "https://rubygems.org" +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +# Require only the frameworks we currently use instead of loading everything. +%w(activerecord actionpack actionview railties activejob activemodel).each { |rails_gem| + gem rails_gem, "~> 7.0.1" +} + +gem "pg", "~> 1.1" +gem "puma", "~> 5.0" + +# Need to reference all locally, otherwise it'd use the the gemspecs to try to find each gem, +# but we want to test against the local dev versions of them, not the released gems. +gem "judoscale-ruby", path: "../../judoscale-ruby" +gem "judoscale-rails", path: "../../judoscale-rails" +gem "judoscale-good_job", path: "../../judoscale-good_job" + +gem "good_job", "~> 3.7" diff --git a/sample-apps/good_job-sample/Gemfile.lock b/sample-apps/good_job-sample/Gemfile.lock new file mode 100644 index 00000000..2fcc7097 --- /dev/null +++ b/sample-apps/good_job-sample/Gemfile.lock @@ -0,0 +1,130 @@ +PATH + remote: ../../judoscale-good_job + specs: + judoscale-good_job (1.2.3) + good_job (>= 3.0) + judoscale-ruby (= 1.2.3) + +PATH + remote: ../../judoscale-rails + specs: + judoscale-rails (1.2.3) + judoscale-ruby (= 1.2.3) + railties + +PATH + remote: ../../judoscale-ruby + specs: + judoscale-ruby (1.2.3) + +GEM + remote: https://rubygems.org/ + specs: + actionpack (7.0.2.3) + actionview (= 7.0.2.3) + activesupport (= 7.0.2.3) + rack (~> 2.0, >= 2.2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actionview (7.0.2.3) + activesupport (= 7.0.2.3) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.2.3) + activesupport (= 7.0.2.3) + globalid (>= 0.3.6) + activemodel (7.0.2.3) + activesupport (= 7.0.2.3) + activerecord (7.0.2.3) + activemodel (= 7.0.2.3) + activesupport (= 7.0.2.3) + activesupport (7.0.2.3) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + builder (3.2.4) + concurrent-ruby (1.1.10) + crass (1.0.6) + erubi (1.10.0) + et-orbi (1.2.7) + tzinfo + fugit (1.8.0) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) + globalid (1.0.0) + activesupport (>= 5.0) + good_job (3.7.4) + activejob (>= 6.0.0) + activerecord (>= 6.0.0) + concurrent-ruby (>= 1.0.2) + fugit (>= 1.1) + railties (>= 6.0.0) + thor (>= 0.14.1) + webrick (>= 1.3) + i18n (1.10.0) + concurrent-ruby (~> 1.0) + loofah (2.16.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + method_source (1.0.0) + minitest (5.15.0) + nio4r (2.5.8) + nokogiri (1.13.3-arm64-darwin) + racc (~> 1.4) + nokogiri (1.13.3-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.13.3-x86_64-linux) + racc (~> 1.4) + pg (1.3.5) + puma (5.6.4) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.6.0) + rack (2.2.3) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.2) + loofah (~> 2.3) + railties (7.0.2.3) + actionpack (= 7.0.2.3) + activesupport (= 7.0.2.3) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rake (13.0.6) + thor (1.2.1) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + webrick (1.7.0) + zeitwerk (2.5.4) + +PLATFORMS + arm64-darwin-20 + arm64-darwin-21 + x86_64-darwin-21 + x86_64-linux + +DEPENDENCIES + actionpack (~> 7.0.1) + actionview (~> 7.0.1) + activejob (~> 7.0.1) + activemodel (~> 7.0.1) + activerecord (~> 7.0.1) + good_job (~> 3.7) + judoscale-good_job! + judoscale-rails! + judoscale-ruby! + pg (~> 1.1) + puma (~> 5.0) + railties (~> 7.0.1) + +BUNDLED WITH + 2.3.9 diff --git a/sample-apps/good_job-sample/Procfile b/sample-apps/good_job-sample/Procfile new file mode 100644 index 00000000..fd963047 --- /dev/null +++ b/sample-apps/good_job-sample/Procfile @@ -0,0 +1,4 @@ +release: bundle exec rails db:migrate +proxy: npx judoscale-adapter-proxy-server +rails: rails server +jobs: DYNO=worker.1 bundle exec good_job start --max-threads=1 diff --git a/sample-apps/good_job-sample/README.md b/sample-apps/good_job-sample/README.md new file mode 100644 index 00000000..db5cc347 --- /dev/null +++ b/sample-apps/good_job-sample/README.md @@ -0,0 +1,53 @@ +# Sample app for judoscale-good_job gem + +This is a minimal Rails app to test the judoscale-good_job gem. + +## Prerequisites + +- Ruby +- Node +- [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) +- Redis + +## Set up the app + +Run `./bin/setup` install necessary dependencies. This will... + +- Run `bundle install` to install gems + +## Run the app + +Run `./bin/dev` to run the app in development mode. This will... + +- Use `heroku local` and a `Procfile` to start the following processes: + - A [tiny proxy server](https://github.com/judoscale/judoscale-adapter-proxy-server) that adds the `X-Request-Start` request header so we can test request queue time reporting. + - The Rails server. + - The GoodJob server to process jobs. + +## How to use this sample app + +Open https://requestinspector.com/p/judoscale-ruby in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. + +Run the app. Both the Rails and GoodJob processes will send an initial request to the API once the app boots up. These can be inspected via request catcher. + +Open http://localhost:5000 to see how many jobs are waiting on each of the available queues, and to enqueue sample jobs on those queues that will be processed by the GoodJob server slowly. + +## Deploy this app to Heroku + +From this directory, run the following to create a new git repo and push it to Heroku: + +```sh +git init +git add . +git commit -m "prep for Heroku" +heroku create +git push heroku main +``` + +To install Rails Autoscale: + +```sh +# scale up a worker dyno before doing this so Rails Autoscale picks it up +heroku ps:scale dj=1 +heroku addons:create judoscale +``` diff --git a/sample-apps/good_job-sample/Rakefile b/sample-apps/good_job-sample/Rakefile new file mode 100644 index 00000000..d1baef06 --- /dev/null +++ b/sample-apps/good_job-sample/Rakefile @@ -0,0 +1,3 @@ +require_relative "config/application" + +Rails.application.load_tasks diff --git a/sample-apps/good_job-sample/app/controllers/application_controller.rb b/sample-apps/good_job-sample/app/controllers/application_controller.rb new file mode 100644 index 00000000..09705d12 --- /dev/null +++ b/sample-apps/good_job-sample/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/sample-apps/good_job-sample/app/controllers/jobs_controller.rb b/sample-apps/good_job-sample/app/controllers/jobs_controller.rb new file mode 100644 index 00000000..4d03ee60 --- /dev/null +++ b/sample-apps/good_job-sample/app/controllers/jobs_controller.rb @@ -0,0 +1,27 @@ +class JobsController < ApplicationController + QUEUES = %w[default low high] + + def index + @available_queues = QUEUES.dup + @queues = [] + # @queues = Delayed::Backend::ActiveRecord::Job.group(:queue).count + end + + def create + job_params = params.require(:job).permit(:queue_name, :enqueue_count) + + queue_name = job_params[:queue_name] + enqueue_count = job_params[:enqueue_count].to_i + + if QUEUES.include?(queue_name) && enqueue_count.between?(1, 100) + 1.upto(enqueue_count) { + SampleJob.set(queue: queue_name).perform_later + } + + flash[:notice] = "#{enqueue_count} #{"job".pluralize(enqueue_count)} enqueued on the #{queue_name.inspect} queue." + else + flash[:alert] = "Please enter queue name & enqueue count." + end + redirect_to action: :index + end +end diff --git a/sample-apps/good_job-sample/app/jobs/application_job.rb b/sample-apps/good_job-sample/app/jobs/application_job.rb new file mode 100644 index 00000000..d394c3d1 --- /dev/null +++ b/sample-apps/good_job-sample/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/sample-apps/good_job-sample/app/jobs/sample_job.rb b/sample-apps/good_job-sample/app/jobs/sample_job.rb new file mode 100644 index 00000000..eee3e10d --- /dev/null +++ b/sample-apps/good_job-sample/app/jobs/sample_job.rb @@ -0,0 +1,7 @@ +class SampleJob < ApplicationJob + retry_on StandardError, wait: 30.seconds, attempts: 3 + + def perform + sleep 5 + end +end diff --git a/sample-apps/good_job-sample/app/views/jobs/index.html.erb b/sample-apps/good_job-sample/app/views/jobs/index.html.erb new file mode 100644 index 00000000..7c1ae72f --- /dev/null +++ b/sample-apps/good_job-sample/app/views/jobs/index.html.erb @@ -0,0 +1,44 @@ +

Rails Autoscale: GoodJob Sample

+ +

+ Rails Autoscale is reporting metrics to https://requestinspector.com/p/judoscale-ruby. +

+

+ Open that page to watch as metrics are being reported, and reload this page multiple times to collect and report web metrics. +

+

+ Enqueue test jobs using the form below. + They will be slowly processed by GoodJob, while Rails Autoscale collects and reports available queue metrics. + <%= link_to "GoodJob Dashboard ↗", "/good_job", target: "_blank" %>. +

+ +

GoodJob Queues

+ +<% if notice %>

<%= notice %><% end %> +<% if alert %>

<%= alert %><% end %> + +<% if @queues.any? %> +

+<% else %> +

No queues found.

+<% end %> + +

Enqueue Jobs

+ +<%= form_with scope: :job, url: jobs_path, method: :post do |f| %> +
+ <%= f.label :queue_name %> + <%= f.select :queue_name, @available_queues, required: true %> +
+
+ <%= f.label :enqueue_count %> + <%= f.number_field :enqueue_count, min: 1, max: 100, required: true %> +
+
+ <%= f.button "Enqueue Jobs" %> +
+<% end %> diff --git a/sample-apps/good_job-sample/app/views/layouts/application.html.erb b/sample-apps/good_job-sample/app/views/layouts/application.html.erb new file mode 100644 index 00000000..aa997e6b --- /dev/null +++ b/sample-apps/good_job-sample/app/views/layouts/application.html.erb @@ -0,0 +1,15 @@ + + + + Rails Autoscale: GoodJob Sample + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "application" %> + + + + <%= yield %> + + diff --git a/sample-apps/good_job-sample/bin/bundle b/sample-apps/good_job-sample/bin/bundle new file mode 100755 index 00000000..a71368e3 --- /dev/null +++ b/sample-apps/good_job-sample/bin/bundle @@ -0,0 +1,114 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= + env_var_version || cli_arg_version || + lockfile_version + end + + def bundler_requirement + return "#{Gem::Requirement.default}.a" unless bundler_version + + bundler_gem_version = Gem::Version.new(bundler_version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/sample-apps/good_job-sample/bin/dev b/sample-apps/good_job-sample/bin/dev new file mode 100755 index 00000000..9bfc4542 --- /dev/null +++ b/sample-apps/good_job-sample/bin/dev @@ -0,0 +1,5 @@ +#!/bin/bash + +# Heroku sets the DYNO environment variable on every dyno. + +DYNO=web.1 heroku local diff --git a/sample-apps/good_job-sample/bin/rails b/sample-apps/good_job-sample/bin/rails new file mode 100755 index 00000000..efc03774 --- /dev/null +++ b/sample-apps/good_job-sample/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/sample-apps/good_job-sample/bin/rake b/sample-apps/good_job-sample/bin/rake new file mode 100755 index 00000000..4fbf10b9 --- /dev/null +++ b/sample-apps/good_job-sample/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/sample-apps/good_job-sample/bin/setup b/sample-apps/good_job-sample/bin/setup new file mode 100755 index 00000000..ec47b79b --- /dev/null +++ b/sample-apps/good_job-sample/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" +end diff --git a/sample-apps/good_job-sample/config.ru b/sample-apps/good_job-sample/config.ru new file mode 100644 index 00000000..4a3c09a6 --- /dev/null +++ b/sample-apps/good_job-sample/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/sample-apps/good_job-sample/config/application.rb b/sample-apps/good_job-sample/config/application.rb new file mode 100644 index 00000000..b249357c --- /dev/null +++ b/sample-apps/good_job-sample/config/application.rb @@ -0,0 +1,45 @@ +require_relative "boot" + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +# require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +# require "action_cable/engine" +require "rails/test_unit/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module GoodJobSample + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 7.0 + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + # Don't generate system test files. + config.generators.system_tests = nil + + # Log to STDOUT (so this sample app can be deployed to Heroku) + logger = ActiveSupport::Logger.new($stdout) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + + config.active_job.queue_adapter = :good_job + config.good_job.execution_mode = :external + end +end diff --git a/sample-apps/good_job-sample/config/boot.rb b/sample-apps/good_job-sample/config/boot.rb new file mode 100644 index 00000000..28201161 --- /dev/null +++ b/sample-apps/good_job-sample/config/boot.rb @@ -0,0 +1,3 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. diff --git a/sample-apps/good_job-sample/config/credentials.yml.enc b/sample-apps/good_job-sample/config/credentials.yml.enc new file mode 100644 index 00000000..8166f0ed --- /dev/null +++ b/sample-apps/good_job-sample/config/credentials.yml.enc @@ -0,0 +1 @@ +FZx7ZsIRbV67L/KOzCHAeZngqWGBUcDmShr3fBMryXJ3wUR/VtfJgDz2+ZebyhFGjpZRsjKilOKSDVf6q0f28JQdBdJUiv0X1VStN63bf5thS2Z37cAoiUYkOo+SsA2tvUhPNrm4sNriWDXF/ld3nr1V2zMB2jO6iUlqAMImWoZeuoZ2ylAsAC3f8tDhbPb/1MFOrj/ZwB8E+ktnkTqSxbEZPcRgU2Af13nH4GKh2Wa4boF+SRyDj602Y/iLngqx01a77n1pmJKJRaFojhgEOfJvOY6nBow1nMKzBfxADzCDKaDUfBto2ZoVLN8BHNVtfOCzUKOw8ZLySZuyAQSJbJiba9lPqpDd6TwGft3C1cKeFxUs85BmBDilCu5ygpCANGrMzYzS9qu5+oDO82eHiyD951VmIVJrEO4B--DvI3jM5h8LwgVqfq--wo14TvZgMPbi8k9/mLM8sg== \ No newline at end of file diff --git a/sample-apps/good_job-sample/config/database.yml b/sample-apps/good_job-sample/config/database.yml new file mode 100644 index 00000000..93a2ab90 --- /dev/null +++ b/sample-apps/good_job-sample/config/database.yml @@ -0,0 +1,14 @@ +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # https://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + +development: + <<: *default + database: rails_autoscale_good_job_sample_development + username: postgres + #password: + host: localhost + #port: 5432 diff --git a/sample-apps/good_job-sample/config/environment.rb b/sample-apps/good_job-sample/config/environment.rb new file mode 100644 index 00000000..cac53157 --- /dev/null +++ b/sample-apps/good_job-sample/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/sample-apps/good_job-sample/config/environments/development.rb b/sample-apps/good_job-sample/config/environments/development.rb new file mode 100644 index 00000000..df4719f2 --- /dev/null +++ b/sample-apps/good_job-sample/config/environments/development.rb @@ -0,0 +1,64 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + config.log_level = ENV.fetch("LOG_LEVEL", :debug) + + # Suppress logger output for asset requests. + # config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true +end diff --git a/sample-apps/good_job-sample/config/initializers/rails_autoscale.rb b/sample-apps/good_job-sample/config/initializers/rails_autoscale.rb new file mode 100644 index 00000000..a4fe3500 --- /dev/null +++ b/sample-apps/good_job-sample/config/initializers/rails_autoscale.rb @@ -0,0 +1,9 @@ +return unless defined?(Judoscale) + +Judoscale.configure do |config| + # Open https://requestinspector.com/p/judoscale-ruby in a browser to monitor requests + config.api_base_url = ENV["JUDOSCALE_URL"] || "https://requestinspector.com/inspect/judoscale-ruby" + + # Enable busy jobs tracking for testing with the sample app. + config.good_job.track_busy_jobs = true +end diff --git a/sample-apps/good_job-sample/config/puma.rb b/sample-apps/good_job-sample/config/puma.rb new file mode 100644 index 00000000..daaf0369 --- /dev/null +++ b/sample-apps/good_job-sample/config/puma.rb @@ -0,0 +1,43 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart diff --git a/sample-apps/good_job-sample/config/routes.rb b/sample-apps/good_job-sample/config/routes.rb new file mode 100644 index 00000000..cd00d780 --- /dev/null +++ b/sample-apps/good_job-sample/config/routes.rb @@ -0,0 +1,5 @@ +Rails.application.routes.draw do + resource :jobs, only: [:index, :create] + mount GoodJob::Engine => "good_job" + root "jobs#index" +end diff --git a/sample-apps/good_job-sample/db/migrate/20230117185228_create_good_jobs.rb b/sample-apps/good_job-sample/db/migrate/20230117185228_create_good_jobs.rb new file mode 100644 index 00000000..6a010e82 --- /dev/null +++ b/sample-apps/good_job-sample/db/migrate/20230117185228_create_good_jobs.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +class CreateGoodJobs < ActiveRecord::Migration[7.0] + def change + enable_extension 'pgcrypto' + + create_table :good_jobs, id: :uuid do |t| + t.text :queue_name + t.integer :priority + t.jsonb :serialized_params + t.datetime :scheduled_at + t.datetime :performed_at + t.datetime :finished_at + t.text :error + + t.timestamps + + t.uuid :active_job_id + t.text :concurrency_key + t.text :cron_key + t.uuid :retried_good_job_id + t.datetime :cron_at + end + + create_table :good_job_processes, id: :uuid do |t| + t.timestamps + t.jsonb :state + end + + create_table :good_job_settings, id: :uuid do |t| + t.timestamps + t.text :key + t.jsonb :value + t.index :key, unique: true + end + + add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)", name: "index_good_jobs_on_scheduled_at" + add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)", name: :index_good_jobs_on_queue_name_and_scheduled_at + add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at + add_index :good_jobs, :concurrency_key, where: "(finished_at IS NULL)", name: :index_good_jobs_on_concurrency_key_when_unfinished + add_index :good_jobs, [:cron_key, :created_at], name: :index_good_jobs_on_cron_key_and_created_at + add_index :good_jobs, [:cron_key, :cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true + add_index :good_jobs, [:active_job_id], name: :index_good_jobs_on_active_job_id + add_index :good_jobs, [:finished_at], where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at + add_index :good_jobs, [:priority, :created_at], order: { priority: "DESC NULLS LAST", created_at: :asc }, + where: "finished_at IS NULL", name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished + end +end diff --git a/sample-apps/good_job-sample/db/schema.rb b/sample-apps/good_job-sample/db/schema.rb new file mode 100644 index 00000000..40778af5 --- /dev/null +++ b/sample-apps/good_job-sample/db/schema.rb @@ -0,0 +1,60 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.0].define(version: 2023_01_17_185228) do + # These are extensions that must be enabled in order to support this database + enable_extension "pgcrypto" + enable_extension "plpgsql" + enable_extension "timescaledb" + enable_extension "timescaledb_toolkit" + + create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.jsonb "state" + end + + create_table "good_job_settings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "key" + t.jsonb "value" + t.index ["key"], name: "index_good_job_settings_on_key", unique: true + end + + create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.text "queue_name" + t.integer "priority" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "performed_at" + t.datetime "finished_at" + t.text "error" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id" + t.text "concurrency_key" + t.text "cron_key" + t.uuid "retried_good_job_id" + t.datetime "cron_at" + t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at" + t.index ["active_job_id"], name: "index_good_jobs_on_active_job_id" + t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)" + t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at" + t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at", unique: true + t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))" + t.index ["priority", "created_at"], name: "index_good_jobs_jobs_on_priority_created_at_when_unfinished", order: { priority: "DESC NULLS LAST" }, where: "(finished_at IS NULL)" + t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)" + t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)" + end + +end diff --git a/sample-apps/good_job-sample/log/.keep b/sample-apps/good_job-sample/log/.keep new file mode 100644 index 00000000..e69de29b diff --git a/sample-apps/que-2-sample/README.md b/sample-apps/que-2-sample/README.md index 07cf97c1..bff81e23 100644 --- a/sample-apps/que-2-sample/README.md +++ b/sample-apps/que-2-sample/README.md @@ -26,7 +26,7 @@ Run `./bin/dev` to run the app in development mode. This will... ## How to use this sample app -Open https://judoscale-adapter-mock.requestcatcher.com in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. +Open https://requestinspector.com/p/judoscale-ruby in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. Run the app. Both the Rails and Que processes will send an initial request to the API once the app boots up. These can be inspected via request catcher. diff --git a/sample-apps/que-2-sample/app/views/jobs/index.html.erb b/sample-apps/que-2-sample/app/views/jobs/index.html.erb index feda57ec..b08d0d31 100644 --- a/sample-apps/que-2-sample/app/views/jobs/index.html.erb +++ b/sample-apps/que-2-sample/app/views/jobs/index.html.erb @@ -1,7 +1,7 @@

Rails Autoscale: Que Sample

- Rails Autoscale is reporting metrics to https://judoscale-adapter-mock.requestcatcher.com. + Rails Autoscale is reporting metrics to https://requestinspector.com/p/judoscale-ruby.

Open that page to watch as metrics are being reported, and reload this page multiple times to collect and report web metrics. diff --git a/sample-apps/que-2-sample/config/initializers/rails_autoscale.rb b/sample-apps/que-2-sample/config/initializers/rails_autoscale.rb index 30c7303b..c78c95d7 100644 --- a/sample-apps/que-2-sample/config/initializers/rails_autoscale.rb +++ b/sample-apps/que-2-sample/config/initializers/rails_autoscale.rb @@ -1,8 +1,8 @@ return unless defined?(Judoscale) Judoscale.configure do |config| - # Open https://judoscale-adapter-mock.requestcatcher.com in a browser to monitor requests - config.api_base_url = ENV["JUDOSCALE_URL"] || "https://judoscale-adapter-mock.requestcatcher.com" + # Open https://requestinspector.com/p/judoscale-ruby in a browser to monitor requests + config.api_base_url = ENV["JUDOSCALE_URL"] || "https://requestinspector.com/inspect/judoscale-ruby" # Enable busy jobs tracking for testing with the sample app. config.que.track_busy_jobs = true diff --git a/sample-apps/rails-sample/README.md b/sample-apps/rails-sample/README.md index cd9813d8..d0bf6e0b 100644 --- a/sample-apps/rails-sample/README.md +++ b/sample-apps/rails-sample/README.md @@ -24,7 +24,7 @@ Run `./bin/dev` to run the app in development mode. This will... ## How to use this sample app -Open https://judoscale-adapter-mock.requestcatcher.com in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. +Open https://requestinspector.com/p/judoscale-ruby in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. Run the app. As soon as it boots up, an initial request to the API is sent, and can be inspected via request catcher. diff --git a/sample-apps/rails-sample/app/views/home/index.html.erb b/sample-apps/rails-sample/app/views/home/index.html.erb index b8a05203..c6bd12ab 100644 --- a/sample-apps/rails-sample/app/views/home/index.html.erb +++ b/sample-apps/rails-sample/app/views/home/index.html.erb @@ -1,7 +1,7 @@

Rails Autoscale: Rails Sample

- Rails Autoscale is reporting metrics to https://judoscale-adapter-mock.requestcatcher.com. + Rails Autoscale is reporting metrics to https://requestinspector.com/p/judoscale-ruby.

Open that page to watch as metrics are being reported, and reload this page multiple times to collect and report web metrics. diff --git a/sample-apps/rails-sample/config/initializers/rails_autoscale.rb b/sample-apps/rails-sample/config/initializers/rails_autoscale.rb index dfa8b990..c16b205c 100644 --- a/sample-apps/rails-sample/config/initializers/rails_autoscale.rb +++ b/sample-apps/rails-sample/config/initializers/rails_autoscale.rb @@ -1,6 +1,6 @@ return unless defined?(Judoscale) Judoscale.configure do |config| - # Open https://judoscale-adapter-mock.requestcatcher.com in a browser to monitor requests - config.api_base_url = ENV["JUDOSCALE_URL"] || "https://judoscale-adapter-mock.requestcatcher.com" + # Open https://requestinspector.com/p/judoscale-ruby in a browser to monitor requests + config.api_base_url = ENV["JUDOSCALE_URL"] || "https://requestinspector.com/inspect/judoscale-ruby" end diff --git a/sample-apps/resque-sample/README.md b/sample-apps/resque-sample/README.md index 344a4dd1..c0c8888b 100644 --- a/sample-apps/resque-sample/README.md +++ b/sample-apps/resque-sample/README.md @@ -26,7 +26,7 @@ Run `./bin/dev` to run the app in development mode. This will... ## How to use this sample app -Open https://judoscale-adapter-mock.requestcatcher.com in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. +Open https://requestinspector.com/p/judoscale-ruby in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. Run the app. Both the Rails and Resque processes will send an initial request to the API once the app boots up. These can be inspected via request catcher. diff --git a/sample-apps/resque-sample/app/views/jobs/index.html.erb b/sample-apps/resque-sample/app/views/jobs/index.html.erb index 6f7c8aff..f1ae9566 100644 --- a/sample-apps/resque-sample/app/views/jobs/index.html.erb +++ b/sample-apps/resque-sample/app/views/jobs/index.html.erb @@ -1,7 +1,7 @@

Rails Autoscale: Resque Sample

- Rails Autoscale is reporting metrics to https://judoscale-adapter-mock.requestcatcher.com. + Rails Autoscale is reporting metrics to https://requestinspector.com/p/judoscale-ruby.

Open that page to watch as metrics are being reported, and reload this page multiple times to collect and report web metrics. diff --git a/sample-apps/resque-sample/config/initializers/rails_autoscale.rb b/sample-apps/resque-sample/config/initializers/rails_autoscale.rb index 78b40807..c5ff3ad6 100644 --- a/sample-apps/resque-sample/config/initializers/rails_autoscale.rb +++ b/sample-apps/resque-sample/config/initializers/rails_autoscale.rb @@ -1,8 +1,8 @@ return unless defined?(Judoscale) Judoscale.configure do |config| - # Open https://judoscale-adapter-mock.requestcatcher.com in a browser to monitor requests - config.api_base_url = ENV["JUDOSCALE_URL"] || "https://judoscale-adapter-mock.requestcatcher.com" + # Open https://requestinspector.com/p/judoscale-ruby in a browser to monitor requests + config.api_base_url = ENV["JUDOSCALE_URL"] || "https://requestinspector.com/inspect/judoscale-ruby" # Enable busy jobs tracking for testing with the sample app. config.resque.track_busy_jobs = true diff --git a/sample-apps/sidekiq-sample/README.md b/sample-apps/sidekiq-sample/README.md index bf95f8a9..b68a450f 100644 --- a/sample-apps/sidekiq-sample/README.md +++ b/sample-apps/sidekiq-sample/README.md @@ -26,7 +26,7 @@ Run `./bin/dev` to run the app in development mode. This will... ## How to use this sample app -Open https://judoscale-adapter-mock.requestcatcher.com in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. +Open https://requestinspector.com/p/judoscale-ruby in a browser. The sample app is configured to use this endpoint as a mock for the Rails Autoscale Adapter API. This page will monitor all API requests sent from the adapter. Run the app. Both the Rails and Sidekiq processes will send an initial request to the API once the app boots up. These can be inspected via request catcher. diff --git a/sample-apps/sidekiq-sample/app/views/jobs/index.html.erb b/sample-apps/sidekiq-sample/app/views/jobs/index.html.erb index b3da0448..fe0b8daa 100644 --- a/sample-apps/sidekiq-sample/app/views/jobs/index.html.erb +++ b/sample-apps/sidekiq-sample/app/views/jobs/index.html.erb @@ -1,7 +1,7 @@

Rails Autoscale: Sidekiq Sample

- Rails Autoscale is reporting metrics to https://judoscale-adapter-mock.requestcatcher.com. + Rails Autoscale is reporting metrics to https://requestinspector.com/p/judoscale-ruby.

Open that page to watch as metrics are being reported, and reload this page multiple times to collect and report web metrics. diff --git a/sample-apps/sidekiq-sample/config/initializers/rails_autoscale.rb b/sample-apps/sidekiq-sample/config/initializers/rails_autoscale.rb index a1b4877c..f173b337 100644 --- a/sample-apps/sidekiq-sample/config/initializers/rails_autoscale.rb +++ b/sample-apps/sidekiq-sample/config/initializers/rails_autoscale.rb @@ -1,8 +1,8 @@ return unless defined?(Judoscale) Judoscale.configure do |config| - # Open https://judoscale-adapter-mock.requestcatcher.com in a browser to monitor requests - config.api_base_url = ENV["JUDOSCALE_URL"] || "https://judoscale-adapter-mock.requestcatcher.com" + # Open https://requestinspector.com/p/judoscale-ruby in a browser to monitor requests + config.api_base_url = ENV["JUDOSCALE_URL"] || "https://requestinspector.com/inspect/judoscale-ruby" # Enable busy jobs tracking for testing with the sample app. config.sidekiq.track_busy_jobs = true diff --git a/sample-apps/sinatra-sample/config.ru b/sample-apps/sinatra-sample/config.ru index c74901c0..11ee9763 100644 --- a/sample-apps/sinatra-sample/config.ru +++ b/sample-apps/sinatra-sample/config.ru @@ -1,8 +1,8 @@ require "./hello" Judoscale.configure do |config| - # Open https://judoscale-adapter-mock.requestcatcher.com in a browser to monitor requests - config.api_base_url = ENV["JUDOSCALE_URL"] || "https://judoscale-adapter-mock.requestcatcher.com" + # Open https://requestinspector.com/p/judoscale-ruby in a browser to monitor requests + config.api_base_url = ENV["JUDOSCALE_URL"] || "https://requestinspector.com/inspect/judoscale-ruby" end use Judoscale::RequestMiddleware