event: detect failure to clone

If the user of a Builder forgets to call Clone and delivers a second
event, panic with a helpful message.

This does seem to slow down the no-op case:

> benchstat2 -C . bench.log
name                  old time/op  new time/op  delta
LogEventNoExporter-8   621ns ± 3%   613ns ± 1%   -1.22%  (p=0.000 n=19+18)
LogEventNoop-8        1.84µs ± 1%  2.04µs ± 2%  +10.83%  (p=0.000 n=17+19)
LogEventDiscard-8     10.7µs ± 3%  10.6µs ± 1%   -0.86%  (p=0.017 n=19+19)
LogEventfDiscard-8    16.4µs ± 3%  16.6µs ± 3%   +1.12%  (p=0.001 n=18+19)
TraceEventNoop-8      40.7µs ± 2%  41.0µs ± 3%     ~     (p=0.119 n=18+20)
MetricEventNoop-8     18.2µs ± 2%  18.3µs ± 3%     ~     (p=0.703 n=20+20)
Logrus-8              53.7µs ± 5%  53.8µs ± 3%     ~     (p=0.779 n=20+20)
Logrusf-8             44.6µs ± 3%  45.2µs ± 5%   +1.20%  (p=0.046 n=19+20)
Baseline-8             278ns ± 2%   278ns ± 1%     ~     (p=0.642 n=18+17)
LogStdlib-8           8.41µs ± 4%  8.39µs ± 2%     ~     (p=0.767 n=19+19)
LogPrintf-8           13.0µs ± 2%  13.0µs ± 2%     ~     (p=0.075 n=19+20)
LogZap-8              19.6µs ± 3%  19.6µs ± 3%     ~     (p=0.390 n=19+19)
LogZapf-8             21.4µs ± 2%  21.5µs ± 2%     ~     (p=0.208 n=19+17)
LogZerolog-8          7.46µs ± 2%  7.50µs ± 2%     ~     (p=0.131 n=18+20)
LogZerologf-8         12.5µs ± 2%  12.6µs ± 3%     ~     (p=0.430 n=18+19)

Change-Id: I94c571685c87681b64ce9a349b1e037ccdb4a888
Reviewed-on: https://go-review.googlesource.com/c/exp/+/323849
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/event/builder.go b/event/builder.go
index 7a5f597..dda25f0 100644
--- a/event/builder.go
+++ b/event/builder.go
@@ -10,13 +10,15 @@
 	"context"
 	"fmt"
 	"sync"
+	"sync/atomic"
 	"time"
 )
 
 // Builder is a fluent builder for construction of new events.
 type Builder struct {
-	ctx  context.Context
-	data *builder
+	ctx       context.Context
+	data      *builder
+	builderID uint64 // equals data.id if all is well
 }
 
 // preallocateLabels controls the space reserved for labels in a builder.
@@ -30,24 +32,31 @@
 	exporter *Exporter
 	Event    Event
 	labels   [preallocateLabels]Label
+	id       uint64
 }
 
 var builderPool = sync.Pool{New: func() interface{} { return &builder{} }}
 
 // To initializes a builder from the values stored in a context.
 func To(ctx context.Context) Builder {
-	return Builder{ctx: ctx, data: newBuilder(ctx)}
-}
-
-func newBuilder(ctx context.Context) *builder {
+	b := Builder{ctx: ctx}
 	exporter, parent := FromContext(ctx)
 	if exporter == nil {
-		return nil
+		return b
 	}
+	b.data = allocBuilder()
+	b.builderID = b.data.id
+	b.data.exporter = exporter
+	b.data.Event.Labels = b.data.labels[:0]
+	b.data.Event.Parent = parent
+	return b
+}
+
+var builderID uint64 // atomic
+
+func allocBuilder() *builder {
 	b := builderPool.Get().(*builder)
-	b.exporter = exporter
-	b.Event.Labels = b.labels[:0]
-	b.Event.Parent = parent
+	b.id = atomic.AddUint64(&builderID, 1)
 	return b
 }
 
@@ -57,8 +66,11 @@
 	if b.data == nil {
 		return b
 	}
-	clone := Builder{ctx: b.ctx, data: builderPool.Get().(*builder)}
+	bb := allocBuilder()
+	bbid := bb.id
+	clone := Builder{ctx: b.ctx, data: bb, builderID: bb.id}
 	*clone.data = *b.data
+	clone.data.id = bbid
 	if len(b.data.Event.Labels) == 0 || &b.data.labels[0] == &b.data.Event.Labels[0] {
 		clone.data.Event.Labels = clone.data.labels[:len(b.data.Event.Labels)]
 	} else {
@@ -71,6 +83,7 @@
 // With adds a new label to the event being constructed.
 func (b Builder) With(label Label) Builder {
 	if b.data != nil {
+		checkValid(b.data, b.builderID)
 		b.data.Event.Labels = append(b.data.Event.Labels, label)
 	}
 	return b
@@ -81,6 +94,7 @@
 	if b.data == nil || len(labels) == 0 {
 		return b
 	}
+	checkValid(b.data, b.builderID)
 	if len(b.data.Event.Labels) == 0 {
 		b.data.Event.Labels = labels
 	} else {
@@ -91,6 +105,7 @@
 
 func (b Builder) At(t time.Time) Builder {
 	if b.data != nil {
+		checkValid(b.data, b.builderID)
 		b.data.Event.At = t
 	}
 	return b
@@ -98,6 +113,7 @@
 
 func (b Builder) Namespace(ns string) Builder {
 	if b.data != nil {
+		checkValid(b.data, b.builderID)
 		b.data.Event.Namespace = ns
 	}
 	return b
@@ -108,7 +124,8 @@
 	if b.data == nil {
 		return
 	}
-	if b.data.exporter.handler != nil {
+	checkValid(b.data, b.builderID)
+	if b.data.exporter.loggingEnabled() {
 		b.data.exporter.mu.Lock()
 		defer b.data.exporter.mu.Unlock()
 		b.data.Event.Labels = append(b.data.Event.Labels, Message.Of(message))
@@ -124,6 +141,7 @@
 	if b.data == nil {
 		return
 	}
+	checkValid(b.data, b.builderID)
 	if b.data.exporter.loggingEnabled() {
 		message := fmt.Sprintf(template, args...)
 		// Duplicate code from Log so Exporter.deliver's invocation of runtime.Callers is correct.
@@ -141,6 +159,7 @@
 	if b.data == nil {
 		return
 	}
+	checkValid(b.data, b.builderID)
 	if b.data.exporter.metricsEnabled() {
 		b.data.exporter.mu.Lock()
 		defer b.data.exporter.mu.Unlock()
@@ -156,6 +175,7 @@
 	if b.data == nil {
 		return
 	}
+	checkValid(b.data, b.builderID)
 	if b.data.exporter.annotationsEnabled() {
 		b.data.exporter.mu.Lock()
 		defer b.data.exporter.mu.Unlock()
@@ -170,6 +190,7 @@
 	if b.data == nil {
 		return
 	}
+	checkValid(b.data, b.builderID)
 	if b.data.exporter.tracingEnabled() {
 		b.data.exporter.mu.Lock()
 		defer b.data.exporter.mu.Unlock()
@@ -182,6 +203,7 @@
 
 // Event returns a copy of the event currently being built.
 func (b Builder) Event() *Event {
+	checkValid(b.data, b.builderID)
 	clone := b.data.Event
 	if len(b.data.Event.Labels) > 0 {
 		clone.Labels = make([]Label, len(b.data.Event.Labels))
@@ -204,6 +226,7 @@
 	if b.data == nil {
 		return b.ctx, func() {}
 	}
+	checkValid(b.data, b.builderID)
 	ctx := b.ctx
 	end := func() {}
 	if b.data.exporter.tracingEnabled() {
@@ -215,7 +238,8 @@
 		b.data.exporter.prepare(&b.data.Event)
 		// create the end builder
 		eb := Builder{}
-		eb.data = builderPool.Get().(*builder)
+		eb.data = allocBuilder()
+		eb.builderID = eb.data.id
 		eb.data.exporter = b.data.exporter
 		eb.data.Event.Parent = traceID
 		// and now deliver the start event
@@ -228,3 +252,9 @@
 	b.done()
 	return ctx, end
 }
+
+func checkValid(b *builder, wantID uint64) {
+	if b.exporter == nil || b.id != wantID {
+		panic("Builder already delivered an event; missing call to Clone")
+	}
+}
diff --git a/event/builder_test.go b/event/builder_test.go
index 8e01474..c7c629d 100644
--- a/event/builder_test.go
+++ b/event/builder_test.go
@@ -9,6 +9,7 @@
 import (
 	"context"
 	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -22,7 +23,7 @@
 		labels = append(labels, keys.Int(fmt.Sprintf("l%d", i)).Of(i))
 	}
 
-	ctx := event.WithExporter(context.Background(), event.NewExporter(nil, nil))
+	ctx := event.WithExporter(context.Background(), event.NewExporter(noopHandler{}, nil))
 	b1 := event.To(ctx)
 	b1.With(labels[0]).With(labels[1])
 	check(t, b1, labels[:2])
@@ -92,3 +93,48 @@
 		t.t.Fatal("Start context not passed to End")
 	}
 }
+
+func TestFailToClone(t *testing.T) {
+	ctx := event.WithExporter(context.Background(), event.NewExporter(noopHandler{}, nil))
+
+	catch := func(f func()) {
+		defer func() {
+			r := recover()
+			if r == nil {
+				t.Error("expected panic, did not get one")
+				return
+			}
+			got, ok := r.(string)
+			if !ok || !strings.Contains(got, "Clone") {
+				t.Errorf("got panic(%v), want string with 'Clone'", r)
+			}
+		}()
+
+		f()
+	}
+
+	catch(func() {
+		b1 := event.To(ctx)
+		b1.Log("msg1")
+		// Reuse of Builder without Clone; b1.data has been cleared.
+		b1.Log("msg2")
+	})
+
+	catch(func() {
+		b1 := event.To(ctx)
+		b1.Log("msg1")
+		_ = event.To(ctx) // re-allocate the builder
+		// b1.data is populated, but with the wrong information.
+		b1.Log("msg2")
+	})
+}
+
+type noopHandler struct{}
+
+func (noopHandler) Log(ctx context.Context, ev *event.Event)      {}
+func (noopHandler) Metric(ctx context.Context, ev *event.Event)   {}
+func (noopHandler) Annotate(ctx context.Context, ev *event.Event) {}
+func (noopHandler) End(ctx context.Context, ev *event.Event)      {}
+func (noopHandler) Start(ctx context.Context, ev *event.Event) context.Context {
+	return ctx
+}
diff --git a/event/export.go b/event/export.go
index 85e8729..e15cd2d 100644
--- a/event/export.go
+++ b/event/export.go
@@ -60,6 +60,9 @@
 // NewExporter creates an Exporter using the supplied handler and options.
 // Event delivery is serialized to enable safe atomic handling.
 func NewExporter(handler Handler, opts *ExporterOptions) *Exporter {
+	if handler == nil {
+		panic("handler must not be nil")
+	}
 	e := &Exporter{handler: handler}
 	if opts != nil {
 		e.opts = *opts