go.uber.org/yarpc@v1.72.1/router.go (about) 1 // Copyright (c) 2022 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package yarpc 22 23 import ( 24 "context" 25 "fmt" 26 "sort" 27 "strconv" 28 "strings" 29 30 "go.uber.org/yarpc/api/transport" 31 "go.uber.org/yarpc/internal/humanize" 32 "go.uber.org/yarpc/yarpcerrors" 33 ) 34 35 var ( 36 _ transport.Router = (*MapRouter)(nil) 37 ) 38 39 type serviceProcedure struct { 40 service string 41 procedure string 42 } 43 44 type serviceProcedureEncoding struct { 45 service string 46 procedure string 47 encoding transport.Encoding 48 } 49 50 // MapRouter is a Router that maintains a map of the registered 51 // procedures. 52 type MapRouter struct { 53 defaultService string 54 serviceProcedures map[serviceProcedure]transport.Procedure 55 serviceProcedureEncodings map[serviceProcedureEncoding]transport.Procedure 56 supportedEncodings map[serviceProcedure][]string 57 serviceNames map[string]struct{} 58 } 59 60 // NewMapRouter builds a new MapRouter that uses the given name as the 61 // default service name. 62 func NewMapRouter(defaultService string) MapRouter { 63 return MapRouter{ 64 defaultService: defaultService, 65 serviceProcedures: make(map[serviceProcedure]transport.Procedure), 66 serviceProcedureEncodings: make(map[serviceProcedureEncoding]transport.Procedure), 67 supportedEncodings: make(map[serviceProcedure][]string), 68 serviceNames: map[string]struct{}{defaultService: {}}, 69 } 70 } 71 72 // Register registers the procedure with the MapRouter. 73 // If the procedure does not specify its service name, the procedure will 74 // inherit the default service name of the router. 75 // Procedures should specify their encoding, and multiple procedures with the 76 // same name and service name can exist if they handle different encodings. 77 // If a procedure does not specify an encoding, it can only support one handler. 78 // The router will select that handler regardless of the encoding. 79 func (m MapRouter) Register(rs []transport.Procedure) { 80 for _, r := range rs { 81 if r.Service == "" { 82 r.Service = m.defaultService 83 } 84 85 if r.Name == "" { 86 panic("Expected procedure name not to be empty string in registration") 87 } 88 89 m.serviceNames[r.Service] = struct{}{} 90 91 sp := serviceProcedure{ 92 service: r.Service, 93 procedure: r.Name, 94 } 95 96 if r.Encoding == "" { 97 // Protect against masking encoding-specific routes. 98 if _, ok := m.serviceProcedures[sp]; ok { 99 panic(fmt.Sprintf("Cannot register multiple handlers for every encoding for service %q and procedure %q", sp.service, sp.procedure)) 100 } 101 if se, ok := m.supportedEncodings[sp]; ok { 102 panic(fmt.Sprintf("Cannot register a handler for every encoding for service %q and procedure %q when there are already handlers for %s", sp.service, sp.procedure, humanize.QuotedJoin(se, "and", "no encodings"))) 103 } 104 // This supports wild card encodings (for backward compatibility, 105 // since type models like Thrift were not previously required to 106 // specify the encoding of every procedure). 107 m.serviceProcedures[sp] = r 108 continue 109 } 110 111 spe := serviceProcedureEncoding{ 112 service: r.Service, 113 procedure: r.Name, 114 encoding: r.Encoding, 115 } 116 117 // Protect against overriding wildcards 118 if _, ok := m.serviceProcedures[sp]; ok { 119 panic(fmt.Sprintf("Cannot register a handler for both (service, procedure) on any * encoding and (service, procedure, encoding), specifically (%q, %q, %q)", r.Service, r.Name, r.Encoding)) 120 } 121 // Route to individual handlers for unique combinations of service, 122 // procedure, and encoding. This shall henceforth be the 123 // recommended way for models to register procedures. 124 m.serviceProcedureEncodings[spe] = r 125 // Record supported encodings. 126 m.supportedEncodings[sp] = append(m.supportedEncodings[sp], string(r.Encoding)) 127 } 128 } 129 130 // Procedures returns a list procedures that 131 // have been registered so far. 132 func (m MapRouter) Procedures() []transport.Procedure { 133 procs := make([]transport.Procedure, 0, len(m.serviceProcedures)+len(m.serviceProcedureEncodings)) 134 for _, v := range m.serviceProcedures { 135 procs = append(procs, v) 136 } 137 for _, v := range m.serviceProcedureEncodings { 138 procs = append(procs, v) 139 } 140 sort.Sort(sortableProcedures(procs)) 141 return procs 142 } 143 144 type sortableProcedures []transport.Procedure 145 146 func (ps sortableProcedures) Len() int { 147 return len(ps) 148 } 149 150 func (ps sortableProcedures) Less(i int, j int) bool { 151 return ps[i].Less(ps[j]) 152 } 153 154 func (ps sortableProcedures) Swap(i int, j int) { 155 ps[i], ps[j] = ps[j], ps[i] 156 } 157 158 // Choose retrives the HandlerSpec for the service, procedure, and encoding 159 // noted on the transport request, or returns an unrecognized procedure error 160 // (testable with transport.IsUnrecognizedProcedureError(err)). 161 func (m MapRouter) Choose(ctx context.Context, req *transport.Request) (transport.HandlerSpec, error) { 162 service, procedure, encoding := req.Service, req.Procedure, req.Encoding 163 if service == "" { 164 service = m.defaultService 165 } 166 167 if _, ok := m.serviceNames[service]; !ok { 168 return transport.HandlerSpec{}, 169 yarpcerrors.Newf(yarpcerrors.CodeUnimplemented, "unrecognized service name %q, "+ 170 "available services: %s", req.Service, getAvailableServiceNames(m.serviceNames)) 171 } 172 173 // Fully specified combinations of service, procedure, and encoding. 174 spe := serviceProcedureEncoding{ 175 service: service, 176 procedure: procedure, 177 encoding: encoding, 178 } 179 if procedure, ok := m.serviceProcedureEncodings[spe]; ok { 180 return procedure.HandlerSpec, nil 181 } 182 183 // Alternately use the original behavior: route all encodings to the same 184 // handler. 185 sp := serviceProcedure{ 186 service: service, 187 procedure: procedure, 188 } 189 if procedure, ok := m.serviceProcedures[sp]; ok { 190 return procedure.HandlerSpec, nil 191 } 192 193 // Supported procedure, unrecognized encoding. 194 if wantEncodings := m.supportedEncodings[sp]; len(wantEncodings) == 1 { 195 // To maintain backward compatibility with the error messages provided 196 // on the wire (as verified by Crossdock across all language 197 // implementations), this routes an invalid encoding to the sole 198 // implementation of a procedure. 199 // The handler is then responsible for detecting the invalid encoding 200 // and providing an error including "failed to decode". 201 spe.encoding = transport.Encoding(wantEncodings[0]) 202 return m.serviceProcedureEncodings[spe].HandlerSpec, nil 203 } 204 205 return transport.HandlerSpec{}, yarpcerrors.Newf(yarpcerrors.CodeUnimplemented, "unrecognized procedure %q for service %q", req.Procedure, req.Service) 206 } 207 208 // Extract keys from service names map and return a formatted string 209 func getAvailableServiceNames(svcMap map[string]struct{}) string { 210 var serviceNames []string 211 for key := range svcMap { 212 serviceNames = append(serviceNames, strconv.Quote(key)) 213 } 214 // Sort the string array to generate consistent result 215 sort.Strings(serviceNames) 216 return strings.Join(serviceNames, ", ") 217 }