-
Notifications
You must be signed in to change notification settings - Fork 375
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
[delayed_job] integration #444
Conversation
8fa35d3
to
c084b13
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good start. Some points regarding configuration, test state, and a few other minor points.
@@ -0,0 +1,34 @@ | |||
require 'active_record' | |||
require 'delayed_job' | |||
require 'delayed_job_active_record' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is some datasource required to run delayed_job
? And is this why we have delayed_job_active_record
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in delayed_job
the datasource needs to be plugged in. The delayed_job
gem is a executor with some interface definitions for the datasources.
# DelayedJob plugin that instruments invoke_job hook | ||
class Plugin < Delayed::Plugin | ||
def self.instrument(job, &block) | ||
pin = Pin.get_from(::Delayed::Worker) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the use of Pin
required? We often use Pin
to handle configuration for disparate objects in the same integration (e.g. multiple databases), but otherwise encourage the use of the configuration directly e.g. Datadog.configuration[:delayed_job][:service_name]
.
The downside I see to the current setup is that calling use :delayed_job
won't update the Pin
after its been initialized, making it difficult to reconfigure.
pin.tracer.shutdown! if pin && pin.tracer | ||
end | ||
|
||
callbacks do |lifecycle| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a nice way to hook into the framework!
let(:tracer) { ::Datadog::Tracer.new(writer: FauxWriter.new) } | ||
|
||
before do | ||
Datadog::Contrib::DelayedJob::Patcher.patch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we call patch
directly instead of Datadog.configure { |c| c.use :delayed_job }
? It would be best to preserve a consistent configuration API experience, whenever possible.
logger = Logger.new(STDOUT) | ||
logger.level = Logger::INFO | ||
|
||
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we've done this a lot in previous test files to configure ActiveRecord with Minitest, but we really shouldn't be establishing database connections simply by virtue of calling require 'app'
. I would consider wrapping this into a module, function, or RSpec reference that can be optionally composed into specific examples, where necessary.
A big downside is that this currently creates a dependence of some kind of preconfigured database state: if this state changes, it could break tests in an indeterminate number of places across the test suite.
In an ideal world, specs should encapsulate and manage the state they need to run successfully, rather than depending on such globals. If specs need to share some kind of common test setup (as opposed to state: they should never share state), then it makes sense to have this setup in composable module within a separate file like this. It gives the specs greater control over what state gets set, and how/when to set it. And if this state changes, it's easier to track down which examples specifically depend upon it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, 💯 for use of in-memory SQLite, rather than depending on an external database service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed requrie 'app'
is not the best approach.
I've switched to using RSpec around
to setup and close the DB for each test.
describe 'instrumenting worker execution' do | ||
let(:worker) { double(:worker, name: 'worker') } | ||
before do | ||
allow(tracer).to receive(:shutdown!).and_call_original |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not a no-op? allow
is a stub that does no assertions, and using and_call_original
, its just passing through like it was never there.
If you'd like to assert that shutdown!
is called, it should be changed to expect(tracer)
. If you want it to simply skip side effects from #shutdown!
, then you can remove and_call_original
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below I check if shutdown! was called at the right moment. i.e. after the executor actually finished executing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expect(tracer).not_to have_received(:shutdown!) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, I've never seen have_received
before, but that's nice.
But the question still remains, why stub with a no-op just to override with an expectation below? Is it just this implementation of a spy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its an implementation of the spy you linked. I use it verify that the shutdown is called in correct order:
worker do
execute do
end
shutdown!
end
In first test I verify if execute callback is called.
Then I verify that in execute callback the shutdown!
wasn't called, but it is called after the execute finishes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay this is fine. Just wanted to make sure this wasn't an unnecessary stub, but it appears to be part of the spy
implementation of have_received
.
Thanks @delner for review! Code is ready for another round. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're missing documentation, and I think we still need to improve on how the migrations run in the tests.
@@ -0,0 +1,24 @@ | |||
RSpec.configure do |c| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an improvement from a syntax perspective, but it now auto-runs around every example in the test suite. Normally we only run one integration at a time in our CI, but we shouldn't operate on that kind of assumption, as it may not always be true. This should only run around the examples it needs to, when the example explicitly configures itself to do so. (Not by virtue of require
.)
klass.plugins << Plugin | ||
end | ||
|
||
def add_pin(klass) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're driving out configuration off the configuration API, which is good, but why are we adding a pin here?
require 'ddtrace' | ||
require_relative 'active_record_setup' | ||
|
||
RSpec.describe Datadog::Contrib::DelayedJob::Patcher do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests looks really clean; nice work!
@@ -952,6 +952,27 @@ Where `options` is an optional `Hash` that accepts the following parameters: | |||
| --- | --- | --- | | |||
| ``service_name`` | Service name used for `sidekiq` instrumentation | sidekiq | | |||
|
|||
### DelayedJob |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're missing important documentation; this section needs to be added to the list of links above, and the table of supported integrations before that, with appropriate details and links to resources wherever required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this has since been added; thanks!
require 'ddtrace' | ||
require_relative 'active_record_setup' | ||
|
||
RSpec.describe Datadog::Contrib::DelayedJob::Patcher, :delayed_job_active_record do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dig the use of the trait here to drive test configuration, but I think we still need to update the active_record_setup.rb
file to match, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -0,0 +1,24 @@ | |||
RSpec.configure do |c| | |||
c.around do |example| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By virtue of being require
d, this is still running around every example in the test suite. This should be changed so it only runs around specific examples when explicitly composed into the example itself (not just by use of require
.) Similarly, any specs that require
this file, but do not explicitly compose it into its examples should not perform these migrations.
|
||
private | ||
|
||
def unpatch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider that this patch is not actually "unpatchable". We should not communicate the expectation that you can "undo" patches, or that you should be able to "undo" patches, since modifying classes and constants is generally irreversible. Trying to support this would be useful for tests, but only for tests; if this kind of feature only needs to exist for tests, then in should exist in the test suite, not in our patcher API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point
@@ -0,0 +1,24 @@ | |||
RSpec.configure do |c| | |||
c.around(:example, :delayed_job_active_record) do |example| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like the use of a trait here. 👍
require 'ddtrace/contrib/delayed_job/plugin' | ||
require_relative 'delayed_job_active_record' | ||
|
||
SampleJob = Struct.new('SampleJob') { def perform; end } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be put into an RSpec constant stub, something like:
let(:job_class) { stub_const('SampleJob', Struct.new('SampleJob') { def perform; end }) }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty solid, just a couple of smaller things with unpatch
and constant stubs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great. Nice work!
|
||
RSpec.describe Datadog::Contrib::DelayedJob::Plugin, :delayed_job_active_record do | ||
let(:sample_job_object) { double('sample_job', perform: nil) } | ||
let(:sample_job_class) { class_double('SampleJob', new: sample_job_object) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat!
Thanks for review @delner! |
Service name:
delayed_job
Resource:
job.name
- this can be a class name or a formatted class + method name, depending on how the job was createdTags:
delayed_job.id
- internal id of a jobdelayed_job.queue
- queue name if it was setdelayed_job.priority
- configured priority of the jobdelayed_job.attempts
- number of retries that has been attemptedDocumentation
The DelayedJob integration uses lifecycle hooks to trace the job executions.
You can enable it through
Datadog.configure
:Where
options
is an optionalHash
that accepts the following parameters:service_name
DelayedJob
instrumentationtracer
Datadog::Tracer
instance used to instrument the application. Usually you don't need to set that.Datadog.tracer