Skip to content

ct-test-srv: cap tracked submissions#8752

Merged
jsha merged 4 commits into
mainfrom
mattm-ct-test-srv-submission-cap
May 18, 2026
Merged

ct-test-srv: cap tracked submissions#8752
jsha merged 4 commits into
mainfrom
mattm-ct-test-srv-submission-cap

Conversation

@mcpherrinm
Copy link
Copy Markdown
Contributor

When using long-running test environments, ct-test-srv grows until it OOMs, which is causing some noise in my efforts to right-size memory usage.

When using long-running test environments, ct-test-srv grows until it
OOMs, which is causing some noise in my efforts to right-size memory usage.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR attempts to bound memory growth in ct-test-srv by capping the number of distinct hostnames tracked for CT submission counts.

Changes:

  • Adds a submissionsCap field and SubmissionsCap config option.
  • Moves submission-count update logic into a helper intended to enforce the cap.
  • Defaults the cap to 100 when unset.
Comments suppressed due to low confidence (2)

test/ct-test-srv/main.go:175

  • The helper indexes the map with hostnames, but that identifier is only local to addChainOrPre and is not in scope here. This causes a compile error and also ignores the hostname argument.
	_, ok := is.submissions[hostnames]
	if ok || len(is.submissions) < is.submissionsCap {
		is.submissions[hostnames]++

test/ct-test-srv/main.go:169

  • After removing the inline increment from addChainOrPre, this new helper is never called anywhere in the package. Even if the compile errors are fixed, successful CT submissions will no longer update is.submissions, so /submissions will always return 0 for new hosts.
func (is *integrationSrv) addSubmission(hostname String) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/ct-test-srv/main.go Outdated
Comment thread test/ct-test-srv/main.go Outdated
Comment on lines +224 to +226
cap := p.SubmissionsCap
if cap == 0 {
cap = 100
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment thread test/ct-test-srv/main.go
Comment on lines +174 to +177
_, ok := is.submissions[hostnames]
if ok || len(is.submissions) < is.submissionsCap {
is.submissions[hostnames]++
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's the idea. I don't think evicting or reset is useful for integration tests

Comment thread test/ct-test-srv/main.go Outdated
Copy link
Copy Markdown
Contributor

@jsha jsha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good in general. For Boulder CI let's configure the cap to something ludicrous like 1M. If we add test cases in the future that do lots more issuance, I don't want to wind up with a mysterious flake because sometimes we hit the cap and sometimes don't.

@mcpherrinm
Copy link
Copy Markdown
Contributor Author

mcpherrinm commented May 18, 2026

Let's just set the default to 1M then.

I asked Claude to do an experiment to measure usage for 1M. It says:

  ┌─────────────────────────────────────────────────────┬───────────────┬────────────┬───────────┐
  │                      Key shape                      │ Avg key bytes │ Heap delta │ Per entry │
  ├─────────────────────────────────────────────────────┼───────────────┼────────────┼───────────┤
  │ Single short hostname (hN.example.com)              │ ~19 B         │ 76 MiB     │ ~80 B     │
  ├─────────────────────────────────────────────────────┼───────────────┼────────────┼───────────┤
  │ Two-SAN typical (hN.example.com,www.hN.example.com) │ ~43 B         │ 99 MiB     │ ~104 B    │
  ├─────────────────────────────────────────────────────┼───────────────┼────────────┼───────────┤
  │ SAN-heavy, 10 names joined                          │ ~218 B        │ 267 MiB    │ ~280 B    │
  └─────────────────────────────────────────────────────┴───────────────┴────────────┴───────────┘

That seems plausible enough to me. We can always lower it (even to zero) if there's any long-running test environments we care about saving memory in.

For reference, the test
package main

import (
	"fmt"
	"runtime"
	"strings"
	"testing"
)

// TestSubmissionsMapMemory measures the memory footprint of a map[string]int64
// populated with 1 million entries shaped like the keys used by ct-test-srv's
// submissions map (joined DNS names from a certificate). It prints the
// retained heap delta so we know roughly what a 1M submissionsCap would cost.
func TestSubmissionsMapMemory(t *testing.T) {
	const n = 1_000_000

	// Try a few hostname shapes to bracket the realistic range. Keys in the
	// real server are strings.Join(cert.DNSNames, ",") so a single short name
	// is the low end, while a SAN-heavy cert lands around the high end.
	cases := []struct {
		name    string
		makeKey func(i int) string
	}{
		{
			name:    "short single hostname (~20 bytes)",
			makeKey: func(i int) string { return fmt.Sprintf("h%d.example.com", i) },
		},
		{
			name: "two-SAN typical (~45 bytes)",
			makeKey: func(i int) string {
				return fmt.Sprintf("h%d.example.com,www.h%d.example.com", i, i)
			},
		},
		{
			name: "SAN-heavy (~250 bytes, 10 names)",
			makeKey: func(i int) string {
				parts := make([]string, 10)
				for j := range parts {
					parts[j] = fmt.Sprintf("h%d-%d.example.com", i, j)
				}
				return strings.Join(parts, ",")
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			runtime.GC()
			var before runtime.MemStats
			runtime.ReadMemStats(&before)

			m := make(map[string]int64, n)
			var keyBytes uint64
			for i := 0; i < n; i++ {
				k := tc.makeKey(i)
				keyBytes += uint64(len(k))
				m[k]++
			}

			runtime.GC()
			var after runtime.MemStats
			runtime.ReadMemStats(&after)

			// Keep m reachable past ReadMemStats so it can't be GC'd early.
			if len(m) != n {
				t.Fatalf("unexpected map size %d", len(m))
			}

			heapDelta := after.HeapAlloc - before.HeapAlloc
			perEntry := float64(heapDelta) / float64(n)
			avgKey := float64(keyBytes) / float64(n)

			t.Logf("entries=%d avg_key_len=%.1fB heap_delta=%.1f MiB (%.1f bytes/entry, key bytes alone = %.1fB/entry)",
				n, avgKey, float64(heapDelta)/(1024*1024), perEntry, avgKey)
		})
	}
}

@mcpherrinm mcpherrinm requested a review from jsha May 18, 2026 19:34
@jsha jsha merged commit 247d2d1 into main May 18, 2026
17 checks passed
@jsha jsha deleted the mattm-ct-test-srv-submission-cap branch May 18, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants