Skip to content

Commit 27dc691

Browse files
author
alrex
authored
addressing concurrency issue in Span API (open-telemetry#1259)
Before this change, the following would cause unintended behaviour in the Span API's add_event, update_name and set_status methods: - thread A calls set_status, locks to check if a span has ended, and releases the lock - thread B obtains the lock and ends a span - thread A continues its call of set_status This change ensures that the update operations are done with the lock being held. It's done in a decorator, but that is completely optional.
1 parent ddf7eeb commit 27dc691

File tree

1 file changed

+33
-48
lines changed
  • opentelemetry-sdk/src/opentelemetry/sdk/trace

1 file changed

+33
-48
lines changed

opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,17 @@ def _create_immutable_attributes(attributes):
371371
return MappingProxyType(attributes.copy() if attributes else {})
372372

373373

374+
def _check_span_ended(func):
375+
def wrapper(self, *args, **kwargs):
376+
with self._lock: # pylint: disable=protected-access
377+
if self.end_time is not None:
378+
logger.warning("Calling %s on an ended span.", func.__name__)
379+
return
380+
func(self, *args, **kwargs)
381+
382+
return wrapper
383+
384+
374385
class Span(trace_api.Span):
375386
"""See `opentelemetry.trace.Span`.
376387
@@ -560,19 +571,18 @@ def get_span_context(self):
560571
return self.context
561572

562573
def set_attribute(self, key: str, value: types.AttributeValue) -> None:
563-
with self._lock:
564-
if not self.is_recording():
565-
return
566-
has_ended = self.end_time is not None
567-
if has_ended:
568-
logger.warning("Setting attribute on ended span.")
574+
if not _is_valid_attribute_value(value):
569575
return
570576

571577
if not key:
572578
logger.warning("invalid key (empty or null)")
573579
return
574580

575-
if _is_valid_attribute_value(value):
581+
with self._lock:
582+
if self.end_time is not None:
583+
logger.warning("Setting attribute on ended span.")
584+
return
585+
576586
# Freeze mutable sequences defensively
577587
if isinstance(value, MutableSequence):
578588
value = tuple(value)
@@ -582,18 +592,10 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None:
582592
except ValueError:
583593
logger.warning("Byte attribute could not be decoded.")
584594
return
585-
with self._lock:
586-
self.attributes[key] = value
595+
self.attributes[key] = value
587596

597+
@_check_span_ended
588598
def _add_event(self, event: EventBase) -> None:
589-
with self._lock:
590-
if not self.is_recording():
591-
return
592-
has_ended = self.end_time is not None
593-
594-
if has_ended:
595-
logger.warning("Calling add_event() on an ended span.")
596-
return
597599
self.events.append(event)
598600

599601
def add_event(
@@ -618,56 +620,39 @@ def start(
618620
parent_context: Optional[context_api.Context] = None,
619621
) -> None:
620622
with self._lock:
621-
if not self.is_recording():
623+
if self.start_time is not None:
624+
logger.warning("Calling start() on a started span.")
622625
return
623-
has_started = self.start_time is not None
624-
if not has_started:
625-
self._start_time = (
626-
start_time if start_time is not None else time_ns()
627-
)
628-
if has_started:
629-
logger.warning("Calling start() on a started span.")
630-
return
626+
self._start_time = (
627+
start_time if start_time is not None else time_ns()
628+
)
629+
631630
self.span_processor.on_start(self, parent_context=parent_context)
632631

633632
def end(self, end_time: Optional[int] = None) -> None:
634633
with self._lock:
635-
if not self.is_recording():
636-
return
637634
if self.start_time is None:
638635
raise RuntimeError("Calling end() on a not started span.")
639-
has_ended = self.end_time is not None
640-
if not has_ended:
641-
if self.status is None:
642-
self.status = Status(canonical_code=StatusCanonicalCode.OK)
636+
if self.end_time is not None:
637+
logger.warning("Calling end() on an ended span.")
638+
return
643639

644-
self._end_time = (
645-
end_time if end_time is not None else time_ns()
646-
)
640+
if self.status is None:
641+
self.status = Status(canonical_code=StatusCanonicalCode.OK)
647642

648-
if has_ended:
649-
logger.warning("Calling end() on an ended span.")
650-
return
643+
self._end_time = end_time if end_time is not None else time_ns()
651644

652645
self.span_processor.on_end(self)
653646

647+
@_check_span_ended
654648
def update_name(self, name: str) -> None:
655-
with self._lock:
656-
has_ended = self.end_time is not None
657-
if has_ended:
658-
logger.warning("Calling update_name() on an ended span.")
659-
return
660649
self.name = name
661650

662651
def is_recording(self) -> bool:
663652
return True
664653

654+
@_check_span_ended
665655
def set_status(self, status: trace_api.Status) -> None:
666-
with self._lock:
667-
has_ended = self.end_time is not None
668-
if has_ended:
669-
logger.warning("Calling set_status() on an ended span.")
670-
return
671656
self.status = status
672657

673658
def __exit__(

0 commit comments

Comments
 (0)