diff --git a/.gitignore b/.gitignore index 59c74047..91330ec6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /tmp /log /public + +fixtures/1M.json +fixtures/10M.json \ No newline at end of file diff --git a/.ruby-version b/.ruby-version index ec1cf33c..a04abec9 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.3 +2.6.10 diff --git a/Gemfile b/Gemfile index e20b1260..f63a83e3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,24 +1,33 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '2.6.3' +ruby '2.6.10' gem 'rails', '~> 5.2.3' gem 'pg', '>= 0.18', '< 2.0' gem 'puma', '~> 3.11' gem 'bootsnap', '>= 1.1.0', require: false +gem 'fast_jsonparser' + +gem 'rack-mini-profiler' 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 'pry' + gem 'bullet' 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 'meta_request' + gem 'annotate' end +gem 'strong_migrations' + group :test do end diff --git a/Gemfile.lock b/Gemfile.lock index fccf6f5f..46a7ae60 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,150 +1,193 @@ 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) + 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) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) + rake (>= 10.4, < 14.0) 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) + bindex (0.8.1) + bootsnap (1.18.3) + msgpack (~> 1.2) + builder (3.2.4) + bullet (7.1.6) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) + byebug (11.1.3) + coderay (1.1.3) + concurrent-ruby (1.2.3) + crass (1.0.6) + date (3.3.4) + erubi (1.12.0) + fast_jsonparser (0.6.0) + ffi (1.16.3) + globalid (1.1.0) + activesupport (>= 5.0) + i18n (1.14.5) 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.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + 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.4) + meta_request (0.8.2) + rack-contrib (>= 1.1, < 3) + railties (>= 3.0.0, < 8) + method_source (1.1.0) + mini_mime (1.1.5) + mini_portile2 (2.8.6) + minitest (5.22.3) + msgpack (1.7.2) + net-imap (0.3.7) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.3) + nokogiri (1.13.10) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + pg (1.5.6) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + puma (3.12.6) + racc (1.7.3) + rack (2.2.9) + rack-contrib (2.4.0) + rack (< 4) + rack-mini-profiler (3.1.1) + 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) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest 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.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) ffi (~> 1.0) ruby_dep (1.5.0) - sprockets (3.7.2) + sprockets (4.2.1) 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) + strong_migrations (1.8.0) + activerecord (>= 5.2) + thor (1.3.1) thread_safe (0.3.6) - tzinfo (1.2.5) + timeout (0.4.1) + 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.6) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) + websocket-extensions (0.1.5) PLATFORMS ruby DEPENDENCIES + annotate bootsnap (>= 1.1.0) + bullet byebug + fast_jsonparser listen (>= 3.0.5, < 3.2) + meta_request pg (>= 0.18, < 2.0) + pry puma (~> 3.11) + rack-mini-profiler rails (~> 5.2.3) + strong_migrations tzinfo-data web-console (>= 3.3.0) RUBY VERSION - ruby 2.6.3p62 + ruby 2.6.10p210 BUNDLED WITH 2.0.2 diff --git a/Readme.md b/Readme.md index 20b4eda3..fa1008be 100644 --- a/Readme.md +++ b/Readme.md @@ -43,10 +43,10 @@ Нужно найти и устранить проблемы, замедляющие формирование этих страниц. Попробуйте воспользоваться -- [ ] `rack-mini-profiler` -- [ ] `rails panel` -- [ ] `bullet` -- [ ] `explain` запросов +- [x] `rack-mini-profiler` +- [x] `rails panel` +- [x] `bullet` +- [x] `explain` запросов ### Сдача задания `PR` в этот репозиторий с кодом и case-study наподобие первых двух недель. На этот раз шаблона нет, законспектируйте ваш процесс оптимизации в свободной форме. diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb index acb38be2..eb2dee12 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.where(from: @from, to: @to).preload(bus: :services).order(:start_time) end end diff --git a/app/models/bus.rb b/app/models/bus.rb index 1dcc54cb..03b3ffae 100644 --- a/app/models/bus.rb +++ b/app/models/bus.rb @@ -1,3 +1,11 @@ +# == Schema Information +# +# Table name: buses +# +# id :bigint not null, primary key +# number :string +# model :string +# class Bus < ApplicationRecord MODELS = [ 'Икарус', diff --git a/app/models/city.rb b/app/models/city.rb index 19ec7f36..957290d8 100644 --- a/app/models/city.rb +++ b/app/models/city.rb @@ -1,3 +1,10 @@ +# == Schema Information +# +# Table name: cities +# +# id :bigint not null, primary key +# name :string +# class City < ApplicationRecord validates :name, presence: true, uniqueness: true validate :name_has_no_spaces diff --git a/app/models/service.rb b/app/models/service.rb index 9cbb2a32..b2cdd9a2 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -1,3 +1,10 @@ +# == Schema Information +# +# Table name: services +# +# id :bigint not null, primary key +# name :string +# class Service < ApplicationRecord SERVICES = [ 'WiFi', diff --git a/app/models/trip.rb b/app/models/trip.rb index 9d63dfff..f610f267 100644 --- a/app/models/trip.rb +++ b/app/models/trip.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: trips +# +# id :bigint not null, primary key +# from_id :integer +# to_id :integer +# start_time :string +# duration_minutes :integer +# price_cents :integer +# bus_id :integer +# class Trip < ApplicationRecord HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/ diff --git a/app/services/utils_service.rb b/app/services/utils_service.rb new file mode 100644 index 00000000..4f708f7f --- /dev/null +++ b/app/services/utils_service.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +class UtilsService + TABLE_NAMES = %i[cities buses services trips buses_services].freeze + + def self.call(file_name) + new.call(file_name) + end + + def initialize + @cities = {} + @buses = Hash.new { |h, k| h[k] = {} } + @services = Service::SERVICES.map.with_index(1).to_h + @services_buses = @services.map { |_, index| [index, []] }.to_h + @next_bus_id = 0 + end + + def call(file_name) + ActiveRecord::Base.transaction do + truncate + copy_services + copy_trips(file_name) + copy_cities + copy_buses + copy_buses_services + end + end + + def truncate + TABLE_NAMES.each do |table_name| + sql = <<~SQL + TRUNCATE #{table_name} RESTART IDENTITY; + SQL + + ActiveRecord::Base.connection.execute(sql) + end + end + + def copy_trips(file_name) + sql = <<~SQL + copy trips (from_id, to_id, start_time, duration_minutes, price_cents, bus_id) from stdin with csv delimiter ';' + SQL + + ActiveRecord::Base.connection.raw_connection.copy_data(sql) do + File.open(file_name) do |ff| + nesting = 0 + str = +"" + + while !ff.eof? + ch = ff.read(1) # читаем по одному символу + case + when ch == '{' # начинается объект, повышается вложенность + nesting += 1 + str << ch + when ch == '}' # заканчивается объект, понижается вложенность + nesting -= 1 + str << ch + + if nesting == 0 # если закончился объкет уровня trip, парсим и импортируем его + trip = FastJsonparser.parse(str) + + copy( + fetch_city_id(trip[:from]).to_s << ';' << + fetch_city_id(trip[:to]).to_s << ';' << + trip[:start_time].to_s << ';' << + trip[:duration_minutes].to_s << ';' << + trip[:price_cents].to_s << ';' << + fetch_bus_id(trip[:bus]).to_s << "\n" + ) + + str.clear + end + when nesting >= 1 + str << ch + end + end + end + end + end + + def fetch_city_id(key) + id = @cities[key] + if !id + id = @cities.size + 1 + @cities[key] = id + end + + id + end + + def fetch_bus_id(bus) + bus_id = @buses[bus[:model]][bus[:number]] + + if !bus_id + bus_id = @next_bus_id += 1 + @buses[bus[:model]][bus[:number]] = bus_id + + bus[:services].each do |service| + @services_buses[@services[service]] << bus_id + end + end + + bus_id + end + + def copy_cities + sql = <<~SQL + copy cities (id, name) from stdin with csv delimiter ';' + SQL + + ActiveRecord::Base.connection.raw_connection.copy_data(sql) do + @cities.each do |name, id| + copy(id.to_s << ';' << name << "\n") + end + end + end + + def copy_buses + sql = <<~SQL + copy buses (id, model, number) from stdin with csv delimiter ';' + SQL + + ActiveRecord::Base.connection.raw_connection.copy_data(sql) do + @buses.each do |model, numbers| + numbers.each do |number, id| + copy(id.to_s << ';' << model << ';' << number << "\n") + end + end + end + end + + def copy_services + sql = <<~SQL + copy services (id, name) from stdin with csv delimiter ';' + SQL + + ActiveRecord::Base.connection.raw_connection.copy_data(sql) do + @services.each do |name, id| + copy(id.to_s << ';' << name << "\n") + end + end + end + + def copy_buses_services + sql = <<~SQL + copy buses_services (bus_id, service_id) from stdin with csv delimiter ';' + SQL + + ActiveRecord::Base.connection.raw_connection.copy_data(sql) do + @services_buses.each do |service_id, bus_ids| + bus_ids.each do |bus_id| + copy(bus_id.to_s << ';' << service_id.to_s << "\n") + end + end + end + end + + def copy(values) + # стримим подготовленный чанк данных в postgres + ActiveRecord::Base.connection.raw_connection.put_copy_data(values) + end +end 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 index fa1de9aa..d86359da 100644 --- a/app/views/trips/_trip.html.erb +++ b/app/views/trips/_trip.html.erb @@ -1,5 +1,14 @@ -
  • <%= "Отправление: #{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..21173df5 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -2,15 +2,11 @@ <%= "Автобусы #{@from.name} – #{@to.name}" %>

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

    -<% @trips.each do |trip| %> - - <%= render "delimiter" %> +<%= render partial: "trip", collection: @trips, spacer_template: 'delimiter' %> + +<% if @trips.length > 0 %> + <%= render "delimiter" %> <% end %> diff --git a/case-study-a.md b/case-study-a.md new file mode 100644 index 00000000..d8c4f863 --- /dev/null +++ b/case-study-a.md @@ -0,0 +1,31 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. + +Необходимо было обработать файл с 1кк данных. + +У нас уже была программа на `ruby`, которая умела делать нужную обработку. + +Она успешно работала с файлом до 1к записей, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время загрузки medium файла (10к записей, 77 секунд в первой итерации) + +## Гарантия корректности работы оптимизированной программы +Программа не поставлялась с тестом, поэтому перед выполнением оптимизации я добавил его самостоятельно: загрузка example файла с дальнейшим сравнением загруженных в бд данных с эталоном. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. + +## Feedback-Loop +Вот как я построил `feedback_loop`: профилирование - изменение кода - тестирование – бенчмаркинг – откат при отсутствии разницы от оптимизации/сохранение результатов + +## Вникаем в детали системы, чтобы найти главные точки роста +Главной точкой роста является изменение загрузки на потоковую: посимвольное чтение файла, формирование целостного объекта trip и загрузка в систему путем использования функционала COPY в PG. + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с с 77 секунд до 1.4с для medium и уложиться в заданный бюджет. +Файл large грузится за 6.5 секунд +Файл 1м стал грузится за 56 секунд. + diff --git a/case-study-b.md b/case-study-b.md new file mode 100644 index 00000000..4e275460 --- /dev/null +++ b/case-study-b.md @@ -0,0 +1,55 @@ +# Case-study оптимизации + +## Актуальная проблема +В нашем проекте возникла серьёзная проблема. +Время загрузки страницы `автобусы/Самара/Москва` при наличии уже 100к поездок в базе данных превышало любой уровень терпения. + +Я решил исправить эту проблему, оптимизировав эту программу. + +## Формирование метрики +Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время загрузки страницы `автобусы/Самара/Москва` при наличии 100к поездок в базе данных. Начальное измерение – 13.3с. + +## Гарантия корректности работы оптимизированной программы +Программа не поставлялась с тестом, поэтому перед выполнением оптимизации я добавил его самостоятельно: результат работы страницы `автобусы/Самара/Москва` для данных из файла `fixtures/example.json` сравнивается с тем, который был сформирован до изменений. + +## Feedback-Loop +Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* + +Вот как я построил `feedback_loop`: профилирование - изменение кода - тестирование – бенчмаркинг – откат при отсутствии разницы от оптимизации/сохранение результатов + +## Вникаем в детали системы, чтобы найти главные точки роста +Для того, чтобы найти "точки роста" для оптимизации я воспользовался rack mini profiler, bullet + +Вот какие проблемы удалось найти и решить + +### Ваша находка №1 +- bullet показал, что N+1 проблему `SELECT "buses".* FROM "buses" WHERE "buses"."id" = $1 LIMIT $2; ` + `SELECT "services".* FROM "services" INNER JOIN "buses_services" ON "services"."id" = "buses_services"."service_id" WHERE "buses_services"."bus_id" = $1;` +- делаю `.preload(bus: :services)` для `trips` +- метрика снизилась до 6 секунд +- количество sql запросов для `trips/index.html.erb` сократилось до 12 + +### Ваша находка № 2 +- rack mini profiler (и логи веб сервера) показал, что основное время тратится на рендеринг шаблонов `trips/index.html.erb 2091.1 +4917.9`. +- рендереринг всех коллекций через `render partial:` +- метрика снизилась до 600мс +- `Rendering: trips/index.html.erb 716.2 975.7` + +### Ваша находка № 3 +- rack mini profiler показал, что производится и запрос по count, и идентичный по выборке данных. +- меняю count на length. +- метрика особенно не снизилась +- запрос count пропал из логов профилировщика + +### Ваша находка № 4 +- rack mini profiler показал, что долго выполняются запрос ` SELECT "trips".* FROM "trips" WHERE "trips"."from_id" = $1 AND "trips"."to_id" = $2 ORDER BY "trips"."start_time" ASC` (делаю explain, он показывает `Seq Scan on trips` как основную точку роста), +- Добавляю индекс на связку `from_id`/`to_id`/`start_time` +- метрика особенно не снизилась +- время работы запроса снизилось с 18 до 2.5мс + +## Результаты +В результате проделанной оптимизации наконец удалось обработать файл с данными. +Удалось улучшить метрику системы с 13.3с до 0.6с. + +## Защита от регрессии производительности +Для защиты от потери достигнутого прогресса при дальнейших изменениях программы *о performance-тестах, которые вы написали* diff --git a/config/environments/development.rb b/config/environments/development.rb index 1311e3e4..2c651d75 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -58,4 +58,8 @@ # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + # Bullet.enable = true + # Bullet.bullet_logger = true + # Bullet.console = true end diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb new file mode 100644 index 00000000..73a0aac4 --- /dev/null +++ b/config/initializers/strong_migrations.rb @@ -0,0 +1,26 @@ +# Mark existing migrations as safe +StrongMigrations.start_after = 20240515192014 + +# 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/20240515205445_add_from_id_to_id_index_to_trips.rb b/db/migrate/20240515205445_add_from_id_to_id_index_to_trips.rb new file mode 100644 index 00000000..297bf4ee --- /dev/null +++ b/db/migrate/20240515205445_add_from_id_to_id_index_to_trips.rb @@ -0,0 +1,7 @@ +class AddFromIdToIdIndexToTrips < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + add_index(:trips, %i[from_id to_id start_time], order: {start_time: :asc}, algorithm: :concurrently) + end +end diff --git a/db/schema.rb b/db/schema.rb index f6921e45..272eea24 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: 2024_05_15_205445) 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", "start_time"], name: "index_trips_on_from_id_and_to_id_and_start_time" end end diff --git a/lib/tasks/build_fixtures.rake b/lib/tasks/build_fixtures.rake new file mode 100644 index 00000000..7e610ec6 --- /dev/null +++ b/lib/tasks/build_fixtures.rake @@ -0,0 +1,10 @@ +task :build_fixtures, [:file_name] => :environment do |_task, args| + ::UtilsService.call("fixtures/#{args.file_name}.json") + + %i[cities buses services trips buses_services].each do |table_name| + File.open("test/fixtures/files/#{args.file_name}_#{table_name}.json", 'w+') do |file| + collection = ActiveRecord::Base.connection.execute("SELECT * FROM #{table_name} ORDER BY ID;").to_a.to_json + file.write(collection) + end + end +end diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 540fe871..24ff057a 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,34 +1,5 @@ # Наивная загрузка данных из json-файла в БД # rake reload_json[fixtures/small.json] task :reload_json, [:file_name] => :environment do |_task, args| - json = JSON.parse(File.read(args.file_name)) - - ActiveRecord::Base.transaction do - City.delete_all - Bus.delete_all - Service.delete_all - Trip.delete_all - ActiveRecord::Base.connection.execute('delete from buses_services;') - - json.each do |trip| - from = City.find_or_create_by(name: trip['from']) - to = City.find_or_create_by(name: trip['to']) - services = [] - trip['bus']['services'].each do |service| - s = Service.find_or_create_by(name: service) - services << s - 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, - start_time: trip['start_time'], - duration_minutes: trip['duration_minutes'], - price_cents: trip['price_cents'], - ) - end - end + UtilsService.call(args.file_name) end diff --git a/test/controllers/trips/index_test.rb b/test/controllers/trips/index_test.rb new file mode 100644 index 00000000..d1025fc8 --- /dev/null +++ b/test/controllers/trips/index_test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative '../../test_helper' + +class TripsController::IndexTest < ActionController::TestCase + def setup + ::UtilsService.call('fixtures/example.json') + end + + def test_html + get(:index, params: {from: 'Самара', to: 'Москва'}) + + assert_response(:success) + assert_equal(@response.body.squish, File.read('test/fixtures/files/example_index.html').squish) + end +end diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/files/example_buses.json b/test/fixtures/files/example_buses.json new file mode 100644 index 00000000..effe6e3c --- /dev/null +++ b/test/fixtures/files/example_buses.json @@ -0,0 +1 @@ +[{"id":1,"number":"123","model":"Икарус"}] \ No newline at end of file diff --git a/test/fixtures/files/example_buses_services.json b/test/fixtures/files/example_buses_services.json new file mode 100644 index 00000000..0e5e177d --- /dev/null +++ b/test/fixtures/files/example_buses_services.json @@ -0,0 +1 @@ +[{"id":1,"bus_id":1,"service_id":1},{"id":2,"bus_id":1,"service_id":2}] \ No newline at end of file diff --git a/test/fixtures/files/example_cities.json b/test/fixtures/files/example_cities.json new file mode 100644 index 00000000..c5c1a303 --- /dev/null +++ b/test/fixtures/files/example_cities.json @@ -0,0 +1 @@ +[{"id":1,"name":"Москва"},{"id":2,"name":"Самара"}] \ No newline at end of file diff --git a/test/fixtures/files/example_index.html b/test/fixtures/files/example_index.html new file mode 100644 index 00000000..f5c4814c --- /dev/null +++ b/test/fixtures/files/example_index.html @@ -0,0 +1,112 @@ + + + + Task4 + + + + + + + + +

    + Автобусы Самара – Москва +

    +

    + В расписании 5 рейсов +

    + + + ==================================================== + + + ==================================================== + + + ==================================================== + + + ==================================================== + + + ==================================================== + + + + diff --git a/test/fixtures/files/example_services.json b/test/fixtures/files/example_services.json new file mode 100644 index 00000000..b7725338 --- /dev/null +++ b/test/fixtures/files/example_services.json @@ -0,0 +1,42 @@ +[ + { + "id": 1, + "name": "WiFi" + }, + { + "id": 2, + "name": "Туалет" + }, + { + "id": 3, + "name": "Работающий туалет" + }, + { + "id": 4, + "name": "Ремни безопасности" + }, + { + "id": 5, + "name": "Кондиционер общий" + }, + { + "id": 6, + "name": "Кондиционер Индивидуальный" + }, + { + "id": 7, + "name": "Телевизор общий" + }, + { + "id": 8, + "name": "Телевизор индивидуальный" + }, + { + "id": 9, + "name": "Стюардесса" + }, + { + "id": 10, + "name": "Можно не печатать билет" + } +] \ No newline at end of file diff --git a/test/fixtures/files/example_trips.json b/test/fixtures/files/example_trips.json new file mode 100644 index 00000000..4e8de20d --- /dev/null +++ b/test/fixtures/files/example_trips.json @@ -0,0 +1 @@ +[{"id":1,"from_id":1,"to_id":2,"start_time":"11:00","duration_minutes":168,"price_cents":474,"bus_id":1},{"id":2,"from_id":2,"to_id":1,"start_time":"17:30","duration_minutes":37,"price_cents":173,"bus_id":1},{"id":3,"from_id":1,"to_id":2,"start_time":"12:00","duration_minutes":323,"price_cents":672,"bus_id":1},{"id":4,"from_id":2,"to_id":1,"start_time":"18:30","duration_minutes":315,"price_cents":969,"bus_id":1},{"id":5,"from_id":1,"to_id":2,"start_time":"13:00","duration_minutes":304,"price_cents":641,"bus_id":1},{"id":6,"from_id":2,"to_id":1,"start_time":"19:30","duration_minutes":21,"price_cents":663,"bus_id":1},{"id":7,"from_id":1,"to_id":2,"start_time":"14:00","duration_minutes":598,"price_cents":629,"bus_id":1},{"id":8,"from_id":2,"to_id":1,"start_time":"20:30","duration_minutes":292,"price_cents":22,"bus_id":1},{"id":9,"from_id":1,"to_id":2,"start_time":"15:00","duration_minutes":127,"price_cents":795,"bus_id":1},{"id":10,"from_id":2,"to_id":1,"start_time":"21:30","duration_minutes":183,"price_cents":846,"bus_id":1}] \ No newline at end of file diff --git a/test/services/utils_service_test.rb b/test/services/utils_service_test.rb new file mode 100644 index 00000000..f8ab00cf --- /dev/null +++ b/test/services/utils_service_test.rb @@ -0,0 +1,13 @@ +require_relative '../test_helper' + +class UtilsServiceTest < ActiveSupport::TestCase + + ::UtilsService.call('fixtures/example.json') + + ::UtilsService::TABLE_NAMES.each do |table_name| + test "validates #{table_name} data" do + collection = ActiveRecord::Base.connection.execute("select * from #{table_name} order by id;").to_a + assert_equal(collection, JSON.parse(File.read("#{Rails.root}/test/fixtures/files/example_#{table_name}.json"))) + end + end +end