Skip to content

Commit 513eb10

Browse files
authored
fix: Clone scope.Context in multiple places to avoid concurrent reads/writes (#638)
1 parent 97a00a4 commit 513eb10

File tree

6 files changed

+86
-7
lines changed

6 files changed

+86
-7
lines changed

_examples/basic/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
//
44
// Try it by running:
55
//
6-
// go run main.go
7-
// go run main.go https://sentry.io
8-
// go run main.go bad-url
6+
// go run main.go
7+
// go run main.go https://sentry.io
8+
// go run main.go bad-url
99
//
1010
// To actually report events to Sentry, set the DSN either by editing the
1111
// appropriate line below or setting the environment variable SENTRY_DSN to

_examples/recover-repanic/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//
66
// Try it by running:
77
//
8-
// go run main.go
8+
// go run main.go
99
//
1010
// To actually report events to Sentry, set the DSN either by editing the
1111
// appropriate line below or setting the environment variable SENTRY_DSN to

scope.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ func (scope *Scope) Clone() *Scope {
287287
clone.tags[key] = value
288288
}
289289
for key, value := range scope.contexts {
290-
clone.contexts[key] = value
290+
clone.contexts[key] = cloneContext(value)
291291
}
292292
for key, value := range scope.extra {
293293
clone.extra[key] = value
@@ -350,7 +350,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
350350

351351
// Ensure we are not overwriting event fields
352352
if _, ok := event.Contexts[key]; !ok {
353-
event.Contexts[key] = value
353+
event.Contexts[key] = cloneContext(value)
354354
}
355355
}
356356
}
@@ -404,3 +404,16 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
404404

405405
return event
406406
}
407+
408+
// cloneContext returns a new context with keys and values copied from the passed one.
409+
//
410+
// Note: a new Context (map) is returned, but the function does NOT do
411+
// a proper deep copy: if some context values are pointer types (e.g. maps),
412+
// they won't be properly copied.
413+
func cloneContext(c Context) Context {
414+
res := Context{}
415+
for k, v := range c {
416+
res[k] = v
417+
}
418+
return res
419+
}

scope_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,3 +618,25 @@ func TestEventProcessorsAddEventProcessor(t *testing.T) {
618618
t.Error("event should be dropped")
619619
}
620620
}
621+
622+
func TestCloneContext(t *testing.T) {
623+
context := Context{
624+
"key1": "value1",
625+
"key2": []string{"s1", "s2"},
626+
}
627+
628+
clone := cloneContext(context)
629+
630+
// Value-wise they should be identical
631+
assertEqual(t, context, clone)
632+
// ..but it shouldn't be the same map
633+
if &context == &clone {
634+
t.Error("original and cloned context should be different objects")
635+
}
636+
637+
sliceOriginal := context["key2"].([]string)
638+
sliceClone := clone["key2"].([]string)
639+
if &sliceOriginal[0] != &sliceClone[0] {
640+
t.Error("complex values are not supposed to be copied")
641+
}
642+
}

tracing.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ func (s *Span) toEvent() *Event {
535535

536536
contexts := map[string]Context{}
537537
for k, v := range s.contexts {
538-
contexts[k] = v
538+
contexts[k] = cloneContext(v)
539539
}
540540
contexts["trace"] = s.traceContext().Map()
541541

tracing_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"math"
1111
"net/http"
1212
"reflect"
13+
"sync"
1314
"testing"
1415
"time"
1516

@@ -891,6 +892,49 @@ func TestDeprecatedSpanOptionTransctionSource(t *testing.T) {
891892
StartSpan(context.Background(), "op", TransctionSource("src"))
892893
}
893894

895+
// This test checks that there are no concurrent reads/writes to
896+
// substructures in scope.contexts.
897+
// See https://github.com/getsentry/sentry-go/issues/570 for more details.
898+
func TestConcurrentContextAccess(t *testing.T) {
899+
ctx := NewTestContext(ClientOptions{
900+
EnableTracing: true,
901+
TracesSampleRate: 1,
902+
})
903+
hub := GetHubFromContext(ctx)
904+
905+
const writersNum = 200
906+
907+
// Unbuffered channel, writing to it will be block if nobody reads
908+
c := make(chan *Span)
909+
910+
// Start writers
911+
for i := 0; i < writersNum; i++ {
912+
go func() {
913+
transaction := StartTransaction(ctx, "test")
914+
c <- transaction
915+
hub.Scope().SetContext("device", Context{"test": "bla"})
916+
}()
917+
}
918+
919+
var wg sync.WaitGroup
920+
wg.Add(writersNum)
921+
922+
// Start readers
923+
go func() {
924+
for transaction := range c {
925+
transaction := transaction
926+
go func() {
927+
defer wg.Done()
928+
// While finalizing every transaction, scope.Contexts and Event.Contexts fields
929+
// will be accessed, e.g. in environmentIntegration.processor()
930+
transaction.Finish()
931+
}()
932+
}
933+
}()
934+
935+
wg.Wait()
936+
}
937+
894938
func TestAdjustingTransactionSourceBeforeSending(t *testing.T) {
895939
tests := []struct {
896940
name string

0 commit comments

Comments
 (0)