Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render past successful deploys on the releases overview #2897

Open
wants to merge 15 commits into
base: master
Choose a base branch
from

Conversation

dasch
Copy link
Contributor

@dasch dasch commented Aug 24, 2018

It's valuable to be able to see at a glance whether a given release has been successfully deployed to a stage in the past, e.g. when you want to know if a build stage has completed.

The past, successful deploys are displayed with a grey background.

/cc @zendesk/samson

Tasks

  • 👍 from team

Risks

  • Level: Low

@dasch dasch requested a review from ragurney August 24, 2018 12:50
next unless deploy.references?(reference)
label = (deploy.active? ? "label-warning" : "label-success")
# The first deploy is the most recent one.
next unless deploy = stage.deploys.where(reference: reference).first
Copy link
Contributor

@ragurney ragurney Aug 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing n where queries, would it be better to do something like:

deploys_hash = Deploy.joins(:stage).where(stage_id: stages.pluck(:id)).where(reference: 'master').group_by(&:stage_id)
stages.each do |stage|
  next unless deploy = deploys_hash[stage.id]&.first
  ...
end

?

samson(dev)> puts Benchmark.measure { deployed_or_running_list(stages, reference)  }
  Deploy Load (0.6ms)  SELECT  `deploys`.* FROM `deploys` WHERE `deploys`.`deleted_at` IS NULL AND `deploys`.`stage_id` = 4 AND `deploys`.`reference` = 'master' ORDER BY `deploys`.`id` DESC LIMIT 1
  Deploy Load (0.5ms)  SELECT  `deploys`.* FROM `deploys` WHERE `deploys`.`deleted_at` IS NULL AND `deploys`.`stage_id` = 2 AND `deploys`.`reference` = 'master' ORDER BY `deploys`.`id` DESC LIMIT 1
  Deploy Load (0.4ms)  SELECT  `deploys`.* FROM `deploys` WHERE `deploys`.`deleted_at` IS NULL AND `deploys`.`stage_id` = 3 AND `deploys`.`reference` = 'master' ORDER BY `deploys`.`id` DESC LIMIT 1
  Deploy Load (0.4ms)  SELECT  `deploys`.* FROM `deploys` WHERE `deploys`.`deleted_at` IS NULL AND `deploys`.`stage_id` = 1 AND `deploys`.`reference` = 'master' ORDER BY `deploys`.`id` DESC LIMIT 1
  Deploy Load (0.4ms)  SELECT  `deploys`.* FROM `deploys` WHERE `deploys`.`deleted_at` IS NULL AND `deploys`.`stage_id` = 5 AND `deploys`.`reference` = 'master' ORDER BY `deploys`.`id` DESC LIMIT 1
  Deploy Load (0.3ms)  SELECT  `deploys`.* FROM `deploys` WHERE `deploys`.`deleted_at` IS NULL AND `deploys`.`stage_id` = 6 AND `deploys`.`reference` = 'master' ORDER BY `deploys`.`id` DESC LIMIT 1
  Deploy Load (0.4ms)  SELECT  `deploys`.* FROM `deploys` WHERE `deploys`.`deleted_at` IS NULL AND `deploys`.`stage_id` = 7 AND `deploys`.`reference` = 'master' ORDER BY `deploys`.`id` DESC LIMIT 1
  0.005582   0.000476   0.006058 (  0.008085)
=> nil


samson(dev)> puts Benchmark.measure { deployed_or_running_list2(stages, reference)  }
  Deploy Load (1.0ms)  SELECT `deploys`.* FROM `deploys` INNER JOIN `stages` ON `stages`.`id` = `deploys`.`stage_id` AND `stages`.`deleted_at` IS NULL WHERE `deploys`.`deleted_at` IS NULL AND `deploys`.`stage_id` IN (4, 2, 3, 1, 5, 6, 7) AND `deploys`.`reference` = 'master' ORDER BY `deploys`.`id` DESC
  0.002177   0.000184   0.002361 (  0.003161)
=> nil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends – if we cache each row in the table based on the Release, and make sure to touch the release whenever it's deployed, then an n + 1 makes sense, as the common case will be that almost all of the releases will be cached and therefore won't trigger any queries. That's preferable to naively preloading everything.

I'd like to add that caching in a separate PR, though, unless it's a blocker for this one.

Copy link
Contributor

@grosser grosser Aug 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like this ?

referenced = stage.deploys.where(ref).first
deploy = stage.last_deploy
if referenced == deploy
  if deploy.success?
    'success'
  elsif deploy.running?
    'warning'
  else
   'default'
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite – I'll push an updated version.

@@ -700,11 +700,23 @@ def render
assert html.html_safe?
end

it "renders succeeded deploys" do
it "renders current, succeeded deploys" do
html = deployed_or_running_list(stage_list, "staging")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be nice to use assert_sql_queries here to lock in the number of queries in case someone comes along and changes this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

end
end

if label.present?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer consistent if thing instead of if thing.nil? + if thing.present?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

end

if label.present?
text = stage.name.html_safe
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this smells like xss ... I can name my stage <script>foo</script>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

html << " "
pieces = stages.map do |stage|
# The first deploy is the most recent one.
deploy = stage.deploys.where(reference: reference).first
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should fetch all the deploys in a single query with stage.project.deploys.select('max(id) as id').where(stage_id: stages.map(&:id), reference: reference).group(:stage_id) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't that just give the Deploy id, not the full deploy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I've pushed an optimized version that only does 1 query per row.

It's valuable to be able to see at a glance whether a given release has
been successfully deployed to a stage in the past, e.g. when you want to
know if a build stage has completed.
Don't use a reference that's identical to the stage name, that's
confusing.
@dasch
Copy link
Contributor Author

dasch commented Aug 28, 2018

@grosser one more review?

select("MAX(id) AS id, stage_id")

# Fetch the actual deploys.
deploys = Deploy.where(id: deploys_and_stages.map(&:id))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we only use the id and stage_id ... so we could avoid an extra query ?

end

# Returns a Hash of Stage => Deploy for the given reference and stages.
def self.of_reference_in_stages(reference, stages)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grouped hashes are usually built from methods with by not of

text << stage.name
html << content_tag(:span, text, class: "label #{label} release-stage")
html << " "
deploys = Deploy.of_reference_in_stages(reference, stages)
Copy link
Contributor

@grosser grosser Aug 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deploy_lookup = Deploy.where(reference: reference).where(stage_id: stages.map(&:id)).group(:stage_id).select("MAX(id) AS id, stage_id").group_by(&:stage_id)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really dislike putting SQL in the view layer – it makes testing so much harder. I'd very much prefer to keep this in the model.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the query logic is closely tied to the compare logic, so could lead to bugs/get out of sync when it's extracted to the model ... but I don't care that much
more important: the group_by line seems much easier to read (assuming it works as before) ?

[stage, deploy]
end.to_h
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all pretty funky and not reused, so I'd keep it inline

@@ -171,6 +171,30 @@ def self.after(deploy)
where("#{table_name}.id > ?", deploy.id)
end

def self.in_stages(stages)
where(stage_id: stages.map(&:id))
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kinda simple query I'd rather keep inline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

elsif deploy.running?
"label-warning"
elsif deploy.succeeded?
if deploy == stage.last_deploy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we build the same map for all last deploys from these stages ... so we end up with 2 queries ? (+ job fetching queries)

... for another PR: we could also prefetch the jobs for all deploys to save the job lookups that are caused by the status calls

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean – once you've called Stage#last_deploy, that should be memoized, right? If it's not, I can easily add that. The stages don't change between the release rows, so we shouldn't need to do any fancy work there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but it's being done once per stage -> n+1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How many stages do we typically have? Is it worth the added complexity?

Copy link
Contributor

@grosser grosser Aug 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projects can have tons of stages ... this PR is not making it worse, so keep it like it is and open a BRE issue to make this more performant ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants