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

Send webhook notifications through an ActiveJob #3507

Merged
merged 1 commit into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ def enqueue_web_hook_jobs(version)
job.fire(
request.protocol.delete("://"),
request.host_with_port,
version.rubygem,
version
)
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/web_hooks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def fire
webhook = @api_key.user.web_hooks.new(url: @url)
@rubygem ||= Rubygem.find_by_name("gemcutter")

if webhook.fire(request.protocol.delete("://"), request.host_with_port, @rubygem,
if webhook.fire(request.protocol.delete("://"), request.host_with_port,
@rubygem.versions.most_recent, delayed: false)
render plain: webhook.deployed_message(@rubygem)
else
Expand Down
38 changes: 0 additions & 38 deletions app/jobs/notifier.rb

This file was deleted.

50 changes: 50 additions & 0 deletions app/jobs/notify_web_hook_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
class NotifyWebHookJob < ApplicationJob
extend StatsD::Instrument
include TraceTagger

queue_as :default
self.queue_adapter = :good_job

before_perform { @kwargs = arguments.last.then { _1 if Hash.ruby2_keywords_hash?(_1) } }
before_perform { @webhook = @kwargs.fetch(:webhook) }
before_perform { @protocol = @kwargs.fetch(:protocol) }
before_perform { @host_with_port = @kwargs.fetch(:host_with_port) }
before_perform { @version = @kwargs.fetch(:version) }
before_perform { @rubygem = @version.rubygem }

attr_reader :webhook, :protocol, :host_with_port, :version, :rubygem

def perform(*)
url = webhook.url
set_tag "gemcutter.notifier.url", url
set_tag "gemcutter.notifier.webhook_id", webhook.id

timeout(5) do
Copy link
Member

@simi simi Feb 28, 2023

Choose a reason for hiding this comment

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

Super minor idea: What about to wrap timeout into constant since it is used 3 times?

Copy link
Member Author

Choose a reason for hiding this comment

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

will probably be replacing this code soon anyways

Copy link
Member

Choose a reason for hiding this comment

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

OK, let's keep it as is for now.

RestClient.post url,
payload,
:timeout => 5,
:open_timeout => 5,
"Content-Type" => "application/json",
"Authorization" => authorization
end
true
rescue *(HTTP_ERRORS + [RestClient::Exception, SocketError, SystemCallError]) => _e
webhook.increment! :failure_count
false
end
statsd_count_success :perform, "Webhook.perform"

def payload
rubygem.payload(version, protocol, host_with_port).to_json
end

def authorization
Digest::SHA2.hexdigest([rubygem.name, version.number, webhook.api_key].compact.join)
end

private

def timeout(sec, &)
Timeout.timeout(sec, &)
end
end
8 changes: 4 additions & 4 deletions app/models/web_hook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ def self.specific
where.not(rubygem_id: nil)
end

def fire(protocol, host_with_port, deploy_gem, version, delayed: true)
job = Notifier.new(url, protocol, host_with_port, deploy_gem, version, api_key)
def fire(protocol, host_with_port, version, delayed: true)
job = NotifyWebHookJob.new(webhook: self, protocol:, host_with_port:, version:)

if delayed
Delayed::Job.enqueue job, priority: PRIORITIES[:web_hook]
job.enqueue
else
job.perform
job.perform_now
end
end

Expand Down
4 changes: 3 additions & 1 deletion test/functional/api/v1/deletions_controller_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require "test_helper"

class Api::V1::DeletionsControllerTest < ActionController::TestCase
include ActiveJob::TestHelper

context "with yank rubygem api key scope" do
setup do
@api_key = create(:api_key, key: "12345", yank_rubygem: true)
Expand Down Expand Up @@ -385,7 +387,7 @@ class Api::V1::DeletionsControllerTest < ActionController::TestCase
number: @v1.number).first
end
should "have enqueued a webhook" do
assert_instance_of Notifier, Delayed::Job.last.payload_object
assert_enqueued_jobs 1, only: NotifyWebHookJob
end
end

Expand Down
6 changes: 4 additions & 2 deletions test/functional/api/v1/rubygems_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,11 @@ def self.should_respond_to(format)
assert_equal "Successfully registered gem: test (1.0.0)", @response.body
end
should "enqueue jobs" do
assert_difference "Delayed::Job.count", 4 do
assert_difference "Delayed::Job.count", 3 do
assert_enqueued_jobs 4, only: FastlyPurgeJob do
post :create, body: gem_file("test-1.0.0.gem").read
assert_enqueued_jobs 1, only: NotifyWebHookJob do
post :create, body: gem_file("test-1.0.0.gem").read
end
end
end
end
Expand Down
40 changes: 40 additions & 0 deletions test/jobs/notify_web_hook_job_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require "test_helper"

class NotifyWebHookJobTest < ActiveJob::TestCase
context "with a rubygem and version" do
setup do
@rubygem = create(:rubygem, name: "foogem", downloads: 42)
@version = create(:version,
rubygem: @rubygem,
number: "3.2.1",
authors: %w[AUTHORS],
description: "DESC")
@hook = create(:web_hook, rubygem: @rubygem)
@job = NotifyWebHookJob.new(webhook: @hook, protocol: "http", host_with_port: "localhost:1234", version: @version)
end

should "have gem properties encoded in JSON" do
payload = @job.run_callbacks(:perform) { JSON.parse(@job.payload) }
assert_equal "foogem", payload["name"]
assert_equal "3.2.1", payload["version"]
assert_equal "ruby", payload["platform"]
assert_equal "DESC", payload["info"]
assert_equal "AUTHORS", payload["authors"]
assert_equal 42, payload["downloads"]
assert_equal "http://localhost:1234/gems/foogem", payload["project_uri"]
assert_equal "http://localhost:1234/gems/foogem-3.2.1.gem", payload["gem_uri"]
end

should "send the right version out even for older gems" do
new_version = create(:version, number: "2.0.0", rubygem: @rubygem)
new_hook = create(:web_hook)
job = NotifyWebHookJob.new(webhook: new_hook, protocol: "http", host_with_port: "localhost:1234", version: new_version)
payload = job.run_callbacks(:perform) { JSON.parse(job.payload) }

assert_equal "foogem", payload["name"]
assert_equal "2.0.0", payload["version"]
assert_equal "http://localhost:1234/gems/foogem", payload["project_uri"]
assert_equal "http://localhost:1234/gems/foogem-2.0.0.gem", payload["gem_uri"]
end
end
end
47 changes: 5 additions & 42 deletions test/models/web_hook_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,43 +137,6 @@ class WebHookTest < ActiveSupport::TestCase
end
end

context "with a rubygem and version" do
setup do
@rubygem = create(:rubygem, name: "foogem", downloads: 42)
@version = create(:version,
rubygem: @rubygem,
number: "3.2.1",
authors: %w[AUTHORS],
description: "DESC")
@hook = create(:web_hook, rubygem: @rubygem)
@job = Notifier.new(@hook.url, "http", "localhost:1234", @rubygem, @version)
end

should "have gem properties encoded in JSON" do
payload = JSON.load(@job.payload)
assert_equal "foogem", payload["name"]
assert_equal "3.2.1", payload["version"]
assert_equal "ruby", payload["platform"]
assert_equal "DESC", payload["info"]
assert_equal "AUTHORS", payload["authors"]
assert_equal 42, payload["downloads"]
assert_equal "http://localhost:1234/gems/foogem", payload["project_uri"]
assert_equal "http://localhost:1234/gems/foogem-3.2.1.gem", payload["gem_uri"]
end

should "send the right version out even for older gems" do
new_version = create(:version, number: "2.0.0", rubygem: @rubygem)
new_hook = create(:web_hook)
job = Notifier.new(new_hook.url, "http", "localhost:1234", @rubygem, new_version)
payload = JSON.load(job.payload)

assert_equal "foogem", payload["name"]
assert_equal "2.0.0", payload["version"]
assert_equal "http://localhost:1234/gems/foogem", payload["project_uri"]
assert_equal "http://localhost:1234/gems/foogem-2.0.0.gem", payload["gem_uri"]
end
end

context "with a non-global hook job" do
setup do
@url = "http://example.com/gemcutter"
Expand All @@ -186,15 +149,15 @@ class WebHookTest < ActiveSupport::TestCase
authorization = Digest::SHA2.hexdigest(@rubygem.name + @version.number + @hook.user.api_key)
RestClient.expects(:post).with(anything, anything, has_entries("Authorization" => authorization))

@hook.fire("https", "rubygems.org", @rubygem, @version, delayed: false)
@hook.fire("https", "rubygems.org", @version)
end

should "include an Authorization header for a user with no API key" do
@hook.user.update(api_key: nil)
authorization = Digest::SHA2.hexdigest(@rubygem.name + @version.number)
RestClient.expects(:post).with(anything, anything, has_entries("Authorization" => authorization))

@hook.fire("https", "rubygems.org", @rubygem, @version, delayed: false)
@hook.fire("https", "rubygems.org", @version)
end

should "include an Authorization header for a user with many API keys" do
Expand All @@ -203,11 +166,11 @@ class WebHookTest < ActiveSupport::TestCase
authorization = Digest::SHA2.hexdigest(@rubygem.name + @version.number + @hook.user.api_keys.first.hashed_key)
RestClient.expects(:post).with(anything, anything, has_entries("Authorization" => authorization))

@hook.fire("https", "rubygems.org", @rubygem, @version, delayed: false)
@hook.fire("https", "rubygems.org", @version)
end

should "not increment failure count for hook" do
@hook.fire("https", "rubygems.org", @rubygem, @version, delayed: false)
@hook.fire("https", "rubygems.org", @version)

assert_predicate @hook.failure_count, :zero?
end
Expand All @@ -233,7 +196,7 @@ class WebHookTest < ActiveSupport::TestCase
Net::ProtocolError].each_with_index do |exception, index|
RestClient.stubs(:post).raises(exception)

@hook.fire("https", "rubygems.org", @rubygem, @version, delayed: false)
@hook.fire("https", "rubygems.org", @version)

assert_equal index + 1, @hook.reload.failure_count
assert_predicate @hook, :global?
Expand Down