diff --git a/.gitignore b/.gitignore index 59c74047..41afc9ef 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /tmp /log /public +.ruby-gemset +.byebug_history diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index e20b1260..94c2b223 100644 --- a/Gemfile +++ b/Gemfile @@ -7,16 +7,26 @@ gem 'rails', '~> 5.2.3' gem 'pg', '>= 0.18', '< 2.0' gem 'puma', '~> 3.11' gem 'bootsnap', '>= 1.1.0', require: false +gem 'activerecord-import' +gem 'fast_jsonparser' +gem 'strong_migrations' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + gem 'rspec-benchmark' + gem 'rspec-rails', '~> 5.0.0' end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' + gem 'rack-mini-profiler' + gem 'flamegraph' + gem 'memory_profiler' + gem 'stackprof' + gem 'bullet' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index fccf6f5f..e7f913bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,145 +1,213 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.2.3) - actionpack (= 5.2.3) + actioncable (5.2.8.1) + actionpack (= 5.2.8.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) + actionmailer (5.2.8.1) + actionpack (= 5.2.8.1) + actionview (= 5.2.8.1) + activejob (= 5.2.8.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.3) - actionview (= 5.2.3) - activesupport (= 5.2.3) - rack (~> 2.0) + actionpack (5.2.8.1) + actionview (= 5.2.8.1) + activesupport (= 5.2.8.1) + rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.3) - activesupport (= 5.2.3) + actionview (5.2.8.1) + activesupport (= 5.2.8.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.3) - activesupport (= 5.2.3) + activejob (5.2.8.1) + activesupport (= 5.2.8.1) globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) + activemodel (5.2.8.1) + activesupport (= 5.2.8.1) + activerecord (5.2.8.1) + activemodel (= 5.2.8.1) + activesupport (= 5.2.8.1) arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) - marcel (~> 0.3.1) - activesupport (5.2.3) + activerecord-import (1.4.1) + activerecord (>= 4.2) + activestorage (5.2.8.1) + actionpack (= 5.2.8.1) + activerecord (= 5.2.8.1) + marcel (~> 1.0.0) + activesupport (5.2.8.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) arel (9.0.0) - bindex (0.6.0) - bootsnap (1.4.2) - msgpack (~> 1.0) - builder (3.2.3) - byebug (11.0.1) - concurrent-ruby (1.1.5) - crass (1.0.4) - erubi (1.8.0) - ffi (1.10.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.6.0) + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) + bindex (0.8.1) + bootsnap (1.16.0) + msgpack (~> 1.2) + builder (3.2.4) + bullet (7.0.7) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) + byebug (11.1.3) + concurrent-ruby (1.2.2) + crass (1.0.6) + date (3.3.3) + diff-lcs (1.5.0) + erubi (1.12.0) + fast_jsonparser (0.6.0) + ffi (1.15.5) + flamegraph (0.9.5) + globalid (1.1.0) + activesupport (>= 5.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - loofah (2.2.3) + loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.1) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (0.9.2) - mimemagic (0.3.3) - mini_mime (1.0.1) - mini_portile2 (2.4.0) - minitest (5.11.3) - msgpack (1.2.9) - nio4r (2.3.1) - nokogiri (1.10.2) - mini_portile2 (~> 2.4.0) - pg (1.1.4) - puma (3.12.1) - rack (2.0.6) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.3) - actioncable (= 5.2.3) - actionmailer (= 5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - activemodel (= 5.2.3) - activerecord (= 5.2.3) - activestorage (= 5.2.3) - activesupport (= 5.2.3) + net-imap + net-pop + net-smtp + marcel (1.0.2) + memory_profiler (1.0.1) + method_source (1.0.0) + mini_mime (1.1.2) + mini_portile2 (2.8.1) + minitest (5.18.0) + msgpack (1.7.0) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol + nio4r (2.5.8) + nokogiri (1.13.10) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + pg (1.4.6) + puma (3.12.6) + racc (1.6.2) + rack (2.2.6.4) + rack-mini-profiler (3.0.0) + rack (>= 1.2.0) + rack-test (2.1.0) + rack (>= 1.3) + rails (5.2.8.1) + actioncable (= 5.2.8.1) + actionmailer (= 5.2.8.1) + actionpack (= 5.2.8.1) + actionview (= 5.2.8.1) + activejob (= 5.2.8.1) + activemodel (= 5.2.8.1) + activerecord (= 5.2.8.1) + activestorage (= 5.2.8.1) + activesupport (= 5.2.8.1) bundler (>= 1.3.0) - railties (= 5.2.3) + railties (= 5.2.8.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.4) - loofah (~> 2.2, >= 2.2.2) - railties (5.2.3) - actionpack (= 5.2.3) - activesupport (= 5.2.3) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) + railties (5.2.8.1) + actionpack (= 5.2.8.1) + activesupport (= 5.2.8.1) method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) - rake (12.3.2) - rb-fsevent (0.10.3) - rb-inotify (0.10.0) + rake (13.0.6) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) ffi (~> 1.0) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-benchmark (0.6.0) + benchmark-malloc (~> 0.2) + benchmark-perf (~> 0.6) + benchmark-trend (~> 0.4) + rspec (>= 3.0) + rspec-core (3.12.1) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (5.0.3) + actionpack (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.12.0) ruby_dep (1.5.0) - sprockets (3.7.2) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) - thor (0.20.3) + stackprof (0.2.24) + strong_migrations (1.4.4) + activerecord (>= 5.2) + thor (1.2.1) thread_safe (0.3.6) - tzinfo (1.2.5) + timeout (0.3.2) + tzinfo (1.2.11) thread_safe (~> 0.1) + uniform_notifier (1.16.0) web-console (3.7.0) actionview (>= 5.0) activemodel (>= 5.0) bindex (>= 0.4.0) railties (>= 5.0) - websocket-driver (0.7.0) + websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) + websocket-extensions (0.1.5) PLATFORMS ruby DEPENDENCIES + activerecord-import bootsnap (>= 1.1.0) + bullet byebug + fast_jsonparser + flamegraph listen (>= 3.0.5, < 3.2) + memory_profiler pg (>= 0.18, < 2.0) puma (~> 3.11) + rack-mini-profiler rails (~> 5.2.3) + rspec-benchmark + rspec-rails (~> 5.0.0) + stackprof + strong_migrations tzinfo-data web-console (>= 3.3.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d12..2127dc81 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,9 @@ class ApplicationController < ActionController::Base + before_action :miniprofiler + + private + + def miniprofiler + Rack::MiniProfiler.authorize_request # if user.admin? + end end diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb index acb38be2..ad823f6d 100644 --- a/app/controllers/trips_controller.rb +++ b/app/controllers/trips_controller.rb @@ -2,6 +2,6 @@ class TripsController < ApplicationController def index @from = City.find_by_name!(params[:from]) @to = City.find_by_name!(params[:to]) - @trips = Trip.where(from: @from, to: @to).order(:start_time) + @trips = Trip.includes(:bus, bus: :services).where(from: @from, to: @to).order(:start_time) end end diff --git a/app/models/bus.rb b/app/models/bus.rb index 1dcc54cb..d97de31f 100644 --- a/app/models/bus.rb +++ b/app/models/bus.rb @@ -13,7 +13,8 @@ class Bus < ApplicationRecord ].freeze has_many :trips - has_and_belongs_to_many :services, join_table: :buses_services + has_many :buses_services + has_many :services, through: :buses_services validates :number, presence: true, uniqueness: true validates :model, inclusion: { in: MODELS } diff --git a/app/models/buses_service.rb b/app/models/buses_service.rb new file mode 100644 index 00000000..6219d44e --- /dev/null +++ b/app/models/buses_service.rb @@ -0,0 +1,4 @@ +class BusesService < ApplicationRecord + belongs_to :bus + belongs_to :service +end diff --git a/app/models/service.rb b/app/models/service.rb index 9cbb2a32..1781543c 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -12,7 +12,8 @@ class Service < ApplicationRecord 'Можно не печатать билет', ].freeze - has_and_belongs_to_many :buses, join_table: :buses_services + has_many :buses_services + has_many :buses, through: :buses_services validates :name, presence: true validates :name, inclusion: { in: SERVICES } diff --git a/app/views/trips/_delimiter.html.erb b/app/views/trips/_delimiter.html.erb deleted file mode 100644 index 3f845ad0..00000000 --- a/app/views/trips/_delimiter.html.erb +++ /dev/null @@ -1 +0,0 @@ -==================================================== diff --git a/app/views/trips/_service.html.erb b/app/views/trips/_service.html.erb deleted file mode 100644 index 178ea8c0..00000000 --- a/app/views/trips/_service.html.erb +++ /dev/null @@ -1 +0,0 @@ -
  • <%= "#{service.name}" %>
  • diff --git a/app/views/trips/_services.html.erb b/app/views/trips/_services.html.erb deleted file mode 100644 index 2de639fc..00000000 --- a/app/views/trips/_services.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -
  • Сервисы в автобусе:
  • - diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb deleted file mode 100644 index fa1de9aa..00000000 --- a/app/views/trips/_trip.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -
  • <%= "Отправление: #{trip.start_time}" %>
  • -
  • <%= "Прибытие: #{(Time.parse(trip.start_time) + trip.duration_minutes.minutes).strftime('%H:%M')}" %>
  • -
  • <%= "В пути: #{trip.duration_minutes / 60}ч. #{trip.duration_minutes % 60}мин." %>
  • -
  • <%= "Цена: #{trip.price_cents / 100}р. #{trip.price_cents % 100}коп." %>
  • -
  • <%= "Автобус: #{trip.bus.model} №#{trip.bus.number}" %>
  • diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index a60bce41..378d42cc 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -1,16 +1,27 @@ -

    - <%= "Автобусы #{@from.name} – #{@to.name}" %> -

    -

    - <%= "В расписании #{@trips.count} рейсов" %> -

    +<% cache([@from.name, @to.name]) do%> +

    + <%= "Автобусы #{@from.name} – #{@to.name}" %> +

    +

    + <%= "В расписании #{@trips.count} рейсов" %> +

    -<% @trips.each do |trip| %> - - <%= render "delimiter" %> + <% @trips.each do |trip| %> + + ==================================================== + <% end %> <% end %> diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..d2748fe1 --- /dev/null +++ b/case-study.md @@ -0,0 +1,30 @@ +### Необходимо выполнить следующие задачи: +- Оптимизировать импорт данных за время < 1 мин. +- Оптимизировать отображение расписания. + +### Метрика +- Тест на импорт файла large.json должен выполнится менее чем за 60 секунд. + +### Инструменты для оптимизации +- гемы activerecord-import, rack-mini-profiler +- Explain sql, pghero + +### Замеры производительности до оптимизации импорта +- файл small.json импортируется за 17 сек. +- файл medium.json импортируется за 137 сек. + +### Замеры производительности после оптимизации импорта +- файд small.json - 2 сек. +- файд medium.json - 5 сек. +- файд large.json - 24 сек. + +Использовал так же стороний гем для парсинга json - 'fast_jsonparser', нашёл его при помощи гугла :) +Считаю данный этап выполненным. + +### Задача 2 + +## Проблема 1 +- Большое количество рендера паршелов, общее время отрисовки - 13688 мс. Решение - перенести всё в один template + +Так же добавил составной индекс, правда по времени загрузка особо быстрее не стала. +После кэширования всей страницы, время отрисовки страницы стало 23 мс. diff --git a/config/database.yml b/config/database.yml index e116cfa6..27db5d25 100644 --- a/config/database.yml +++ b/config/database.yml @@ -20,6 +20,8 @@ default: &default # For details on connection pooling, see Rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + user: postgres + password: postgres development: <<: *default diff --git a/config/environments/development.rb b/config/environments/development.rb index 1311e3e4..903a8661 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,13 @@ Rails.application.configure do + config.after_initialize do + Bullet.enable = true + Bullet.alert = true + Bullet.bullet_logger = true + Bullet.console = true + Bullet.rails_logger = true + Bullet.add_footer = true + end + # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on diff --git a/config/environments/test.rb b/config/environments/test.rb index 0a38fd3c..bc5971ab 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,4 +1,10 @@ Rails.application.configure do + config.after_initialize do + Bullet.enable = true + Bullet.bullet_logger = true + Bullet.raise = true # raise an error if n+1 query occurs + end + # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's diff --git a/config/initializers/rack_mini_profiler.rb b/config/initializers/rack_mini_profiler.rb new file mode 100644 index 00000000..7784d1f6 --- /dev/null +++ b/config/initializers/rack_mini_profiler.rb @@ -0,0 +1 @@ +Rack::MiniProfiler.config.storage = Rack::MiniProfiler::MemoryStore diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb new file mode 100644 index 00000000..531e771d --- /dev/null +++ b/config/initializers/strong_migrations.rb @@ -0,0 +1,26 @@ +# Mark existing migrations as safe +StrongMigrations.start_after = 20230403132428 + +# Set timeouts for migrations +# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user +StrongMigrations.lock_timeout = 10.seconds +StrongMigrations.statement_timeout = 1.hour + +# Analyze tables after indexes are added +# Outdated statistics can sometimes hurt performance +StrongMigrations.auto_analyze = true + +# Set the version of the production database +# so the right checks are run in development +# StrongMigrations.target_version = 10 + +# Add custom checks +# StrongMigrations.add_check do |method, args| +# if method == :add_index && args[0].to_s == "users" +# stop! "No more indexes on the users table" +# end +# end + +# Make some operations safe by default +# See https://github.com/ankane/strong_migrations#safe-by-default +# StrongMigrations.safe_by_default = true diff --git a/db/migrate/20230403132254_add_index_to_trip.rb b/db/migrate/20230403132254_add_index_to_trip.rb new file mode 100644 index 00000000..fcdebadc --- /dev/null +++ b/db/migrate/20230403132254_add_index_to_trip.rb @@ -0,0 +1,7 @@ +class AddIndexToTrip < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + add_index :trips, [:from_id, :to_id], algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index f6921e45..e8d7515b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_30_193044) do +ActiveRecord::Schema.define(version: 2023_04_03_132254) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -40,6 +40,7 @@ t.integer "duration_minutes" t.integer "price_cents" t.integer "bus_id" + t.index ["from_id", "to_id"], name: "index_trips_on_from_id_and_to_id" end end diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 540fe871..ad716069 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,34 +1,52 @@ # Наивная загрузка данных из json-файла в БД # rake reload_json[fixtures/small.json] task :reload_json, [:file_name] => :environment do |_task, args| - json = JSON.parse(File.read(args.file_name)) + json = FastJsonparser.parse(File.read(args.file_name), symbolize_keys: false) ActiveRecord::Base.transaction do - City.delete_all - Bus.delete_all - Service.delete_all - Trip.delete_all - ActiveRecord::Base.connection.execute('delete from buses_services;') + [ + City.table_name, Bus.table_name, Service.table_name, + Trip.table_name, BusesService.table_name + ].each do |table| + ActiveRecord::Base.connection.truncate(table) + end + + cities = {} + trips = [] + services = {} + busses = {} + busses_services = {} + trips = [] json.each do |trip| - from = City.find_or_create_by(name: trip['from']) - to = City.find_or_create_by(name: trip['to']) - services = [] + cities[trip['from']] ||= City.create!(name: trip['from']) + cities[trip['to']] ||= City.create!(name: trip['to']) + from = cities[trip['from']] + to = cities[trip['to']] + busses[trip['bus']['number']] ||= Bus.create!( + number: trip['bus']['number'], + model: trip['bus']['model'] + ) + trip['bus']['services'].each do |service| - s = Service.find_or_create_by(name: service) - services << s + services[service] ||= Service.create!(name: service) + bs_key = "#{trip['bus']['number']}_#{trip['bus']['model']}" + busses_services[bs_key] ||= BusesService.create!( + bus: busses[trip['bus']['number']], + service: services[service] + ) end - bus = Bus.find_or_create_by(number: trip['bus']['number']) - bus.update(model: trip['bus']['model'], services: services) - Trip.create!( - from: from, - to: to, - bus: bus, + trips << { + from_id: from.id, + to_id: to.id, + bus_id: busses[trip['bus']['number']].id, start_time: trip['start_time'], duration_minutes: trip['duration_minutes'], price_cents: trip['price_cents'], - ) + } end + + Trip.import!(trips) end end diff --git a/spec/lib/tasks/utils_spec.rb b/spec/lib/tasks/utils_spec.rb new file mode 100644 index 00000000..49688bf1 --- /dev/null +++ b/spec/lib/tasks/utils_spec.rb @@ -0,0 +1,9 @@ +require 'rails_helper' + +Rails.application.load_tasks + +describe 'utils.rake' do + it 'import large.json' do + expect { Rake::Task['reload_json'].invoke('fixtures/large.json') }.to perform_under(60).sec + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..d3c2ba31 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,19 @@ +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' + +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' + +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.use_transactional_fixtures = true + config.infer_spec_type_from_file_location! + config.filter_rails_from_backtrace! +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..9f9bed90 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,15 @@ +require 'rspec-benchmark' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers + + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups +end