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

[delayed_job] integration #444

Merged
merged 26 commits into from
Jul 16, 2018
Merged

Conversation

pawelchcki
Copy link
Contributor

@pawelchcki pawelchcki commented Jun 5, 2018

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 created
Tags:

  • delayed_job.id - internal id of a job
  • delayed_job.queue - queue name if it was set
  • delayed_job.priority - configured priority of the job
  • delayed_job.attempts - number of retries that has been attempted

Documentation

The DelayedJob integration uses lifecycle hooks to trace the job executions.

You can enable it through Datadog.configure:

require 'ddtrace'

Datadog.configure do |c|
  c.use :delayed_job, options
end

Where options is an optional Hash that accepts the following parameters:

Key Description Default
service_name Service name used for DelayedJob instrumentation delayed_job
tracer A Datadog::Tracer instance used to instrument the application. Usually you don't need to set that. Datadog.tracer

@pawelchcki pawelchcki added the do-not-merge/WIP Not ready for merge label Jun 5, 2018
@pawelchcki pawelchcki self-assigned this Jun 5, 2018
@pawelchcki pawelchcki force-pushed the feature/add_delayed_job_integration branch from 8fa35d3 to c084b13 Compare June 11, 2018 13:58
@pawelchcki pawelchcki added integrations Involves tracing integrations feature Involves a product feature and removed do-not-merge/WIP Not ready for merge labels Jun 12, 2018
@pawelchcki pawelchcki added this to the 0.13.0 milestone Jun 12, 2018
@pawelchcki pawelchcki requested a review from delner June 12, 2018 13:19
@pawelchcki pawelchcki assigned delner and unassigned pawelchcki Jun 12, 2018
@delner delner modified the milestones: 0.13.0, 0.14.0 Jun 15, 2018
@pawelchcki pawelchcki changed the base branch from 0.13-dev to 0.14-dev June 21, 2018 10:44
Copy link
Contributor

@delner delner left a 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'
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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|
Copy link
Contributor

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
Copy link
Contributor

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:')
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@pawelchcki pawelchcki Jun 28, 2018

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!)

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@pawelchcki
Copy link
Contributor Author

Thanks @delner for review! Code is ready for another round.

Copy link
Contributor

@delner delner left a 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|
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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|
Copy link
Contributor

Choose a reason for hiding this comment

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

By virtue of being required, 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.

@delner delner assigned pawelchcki and unassigned delner Jul 11, 2018

private

def unpatch
Copy link
Contributor

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.

Copy link
Contributor Author

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|
Copy link
Contributor

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 }
Copy link
Contributor

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 }) }

Copy link
Contributor

@delner delner left a 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.

Copy link
Contributor

@delner delner left a 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) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Neat!

@pawelchcki pawelchcki merged commit 20a8bfc into 0.14-dev Jul 16, 2018
@pawelchcki pawelchcki deleted the feature/add_delayed_job_integration branch July 16, 2018 16:45
@pawelchcki
Copy link
Contributor Author

Thanks for review @delner!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Involves a product feature integrations Involves tracing integrations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants