github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/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  }