Skip to content

Commit 430f734

Browse files
committed
Add MD.Clone
Signed-off-by: Jin Dong <[email protected]>
1 parent b71d9de commit 430f734

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

metadata.go

+28
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,34 @@ func (m MD) Append(key string, values ...string) {
6262
}
6363
}
6464

65+
// Clone returns a copy of MD or nil if it's nil.
66+
// It's copied from golang's `http.Header.Clone` implementation:
67+
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/net/http/header.go;l=94
68+
func (m MD) Clone() MD {
69+
if m == nil {
70+
return nil
71+
}
72+
73+
// Find total number of values.
74+
nv := 0
75+
for _, vv := range m {
76+
nv += len(vv)
77+
}
78+
sv := make([]string, nv) // shared backing array for headers' values
79+
m2 := make(MD, len(m))
80+
for k, vv := range m {
81+
if vv == nil {
82+
// Preserve nil values.
83+
m2[k] = nil
84+
continue
85+
}
86+
n := copy(sv, vv)
87+
m2[k] = sv[:n:n]
88+
sv = sv[n:]
89+
}
90+
return m2
91+
}
92+
6593
func (m MD) setRequest(r *Request) {
6694
for k, values := range m {
6795
for _, v := range values {

metadata_test.go

+99
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package ttrpc
1818

1919
import (
2020
"context"
21+
"fmt"
22+
"sync"
2123
"testing"
2224
)
2325

@@ -106,3 +108,100 @@ func TestMetadataContext(t *testing.T) {
106108
t.Errorf("invalid metadata value: %q", bar)
107109
}
108110
}
111+
112+
func TestMetadataClone(t *testing.T) {
113+
var metadata MD
114+
m2 := metadata.Clone()
115+
if m2 != nil {
116+
t.Error("MD.Clone() on nil metadata should return nil")
117+
}
118+
119+
metadata = MD{"nil": nil, "foo": {"bar"}, "baz": {"qux", "quxx"}}
120+
m2 = metadata.Clone()
121+
122+
if len(metadata) != len(m2) {
123+
t.Errorf("unexpected number of keys: %d, expected: %d", len(m2), len(metadata))
124+
}
125+
126+
for k, v := range metadata {
127+
v2, ok := m2[k]
128+
if !ok {
129+
t.Errorf("key not found: %s", k)
130+
}
131+
if v == nil && v2 == nil {
132+
continue
133+
}
134+
if v == nil || v2 == nil {
135+
t.Errorf("unexpected nil value: %v, expected: %v", v2, v)
136+
}
137+
if len(v) != len(v2) {
138+
t.Errorf("unexpected number of values: %d, expected: %d", len(v2), len(v))
139+
}
140+
for i := range v {
141+
if v[i] != v2[i] {
142+
t.Errorf("unexpected value: %s, expected: %s", v2[i], v[i])
143+
}
144+
}
145+
}
146+
}
147+
148+
func TestMetadataCloneConcurrent(t *testing.T) {
149+
metadata := make(MD)
150+
metadata.Set("foo", "bar")
151+
152+
var wg sync.WaitGroup
153+
for i := 0; i < 20; i++ {
154+
wg.Add(1)
155+
go func() {
156+
defer wg.Done()
157+
m2 := metadata.Clone()
158+
m2.Set("foo", "baz")
159+
}()
160+
}
161+
wg.Wait()
162+
// Concurrent modification should clone the metadata first to avoid panic
163+
// due to concurrent map writes.
164+
if val, ok := metadata.Get("foo"); !ok {
165+
t.Error("metadata not found")
166+
} else if val[0] != "bar" {
167+
t.Errorf("invalid metadata value: %q", val[0])
168+
}
169+
}
170+
171+
func simpleClone(src MD) MD {
172+
md := MD{}
173+
for k, v := range src {
174+
md[k] = append(md[k], v...)
175+
}
176+
return md
177+
}
178+
179+
func BenchmarkMetadataClone(b *testing.B) {
180+
for _, sz := range []int{5, 10, 20, 50} {
181+
b.Run(fmt.Sprintf("size=%d", sz), func(b *testing.B) {
182+
metadata := make(MD)
183+
for i := 0; i < sz; i++ {
184+
metadata.Set("foo"+fmt.Sprint(i), "bar"+fmt.Sprint(i))
185+
}
186+
187+
for i := 0; i < b.N; i++ {
188+
_ = metadata.Clone()
189+
}
190+
})
191+
}
192+
}
193+
194+
func BenchmarkSimpleMetadataClone(b *testing.B) {
195+
for _, sz := range []int{5, 10, 20, 50} {
196+
b.Run(fmt.Sprintf("size=%d", sz), func(b *testing.B) {
197+
metadata := make(MD)
198+
for i := 0; i < sz; i++ {
199+
metadata.Set("foo"+fmt.Sprint(i), "bar"+fmt.Sprint(i))
200+
}
201+
202+
for i := 0; i < b.N; i++ {
203+
_ = simpleClone(metadata)
204+
}
205+
})
206+
}
207+
}

0 commit comments

Comments
 (0)