Skip to content

Commit 63dc53d

Browse files
fix(envoy): cache last validated config to skip redundant validation
Every config change revalidates the entire merged config via the embedded envoy subprocess, even when reconciling an unrelated ingress produces a config identical to one already validated. Cache the SHA-256 of the last successfully validated bootstrap and skip the subprocess on a match. The cache is a single-slot, mutex-guarded value shared across concurrent reconciles; the check-then-store gap can only cause a redundant (correct) revalidation, never an incorrect skip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 643f059 commit 63dc53d

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

pomerium/envoy/validate_cache.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Package envoy contains functions for working with an embedded envoy binary.
2+
package envoy
3+
4+
import (
5+
"crypto/sha256"
6+
"sync"
7+
)
8+
9+
// defaultValidationCache is the process-wide cache of the last successfully
10+
// validated config hash. It is shared across concurrent reconciles.
11+
var defaultValidationCache = &validationCache{}
12+
13+
// validationCache caches the hash of the most recently successfully-validated
14+
// config so that repeated validation of an unchanged config can skip the
15+
// expensive embedded-envoy subprocess. It is safe for concurrent use.
16+
type validationCache struct {
17+
mu sync.RWMutex
18+
lastHash [sha256.Size]byte
19+
valid bool
20+
}
21+
22+
// hit reports whether hash matches the last successfully-validated config.
23+
func (c *validationCache) hit(hash [sha256.Size]byte) bool {
24+
c.mu.RLock()
25+
defer c.mu.RUnlock()
26+
return c.valid && c.lastHash == hash
27+
}
28+
29+
// store records hash as the last successfully-validated config.
30+
func (c *validationCache) store(hash [sha256.Size]byte) {
31+
c.mu.Lock()
32+
defer c.mu.Unlock()
33+
c.lastHash, c.valid = hash, true
34+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package envoy
2+
3+
import (
4+
"crypto/sha256"
5+
"sync"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestValidationCache(t *testing.T) {
12+
c := &validationCache{}
13+
h1 := sha256.Sum256([]byte("config-1"))
14+
h2 := sha256.Sum256([]byte("config-2"))
15+
16+
// empty cache: never a hit
17+
assert.False(t, c.hit(h1), "empty cache should not hit")
18+
19+
// after storing h1, only h1 hits
20+
c.store(h1)
21+
assert.True(t, c.hit(h1), "stored hash should hit")
22+
assert.False(t, c.hit(h2), "different hash should miss")
23+
24+
// storing h2 replaces h1 (single-slot cache)
25+
c.store(h2)
26+
assert.True(t, c.hit(h2), "newly stored hash should hit")
27+
assert.False(t, c.hit(h1), "previous hash should no longer hit")
28+
}
29+
30+
func TestValidationCacheConcurrent(t *testing.T) {
31+
c := &validationCache{}
32+
h := sha256.Sum256([]byte("config"))
33+
c.store(h)
34+
35+
// concurrent readers and writers must not race (run with -race).
36+
var wg sync.WaitGroup
37+
for i := 0; i < 50; i++ {
38+
wg.Add(2)
39+
go func() { defer wg.Done(); _ = c.hit(h) }()
40+
go func() { defer wg.Done(); c.store(h) }()
41+
}
42+
wg.Wait()
43+
assert.True(t, c.hit(h))
44+
}

pomerium/envoy/validate_envoy.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package envoy
66

77
import (
88
"context"
9-
9+
"crypto/sha256"
1010
"os"
1111
"os/exec"
1212
"path/filepath"
@@ -33,6 +33,14 @@ func Validate(ctx context.Context, bootstrap *envoy_config_bootstrap_v3.Bootstra
3333
return nil, err
3434
}
3535

36+
// Skip the (expensive) envoy subprocess if this exact config was already
37+
// validated successfully. saveConfig revalidates the whole config on every
38+
// change, so identical configs are validated repeatedly without this cache.
39+
hash := sha256.Sum256(bs)
40+
if defaultValidationCache.hit(hash) {
41+
return &ValidateResult{Valid: true, Message: "OK (cached)"}, nil
42+
}
43+
3644
cfgName := filepath.Join(os.TempDir(), id+".pb")
3745
err = os.WriteFile(cfgName, bs, ownerRW)
3846
if err != nil {
@@ -62,6 +70,7 @@ func Validate(ctx context.Context, bootstrap *envoy_config_bootstrap_v3.Bootstra
6270
// all other errors are returned as errors
6371
return nil, err
6472
}
73+
defaultValidationCache.store(hash)
6574
return &ValidateResult{
6675
Valid: true,
6776
Message: "OK",

0 commit comments

Comments
 (0)