Skip to content

Commit 4cfba4f

Browse files
committed
Instrument Que job enqueues
Record an `enqueue.que` event when a Que job is enqueued, and a `bulk_enqueue.que` event for the whole batch on a bulk enqueue, so enqueues show up in the transaction timeline. The bulk wrapper records one event and its inner enqueues pass through. The bulk module is only prepended on Que 2+, which has `bulk_enqueue`, so no phantom method is defined on Que 1.
1 parent 99bfd18 commit 4cfba4f

4 files changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
bump: minor
3+
type: add
4+
---
5+
6+
Instrument Que job enqueues. AppSignal now records an `enqueue.que` event when a Que job is enqueued, and a `bulk_enqueue.que` event for bulk enqueues on Que 2, so the enqueue shows up in the transaction timeline.

lib/appsignal/hooks/que.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ def dependencies_present?
1313
def install
1414
require "appsignal/integrations/que"
1515
::Que::Job.prepend Appsignal::Integrations::QuePlugin
16+
::Que::Job.singleton_class.prepend Appsignal::Integrations::QueClientPlugin
17+
18+
# `bulk_enqueue` exists only on Que 2+; don't define one where it's absent.
19+
if ::Que::Job.respond_to?(:bulk_enqueue)
20+
::Que::Job.singleton_class.prepend Appsignal::Integrations::QueBulkClientPlugin
21+
end
1622

1723
::Que.error_notifier = proc do |error, _job|
1824
Appsignal::Transaction.current.set_error(error)

lib/appsignal/integrations/que.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,41 @@ def _run(*args)
3434
end
3535
end
3636
end
37+
38+
# @!visibility private
39+
#
40+
# Prepended to `Que::Job`'s singleton so it records each enqueue as an
41+
# `enqueue.que` event under the active transaction. Like all AppSignal
42+
# events, it only records when there's an active transaction (e.g. enqueuing
43+
# from within a web request or another job); otherwise it's a transparent
44+
# pass-through.
45+
module QueClientPlugin
46+
def enqueue(*_args, **_rest)
47+
# Inside a Que 2 `bulk_enqueue` block the batch is recorded once by the
48+
# `bulk_enqueue` wrapper, so each inner enqueue is a pass-through to
49+
# avoid recording an event per job.
50+
return super if bulk_insert_in_progress?
51+
52+
Appsignal.instrument("enqueue.que") { super }
53+
end
54+
55+
private
56+
57+
def bulk_insert_in_progress?
58+
!Thread.current[:que_jobs_to_bulk_insert].nil?
59+
end
60+
end
61+
62+
# @!visibility private
63+
#
64+
# `bulk_enqueue` exists only on Que 2+, so this lives in its own module that
65+
# the hook prepends only when Que has the method -- otherwise we'd define a
66+
# `bulk_enqueue` on Que versions that have none. The whole batch records a
67+
# single `bulk_enqueue.que` event; the inner enqueues are pass-throughs.
68+
module QueBulkClientPlugin
69+
def bulk_enqueue(*_args, **_rest)
70+
Appsignal.instrument("bulk_enqueue.que") { super }
71+
end
72+
end
3773
end
3874
end

spec/lib/appsignal/integrations/que_spec.rb

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,94 @@ def run(*_args)
184184
end
185185
end
186186
end
187+
188+
describe Appsignal::Integrations::QueClientPlugin do
189+
let(:job) do
190+
Class.new(::Que::Job) do
191+
def self.name
192+
"MyQueJob"
193+
end
194+
end
195+
end
196+
197+
# Capture what Que would persist, without needing a database.
198+
let(:captured) { {} }
199+
before do
200+
allow(Que).to receive(:execute) do |command, values|
201+
captured[:values] = values if command == :insert_job
202+
[{}]
203+
end
204+
205+
start_agent
206+
end
207+
around { |example| keep_transactions { example.run } }
208+
209+
# `data` is the last value Que passes to its `:insert_job` query on both Que
210+
# 1 and Que 2 (Que 2 inserts `kwargs` before it, shifting its index); the
211+
# tags live under it as a JSON string.
212+
def enqueued_tags
213+
data = captured[:values]&.last
214+
data ? JSON.parse(data)["tags"] : nil
215+
end
216+
217+
def enqueue(tags: ["user:42"])
218+
job.enqueue("post_id_123", :job_options => { :tags => tags })
219+
end
220+
221+
context "with an active transaction" do
222+
it "records an enqueue event and leaves the job's tags untouched" do
223+
transaction = http_request_transaction
224+
set_current_transaction(transaction)
225+
226+
enqueue
227+
228+
event_names = transaction.to_h["events"].map { |event| event["name"] }
229+
expect(event_names).to include("enqueue.que")
230+
expect(enqueued_tags).to eq(["user:42"])
231+
end
232+
end
233+
234+
context "without an active transaction" do
235+
it "is a transparent pass-through" do
236+
expect { enqueue }.to_not raise_error
237+
238+
expect(enqueued_tags).to eq(["user:42"])
239+
end
240+
end
241+
242+
# `bulk_enqueue` is Que 2 only. The whole batch records a single
243+
# `bulk_enqueue.que` event; the inner enqueues are pass-throughs.
244+
describe "#bulk_enqueue", :if => DependencyHelper.que2_present? do
245+
before do
246+
# Que's bulk path constantizes the job class by name, so it needs a real
247+
# constant (the single-enqueue path uses `new` and doesn't).
248+
stub_const("MyQueJob", job)
249+
allow(Que).to receive(:transaction).and_yield
250+
allow(Que).to receive(:execute) do |command, values|
251+
captured[:values] = values if command == :bulk_insert_jobs
252+
[{}]
253+
end
254+
end
255+
256+
def bulk_enqueue(tags: ["user:42"])
257+
job.bulk_enqueue(:job_options => { :tags => tags }) do
258+
job.enqueue("post_id_123")
259+
job.enqueue("post_id_456")
260+
end
261+
end
262+
263+
it "records one bulk_enqueue event for the whole batch" do
264+
transaction = http_request_transaction
265+
set_current_transaction(transaction)
266+
267+
bulk_enqueue
268+
269+
event_names = transaction.to_h["events"].map { |event| event["name"] }
270+
# One event for the whole batch -- the inner enqueues don't add their own.
271+
expect(event_names.count { |name| name == "bulk_enqueue.que" }).to eq(1)
272+
expect(event_names).to_not include("enqueue.que")
273+
expect(enqueued_tags).to eq(["user:42"])
274+
end
275+
end
276+
end
187277
end

0 commit comments

Comments
 (0)