~bigbes/sr-ht-ecore

ref: 92fea80afcf9d123988f58fbf2aea125124d7a1c sr-ht-ecore/bearer/bearer_bench_test.go -rw-r--r-- 5.6 KiB
92fea80a — Eugene Blikh benchmarks for the hot path 2 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package bearer

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
)

// The grant strings these benchmarks present. The stateless one is what a short
// token carries; the registered one adds the id: member that turns step 4 from
// a no-op into a cache lookup.
const (
	benchGrants           = "bench:upload cov:upload artifacts:upload"
	benchGrantsRegistered = benchGrants + " id:4711"
)

// The sinks keep a validated token from being discarded as an unread result.
var (
	sinkToken *Token
	sinkErr   error
)

// liveDaemon stands in for tokens.sr.ht's revocation endpoint and answers 204
// — live — to everything.
//
// It is only ever asked once per benchmark: the warm-up call below populates
// the cache, and every measured iteration is then the cached path, which is the
// one a running service spends its time in. A benchmark that hit the server on
// every iteration would be measuring httptest's loopback, not this package.
func liveDaemon(b *testing.B) *httptest.Server {
	b.Helper()

	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	}))
	b.Cleanup(s.Close)
	return s
}

// benchValidator builds a validator pointed at a daemon that says "live".
func benchValidator(b *testing.B) *Validator {
	b.Helper()

	s := liveDaemon(b)
	v, err := New(Options{
		Origin:     s.URL,
		ClientID:   callerClientID,
		NodeID:     callerNodeID,
		HTTPClient: s.Client(),
	})
	if err != nil {
		b.Fatalf("building the validator: %v", err)
	}
	return v
}

// BenchmarkValidate is the whole of SPEC ch. 6 on one presented token, which is
// what a service pays per authenticated request.
//
// The four cases are the four outcomes that happen in production:
//
//   - stateless: an HMAC, a grant parse and a map lookup, and no network at all.
//     This is the common case under the default configuration and the number
//     that matters.
//   - registered_cached: the same, plus step 4 answered from the cache under the
//     validator's mutex. The difference between it and stateless is the price of
//     revocability.
//   - forbidden: a good credential that does not carry the action. It costs more
//     than a success, because the refusal renders the grant set into its message
//     — worth knowing, since a misconfigured client retries this in a loop.
//   - invalid: junk. Step 1 refuses it without allocating a token, and this is
//     the path an instance under a flood of forged credentials runs; it must
//     stay the cheapest thing here.
func BenchmarkValidate(b *testing.B) {
	ctx := context.Background()
	v := benchValidator(b)

	stateless := ourToken(benchGrants)
	registered := ourToken(benchGrantsRegistered)

	// Warm the revocation cache, and check that the fixture validates at all —
	// a token that fails here would turn every measured iteration into an early
	// refusal and report it as a fast success.
	if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil {
		b.Fatalf("the registered fixture must validate: %v", err)
	}

	b.Run("stateless", func(b *testing.B) {
		b.ReportAllocs()
		for b.Loop() {
			sinkToken, sinkErr = v.Validate(ctx, stateless, "bench:upload")
		}
		if sinkErr != nil {
			b.Fatalf("unexpected refusal: %v", sinkErr)
		}
	})

	b.Run("registered_cached", func(b *testing.B) {
		b.ReportAllocs()
		for b.Loop() {
			sinkToken, sinkErr = v.Validate(ctx, registered, "bench:upload")
		}
		if sinkErr != nil {
			b.Fatalf("unexpected refusal: %v", sinkErr)
		}
	})

	b.Run("forbidden", func(b *testing.B) {
		b.ReportAllocs()
		for b.Loop() {
			sinkToken, sinkErr = v.Validate(ctx, stateless, "dolt:push")
		}
		if !errors.Is(sinkErr, ErrForbidden) {
			b.Fatalf("this case must be refused with ErrForbidden, got %v", sinkErr)
		}
	})

	b.Run("invalid", func(b *testing.B) {
		// Not a token: the shape a scanner presents.
		const junk = "not-a-token-at-all-just-a-string-somebody-tried"

		b.ReportAllocs()
		for b.Loop() {
			sinkToken, sinkErr = v.Validate(ctx, junk, "bench:upload")
		}
		if !errors.Is(sinkErr, ErrInvalid) {
			b.Fatalf("this case must be refused with ErrInvalid, got %v", sinkErr)
		}
	})
}

// BenchmarkInspect is the path a service's identity middleware runs: the same
// four steps without step 3, once per request, upstream of the router. Read it
// beside BenchmarkValidate/stateless — the gap is the grant check that Inspect
// leaves to the handler.
func BenchmarkInspect(b *testing.B) {
	ctx := context.Background()
	v := benchValidator(b)
	presented := ourToken(benchGrants)

	b.ReportAllocs()
	for b.Loop() {
		sinkToken, sinkErr = v.Inspect(ctx, presented)
	}
	if sinkErr != nil {
		b.Fatalf("unexpected refusal: %v", sinkErr)
	}
}

// BenchmarkValidateParallel is the same registered token validated from many
// goroutines at once, which is how a service actually holds a Validator: one of
// them, behind every request handler.
//
// The revocation cache is a map behind a single mutex, so this is where that
// choice shows up. A per-request HMAC is embarrassingly parallel; a shared lock
// is not, and the point of measuring it is to know which of the two dominates
// before somebody proposes sharding the cache.
func BenchmarkValidateParallel(b *testing.B) {
	ctx := context.Background()
	v := benchValidator(b)
	registered := ourToken(benchGrantsRegistered)

	if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil {
		b.Fatalf("the registered fixture must validate: %v", err)
	}

	b.ReportAllocs()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil {
				b.Errorf("unexpected refusal: %v", err)
				return
			}
		}
	})
}