Commit 955851b
metadata: Add ValueFromOutgoingContext (#9282)
Partially Fixes: #8860
### Why
There are use cases where only a single value needs to be read from
outgoing gRPC metadata. Today the only way to do that is
`metadata.FromOutgoingContext`, which merges and copies every header
already staged into a brand-new map, even though the caller only wants
one of them. The cost of that copy grows with however many headers have
already accumulated in outgoing context — exactly the complaint raised
in #8860.
### What
This PR adds `metadata.ValueFromOutgoingContext(ctx, key) []string`,
symmetric to the existing `ValueFromIncomingContext`, for reading a
single header from outgoing gRPC metadata without merging and copying
every other header already staged via `AppendToOutgoingContext`.
This is the API @easwars suggested in
#8860 (comment):
> Would adding a `ValueFromOutgoingContext` that is similar to
`ValueFromIncomingContext` work for you? Please note that
`ValueFromOutgoingContext` will not be as fast as
`ValueFromIncomingContext` as the implementation would have check
entries from the `map` **and** the `added` entries.
per #8860 (comment).
That tradeoff is exactly what this implementation does: it checks
`rawMD.md` (case-insensitively, matching `ValueFromIncomingContext`'s
semantics) and then walks `rawMD.added`, accumulating matches in the
same order `FromOutgoingContext` would, without allocating a new map or
copying unrelated keys/values.
Matching `rawMD.added` entries case-insensitively via
`strings.EqualFold`, rather than assuming they're already lowercased by
`AppendToOutgoingContext`, makes no assumption about how `rawMD.added`
was populated — the same guarantee `FromOutgoingContext` already makes
for both `rawMD.md` and `rawMD.added`.
The allocation of `vals` is deferred until a match is actually found in
`rawMD.added` (per `gemini-code-assist`'s review suggestion), rather
than always calling `copyOf` as soon as `rawMD.md` matches. When the key
is found in both `rawMD.md` and `rawMD.added`, this avoids paying for a
`copyOf` allocation that would otherwise immediately be discarded by the
first `append`'s growth — roughly halving allocations for that case (see
`key-found-in-md-and-added` in the benchmark below).
The `rawMD.added` loop also tries a direct `==` before falling back to
`strings.EqualFold` (a second `gemini-code-assist` suggestion), since
`AppendToOutgoingContext` already lowercases keys in practice.
Benchmarked with realistic-length keys (e.g. `"grpc-timeout"`, not
`"k1"`), this is a modest win when the key is found (~2-7% faster) at
the cost of a small regression when it isn't (~6% slower — one extra
comparison with no payoff) — a reasonable trade since looking up a
header you expect to be present is the common case.
### Benchmark
`n` is the number of unrelated headers already staged (one
`NewOutgoingContext` call plus one `AppendToOutgoingContext` call)
before reading the target key:
```
goos: darwin
goarch: arm64
pkg: google.golang.org/grpc/metadata
cpu: Apple M4 Pro
FromOutgoingContext/n=1 141.7 ns/op 432 B/op 4 allocs/op
ValueFromOutgoingContext/n=1 56.0 ns/op 16 B/op 1 allocs/op
FromOutgoingContext/n=10 404.9 ns/op 968 B/op 15 allocs/op
ValueFromOutgoingContext/n=10 107.2 ns/op 16 B/op 1 allocs/op
FromOutgoingContext/n=50 1596.0 ns/op 3592 B/op 55 allocs/op
ValueFromOutgoingContext/n=50 314.7 ns/op 16 B/op 1 allocs/op
```
(measured with `b.Loop`, per review feedback, which also removes the
need for the manual anti-optimization `b.Fatal` checks the previous
numbers were measured with)
At n=50, roughly 5x faster with 55x fewer allocations. This is still
O(n) in the number of already-staged headers in the worst case, since
`rawMD.added` isn't indexed by key, but it avoids the wasted work of
copying and lowercasing every header the caller doesn't want.
Separately, `BenchmarkValueFromOutgoingContext` (now using
realistic-length keys like `"grpc-timeout"`/`"content-type"` instead of
`"k1"`/`"k3"`, which understated `strings.EqualFold`'s cost) shows both
`rawMD.added` optimizations above:
```
key-found 59.6 ns/op 16 B/op 1 allocs/op
key-not-found 50.7 ns/op 0 B/op 0 allocs/op
key-found-in-md-and-added 38.7 ns/op 32 B/op 1 allocs/op
```
### Testing
- `TestValueFromOutgoingContext` covers exact match, case-insensitive
match, a value present in `rawMD.md` accumulated with two later
`AppendToOutgoingContext` calls (must match `FromOutgoingContext`'s
order, per `TestAppendToOutgoingContext`), values split solely across
multiple `AppendToOutgoingContext` calls, not-found, and
no-outgoing-metadata-at-all.
- `TestValueFromOutgoingContext_AddedCaseInsensitive` constructs
`rawMD.added` directly with a mixed-case key (bypassing
`AppendToOutgoingContext`'s lowercasing) to verify the match is still
found.
- `TestValueFromOutgoingContext_PanicsOnOddPairs` covers the defensive
panic on a malformed `rawMD.added` entry, mirroring the identical guard
already present in `FromOutgoingContext`.
- `BenchmarkValueFromOutgoingContext` mirrors the existing
`BenchmarkValueFromIncomingContext` shape (key-found / key-not-found),
plus a `key-found-in-md-and-added` case covering the deferred-allocation
path above.
- `BenchmarkValueFromOutgoingContextVsFromOutgoingContext` produced the
comparison table above.
- `ValueFromOutgoingContext` itself is at 100% statement coverage (`go
test -cover`).
- `go test ./metadata/...`, `go vet ./metadata/...`, and `gofmt` are all
clean. This branch is rebased on current `master`.
This is additive only — no existing exported behavior changes.
RELEASE NOTES:
* metadata: Add ValueFromOutgoingContext, which reads a single metadata
value from outgoing context without copying the entire outgoing metadata
into a new map.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ce9c112 commit 955851b
2 files changed
Lines changed: 213 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
360 | 360 | | |
361 | 361 | | |
362 | 362 | | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
363 | 423 | | |
364 | 424 | | |
365 | 425 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
342 | 344 | | |
343 | 345 | | |
344 | 346 | | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
345 | 440 | | |
346 | 441 | | |
347 | 442 | | |
| |||
379 | 474 | | |
380 | 475 | | |
381 | 476 | | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
382 | 503 | | |
383 | 504 | | |
384 | 505 | | |
| |||
416 | 537 | | |
417 | 538 | | |
418 | 539 | | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
419 | 572 | | |
420 | 573 | | |
421 | 574 | | |
| |||
0 commit comments