Skip to content

Commit 446ce28

Browse files
lairtonmendesLairton Mendes
authored andcommitted
Add support for Solid Queue
1 parent 9fee098 commit 446ce28

7 files changed

Lines changed: 238 additions & 1 deletion

File tree

Gemfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,17 @@ else
136136
gem 'shoryuken', require: nil
137137
end
138138

139+
# Solid Queue requires Rails 7.1+ (Active Record + Active Job).
140+
if frameworks_versions.key?('rails')
141+
rails_version = frameworks_versions['rails']
142+
solid_queue_compatible =
143+
rails_version == 'main' ||
144+
rails_version.to_s.empty? ||
145+
(rails_version =~ /\A\d+(\.\d+)?\z/ &&
146+
Gem::Version.new(rails_version) >= Gem::Version.new('7.1'))
147+
gem 'solid_queue', require: nil if solid_queue_compatible
148+
end
149+
139150
if RUBY_PLATFORM == 'java'
140151
# See issue #6547 in the JRuby repo. It is fixed in JRuby 9.3
141152
gem 'i18n', '< 1.8.8' if JRUBY_VERSION < '9.3'

docs/reference/supported-technologies.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ We automatically instrument background processing using:
8080
* Sneakers (2.12.0) (Experimental, see [#676](https://github.com/elastic/apm-agent-ruby/pull/676))
8181
* Resque (>= 2.0.0 <= 2.7.0)
8282
* SuckerPunch (>= 2.0.0 <= 3.3.0)
83+
* Solid Queue (>= 1.0.0)
8384

8485

8586
## Resque [supported-technologies-resque]

docs/release-notes/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ All notable changes to this project will be documented here. This project adhere
2828

2929
% ### Fixes [elastic-apm-ruby-agent-versionext-fixes]
3030

31+
## X.X.X [elastic-apm-ruby-agent-XXX-release-notes]
32+
33+
### Features and enhancements [elastic-apm-ruby-agent-XXX-features-enhancements]
34+
* Support Solid Queue [#1623](https://github.com/elastic/apm-agent-ruby/pull/1623)
35+
3136
## 4.8.0 [elastic-apm-ruby-agent-480-release-notes]
3237

3338
### Features and enhancements [elastic-apm-ruby-agent-480-features-enhancements]

lib/elastic_apm/config.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def available_instrumentations
171171
sinatra
172172
sneakers
173173
sns
174+
solid_queue
174175
sqs
175176
sucker_punch
176177
tilt
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Licensed to Elasticsearch B.V. under one or more contributor
2+
# license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright
4+
# ownership. Elasticsearch B.V. licenses this file to you under
5+
# the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# frozen_string_literal: true
19+
20+
module ElasticAPM
21+
# @api private
22+
module Spies
23+
# @api private
24+
class SolidQueueSpy
25+
TYPE = 'SolidQueue'
26+
27+
# @api private
28+
module Ext
29+
def perform
30+
name = job&.class_name
31+
transaction = ElasticAPM.start_transaction(name, TYPE)
32+
ElasticAPM.set_label(:queue, job.queue_name) if job&.queue_name
33+
34+
super
35+
36+
transaction&.done 'success'
37+
transaction&.outcome = Transaction::Outcome::SUCCESS
38+
rescue ::Exception => e
39+
ElasticAPM.report(e, handled: false)
40+
transaction&.done 'error'
41+
transaction&.outcome = Transaction::Outcome::FAILURE
42+
raise
43+
ensure
44+
ElasticAPM.end_transaction
45+
end
46+
end
47+
48+
def install
49+
# +SolidQueue::ClaimedExecution+ lives under +app/models+ and is
50+
# autoloaded via Zeitwerk by the Rails engine.
51+
#
52+
# Two hooks are needed:
53+
# - +after_initialize+ fires after eager loading in production, which
54+
# is when +ClaimedExecution+ is first defined. +to_prepare+ alone is
55+
# not enough because Rails runs +to_prepare+ *before* eager loading.
56+
# - +to_prepare+ handles code reloads in development so the patch
57+
# survives class unloading between reloads.
58+
#
59+
# Fork mode (the default solid_queue supervisor) is handled by the agent
60+
# itself via +Agent#detect_forking!+ on each +start_transaction+, so no
61+
# extra lifecycle hook wiring is needed here.
62+
if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
63+
::Rails.application.config.after_initialize { SolidQueueSpy.prepend_ext }
64+
::Rails.application.reloader.to_prepare { SolidQueueSpy.prepend_ext }
65+
end
66+
67+
SolidQueueSpy.prepend_ext
68+
end
69+
70+
def self.prepend_ext
71+
return unless defined?(::SolidQueue::ClaimedExecution)
72+
return if ::SolidQueue::ClaimedExecution.include?(Ext)
73+
74+
::SolidQueue::ClaimedExecution.prepend(Ext)
75+
end
76+
end
77+
78+
register 'SolidQueue', 'solid_queue', SolidQueueSpy.new
79+
end
80+
end

lib/elastic_apm/transport/connection/proxy_pipe.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,15 @@ def initialize(io, compress: true)
5252
end
5353

5454
def self.finalize(io)
55-
proc { io.close }
55+
proc do
56+
io.close
57+
rescue ThreadError
58+
# io.close is forbidden inside a signal trap context (Ruby raises
59+
# ThreadError). SolidQueue and other job backends install persistent
60+
# Signal.trap handlers; GC triggered while a trap is active will
61+
# fire this finalizer in that context. The OS closes the fd on
62+
# process exit, so silently skipping here is safe.
63+
end
5664
end
5765

5866
attr_reader :io
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Licensed to Elasticsearch B.V. under one or more contributor
2+
# license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright
4+
# ownership. Elasticsearch B.V. licenses this file to you under
5+
# the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
# frozen_string_literal: true
19+
20+
require 'spec_helper'
21+
22+
# Solid Queue ships +SolidQueue::ClaimedExecution+ as an ActiveRecord model
23+
# under +app/models+, loaded via the engine through Zeitwerk. To avoid
24+
# requiring a full Rails boot here, the spec defines a stand-in constant with
25+
# the same shape and lets the spy hook it.
26+
module SolidQueue
27+
class ClaimedExecution
28+
attr_reader :job
29+
30+
def initialize(job)
31+
@job = job
32+
end
33+
34+
# Real solid_queue runs +ActiveJob::Base.execute+ here. For the spec we
35+
# just dispatch to the test job stub so that +super+ inside the spy's
36+
# +Ext+ module is exercised end-to-end.
37+
def perform
38+
job.run if job.respond_to?(:run)
39+
end
40+
end
41+
end
42+
43+
require 'elastic_apm/spies/solid_queue'
44+
45+
module ElasticAPM
46+
RSpec.describe 'Spy: SolidQueue', :intercept do
47+
class TestSolidQueueJob
48+
attr_reader :queue_name
49+
50+
def initialize(queue_name: 'default')
51+
@queue_name = queue_name
52+
end
53+
54+
def class_name
55+
self.class.name
56+
end
57+
58+
def run
59+
end
60+
end
61+
62+
class ExplodingSolidQueueJob < TestSolidQueueJob
63+
def run
64+
raise ZeroDivisionError, 'boom'
65+
end
66+
end
67+
68+
it 'instruments successful job perform' do
69+
with_agent do
70+
::SolidQueue::ClaimedExecution.new(TestSolidQueueJob.new).perform
71+
end
72+
73+
transaction, = @intercepted.transactions
74+
expect(transaction).to_not be_nil
75+
expect(transaction.name).to eq 'ElasticAPM::TestSolidQueueJob'
76+
expect(transaction.type).to eq 'SolidQueue'
77+
expect(transaction.result).to eq 'success'
78+
expect(transaction.outcome).to eq 'success'
79+
80+
labels = transaction.context.labels
81+
expect(labels[:queue]).to eq 'default'
82+
end
83+
84+
it 'reports errors and marks transaction as failure' do
85+
expect do
86+
with_agent do
87+
::SolidQueue::ClaimedExecution.new(ExplodingSolidQueueJob.new).perform
88+
end
89+
end.to raise_error(ZeroDivisionError)
90+
91+
transaction, = @intercepted.transactions
92+
expect(transaction.name).to eq 'ElasticAPM::ExplodingSolidQueueJob'
93+
expect(transaction.type).to eq 'SolidQueue'
94+
expect(transaction.outcome).to eq 'failure'
95+
expect(transaction.result).to eq 'error'
96+
97+
error, = @intercepted.errors
98+
expect(error.exception.type).to eq 'ZeroDivisionError'
99+
end
100+
101+
it 'captures the queue label from the job' do
102+
with_agent do
103+
job = TestSolidQueueJob.new(queue_name: 'critical')
104+
::SolidQueue::ClaimedExecution.new(job).perform
105+
end
106+
107+
transaction, = @intercepted.transactions
108+
expect(transaction.context.labels[:queue]).to eq 'critical'
109+
end
110+
111+
it 'creates a transaction for each perform call' do
112+
with_agent do
113+
::SolidQueue::ClaimedExecution.new(TestSolidQueueJob.new).perform
114+
::SolidQueue::ClaimedExecution.new(TestSolidQueueJob.new).perform
115+
end
116+
117+
expect(@intercepted.transactions.size).to eq 2
118+
end
119+
120+
it 'prepends the Ext module onto SolidQueue::ClaimedExecution' do
121+
expect(::SolidQueue::ClaimedExecution.ancestors)
122+
.to include(Spies::SolidQueueSpy::Ext)
123+
end
124+
125+
it "runs when the agent doesn't" do
126+
expect do
127+
::SolidQueue::ClaimedExecution.new(TestSolidQueueJob.new).perform
128+
end.to_not raise_error
129+
end
130+
end
131+
end

0 commit comments

Comments
 (0)