Merge pull request #177 from djdongjin/metadata-clone
Add MD.Clone function
This commit is contained in:
		
							
								
								
									
										28
									
								
								metadata.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								metadata.go
									
									
									
									
									
								
							| @@ -62,6 +62,34 @@ func (m MD) Append(key string, values ...string) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Clone returns a copy of MD or nil if it's nil. | ||||||
|  | // It's copied from golang's `http.Header.Clone` implementation: | ||||||
|  | // https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/net/http/header.go;l=94 | ||||||
|  | func (m MD) Clone() MD { | ||||||
|  | 	if m == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Find total number of values. | ||||||
|  | 	nv := 0 | ||||||
|  | 	for _, vv := range m { | ||||||
|  | 		nv += len(vv) | ||||||
|  | 	} | ||||||
|  | 	sv := make([]string, nv) // shared backing array for headers' values | ||||||
|  | 	m2 := make(MD, len(m)) | ||||||
|  | 	for k, vv := range m { | ||||||
|  | 		if vv == nil { | ||||||
|  | 			// Preserve nil values. | ||||||
|  | 			m2[k] = nil | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		n := copy(sv, vv) | ||||||
|  | 		m2[k] = sv[:n:n] | ||||||
|  | 		sv = sv[n:] | ||||||
|  | 	} | ||||||
|  | 	return m2 | ||||||
|  | } | ||||||
|  |  | ||||||
| func (m MD) setRequest(r *Request) { | func (m MD) setRequest(r *Request) { | ||||||
| 	for k, values := range m { | 	for k, values := range m { | ||||||
| 		for _, v := range values { | 		for _, v := range values { | ||||||
|   | |||||||
| @@ -18,6 +18,8 @@ package ttrpc | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"sync" | ||||||
| 	"testing" | 	"testing" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -106,3 +108,100 @@ func TestMetadataContext(t *testing.T) { | |||||||
| 		t.Errorf("invalid metadata value: %q", bar) | 		t.Errorf("invalid metadata value: %q", bar) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestMetadataClone(t *testing.T) { | ||||||
|  | 	var metadata MD | ||||||
|  | 	m2 := metadata.Clone() | ||||||
|  | 	if m2 != nil { | ||||||
|  | 		t.Error("MD.Clone() on nil metadata should return nil") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	metadata = MD{"nil": nil, "foo": {"bar"}, "baz": {"qux", "quxx"}} | ||||||
|  | 	m2 = metadata.Clone() | ||||||
|  |  | ||||||
|  | 	if len(metadata) != len(m2) { | ||||||
|  | 		t.Errorf("unexpected number of keys: %d, expected: %d", len(m2), len(metadata)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for k, v := range metadata { | ||||||
|  | 		v2, ok := m2[k] | ||||||
|  | 		if !ok { | ||||||
|  | 			t.Errorf("key not found: %s", k) | ||||||
|  | 		} | ||||||
|  | 		if v == nil && v2 == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if v == nil || v2 == nil { | ||||||
|  | 			t.Errorf("unexpected nil value: %v, expected: %v", v2, v) | ||||||
|  | 		} | ||||||
|  | 		if len(v) != len(v2) { | ||||||
|  | 			t.Errorf("unexpected number of values: %d, expected: %d", len(v2), len(v)) | ||||||
|  | 		} | ||||||
|  | 		for i := range v { | ||||||
|  | 			if v[i] != v2[i] { | ||||||
|  | 				t.Errorf("unexpected value: %s, expected: %s", v2[i], v[i]) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMetadataCloneConcurrent(t *testing.T) { | ||||||
|  | 	metadata := make(MD) | ||||||
|  | 	metadata.Set("foo", "bar") | ||||||
|  |  | ||||||
|  | 	var wg sync.WaitGroup | ||||||
|  | 	for i := 0; i < 20; i++ { | ||||||
|  | 		wg.Add(1) | ||||||
|  | 		go func() { | ||||||
|  | 			defer wg.Done() | ||||||
|  | 			m2 := metadata.Clone() | ||||||
|  | 			m2.Set("foo", "baz") | ||||||
|  | 		}() | ||||||
|  | 	} | ||||||
|  | 	wg.Wait() | ||||||
|  | 	// Concurrent modification should clone the metadata first to avoid panic | ||||||
|  | 	// due to concurrent map writes. | ||||||
|  | 	if val, ok := metadata.Get("foo"); !ok { | ||||||
|  | 		t.Error("metadata not found") | ||||||
|  | 	} else if val[0] != "bar" { | ||||||
|  | 		t.Errorf("invalid metadata value: %q", val[0]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func simpleClone(src MD) MD { | ||||||
|  | 	md := MD{} | ||||||
|  | 	for k, v := range src { | ||||||
|  | 		md[k] = append(md[k], v...) | ||||||
|  | 	} | ||||||
|  | 	return md | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func BenchmarkMetadataClone(b *testing.B) { | ||||||
|  | 	for _, sz := range []int{5, 10, 20, 50} { | ||||||
|  | 		b.Run(fmt.Sprintf("size=%d", sz), func(b *testing.B) { | ||||||
|  | 			metadata := make(MD) | ||||||
|  | 			for i := 0; i < sz; i++ { | ||||||
|  | 				metadata.Set("foo"+fmt.Sprint(i), "bar"+fmt.Sprint(i)) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			for i := 0; i < b.N; i++ { | ||||||
|  | 				_ = metadata.Clone() | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func BenchmarkSimpleMetadataClone(b *testing.B) { | ||||||
|  | 	for _, sz := range []int{5, 10, 20, 50} { | ||||||
|  | 		b.Run(fmt.Sprintf("size=%d", sz), func(b *testing.B) { | ||||||
|  | 			metadata := make(MD) | ||||||
|  | 			for i := 0; i < sz; i++ { | ||||||
|  | 				metadata.Set("foo"+fmt.Sprint(i), "bar"+fmt.Sprint(i)) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			for i := 0; i < b.N; i++ { | ||||||
|  | 				_ = simpleClone(metadata) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Fu Wei
					Fu Wei