Skip to content

[v2] fix: implement single-call enforcement for Span.Finish#3334

Merged
darccio merged 4 commits into
v2-devfrom
dario.castane/finish-once
Mar 26, 2025
Merged

[v2] fix: implement single-call enforcement for Span.Finish#3334
darccio merged 4 commits into
v2-devfrom
dario.castane/finish-once

Conversation

@darccio

@darccio darccio commented Mar 24, 2025

Copy link
Copy Markdown
Member

What does this PR do?

This PR adds a sentinel value to ensure that calling Finish on any given span will only trigger the finishing flow once.

Motivation

A potential source of data races is when a user misuses the tracer, as finishing the same span multiple times by calling Finish more than once, concurrently or serially in a tight loop.

Reviewer's Checklist

  • Changed code has unit tests for its functionality at or near 100% coverage.
  • System-Tests covering this feature have been added and enabled with the va.b.c-dev version tag.
  • There is a benchmark for any new code, or changes to existing code.
  • If this interacts with the agent in a new way, a system test has been added.
  • Add an appropriate team label so this PR gets put in the right place for the release notes.
  • Non-trivial go.mod changes, e.g. adding new modules, are reviewed by @DataDog/dd-trace-go-guild.
  • For internal contributors, a matching PR should be created to the v2-dev branch and reviewed by @DataDog/apm-go.

Unsure? Have a question? Request a review!

@darccio
darccio requested a review from a team as a code owner March 24, 2025 12:00
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Mar 24, 2025

Copy link
Copy Markdown

Datadog Report

Branch report: dario.castane/finish-once
Commit report: 9bdee17
Test service: dd-trace-go

✅ 0 Failed, 4474 Passed, 66 Skipped, 27m 14.85s Total Time

@pr-commenter

pr-commenter Bot commented Mar 24, 2025

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2025-03-25 18:27:02

Comparing candidate commit 179f955 in PR branch dario.castane/finish-once with baseline commit 8cc7cca in branch v2-dev.

Found 2 performance improvements and 0 performance regressions! Performance is the same for 54 metrics, 0 unstable metrics.

scenario:BenchmarkSetTagString-24

  • 🟩 execution_time [-9.893ns; -6.947ns] or [-8.199%; -5.757%]

scenario:BenchmarkSetTagStringer-24

  • 🟩 execution_time [-11.860ns; -9.000ns] or [-7.825%; -5.938%]

@kakkoyun kakkoyun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM 🚀

Only one nit.

Comment thread ddtrace/tracer/span.go Outdated

pprofCtxActive context.Context `msg:"-"` // contains pprof.WithLabel labels to tell the profiler more about this span
pprofCtxRestore context.Context `msg:"-"` // contains pprof.WithLabel labels of the parent span (if any) that need to be restored when this span finishes
sentinel atomic.Uint32 `msg:"-"` // sentinel value to detect if Span.Finish has been called to only finish the span once

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if we can do better with the naming? Or maybe we can add more explanation.

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.

agree with this, could this be called finished instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I changed it to finishGuard.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@rarguelloF finished already exists.

darccio added 3 commits March 25, 2025 10:38
Updated the testAsyncSpanRace function to better simulate concurrent Finish calls on spans. Introduced a WaitGroup to synchronize concurrent finishes and ensure proper execution order, addressing potential data races during testing.

@kakkoyun kakkoyun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Comment thread ddtrace/tracer/span.go
Comment thread ddtrace/tracer/spancontext_test.go
@darccio
darccio merged commit 57473e2 into v2-dev Mar 26, 2025
@darccio
darccio deleted the dario.castane/finish-once branch March 26, 2025 09:18
@hannahkm hannahkm mentioned this pull request Apr 11, 2025
8 tasks
@felixge

felixge commented Apr 14, 2025

Copy link
Copy Markdown
Member

I am not sure if this is a complete fix. Everything in s.Finish assumes that s.Lock must be held when modifying s. However, we're still allowing serializeSpanEvents and serializeSpanLinksInMeta to modify s.meta without holding the lock. I.e. a call to s.SetTag could still clash with these operations and cause crashes.

Why didn't we consider acquiring the s.Lock inside of serializeSpanEvents and serializeSpanLinksInMeta? TBH, I think all of the span link and event functions (s.AddLink and s.AddEvent) are prone to race conditions right now. We should consider rolling back those PRs or doing a deep fix/audit ASAP.

@darccio

darccio commented Apr 14, 2025

Copy link
Copy Markdown
Member Author

@felixge The guard should be enough. It's an atomic value. Two concurrent calls to s.Finish should end with one going forward and the other returning.

Regarding considering acquiring the s.Lock, we tried that but it caused deadlocks. We tried to fix them, but found a roadblock on sampling code that was too complex - required a large refactor - to fix properly in a short span.

@felixge

felixge commented Apr 14, 2025

Copy link
Copy Markdown
Member

The guard should be enough.

The guard is only enough for the double Finish issue. It's not enough for a Finish call concurrent with a SetTag call (or a number of other span modifying calls). The span API is designed to be concurrency safe even under incorrect usage. This assumption no longer holds in various places right now. This is very dangerous IMO.

@kakkoyun

Copy link
Copy Markdown
Member

You’re right to raise this concern. We worked on this together with @darccio and evaluated a few different approaches. In the end, we opted to keep the changes minimal and targeted to avoid introducing broader side effects.

That said, we agree this isn’t a complete fix. The current implementation does have inconsistencies around locking, especially in how nested functions operate on shared state. It’s becoming increasingly difficult to track which paths require s.Lock, and the recent incident clearly highlighted the risks.

We should prioritize a deeper audit of the span code—especially AddEvent, AddLink, and serialization logic—and move toward a more consistent and maintainable locking strategy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants