Skip to content

Fix Span/Transaction's nesting issue#1382

Merged
st0012 merged 4 commits intomasterfrom
fix-#1372
Apr 6, 2021
Merged

Fix Span/Transaction's nesting issue#1382
st0012 merged 4 commits intomasterfrom
fix-#1372

Conversation

@st0012
Copy link
Copy Markdown
Contributor

@st0012 st0012 commented Apr 4, 2021

The current #start_child implementation on the Span and Transaction classes are both incorrect, which makes it only possible to create 2-level spans. This PR fixes the issue by making the behavior matching sentry-python's.

Fixes #1372

@st0012 st0012 added this to the sentry-ruby-4.3.2 milestone Apr 4, 2021
@st0012 st0012 self-assigned this Apr 4, 2021
@st0012 st0012 requested a review from jan-auer April 4, 2021 14:45
@rhcarvalho rhcarvalho requested review from a team, lobsterkatie and rhcarvalho and removed request for a team and jan-auer April 6, 2021 07:36
@rhcarvalho
Copy link
Copy Markdown
Contributor

Thanks @st0012! I like that I can learn some Ruby from you :)


protected

def set_span_recorder(limit = 1000)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the protected statement above makes this method only callable within Span or Transaction objects. so Span#dup can still call it. but users won't be able to call it from their codebase anymore.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the discussion in https://www.rubyguides.com/2018/10/method-visibility/ relevant to us, in particular (see "Private vs Protected Methods") about preferring private over protected and the performance penalty of using protected?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  1. it's a micro difference in most of the Ruby applications. I think the SDK has other places to improve if we start caring about the pure-Ruby computation performance. (I do watch memory consumption though from time to time)
  2. the method is called once per transaction, which in most cases equals once per request. so I don't think the micro performance difference would add up to anything worth worrying.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm I had to play a little bit with this to understand it.

Details
$ cat t.rb
class Span
  def do
    set_span_recorder
  end

  private

  def set_span_recorder
    puts "hello"
  end
end

class Transaction < Span
  def tdo
    set_span_recorder
  end
end

span = Span.new
span.do

transaction = Transaction.new
transaction.do
transaction.tdo

$ ruby t.rb
hello
hello
hello

So both private and protected would work here, and since we never need to call Span.set_span_recorder I was curious why you went with protected.


On trying it out and reading the code more closely, I realized that set_span_recorder is really instantiating a new SpanRecorder object, assigning the instance to an instance variable @span_recorder and adding a reference to the current span.

We call the equivalent method init_span_recorder in Python and initSpanRecorder in JS. In Python we define it on Span, but in JS we define it on Transaction, which seems to make more sense to me since we only ever start recording spans from a transaction. In both Python and JS it is the start_transaction method on Hub that initializes the recorder (I don't particularly like how the method leaks into the public API).

If we copy from Python/JS we'd end up making this method public again 😞

The alternative would be to keep it as an internal implementation detail, but probably in transaction.rb, since we only ever need to create a new instance there. Spans need to get a reference to the private/internal span recorder from their parent.

@st0012 st0012 requested a review from rhcarvalho April 6, 2021 14:52
Comment on lines +48 to +50
def deep_dup
copy = super
copy.set_span_recorder
copy.set_span_recorder(@span_recorder.max_length)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmmm this is interesting. What are the use cases for deep cloning a Span / Transaction, why do we implement this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I forgot the original motivation for it. but generally I want to avoid a mutation on copy being shared with its origin, like:

      span = Sentry::Transaction.new(sampled: true)
      subject.set_span(span)
      copy = subject.dup

      span.start_child

      # the copy probably shouldn't sees the same recorder change, no?
      expect(copy.span.span_recorder.spans.count).to eq(1)

I've written several test cases to prevent sharing mutations between transaction/spans and their copies. so if we decide this is unnecessary and want to drop it, I prefer to do it in another PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Definitely something for another PR, sorry for the distraction. It caught my attention since it is the first time I'm looking in more detail in this SDK.

the copy probably shouldn't sees the same recorder change, no?

I don't know what I would expect... both possibilities could be okay depending on what it means to duplicate a span.

I understand and noticed how it is used in tests. In production code, nobody should probably be calling Transaction.dup or Span.dup?! I am genuinely curious for the use cases of duplicating spans I found one.

I see this is used when cloning a Scope. Unfortunately I think the behavior here is not clearly specified in the SDK specs. Python and JS do not perform a deep copy, only shallow, the recorder is shared (whether that is useful or not).

st0012 added 4 commits April 7, 2021 01:08
`Span#start_child` should also assign the parent span's span recorder to
the child span and record the child span with it. Previously, only
`Transaction#start_child`, which makes it only possible to create
2-level nestings.
This attribute would make it easier for child spans to trace back to
their root transaction.
Copy link
Copy Markdown
Contributor

@rhcarvalho rhcarvalho left a comment

Choose a reason for hiding this comment

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

Thanks @st0012 !

@st0012
Copy link
Copy Markdown
Contributor Author

st0012 commented Apr 6, 2021

@rhcarvalho thanks for the thorough review! 👍

@st0012 st0012 merged commit c94747f into master Apr 6, 2021
@st0012 st0012 deleted the fix-#1372 branch April 6, 2021 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trouble nesting children successfully with start_child.

2 participants