From 33075974cb9e79786274a93d1feaa5f6e0bad066 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Thu, 21 May 2026 10:37:47 +0200 Subject: [PATCH 1/6] chore(stcrashreceiver): metrics on ignore matches Signed-off-by: Jakob Borg --- cmd/infra/stcrashreceiver/main.go | 3 ++- cmd/infra/stcrashreceiver/metrics.go | 5 +++++ cmd/infra/stcrashreceiver/stcrashreceiver.go | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/infra/stcrashreceiver/main.go b/cmd/infra/stcrashreceiver/main.go index 30c834c2a..c2e7922ca 100644 --- a/cmd/infra/stcrashreceiver/main.go +++ b/cmd/infra/stcrashreceiver/main.go @@ -123,7 +123,8 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http return } - if _, ok := ignore.match(bs); ok { + if pat, ok := ignore.match(bs); ok { + metricIgnoreMatchesTotal.WithLabelValues(pat).Inc() result = "ignored" return } diff --git a/cmd/infra/stcrashreceiver/metrics.go b/cmd/infra/stcrashreceiver/metrics.go index c073c8c39..f716b12ca 100644 --- a/cmd/infra/stcrashreceiver/metrics.go +++ b/cmd/infra/stcrashreceiver/metrics.go @@ -42,4 +42,9 @@ var ( Subsystem: "crashreceiver", Name: "sentry_reports_total", }, []string{"result"}) + metricIgnoreMatchesTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "syncthing", + Subsystem: "crashreceiver", + Name: "ignore_matches_total", + }, []string{"pattern"}) ) diff --git a/cmd/infra/stcrashreceiver/stcrashreceiver.go b/cmd/infra/stcrashreceiver/stcrashreceiver.go index 18776f338..0106efee5 100644 --- a/cmd/infra/stcrashreceiver/stcrashreceiver.go +++ b/cmd/infra/stcrashreceiver/stcrashreceiver.go @@ -90,6 +90,7 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0])) if pat, ok := r.ignore.match(bs); ok { + metricIgnoreMatchesTotal.WithLabelValues(pat).Inc() result = "ignored" log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first) return From 79423edbdf2df669837d803b9c3c607802ca8cd0 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Thu, 21 May 2026 10:46:51 +0200 Subject: [PATCH 2/6] chore(stcrashreceiver): better source cache & metrics Signed-off-by: Jakob Borg --- cmd/infra/stcrashreceiver/metrics.go | 10 ++++++ cmd/infra/stcrashreceiver/sourcecodeloader.go | 35 +++++++++++++------ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/cmd/infra/stcrashreceiver/metrics.go b/cmd/infra/stcrashreceiver/metrics.go index f716b12ca..c498475f7 100644 --- a/cmd/infra/stcrashreceiver/metrics.go +++ b/cmd/infra/stcrashreceiver/metrics.go @@ -47,4 +47,14 @@ var ( Subsystem: "crashreceiver", Name: "ignore_matches_total", }, []string{"pattern"}) + metricSourceCodeLoadsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "syncthing", + Subsystem: "crashreceiver", + Name: "source_code_loads_total", + }, []string{"result"}) + metricSourceCodeCacheSize = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "syncthing", + Subsystem: "crashreceiver", + Name: "source_code_cache_size", + }) ) diff --git a/cmd/infra/stcrashreceiver/sourcecodeloader.go b/cmd/infra/stcrashreceiver/sourcecodeloader.go index c76f1f283..564981926 100644 --- a/cmd/infra/stcrashreceiver/sourcecodeloader.go +++ b/cmd/infra/stcrashreceiver/sourcecodeloader.go @@ -15,23 +15,33 @@ import ( "strings" "sync" "time" + + lru "github.com/hashicorp/golang-lru/v2" ) const ( - urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/" - httpTimeout = 10 * time.Second + urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/" + httpTimeout = 10 * time.Second + maxCacheEntries = 1000 ) +type cacheKey struct { + version string + file string +} + type githubSourceCodeLoader struct { mut sync.Mutex version string - cache map[string]map[string][][]byte // version -> file -> lines - client *http.Client + + cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines + client *http.Client } func newGithubSourceCodeLoader() *githubSourceCodeLoader { + cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries) return &githubSourceCodeLoader{ - cache: make(map[string]map[string][][]byte), + cache: cache, client: &http.Client{Timeout: httpTimeout}, } } @@ -39,9 +49,6 @@ func newGithubSourceCodeLoader() *githubSourceCodeLoader { func (l *githubSourceCodeLoader) LockWithVersion(version string) { l.mut.Lock() l.version = version - if _, ok := l.cache[version]; !ok { - l.cache[version] = make(map[string][][]byte) - } } func (l *githubSourceCodeLoader) Unlock() { @@ -50,11 +57,13 @@ func (l *githubSourceCodeLoader) Unlock() { func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) { filename = filepath.ToSlash(filename) - lines, ok := l.cache[l.version][filename] + key := cacheKey{version: l.version, file: filename} + lines, ok := l.cache.Get(key) if !ok { // Cache whatever we managed to find (or nil if nothing, so we don't try again) defer func() { - l.cache[l.version][filename] = lines + l.cache.Add(key, lines) + metricSourceCodeCacheSize.Set(float64(l.cache.Len())) }() knownPrefixes := []string{"/lib/", "/cmd/"} @@ -73,19 +82,25 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b resp, err := l.client.Get(url) if err != nil { fmt.Println("Loading source:", err) + metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc() return nil, 0 } if resp.StatusCode != http.StatusOK { fmt.Println("Loading source:", resp.Status) + metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc() return nil, 0 } data, err := io.ReadAll(resp.Body) _ = resp.Body.Close() if err != nil { fmt.Println("Loading source:", err.Error()) + metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc() return nil, 0 } lines = bytes.Split(data, []byte{'\n'}) + metricSourceCodeLoadsTotal.WithLabelValues("loaded").Inc() + } else { + metricSourceCodeLoadsTotal.WithLabelValues("cached").Inc() } return getLineFromLines(lines, line, context) From b537090d9148d22db75c18f0b6569a0cef6e6842 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Thu, 21 May 2026 11:17:39 +0200 Subject: [PATCH 3/6] chore(stcrashreceiver): compact diskstore in-memory representation Signed-off-by: Jakob Borg --- cmd/infra/stcrashreceiver/diskstore.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/cmd/infra/stcrashreceiver/diskstore.go b/cmd/infra/stcrashreceiver/diskstore.go index fca0fdc01..2e3788be7 100644 --- a/cmd/infra/stcrashreceiver/diskstore.go +++ b/cmd/infra/stcrashreceiver/diskstore.go @@ -136,15 +136,25 @@ func (d *diskStore) Exists(path string) bool { } func (d *diskStore) clean() { - for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) { - f := d.currentFiles[0] + numDeleted := 0 + for idx := range d.currentFiles { + if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes { + break + } + + f := d.currentFiles[idx] log.Println("Removing", f.path) if err := os.Remove(f.path); err != nil { log.Println("Failed to remove file:", err) } - d.currentFiles = d.currentFiles[1:] d.currentSize -= f.size + numDeleted = idx + 1 } + + // Compact currentFiles + copy(d.currentFiles, d.currentFiles[numDeleted:]) + d.currentFiles = d.currentFiles[:len(d.currentFiles)-numDeleted] + var oldest time.Duration if len(d.currentFiles) > 0 { oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute) @@ -158,7 +168,7 @@ func (d *diskStore) clean() { } func (d *diskStore) inventory() error { - d.currentFiles = nil + d.currentFiles = d.currentFiles[:0] d.currentSize = 0 err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error { if err != nil { From 4404b4dfb460987b17b3648e2a9472072c95cb0e Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Sat, 23 May 2026 08:45:03 +0200 Subject: [PATCH 4/6] chore(stcrashreceiver): add profiler on metrics port Signed-off-by: Jakob Borg --- cmd/infra/stcrashreceiver/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/infra/stcrashreceiver/main.go b/cmd/infra/stcrashreceiver/main.go index c2e7922ca..c82d40ee9 100644 --- a/cmd/infra/stcrashreceiver/main.go +++ b/cmd/infra/stcrashreceiver/main.go @@ -20,6 +20,7 @@ import ( "io" "log" "net/http" + "net/http/pprof" "os" "path/filepath" "regexp" @@ -89,6 +90,7 @@ func main() { if params.MetricsListen != "" { mmux := http.NewServeMux() mmux.Handle("/metrics", promhttp.Handler()) + mmux.HandleFunc("/debug/pprof/", pprof.Index) go func() { if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil { log.Fatalln("HTTP serve metrics:", err) From 9152d7fb2f9a0b849056366a65c69f259dade36a Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Sat, 23 May 2026 09:13:42 +0200 Subject: [PATCH 5/6] chore(ur): move structs to reduce dependency chain lib/ur brings in a lot of dependencies we don't need in e.g. stcrashreceiver, who only needs the small failure reporting structs. Make those part of the lean `contract` package instead. Signed-off-by: Jakob Borg --- cmd/infra/stcrashreceiver/main.go | 6 ++--- lib/model/indexhandler.go | 4 ++-- lib/ur/contract/contract.go | 13 ++++++++++ lib/ur/failurereporting.go | 40 +++++++++++-------------------- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/cmd/infra/stcrashreceiver/main.go b/cmd/infra/stcrashreceiver/main.go index c82d40ee9..54356f81f 100644 --- a/cmd/infra/stcrashreceiver/main.go +++ b/cmd/infra/stcrashreceiver/main.go @@ -30,7 +30,7 @@ import ( raven "github.com/getsentry/raven-go" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/syncthing/syncthing/lib/build" - "github.com/syncthing/syncthing/lib/ur" + "github.com/syncthing/syncthing/lib/ur/contract" ) const maxRequestSize = 1 << 20 // 1 MiB @@ -131,7 +131,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http return } - var reports []ur.FailureReport + var reports []contract.FailureReport err = json.Unmarshal(bs, &reports) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -179,7 +179,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http } } -func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) { +func saveFailureWithGoroutines(data contract.FailureData, failureDir string) (string, error) { bs := make([]byte, len(data.Description)+len(data.Goroutines)) copy(bs, data.Description) copy(bs[len(data.Description):], data.Goroutines) diff --git a/lib/model/indexhandler.go b/lib/model/indexhandler.go index 5cf73ab3e..c1c03cec8 100644 --- a/lib/model/indexhandler.go +++ b/lib/model/indexhandler.go @@ -20,7 +20,7 @@ import ( "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/svcutil" - "github.com/syncthing/syncthing/lib/ur" + "github.com/syncthing/syncthing/lib/ur/contract" ) type indexHandler struct { @@ -470,7 +470,7 @@ func (s *indexHandler) logSequenceAnomaly(msg string, extra map[string]any) { extraStrs[k] = fmt.Sprint(v) } - s.evLogger.Log(events.Failure, ur.FailureData{ + s.evLogger.Log(events.Failure, contract.FailureData{ Description: msg, Extra: extraStrs, }) diff --git a/lib/ur/contract/contract.go b/lib/ur/contract/contract.go index 1ea77fe7e..15b52420c 100644 --- a/lib/ur/contract/contract.go +++ b/lib/ur/contract/contract.go @@ -282,3 +282,16 @@ func clear(v interface{}, since int) error { } return nil } + +type FailureReport struct { + FailureData + + Count int + Version string +} + +type FailureData struct { + Description string + Goroutines string + Extra map[string]string +} diff --git a/lib/ur/failurereporting.go b/lib/ur/failurereporting.go index 8d0626b2d..bbf2c53ec 100644 --- a/lib/ur/failurereporting.go +++ b/lib/ur/failurereporting.go @@ -23,6 +23,7 @@ import ( "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/tlsutil" + "github.com/syncthing/syncthing/lib/ur/contract" "github.com/thejerf/suture/v4" ) @@ -39,23 +40,10 @@ var ( invalidEventDataType = "failure event data is not a string" ) -type FailureReport struct { - FailureData - - Count int - Version string -} - -type FailureData struct { - Description string - Goroutines string - Extra map[string]string -} - -func FailureDataWithGoroutines(description string) FailureData { +func FailureDataWithGoroutines(description string) contract.FailureData { var buf strings.Builder pprof.Lookup("goroutine").WriteTo(&buf, 1) - return FailureData{ + return contract.FailureData{ Description: description, Goroutines: buf.String(), Extra: make(map[string]string), @@ -86,7 +74,7 @@ type failureHandler struct { type failureStat struct { first, last time.Time count int - data FailureData + data contract.FailureData } func (h *failureHandler) Serve(ctx context.Context) error { @@ -105,24 +93,24 @@ func (h *failureHandler) Serve(ctx context.Context) error { if !ok { // Just to be safe - shouldn't ever happen, as // evChan is set to nil when unsubscribing. - h.addReport(FailureData{Description: evChanClosed}, time.Now()) + h.addReport(contract.FailureData{Description: evChanClosed}, time.Now()) evChan = nil continue } - var data FailureData + var data contract.FailureData switch d := e.Data.(type) { case string: data.Description = d - case FailureData: + case contract.FailureData: data = d default: // Same here, shouldn't ever happen. - h.addReport(FailureData{Description: invalidEventDataType}, time.Now()) + h.addReport(contract.FailureData{Description: invalidEventDataType}, time.Now()) continue } h.addReport(data, e.Time) case <-timer.C: - reports := make([]FailureReport, 0, len(h.buf)) + reports := make([]contract.FailureReport, 0, len(h.buf)) now := time.Now() for descr, stat := range h.buf { if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay { @@ -152,7 +140,7 @@ func (h *failureHandler) Serve(ctx context.Context) error { if sub != nil { sub.Unsubscribe() if len(h.buf) > 0 { - reports := make([]FailureReport, 0, len(h.buf)) + reports := make([]contract.FailureReport, 0, len(h.buf)) for _, stat := range h.buf { reports = append(reports, newFailureReport(stat)) } @@ -179,7 +167,7 @@ func (h *failureHandler) applyOpts(opts config.OptionsConfiguration, sub events. return url, nil, nil } -func (h *failureHandler) addReport(data FailureData, evTime time.Time) { +func (h *failureHandler) addReport(data contract.FailureData, evTime time.Time) { if stat, ok := h.buf[data.Description]; ok { stat.last = evTime stat.count++ @@ -204,7 +192,7 @@ func (*failureHandler) String() string { return "FailureHandler" } -func sendFailureReports(ctx context.Context, reports []FailureReport, url string) { +func sendFailureReports(ctx context.Context, reports []contract.FailureReport, url string) { var b bytes.Buffer if err := json.NewEncoder(&b).Encode(reports); err != nil { panic(err) @@ -235,8 +223,8 @@ func sendFailureReports(ctx context.Context, reports []FailureReport, url string resp.Body.Close() } -func newFailureReport(stat *failureStat) FailureReport { - return FailureReport{ +func newFailureReport(stat *failureStat) contract.FailureReport { + return contract.FailureReport{ FailureData: stat.data, Count: stat.count, Version: build.LongVersion, From 05b4f6abda4bea16592513ba9a533cdec24091d3 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Sat, 23 May 2026 09:18:15 +0200 Subject: [PATCH 6/6] build: let infra containers builds fail individually Signed-off-by: Jakob Borg --- .github/workflows/build-infra-dockers.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-infra-dockers.yaml b/.github/workflows/build-infra-dockers.yaml index d31cf89fe..85284a1a1 100644 --- a/.github/workflows/build-infra-dockers.yaml +++ b/.github/workflows/build-infra-dockers.yaml @@ -22,6 +22,7 @@ jobs: if: github.repository_owner == 'syncthing' runs-on: ubuntu-latest strategy: + fail-fast: false matrix: pkg: - stcrashreceiver