Various optimizations (#6249) [deploy]

* Pluck over map
* Explain why map makes sense there
* Refactor reactions controller a bit and iterate only once
* Use group by instead of N counting queries
* More positive
* Simplify BufferedArticlesController
* Less queries for MailchimpBot
* Use Rails instead of SQL
* Build comment_ids only when needed
This commit is contained in:
rhymes 2020-02-25 19:42:24 +01:00 committed by GitHub
parent cf11268949
commit 33a11adca8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 143 additions and 73 deletions

View file

@ -1,20 +1,21 @@
class BufferedArticlesController < ApplicationController
# No authorization required for entirely public controller
def index
@article_urls = buffered_article_urls
render json: {
urls: @article_urls
}.to_json
render json: { urls: buffered_articles_urls }.to_json
end
def buffered_article_urls
if Rails.env.production?
Article.
where("last_buffered > ? OR published_at > ?", 24.hours.ago, 20.minutes.ago).
select(:path).
map { |a| "https://#{ApplicationConfig['APP_DOMAIN']}#{a.path}" }
else
Article.all.map { |a| "https://#{ApplicationConfig['APP_DOMAIN']}#{a.path}" }
end
private
def buffered_articles_urls
relation = if Rails.env.production?
Article.where("last_buffered > ?", 24.hours.ago).
or(Article.where("published_at > ?", 20.minutes.ago))
else
Article.all
end
paths = relation.pluck(:path)
paths.map { |path| "https://#{ApplicationConfig['APP_DOMAIN']}#{path}" }
end
end

View file

@ -4,37 +4,46 @@ class ReactionsController < ApplicationController
def index
skip_authorization
if params[:article_id]
id = params[:article_id]
reactions = if session_current_user_id.present?
Reaction.where(reactable_id: id,
reactable_type: "Article",
user_id: session_current_user_id).
where("points > ?", 0)
reactions = if session_current_user_id
Reaction.positive.
where(
reactable_id: id,
reactable_type: "Article",
user_id: session_current_user_id,
)
else
[]
Reaction.none
end
render json:
{
current_user: { id: session_current_user_id },
article_reaction_counts: Reaction.count_for_article(id),
reactions: reactions
}.to_json
result = { article_reaction_counts: Reaction.count_for_article(id) }
else
comments = Comment.where(
commentable_id: params[:commentable_id],
commentable_type: params[:commentable_type],
).select(%i[id positive_reactions_count])
comment_ids = comments.map(&:id)
reaction_counts = comments.map { |c| { id: c.id, count: c.positive_reactions_count } }
reactions = session_current_user_id ? cached_user_positive_reactions(current_user).where(reactable_id: comment_ids) : []
render json:
{
current_user: { id: session_current_user_id },
positive_reaction_counts: reaction_counts,
reactions: reactions
}.to_json
comments = Comment.
where(commentable_id: params[:commentable_id], commentable_type: params[:commentable_type]).
select(%i[id positive_reactions_count])
reaction_counts = comments.map do |comment|
{ id: comment.id, count: comment.positive_reactions_count }
end
reactions = if session_current_user_id
comment_ids = reaction_counts.map { |rc| rc[:id] }
cached_user_positive_reactions(current_user).where(reactable_id: comment_ids)
else
Reaction.none
end
result = { positive_reaction_counts: reaction_counts }
end
render json: {
current_user: { id: session_current_user_id },
reactions: reactions
}.merge(result).to_json
set_surrogate_key_header params.to_s unless session_current_user_id
end
@ -79,8 +88,7 @@ class ReactionsController < ApplicationController
def cached_user_positive_reactions(user)
Rails.cache.fetch("cached_user_reactions-#{user.id}-#{user.updated_at}", expires_in: 24.hours) do
Reaction.where(user_id: user.id).
where("points > ?", 0)
user.reactions.positive
end
end

View file

@ -76,8 +76,12 @@ class MailchimpBot
return false unless user.tag_moderator?
success = false
tags = user.roles.where(name: "tag_moderator").map { |tag| Tag.find(tag.resource_id).name }
tag_ids = user.roles.where(name: "tag_moderator").pluck(:resource_id)
tag_names = Tag.where(id: tag_ids).pluck(:name)
status = user.email_tag_mod_newsletter ? "subscribed" : "unsubscribed"
begin
gibbon.lists(SiteConfig.mailchimp_tag_moderators_id).members(target_md5_email).upsert(
body: {
@ -89,7 +93,7 @@ class MailchimpBot
TWITTER: user.twitter_username.to_s,
GITHUB: user.github_username.to_s,
IMAGE_URL: user.profile_image_url.to_s,
TAGS: tags.join(", ")
TAGS: tag_names.join(", ")
}
},
)

View file

@ -55,7 +55,7 @@ class ChatChannel < ApplicationRecord
raise "Invalid direct channel" if users.size != 2 && channel_type == "direct"
if channel_type == "direct"
usernames = users.map(&:username).sort
usernames = users.map(&:username).sort # .map as `users` is an array
contrived_name = "Direct chat between " + usernames.join(" and ")
slug = usernames.join("/")
else

View file

@ -6,6 +6,6 @@ module Reactable
end
def sync_reactions_count
update_column(:positive_reactions_count, reactions.where("points > ?", 0).size)
update_column(:positive_reactions_count, reactions.positive.size)
end
end

View file

@ -14,6 +14,8 @@ class Reaction < ApplicationRecord
}
counter_culture :user
scope :positive, -> { where("points > ?", 0) }
validates :category, inclusion: { in: CATEGORIES }
validates :reactable_type, inclusion: { in: REACTABLE_TYPES }
validates :status, inclusion: { in: STATUSES }
@ -48,8 +50,10 @@ class Reaction < ApplicationRecord
def count_for_article(id)
Rails.cache.fetch("count_for_reactable-Article-#{id}", expires_in: 1.hour) do
reactions = Reaction.where(reactable_id: id, reactable_type: "Article")
counts = reactions.group(:category).count
%w[like readinglist unicorn].map do |type|
{ category: type, count: reactions.where(category: type).size }
{ category: type, count: counts.fetch(type, 0) }
end
end
end

View file

@ -85,9 +85,8 @@ class AnalyticsService
where("score > 0")
@follow_data = Follow.
where(followable_type: user_or_org.class.name, followable_id: user_or_org.id)
@reaction_data = Reaction.
where(reactable_id: article_ids, reactable_type: "Article").
where("points > 0")
@reaction_data = Reaction.positive.
where(reactable_id: article_ids, reactable_type: "Article")
@page_view_data = PageView.where(article_id: article_ids)
# filter data by date if needed

View file

@ -92,7 +92,7 @@ Article.clear_index!
num_articles.times do |i|
tags = []
tags << "discuss" if (i % 3).zero?
tags.concat Tag.order(Arel.sql("RANDOM()")).select("name").first(3).map(&:name)
tags.concat Tag.order(Arel.sql("RANDOM()")).limit(3).pluck(:name)
markdown = <<~MARKDOWN
---

View file

@ -111,6 +111,20 @@ RSpec.describe Reaction, type: :model do
end
end
describe ".count_for_article" do
it "counts the reactions an article has grouped by category" do
create(:reaction, reactable: article, user: user, category: "like")
create(:reaction, reactable: article, user: user, category: "unicorn")
expected_result = [
{ category: "like", count: 1 },
{ category: "readinglist", count: 0 },
{ category: "unicorn", count: 1 },
]
expect(described_class.count_for_article(article.id)).to eq(expected_result)
end
end
context "when callbacks are called after save" do
let!(:reaction) { build(:reaction, category: "like", reactable: article, user: user) }

View file

@ -11,7 +11,7 @@ RSpec.shared_examples "#sync_reactions_count" do |reactable_type|
expect(reactable.positive_reactions_count).to eq(0)
reactable.sync_reactions_count
reactable.reload
expected_count = reactable.reactions.where("points > ?", 0).size
expected_count = reactable.reactions.positive.size
expect(reactable.positive_reactions_count).to eq(expected_count)
end
end

View file

@ -2,20 +2,23 @@ require "rails_helper"
RSpec.describe "BufferedArticles", type: :request do
describe "GET /buffered_articles" do
it "works! (now write some real specs)" do
get "/buffered_articles"
it "works successfully" do
get buffered_articles_path
expect(response).to have_http_status(:ok)
end
it "responds with json" do
get "/buffered_articles"
get buffered_articles_path
expect(response.content_type).to eq("application/json")
end
it "responds with at least one url" do
create(:article)
get "/buffered_articles"
expect(response.body).to include(ApplicationConfig["APP_DOMAIN"])
article = create(:article)
get buffered_articles_path
expect(response.parsed_body["urls"].first).to eq(article.decorate.url)
end
end
end

View file

@ -2,20 +2,35 @@ require "rails_helper"
RSpec.describe "Reactions", type: :request do
let(:user) { create(:user) }
let(:article) { create(:article, user_id: user.id) }
let(:article) { create(:article, user: user) }
let(:comment) { create(:comment, commentable: article) }
let(:max_age) { FastlyRails.configuration.max_age }
let(:stale_if_error) { FastlyRails.configuration.stale_if_error }
let_it_be(:max_age) { FastlyRails.configuration.max_age }
let_it_be(:stale_if_error) { FastlyRails.configuration.stale_if_error }
describe "GET /reactions?article_id=:article_id" do
before do
create(:reaction, reactable: article, user: user, points: 1)
end
context "when signed in" do
before do
sign_in user
get "/reactions?article_id=#{article.id}"
get reactions_path(article_id: article.id)
end
it "returns reactions count for article" do
expect(response.body).to include("article_reaction_counts")
it "returns the correct json response" do
result = response.parsed_body
expect(result["current_user"]).to eq("id" => user.id)
expected_reactions_counts = [
{ "category" => "like", "count" => 1 },
{ "category" => "readinglist", "count" => 0 },
{ "category" => "unicorn", "count" => 0 },
]
expect(result["article_reaction_counts"]).to eq(expected_reactions_counts)
expect(result["reactions"].to_json).to eq(user.reactions.where(reactable: article).to_json)
end
it "does not set cache control headers" do
@ -31,10 +46,19 @@ RSpec.describe "Reactions", type: :request do
end
context "when signed out" do
before { get "/reactions?article_id=#{article.id}" }
before { get reactions_path(article_id: article.id) }
it "returns reactions count for article" do
expect(response.body).to include("article_reaction_counts")
it "returns the correct json response" do
result = response.parsed_body
expect(result["current_user"]).to eq("id" => nil)
expected_reactions = [
{ "category" => "like", "count" => 1 },
{ "category" => "readinglist", "count" => 0 },
{ "category" => "unicorn", "count" => 0 },
]
expect(result["article_reaction_counts"]).to eq(expected_reactions)
expect(result["reactions"]).to be_empty
end
it "sets the surrogate key header equal to params for article" do
@ -50,19 +74,28 @@ RSpec.describe "Reactions", type: :request do
end
end
describe "GET /reactions?commentable_id=:article.id&commentable_type=Comment" do
describe "GET /reactions?commentable_id=:article.id&commentable_type=Article" do
before do
create(:reaction, reactable: comment, user: user, points: 1)
end
context "when signed in" do
before do
sign_in user
get "/reactions?commentable_id=#{article.id}&commentable_type=Comment"
get reactions_path(commentable_id: article.id, commentable_type: "Article")
end
it "returns positive reaction counts" do
expect(response.body).to include("positive_reaction_counts")
it "returns the correct json response" do
result = response.parsed_body
expect(result["current_user"]).to eq("id" => user.id)
expect(result["positive_reaction_counts"]).to eq([{ "id" => article.comments.last.id, "count" => 1 }])
expect(result["reactions"].to_json).to eq(user.reactions.where(reactable: comment).to_json)
end
it "does not set surrogate key headers" do
expect(response.headers["Surrogate-Key"]).to eq(nil)
expect(response.headers["surrogate-key"]).to be_nil
end
it "does not set Fastly cache control and surrogate control headers" do
@ -74,10 +107,14 @@ RSpec.describe "Reactions", type: :request do
end
context "when signed out" do
before { get "/reactions?commentable_id=#{article.id}&commentable_type=Comment" }
before { get reactions_path(commentable_id: article.id, commentable_type: "Article") }
it "returns positive reaction counts" do
expect(response.body).to include("positive_reaction_counts")
it "returns the correct json response" do
result = response.parsed_body
expect(result["current_user"]).to eq("id" => nil)
expect(result["positive_reaction_counts"]).to eq([{ "id" => article.comments.last.id, "count" => 1 }])
expect(result["reactions"]).to be_empty
end
it "sets the surrogate key header equal to params" do