Skip to content

Commit f51ecf0

Browse files
committed
all: store an asof date in mode, and use it to guard uploads
As described in golang/go#63218, there were various modes where we could upload data from before gotelemetry was enabled, or before the week-long grace period had expired. Fix this rather explicitly by recording the date that telemetry was enabled in the mode file, and using this to guard uploads. There are several places where the logic could be tightened, but I opted for defensiveness given the late nature of this change. For golang/go#63218 Change-Id: I9d5f5c5ec9908345697e0e0c21859ad5e34605c6 Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/530995 Reviewed-by: Hyang-Ah Hana Kim <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Peter Weinberger <[email protected]>
1 parent a3c1704 commit f51ecf0

File tree

11 files changed

+200
-29
lines changed

11 files changed

+200
-29
lines changed

cmd/gotelemetry/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ func runView(_ []string) {
209209
}
210210

211211
func runEnv(_ []string) {
212-
fmt.Println("mode:", it.Mode())
212+
m, t := it.Mode()
213+
fmt.Printf("mode: %s %s\n", m, t)
213214
fmt.Println()
214215
fmt.Println("modefile:", it.ModeFile)
215216
fmt.Println("localdir:", it.LocalDir)

internal/counter/file.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ func fileValidity(now time.Time) (int, error) {
232232
if err != nil {
233233
return 0, err
234234
}
235+
// TODO(rfindley): once we can rely on telemetry users setting a mode date,
236+
// we don't need to implement this heuristic about the existing of json
237+
// files.
235238
for _, f := range fi {
236239
if strings.HasSuffix(f.Name(), ".json") {
237240
return int(incr), nil

internal/telemetry/mode.go

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"path/filepath"
1212
"strings"
13+
"time"
1314
)
1415

1516
// The followings are the process' default Settings.
@@ -42,11 +43,21 @@ func init() {
4243

4344
// SetMode updates the telemetry mode with the given mode.
4445
// Acceptable values for mode are "on" or "off".
46+
//
47+
// SetMode always writes the mode file, and explicitly records the date at
48+
// which the modefile was updated. This means that calling SetMode with "on"
49+
// effectively resets the timeout before the next telemetry report is uploaded.
4550
func SetMode(mode string) error {
4651
return ModeFile.SetMode(mode)
4752
}
4853

4954
func (m ModeFilePath) SetMode(mode string) error {
55+
return m.SetModeAsOf(mode, time.Now())
56+
}
57+
58+
// SetModeAsOf is like SetMode, but accepts an explicit time to use to
59+
// back-date the mode state. This exists only for testing purposes.
60+
func (m ModeFilePath) SetModeAsOf(mode string, asofTime time.Time) error {
5061
mode = strings.TrimSpace(mode)
5162
switch mode {
5263
case "on", "off":
@@ -60,25 +71,47 @@ func (m ModeFilePath) SetMode(mode string) error {
6071
if err := os.MkdirAll(filepath.Dir(fname), 0755); err != nil {
6172
return fmt.Errorf("cannot create a telemetry mode file: %w", err)
6273
}
63-
data := []byte(mode)
74+
75+
asof := asofTime.UTC().Format("2006-01-02")
76+
// Defensively guarantee that we can parse the asof time.
77+
if _, err := time.Parse("2006-01-02", asof); err != nil {
78+
return fmt.Errorf("internal error: invalid mode date %q: %v", asof, err)
79+
}
80+
81+
data := []byte(mode + " " + asof)
6482
return os.WriteFile(fname, data, 0666)
6583
}
6684

67-
// Mode returns the current telemetry mode.
68-
func Mode() string {
85+
// Mode returns the current telemetry mode, as well as the time that the mode
86+
// was effective.
87+
//
88+
// If there is no effective time, the second result is the zero time.
89+
func Mode() (string, time.Time) {
6990
return ModeFile.Mode()
7091
}
7192

72-
func (m ModeFilePath) Mode() string {
93+
func (m ModeFilePath) Mode() (string, time.Time) {
7394
fname := string(m)
7495
if fname == "" {
75-
return "off" // it's likely LocalDir/UploadDir are empty too. Turn off telemetry.
96+
return "off", time.Time{} // it's likely LocalDir/UploadDir are empty too. Turn off telemetry.
7697
}
7798
data, err := os.ReadFile(fname)
7899
if err != nil {
79-
return "off" // default
100+
return "off", time.Time{} // default
80101
}
81102
mode := string(data)
82103
mode = strings.TrimSpace(mode)
83-
return mode
104+
105+
// Forward compatibility for https://go.dev/issue/63142#issuecomment-1734025130
106+
//
107+
// If the modefile contains a date, return it.
108+
if idx := strings.Index(mode, " "); idx >= 0 {
109+
d, err := time.Parse("2006-01-02", mode[idx+1:])
110+
if err != nil {
111+
d = time.Time{}
112+
}
113+
return mode[:idx], d
114+
}
115+
116+
return mode, time.Time{}
84117
}

internal/telemetry/mode_test.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"path/filepath"
1212
"testing"
13+
"time"
1314
)
1415

1516
func TestTelemetryDefault(t *testing.T) {
@@ -27,6 +28,7 @@ func TestTelemetryDefault(t *testing.T) {
2728
}
2829
}
2930
}
31+
3032
func TestTelemetryModeWithNoModeConfig(t *testing.T) {
3133
tmp := t.TempDir()
3234
tests := []struct {
@@ -37,14 +39,14 @@ func TestTelemetryModeWithNoModeConfig(t *testing.T) {
3739
{"", "off"},
3840
}
3941
for _, tt := range tests {
40-
if got := tt.modefile.Mode(); got != tt.want {
42+
if got, _ := tt.modefile.Mode(); got != tt.want {
4143
t.Logf("Mode file: %q", tt.modefile)
4244
t.Errorf("Mode() = %v, want %v", got, tt.want)
4345
}
4446
}
4547
}
4648

47-
func TestTelemetryMode(t *testing.T) {
49+
func TestSetMode(t *testing.T) {
4850
tests := []struct {
4951
in string
5052
wantErr bool // want error when setting.
@@ -68,9 +70,36 @@ func TestTelemetryMode(t *testing.T) {
6870
if setErr != nil {
6971
return
7072
}
71-
if got := modefile.Mode(); got != tt.in {
73+
if got, _ := modefile.Mode(); got != tt.in {
7274
t.Errorf("LookupMode() = %q, want %q", got, tt.in)
7375
}
7476
})
7577
}
7678
}
79+
80+
func TestMode(t *testing.T) {
81+
tests := []struct {
82+
in string
83+
wantMode string
84+
wantTime time.Time
85+
}{
86+
{"on", "on", time.Time{}},
87+
{"on 2023-09-26", "on", time.Date(2023, time.September, 26, 0, 0, 0, 0, time.UTC)},
88+
{"off", "off", time.Time{}},
89+
{"local", "local", time.Time{}},
90+
}
91+
tmp := t.TempDir()
92+
for i, tt := range tests {
93+
t.Run("mode="+tt.in, func(t *testing.T) {
94+
fname := filepath.Join(tmp, fmt.Sprintf("modefile%d", i))
95+
if err := os.WriteFile(fname, []byte(tt.in), 0666); err != nil {
96+
t.Fatal(err)
97+
}
98+
// Note: the checks below intentionally do not use time.Equal:
99+
// we want this exact representation of time.
100+
if gotMode, gotTime := ModeFilePath(fname).Mode(); gotMode != tt.wantMode || gotTime != tt.wantTime {
101+
t.Errorf("ModeFilePath(contents=%s).Mode() = %q, %v, want %q, %v", tt.in, gotMode, gotTime, tt.wantMode, tt.wantTime)
102+
}
103+
})
104+
}
105+
}

internal/upload/dates_test.go

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
// doing them subtest() checks the correctness of a single upload to the local server.
2626
func TestDates(t *testing.T) {
2727
skipIfUnsupportedPlatform(t)
28-
setup(t)
28+
setup(t, "2019-12-01") // back-date the telemetry acceptance
2929
defer restore()
3030
thisInstant = future(0)
3131
finished := counter.Open()
@@ -45,9 +45,40 @@ func TestDates(t *testing.T) {
4545
subtest(t) // do and check a report
4646

4747
// create a lot of tests, and run them
48-
const today = "2020-01-24" // same for each test
48+
const today = "2020-01-24"
4949
const yesterday = "2020-01-23"
5050
tests := []Test{ // each date must be different to subvert the parse cache
51+
{ // test that existing counters and ready files are not uploaded if telemetry was recently enabled.
52+
name: "beforefirstupload",
53+
today: "2019-12-04",
54+
date: "2019-12-03",
55+
begins: "2019-12-01",
56+
ends: "2019-12-03",
57+
readys: []string{"2019-12-02"},
58+
wantLocal: 1,
59+
wantReady: 1,
60+
},
61+
{ // test that existing counters and ready files are not uploaded if telemetry was recently enabled.
62+
name: "oktoupload",
63+
today: "2019-12-10",
64+
date: "2019-12-09",
65+
begins: "2019-12-02",
66+
ends: "2019-12-09",
67+
readys: []string{"2019-12-07"},
68+
wantLocal: 1,
69+
wantReady: 1, // ready report is before the first upload time
70+
wantUploadeds: 1, // ...but the new report is uploaded
71+
},
72+
{ // test that existing counters and ready files are not uploaded if telemetry was recently enabled.
73+
name: "uploadnewreport",
74+
today: "2019-12-14",
75+
date: "2019-12-12",
76+
begins: "2019-12-04",
77+
ends: "2019-12-12",
78+
readys: []string{"2019-12-13"},
79+
wantLocal: 1,
80+
wantUploadeds: 2, // ready report was uploaded
81+
},
5182
{ // test that an old countfile is removed and no reports generated
5283
name: "oldcountfile",
5384
today: today,
@@ -106,7 +137,8 @@ func TestDates(t *testing.T) {
106137
wantReady: 1, // existing premature ready file
107138
},
108139
}
109-
used := make(map[string]string) // dates have to be different, names should be
140+
// Used maps ensures that test cases are for distinct dates.
141+
used := make(map[string]string)
110142
for _, tx := range tests {
111143
if used[tx.name] != "" || used[tx.date] != "" {
112144
t.Errorf("test %s reusing name or date. name:%s, date:%s",
@@ -172,17 +204,23 @@ func createUploadConfig(cfilename string) (*telemetry.UploadConfig, string) {
172204
return &ans, strings.Join(flds[:4], "-") + "-"
173205
}
174206

175-
// a single test
207+
// Test is a single test.
208+
//
209+
// All dates are in YYYY-MM-DD format.
176210
type Test struct {
177-
name string
178-
today string
211+
name string // the test name; only used for descriptive output
212+
today string // the date of the fake upload
179213
// count file
180-
date string // in the name
181-
begins, ends string // in the metadata
182-
// reports in the local dir (entries are YYYY-MM-DD)
214+
date string // the date in of the upload file name; must be unique among tests
215+
begins, ends string // the begin and end date stored in the counter metadata
216+
217+
// Dates of load reports in the local dir.
183218
locals []string
219+
220+
// Dates of upload reports in the local dir.
184221
readys []string
185-
// already uploaded
222+
223+
// Dates of reports already uploaded.
186224
uploads []string
187225

188226
// number of expected results

internal/upload/findwork.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"os"
99
"path/filepath"
1010
"strings"
11+
12+
it "golang.org/x/telemetry/internal/telemetry"
1113
)
1214

1315
// files to handle
@@ -29,6 +31,9 @@ func findWork(localdir, uploaddir string) work {
2931
logger.Printf("could not read %s, progress impossible (%v)", localdir, err)
3032
return ans
3133
}
34+
35+
mode, asof := it.Mode()
36+
3237
// count files end in .v1.count
3338
// reports end in .json. If they are not to be uploaded they
3439
// start with local.
@@ -41,10 +46,29 @@ func findWork(localdir, uploaddir string) work {
4146
ans.countfiles = append(ans.countfiles, fname)
4247
} else if strings.HasPrefix(fi.Name(), "local.") {
4348
// skip
44-
} else if strings.HasSuffix(fi.Name(), ".json") {
45-
ans.readyfiles = append(ans.readyfiles, filepath.Join(localdir, fi.Name()))
49+
} else if strings.HasSuffix(fi.Name(), ".json") && mode == "on" {
50+
// Collect reports that are ready for upload.
51+
reportDate := uploadReportDate(fi.Name())
52+
if !asof.IsZero() && !reportDate.IsZero() {
53+
// If both the mode asof date and the report date are present, do the
54+
// right thing...
55+
//
56+
// (see https://github.com/golang/go/issues/63142#issuecomment-1734025130)
57+
if asof.AddDate(0, 0, 7).Before(reportDate) {
58+
ans.readyfiles = append(ans.readyfiles, filepath.Join(localdir, fi.Name()))
59+
}
60+
} else {
61+
// ...otherwise fall back on the old behavior of uploading all
62+
// unuploaded files.
63+
//
64+
// TODO(rfindley): invert this logic following more testing. We
65+
// should only upload if we know both the asof date and the report
66+
// date, and they are acceptable.
67+
ans.readyfiles = append(ans.readyfiles, filepath.Join(localdir, fi.Name()))
68+
}
4669
}
4770
}
71+
4872
fis, err = os.ReadDir(uploaddir)
4973
if err != nil {
5074
os.MkdirAll(uploaddir, 0777)

internal/upload/first_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
// In practice this test runs last, so is somewhat superfluous,
1616
// but it also checks that uploads and reads from the channel are matched
1717
func TestSimpleServer(t *testing.T) {
18-
setup(t)
18+
setup(t, "2023-01-01")
1919
defer restore()
2020
url := uploadURL
2121
resp, err := http.Post(url+"/foo", "text/plain", strings.NewReader("hello"))

internal/upload/reports.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"path/filepath"
1515
"strings"
16+
"time"
1617

1718
"golang.org/x/telemetry"
1819
"golang.org/x/telemetry/internal/configstore"
@@ -110,12 +111,26 @@ func createReport(date string, files []string, lastWeek string) (string, error)
110111
configVersion = v
111112
}
112113
uploadOK := true
113-
if uploadConfig == nil || it.Mode() != "on" {
114+
mode, asof := it.Mode()
115+
if uploadConfig == nil || mode != "on" {
114116
uploadOK = false // no config, nothing to upload
115117
}
116118
if tooOld(date) {
117119
uploadOK = false
118120
}
121+
if uploadOK && !asof.IsZero() {
122+
// If the mode is recorded with an asof date, don't upload if the asof date
123+
// is too recent.
124+
uploadOK = false
125+
t, err := time.Parse("2006-01-02", date)
126+
if err == nil {
127+
if asof.AddDate(0, 0, 7).Before(t) {
128+
uploadOK = true
129+
}
130+
} else {
131+
logger.Printf("parsing report date: %v", err)
132+
}
133+
}
119134
// should we check that all the x.Meta are consistent for GOOS, GOARCH, etc?
120135
report := &telemetry.Report{
121136
Config: configVersion,

0 commit comments

Comments
 (0)