counter: avoid the rotation timer in counter.Open

As observed in golang/go#68497, scheduling a rotation timer can break
runtime deadlock detection (all goroutines are asleep...). Furthermore,
for short-lived processes such as command line tools, there is no reason
to schedule a file rotation.

Therefore, change the default behavior of counter.Open not to rotate the
counter file, and introduce a new OpenAndRotate API to be used by gopls.

For golang/go#68497

Change-Id: I7929c2d622d15e36ca99036d8c7eac1eed8fabf5
Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/599075
Auto-Submit: Robert Findley <rfindley@google.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/counter/counter.go b/counter/counter.go
index ff727ad..fe2d0f6 100644
--- a/counter/counter.go
+++ b/counter/counter.go
@@ -83,8 +83,21 @@
 // If the telemetry mode is "off", Open is a no-op. Otherwise, it opens the
 // counter file on disk and starts to mmap telemetry counters to the file.
 // Open also persists any counters already created in the current process.
+//
+// Open should only be called from short-lived processes such as command line
+// tools. If your process is long-running, use [OpenAndRotate].
 func Open() {
-	counter.Open()
+	counter.Open(false)
+}
+
+// OpenAndRotate is like [Open], but also schedules a rotation of the counter
+// file when it expires.
+//
+// See golang/go#68497 for background on why [OpenAndRotate] is a separate API.
+//
+// TODO(rfindley): refactor Open and OpenAndRotate for Go 1.24.
+func OpenAndRotate() {
+	counter.Open(true)
 }
 
 // OpenDir prepares telemetry counters for recording to the file system, using
@@ -97,7 +110,7 @@
 	if telemetryDir != "" {
 		telemetry.Default = telemetry.NewDir(telemetryDir)
 	}
-	counter.Open()
+	counter.Open(false)
 }
 
 // CountFlags creates a counter for every flag that is set
diff --git a/counter/counter_test.go b/counter/counter_test.go
new file mode 100644
index 0000000..999f4dd
--- /dev/null
+++ b/counter/counter_test.go
@@ -0,0 +1,44 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package counter_test
+
+import (
+	"os"
+	"os/exec"
+	"testing"
+
+	"golang.org/x/telemetry/counter"
+	"golang.org/x/telemetry/internal/telemetry"
+	"golang.org/x/telemetry/internal/testenv"
+)
+
+const telemetryDirEnvVar = "_COUNTER_TEST_TELEMETRY_DIR"
+
+func TestMain(m *testing.M) {
+	if dir := os.Getenv(telemetryDirEnvVar); dir != "" {
+		// Run for TestOpenAPIMisuse.
+		telemetry.Default = telemetry.NewDir(dir)
+		counter.Open()
+		counter.OpenAndRotate() // should panic
+		os.Exit(0)
+	}
+	os.Exit(m.Run())
+}
+
+func TestOpenAPIMisuse(t *testing.T) {
+	testenv.SkipIfUnsupportedPlatform(t)
+
+	// Test that Open and OpenAndRotate cannot be used simultaneously.
+	exe, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+	}
+	cmd := exec.Command(exe)
+	cmd.Env = append(os.Environ(), telemetryDirEnvVar+"="+t.TempDir())
+
+	if err := cmd.Run(); err == nil {
+		t.Error("Failed to detect API misuse: no error from calling both Open and OpenAndRotate")
+	}
+}
diff --git a/counter/countertest/countertest.go b/counter/countertest/countertest.go
index dc8bb11..533f5e5 100644
--- a/counter/countertest/countertest.go
+++ b/counter/countertest/countertest.go
@@ -40,6 +40,9 @@
 	}
 	telemetry.Default = telemetry.NewDir(telemetryDir)
 
+	// TODO(rfindley): reinstate test coverage with counter rotation enabled.
+	// Before the [counter.Open] and [counter.OpenAndRotate] APIs were split,
+	// this called counter.Open (which rotated!).
 	counter.Open()
 	opened = true
 }
diff --git a/internal/counter/file.go b/internal/counter/file.go
index 56fa516..5df6dd7 100644
--- a/internal/counter/file.go
+++ b/internal/counter/file.go
@@ -357,27 +357,42 @@
 	return v, cleanup
 }
 
-var openOnce sync.Once
+var (
+	openOnce sync.Once
+	// rotating reports whether the call to Open had rotate = true.
+	//
+	// In golang/go#68497, we observed that file rotation can break runtime
+	// deadlock detection. To minimize the fix for 1.23, we are splitting the
+	// Open API into one version that rotates the counter file, and another that
+	// does not. The rotating variable guards against use of both APIs from the
+	// same process.
+	rotating bool
+)
 
 // Open associates counting with the defaultFile.
 // The returned function is for testing only, and should
 // be called after all Inc()s are finished, but before
 // any reports are generated.
 // (Otherwise expired count files will not be deleted on Windows.)
-func Open() func() {
+func Open(rotate bool) func() {
 	if telemetry.DisabledOnPlatform {
 		return func() {}
 	}
 	close := func() {}
 	openOnce.Do(func() {
+		rotating = rotate
 		if mode, _ := telemetry.Default.Mode(); mode == "off" {
 			// Don't open the file when telemetry is off.
 			defaultFile.err = ErrDisabled
 			// No need to clean up.
 			return
 		}
-		debugPrintf("Open")
-		defaultFile.rotate()
+		debugPrintf("Open(%v)", rotate)
+		if rotate {
+			defaultFile.rotate() // calls rotate1 and schedules a rotation
+		} else {
+			defaultFile.rotate1()
+		}
 		close = func() {
 			// Once this has been called, the defaultFile is no longer usable.
 			mf := defaultFile.current.Load()
@@ -388,6 +403,9 @@
 			mf.close()
 		}
 	})
+	if rotating != rotate {
+		panic("BUG: Open called with inconsistent values for 'rotate'")
+	}
 	return close
 }