github.com/m3db/m3@v1.5.0/src/dbnode/namespace/schema.go (about) 1 // Copyright (c) 2019 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 namespace 22 23 import ( 24 "errors" 25 "io" 26 "io/ioutil" 27 "os" 28 "strings" 29 30 nsproto "github.com/m3db/m3/src/dbnode/generated/proto/namespace" 31 xerrors "github.com/m3db/m3/src/x/errors" 32 "github.com/m3db/m3/src/x/ident" 33 34 "github.com/golang/protobuf/proto" 35 dpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 36 "github.com/jhump/protoreflect/desc" 37 "github.com/jhump/protoreflect/desc/protoparse" 38 ) 39 40 var ( 41 errInvalidSchema = errors.New("invalid schema definition") 42 errSchemaRegistryEmpty = errors.New("schema registry is empty") 43 errInvalidSchemaOptions = errors.New("invalid schema options") 44 errEmptyProtoFile = errors.New("empty proto file") 45 errSyntaxNotProto3 = errors.New("proto syntax is not proto3") 46 errEmptyDeployID = errors.New("schema deploy ID can not be empty") 47 errDuplicateDeployID = errors.New("schema deploy ID already exists") 48 ) 49 50 type MessageDescriptor struct { 51 *desc.MessageDescriptor 52 } 53 54 type schemaDescr struct { 55 deployId string 56 prevDeployId string 57 md MessageDescriptor 58 } 59 60 func newSchemaDescr(deployId, prevId string, md MessageDescriptor) *schemaDescr { 61 return &schemaDescr{deployId: deployId, prevDeployId: prevId, md: md} 62 } 63 64 func (s *schemaDescr) DeployId() string { 65 return s.deployId 66 } 67 68 func (s *schemaDescr) PrevDeployId() string { 69 return s.prevDeployId 70 } 71 72 func (s *schemaDescr) Equal(o SchemaDescr) bool { 73 if s == nil && o == nil { 74 return true 75 } 76 if s != nil && o == nil || s == nil && o != nil { 77 return false 78 } 79 return s.DeployId() == o.DeployId() && s.PrevDeployId() == o.PrevDeployId() 80 } 81 82 func (s *schemaDescr) Get() MessageDescriptor { 83 return s.md 84 } 85 86 func (s *schemaDescr) String() string { 87 if s.md.MessageDescriptor == nil { 88 return "" 89 } 90 return s.md.MessageDescriptor.String() 91 } 92 93 type schemaHistory struct { 94 options *nsproto.SchemaOptions 95 latestId string 96 // a map of schema version to schema descriptor. 97 versions map[string]*schemaDescr 98 } 99 100 func (sr *schemaHistory) Equal(o SchemaHistory) bool { 101 var osr *schemaHistory 102 var ok bool 103 104 if osr, ok = o.(*schemaHistory); !ok { 105 return false 106 } 107 // compare latest version 108 if sr.latestId != osr.latestId { 109 return false 110 } 111 112 // compare version map 113 if len(sr.versions) != len(osr.versions) { 114 return false 115 } 116 for v, sd := range sr.versions { 117 osd, ok := osr.versions[v] 118 if !ok { 119 return false 120 } 121 if !sd.Equal(osd) { 122 return false 123 } 124 } 125 126 return true 127 } 128 129 func (sr *schemaHistory) Extends(v SchemaHistory) bool { 130 cur, hasMore := v.GetLatest() 131 132 for hasMore { 133 srCur, inSr := sr.Get(cur.DeployId()) 134 if !inSr || !cur.Equal(srCur) { 135 return false 136 } 137 cur, hasMore = v.Get(cur.PrevDeployId()) 138 } 139 return true 140 } 141 142 func (sr *schemaHistory) Get(id string) (SchemaDescr, bool) { 143 sd, ok := sr.versions[id] 144 if !ok { 145 return nil, false 146 } 147 return sd, true 148 } 149 150 func (sr *schemaHistory) GetLatest() (SchemaDescr, bool) { 151 return sr.Get(sr.latestId) 152 } 153 154 // toSchemaOptions returns the corresponding SchemaOptions proto for the provided SchemaHistory 155 func toSchemaOptions(sr SchemaHistory) *nsproto.SchemaOptions { 156 if sr == nil { 157 return nil 158 } 159 _, ok := sr.(*schemaHistory) 160 if !ok { 161 return nil 162 } 163 return sr.(*schemaHistory).options 164 } 165 166 func emptySchemaHistory() SchemaHistory { 167 return &schemaHistory{options: nil, versions: make(map[string]*schemaDescr)} 168 } 169 170 // LoadSchemaHistory loads schema registry from SchemaOptions proto. 171 func LoadSchemaHistory(options *nsproto.SchemaOptions) (SchemaHistory, error) { 172 sr := &schemaHistory{options: options, versions: make(map[string]*schemaDescr)} 173 if options == nil || 174 options.GetHistory() == nil || 175 len(options.GetHistory().GetVersions()) == 0 { 176 return sr, nil 177 } 178 179 msgName := options.GetDefaultMessageName() 180 if len(msgName) == 0 { 181 return nil, xerrors.Wrap(errInvalidSchemaOptions, "default message name is not specified") 182 } 183 184 var prevId string 185 for _, fdbSet := range options.GetHistory().GetVersions() { 186 if len(prevId) > 0 && fdbSet.PrevId != prevId { 187 return nil, xerrors.Wrapf(errInvalidSchemaOptions, "schema history is not sorted by deploy id in ascending order") 188 } 189 sd, err := loadFileDescriptorSet(fdbSet, msgName) 190 if err != nil { 191 return nil, err 192 } 193 sr.versions[sd.DeployId()] = sd 194 prevId = sd.DeployId() 195 } 196 sr.latestId = prevId 197 198 return sr, nil 199 } 200 201 func loadFileDescriptorSet(fdSet *nsproto.FileDescriptorSet, msgName string) (*schemaDescr, error) { 202 // assuming file descriptors are topological sorted 203 var dependencies []*desc.FileDescriptor 204 var curfd *desc.FileDescriptor 205 for i, fdb := range fdSet.Descriptors { 206 fdp, err := decodeFileDescriptorProto(fdb) 207 if err != nil { 208 return nil, xerrors.Wrapf(err, "failed to decode file descriptor(%d) in version(%s)", i, fdSet.DeployId) 209 } 210 fd, err := desc.CreateFileDescriptor(fdp, dependencies...) 211 if err != nil { 212 return nil, xerrors.Wrapf(err, "failed to create file descriptor(%d) in version(%s)", i, fdSet.DeployId) 213 } 214 if !fd.IsProto3() { 215 return nil, xerrors.Wrapf(errSyntaxNotProto3, "file descriptor(%s) is not proto3", fd.GetFullyQualifiedName()) 216 } 217 curfd = fd 218 dependencies = append(dependencies, curfd) 219 } 220 221 md := curfd.FindMessage(msgName) 222 if md != nil { 223 return newSchemaDescr(fdSet.DeployId, fdSet.PrevId, MessageDescriptor{md}), nil 224 } 225 return nil, xerrors.Wrapf(errInvalidSchemaOptions, "failed to find message (%s) in deployment(%s)", msgName, fdSet.DeployId) 226 } 227 228 // decodeFileDescriptorProto decodes the bytes of proto file descriptor. 229 func decodeFileDescriptorProto(fdb []byte) (*dpb.FileDescriptorProto, error) { 230 fd := dpb.FileDescriptorProto{} 231 232 if err := proto.Unmarshal(fdb, &fd); err != nil { 233 return nil, err 234 } 235 return &fd, nil 236 } 237 238 // genDependencyDescriptors produces a topological sort of the dependency descriptors for the provided 239 // file descriptor, the result contains the input file descriptor as the last in the slice, 240 // the result contains indirect dependencies as well, dependencies in the return are distinct. 241 func genDependencyDescriptors(infd *desc.FileDescriptor) []*desc.FileDescriptor { 242 var depfds []*desc.FileDescriptor 243 dedup := make(map[string]struct{}) 244 245 for _, dep := range infd.GetDependencies() { 246 depfs2 := genDependencyDescriptors(dep) 247 for _, fd := range depfs2 { 248 if _, ok := dedup[fd.GetFullyQualifiedName()]; !ok { 249 dedup[fd.GetFullyQualifiedName()] = struct{}{} 250 depfds = append(depfds, fd) 251 } 252 } 253 } 254 if _, ok := dedup[infd.GetFullyQualifiedName()]; !ok { 255 depfds = append(depfds, infd) 256 dedup[infd.GetFullyQualifiedName()] = struct{}{} 257 } 258 return depfds 259 } 260 261 // protoStringProvider provides proto contents from strings. 262 func protoStringProvider(source map[string]string) protoparse.FileAccessor { 263 if len(source) == 0 { 264 return nil 265 } 266 return func(filename string) (io.ReadCloser, error) { 267 if contents, ok := source[filename]; ok { 268 return ioutil.NopCloser(strings.NewReader(contents)), nil 269 } else { 270 return nil, os.ErrNotExist 271 } 272 } 273 } 274 275 func parseProto(protoFile string, accessor protoparse.FileAccessor, importPaths ...string) ([]*desc.FileDescriptor, error) { 276 p := protoparse.Parser{ImportPaths: importPaths, Accessor: accessor, IncludeSourceCodeInfo: true} 277 fds, err := p.ParseFiles(protoFile) 278 if err != nil { 279 return nil, xerrors.Wrapf(err, "failed to parse proto file: %s", protoFile) 280 } 281 if len(fds) == 0 { 282 return nil, xerrors.Wrapf(errEmptyProtoFile, "proto file (%s) can not be parsed", protoFile) 283 } 284 if !fds[0].IsProto3() { 285 return nil, xerrors.Wrapf(errSyntaxNotProto3, "proto file (%s) is not proto3", protoFile) 286 } 287 return genDependencyDescriptors(fds[0]), nil 288 } 289 290 func marshalFileDescriptors(fdList []*desc.FileDescriptor) ([][]byte, error) { 291 var dlist [][]byte 292 for _, fd := range fdList { 293 fdbytes, err := proto.Marshal(fd.AsProto()) 294 if err != nil { 295 return nil, xerrors.Wrapf(err, "failed to marshal file descriptor: %s", fd.GetFullyQualifiedName()) 296 } 297 dlist = append(dlist, fdbytes) 298 } 299 return dlist, nil 300 } 301 302 // AppendSchemaOptions appends to a provided SchemaOptions with a new version of schema. 303 // The new version of schema is parsed out of the provided protoFile/msgName/contents. 304 // schemaOpt: the SchemaOptions to be appended to, if nil, a new SchemaOption is created. 305 // deployID: the version ID of the new schema. 306 // protoFile: name of the top level proto file. 307 // msgName: name of the top level proto message. 308 // contents: map of name to proto strings. 309 // Except for the top level proto file, other imported proto files' key must be exactly the same 310 // as how they are imported in the import statement: 311 // E.g. if import.proto is imported as below 312 // import "mainpkg/imported.proto"; 313 // Then the map key for improted.proto must be "mainpkg/imported.proto" 314 // See src/dbnode/namesapce/kvadmin test for example. 315 func AppendSchemaOptions(schemaOpt *nsproto.SchemaOptions, protoFile, msgName string, contents map[string]string, deployID string) (*nsproto.SchemaOptions, error) { 316 // Verify schema options 317 schemaHist, err := LoadSchemaHistory(schemaOpt) 318 if err != nil { 319 return nil, xerrors.Wrap(err, "can not append to invalid schema history") 320 } 321 // Verify deploy ID 322 if deployID == "" { 323 return nil, errEmptyDeployID 324 } 325 if _, ok := schemaHist.Get(deployID); ok { 326 return nil, errDuplicateDeployID 327 } 328 329 var prevID string 330 if descr, ok := schemaHist.GetLatest(); ok { 331 prevID = descr.DeployId() 332 } 333 334 out, err := parseProto(protoFile, protoStringProvider(contents)) 335 if err != nil { 336 return nil, xerrors.Wrapf(err, "failed to parse schema from %v", protoFile) 337 } 338 339 dlist, err := marshalFileDescriptors(out) 340 if err != nil { 341 return nil, err 342 } 343 344 if schemaOpt == nil { 345 schemaOpt = &nsproto.SchemaOptions{ 346 History: &nsproto.SchemaHistory{}, 347 DefaultMessageName: msgName, 348 } 349 } 350 schemaOpt.History.Versions = append(schemaOpt.History.Versions, &nsproto.FileDescriptorSet{DeployId: deployID, PrevId: prevID, Descriptors: dlist}) 351 schemaOpt.DefaultMessageName = msgName 352 353 if _, err := LoadSchemaHistory(schemaOpt); err != nil { 354 return nil, xerrors.Wrap(err, "new schema is not valid") 355 } 356 357 return schemaOpt, nil 358 } 359 360 func LoadSchemaRegistryFromFile(schemaReg SchemaRegistry, nsID ident.ID, deployID string, protoFile string, msgName string, importPath ...string) error { 361 out, err := parseProto(protoFile, nil, importPath...) 362 if err != nil { 363 return xerrors.Wrapf(err, "failed to parse input proto file %v", protoFile) 364 } 365 366 dlist, err := marshalFileDescriptors(out) 367 if err != nil { 368 return err 369 } 370 371 schemaOpt := &nsproto.SchemaOptions{ 372 History: &nsproto.SchemaHistory{ 373 Versions: []*nsproto.FileDescriptorSet{{DeployId: deployID, Descriptors: dlist}}, 374 }, 375 DefaultMessageName: msgName, 376 } 377 schemaHis, err := LoadSchemaHistory(schemaOpt) 378 if err != nil { 379 return xerrors.Wrapf(err, "failed to load schema history from file: %v with msg: %v", protoFile, msgName) 380 } 381 err = schemaReg.SetSchemaHistory(nsID, schemaHis) 382 if err != nil { 383 return xerrors.Wrapf(err, "failed to update schema registry for %v", nsID.String()) 384 } 385 return nil 386 } 387 388 func GenTestSchemaOptions(protoFile string, importPath ...string) *nsproto.SchemaOptions { 389 out, _ := parseProto(protoFile, nil, importPath...) 390 391 dlist, _ := marshalFileDescriptors(out) 392 393 return &nsproto.SchemaOptions{ 394 History: &nsproto.SchemaHistory{ 395 Versions: []*nsproto.FileDescriptorSet{ 396 {DeployId: "first", Descriptors: dlist}, 397 {DeployId: "second", PrevId: "first", Descriptors: dlist}, 398 {DeployId: "third", PrevId: "second", Descriptors: dlist}, 399 }, 400 }, 401 DefaultMessageName: "mainpkg.TestMessage", 402 } 403 } 404 405 func GetTestSchemaDescr(md *desc.MessageDescriptor) SchemaDescr { 406 return &schemaDescr{md: MessageDescriptor{md}} 407 } 408 409 func GetTestSchemaDescrWithDeployID(md *desc.MessageDescriptor, deployID string) SchemaDescr { 410 return &schemaDescr{md: MessageDescriptor{md}, deployId: deployID} 411 }