Viewing files at commit c10cefe7abe5Commit detailsView latest

oakmark/agent.go 124 lines · 4.3 KB · go
1
package main
2
3
import (
4
	"context"
5
	"fmt"
6
	"math/rand"
7
	"regexp"
8
	"time"
9
)
10
11
// agent is one synthetic writer. It drives a Transport (cli or protocol) to
12
// push a stream of small changesets to its own branch on its assigned repo.
13
// Each agent owns its own branch (oak's flat model — every branch parents onto
14
// main and has an independent head), so in branch/repo strategies its pushes
15
// never lose a CAS race; what we measure is the server's per-repo push
16
// throughput, not client retry. This mirrors ForgeMark's agent, with the
17
// go-git calls replaced by the Transport seam.
18
type agent struct {
19
	id   int
20
	cfg  *commitConfig
21
	sess *sessionConfig // non-nil → "session" strategy (clone+push loop)
22
	tr   Transport
23
24
	samples []sample
25
}
26
27
func newAgent(id int, cfg *commitConfig, sess *sessionConfig, tr Transport) *agent {
28
	return &agent{id: id, cfg: cfg, sess: sess, tr: tr}
29
}
30
31
// run pushes changesets in a tight loop until ctx is cancelled, recording one
32
// sample per attempt. start anchors sample offsets so the caller drops the
33
// warm-up window afterwards. Identical control flow to ForgeMark's agent.run.
34
func (a *agent) run(ctx context.Context, start time.Time) {
35
	defer a.tr.Close()
36
	if a.sess != nil {
37
		a.runSessions(ctx, start)
38
		return
39
	}
40
	rng := rand.New(rand.NewSource(int64(a.id)*1_000_003 + 1)) //nolint:gosec // synthetic load, not security-sensitive
41
	if err := a.tr.Init(ctx); err != nil {
42
		// A setup failure takes this agent out of the level; record it so one
43
		// bad agent is visible rather than silently absent.
44
		a.samples = append(a.samples, sample{offset: time.Since(start), res: outcomeErr, msg: fmt.Sprintf("init: %v", err)})
45
		return
46
	}
47
	for ctx.Err() == nil {
48
		files := generateFiles(rng, a.cfg)
49
		t0 := time.Now()
50
		err := a.tr.Push(ctx, files)
51
		dur := time.Since(t0)
52
		// Drop the attempt that lost the ctx-cancellation race: it's a truncated
53
		// measurement, not real latency.
54
		if ctx.Err() != nil {
55
			break
56
		}
57
		o := classifyOak(err)
58
		a.samples = append(a.samples, sample{offset: t0.Sub(start), dur: dur, res: o, msg: errMsg(o, err)})
59
	}
60
}
61
62
// runSessions is the session-strategy loop: shallow-clone the base repo (the
63
// measured read), push `commits` checkpoints to a fresh ephemeral branch (the
64
// measured writes), then abandon it. Clone latency is op==clone, push latency
65
// op==push, so they summarize separately. Mirrors ForgeMark's runSessions.
66
func (a *agent) runSessions(ctx context.Context, start time.Time) {
67
	rng := rand.New(rand.NewSource(int64(a.id)*1_000_003 + 1)) //nolint:gosec // synthetic load
68
	for session := 0; ctx.Err() == nil; session++ {
69
		t0 := time.Now()
70
		err := a.tr.CloneSession(ctx)
71
		if ctx.Err() != nil {
72
			return
73
		}
74
		res := readOutcome(err)
75
		a.samples = append(a.samples, sample{offset: t0.Sub(start), dur: time.Since(t0), res: res, op: opClone, msg: errMsg(res, err)})
76
		if err != nil {
77
			continue // clone failed — next session retries
78
		}
79
		pushed := false
80
		for k := 0; k < a.sess.commits && ctx.Err() == nil; k++ {
81
			files := generateFiles(rng, a.cfg)
82
			t1 := time.Now()
83
			perr := a.tr.PushCheckpoint(ctx, session, files)
84
			if perr == nil {
85
				pushed = true
86
			}
87
			if ctx.Err() != nil {
88
				break // window closed mid-push: stop, but still clean up
89
			}
90
			o := classifyOak(perr)
91
			a.samples = append(a.samples, sample{offset: t1.Sub(start), dur: time.Since(t1), res: o, op: opPush, msg: errMsg(o, perr)})
92
		}
93
		if pushed {
94
			a.tr.CleanupSession(session) // best-effort, unmeasured
95
		}
96
	}
97
}
98
99
// maxErrMsgLen caps a retained error message so a long URL can't crowd the
100
// signal out of the window.
101
const maxErrMsgLen = 200
102
103
// reURL/reRef collapse the volatile substrings that vary by node/repo/agent
104
// (server URL, branch ref) so identical failures group together in the results.
105
var (
106
	reURL = regexp.MustCompile(`https?://\S+`)
107
	reRef = regexp.MustCompile(`refs?/\S+|oakmark-\S+`)
108
)
109
110
// normalizeErr turns a raw error message into a stable grouping key: collapse
111
// this run's volatile URLs/branch refs, then truncate. Order matters —
112
// truncating first could cut a status code that follows a long URL. Same
113
// contract as ForgeMark's normalizeErr (consumed by stats.go errGroups).
114
func normalizeErr(msg string) string {
115
	if msg == "" {
116
		return "(no message)"
117
	}
118
	msg = reURL.ReplaceAllString(msg, "<url>")
119
	msg = reRef.ReplaceAllString(msg, "<ref>")
120
	if len(msg) > maxErrMsgLen {
121
		msg = msg[:maxErrMsgLen]
122
	}
123
	return msg
124
}