go mod vendor
+ move k8s.io/apimachinery fork from go.work to go.mod (and include it in vendor)
This commit is contained in:
11
vendor/go.mongodb.org/mongo-driver/mongo/description/description.go
generated
vendored
Normal file
11
vendor/go.mongodb.org/mongo-driver/mongo/description/description.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (C) MongoDB, Inc. 2017-present.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
// Package description contains types and functions for describing the state of MongoDB clusters.
|
||||
package description // import "go.mongodb.org/mongo-driver/mongo/description"
|
||||
|
||||
// Unknown is an unknown server or topology kind.
|
||||
const Unknown = 0
|
488
vendor/go.mongodb.org/mongo-driver/mongo/description/server.go
generated
vendored
Normal file
488
vendor/go.mongodb.org/mongo-driver/mongo/description/server.go
generated
vendored
Normal file
@@ -0,0 +1,488 @@
|
||||
// Copyright (C) MongoDB, Inc. 2017-present.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
package description
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/internal"
|
||||
"go.mongodb.org/mongo-driver/mongo/address"
|
||||
"go.mongodb.org/mongo-driver/tag"
|
||||
)
|
||||
|
||||
// SelectedServer augments the Server type by also including the TopologyKind of the topology that includes the server.
|
||||
// This type should be used to track the state of a server that was selected to perform an operation.
|
||||
type SelectedServer struct {
|
||||
Server
|
||||
Kind TopologyKind
|
||||
}
|
||||
|
||||
// Server contains information about a node in a cluster. This is created from hello command responses. If the value
|
||||
// of the Kind field is LoadBalancer, only the Addr and Kind fields will be set. All other fields will be set to the
|
||||
// zero value of the field's type.
|
||||
type Server struct {
|
||||
Addr address.Address
|
||||
|
||||
Arbiters []string
|
||||
AverageRTT time.Duration
|
||||
AverageRTTSet bool
|
||||
Compression []string // compression methods returned by server
|
||||
CanonicalAddr address.Address
|
||||
ElectionID primitive.ObjectID
|
||||
HeartbeatInterval time.Duration
|
||||
HelloOK bool
|
||||
Hosts []string
|
||||
IsCryptd bool
|
||||
LastError error
|
||||
LastUpdateTime time.Time
|
||||
LastWriteTime time.Time
|
||||
MaxBatchCount uint32
|
||||
MaxDocumentSize uint32
|
||||
MaxMessageSize uint32
|
||||
Members []address.Address
|
||||
Passives []string
|
||||
Passive bool
|
||||
Primary address.Address
|
||||
ReadOnly bool
|
||||
ServiceID *primitive.ObjectID // Only set for servers that are deployed behind a load balancer.
|
||||
SessionTimeoutMinutes uint32
|
||||
SetName string
|
||||
SetVersion uint32
|
||||
Tags tag.Set
|
||||
TopologyVersion *TopologyVersion
|
||||
Kind ServerKind
|
||||
WireVersion *VersionRange
|
||||
}
|
||||
|
||||
// NewServer creates a new server description from the given hello command response.
|
||||
func NewServer(addr address.Address, response bson.Raw) Server {
|
||||
desc := Server{Addr: addr, CanonicalAddr: addr, LastUpdateTime: time.Now().UTC()}
|
||||
elements, err := response.Elements()
|
||||
if err != nil {
|
||||
desc.LastError = err
|
||||
return desc
|
||||
}
|
||||
var ok bool
|
||||
var isReplicaSet, isWritablePrimary, hidden, secondary, arbiterOnly bool
|
||||
var msg string
|
||||
var versionRange VersionRange
|
||||
for _, element := range elements {
|
||||
switch element.Key() {
|
||||
case "arbiters":
|
||||
var err error
|
||||
desc.Arbiters, err = internal.StringSliceFromRawElement(element)
|
||||
if err != nil {
|
||||
desc.LastError = err
|
||||
return desc
|
||||
}
|
||||
case "arbiterOnly":
|
||||
arbiterOnly, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'arbiterOnly' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "compression":
|
||||
var err error
|
||||
desc.Compression, err = internal.StringSliceFromRawElement(element)
|
||||
if err != nil {
|
||||
desc.LastError = err
|
||||
return desc
|
||||
}
|
||||
case "electionId":
|
||||
desc.ElectionID, ok = element.Value().ObjectIDOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'electionId' to be a objectID but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "iscryptd":
|
||||
desc.IsCryptd, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'iscryptd' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "helloOk":
|
||||
desc.HelloOK, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'helloOk' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "hidden":
|
||||
hidden, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'hidden' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "hosts":
|
||||
var err error
|
||||
desc.Hosts, err = internal.StringSliceFromRawElement(element)
|
||||
if err != nil {
|
||||
desc.LastError = err
|
||||
return desc
|
||||
}
|
||||
case "isWritablePrimary":
|
||||
isWritablePrimary, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'isWritablePrimary' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case internal.LegacyHelloLowercase:
|
||||
isWritablePrimary, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected legacy hello to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "isreplicaset":
|
||||
isReplicaSet, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'isreplicaset' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "lastWrite":
|
||||
lastWrite, ok := element.Value().DocumentOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'lastWrite' to be a document but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
dateTime, err := lastWrite.LookupErr("lastWriteDate")
|
||||
if err == nil {
|
||||
dt, ok := dateTime.DateTimeOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'lastWriteDate' to be a datetime but it's a BSON %s", dateTime.Type)
|
||||
return desc
|
||||
}
|
||||
desc.LastWriteTime = time.Unix(dt/1000, dt%1000*1000000).UTC()
|
||||
}
|
||||
case "logicalSessionTimeoutMinutes":
|
||||
i64, ok := element.Value().AsInt64OK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'logicalSessionTimeoutMinutes' to be an integer but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
desc.SessionTimeoutMinutes = uint32(i64)
|
||||
case "maxBsonObjectSize":
|
||||
i64, ok := element.Value().AsInt64OK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'maxBsonObjectSize' to be an integer but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
desc.MaxDocumentSize = uint32(i64)
|
||||
case "maxMessageSizeBytes":
|
||||
i64, ok := element.Value().AsInt64OK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'maxMessageSizeBytes' to be an integer but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
desc.MaxMessageSize = uint32(i64)
|
||||
case "maxWriteBatchSize":
|
||||
i64, ok := element.Value().AsInt64OK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'maxWriteBatchSize' to be an integer but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
desc.MaxBatchCount = uint32(i64)
|
||||
case "me":
|
||||
me, ok := element.Value().StringValueOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'me' to be a string but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
desc.CanonicalAddr = address.Address(me).Canonicalize()
|
||||
case "maxWireVersion":
|
||||
versionRange.Max, ok = element.Value().AsInt32OK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'maxWireVersion' to be an integer but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "minWireVersion":
|
||||
versionRange.Min, ok = element.Value().AsInt32OK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'minWireVersion' to be an integer but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "msg":
|
||||
msg, ok = element.Value().StringValueOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'msg' to be a string but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "ok":
|
||||
okay, ok := element.Value().AsInt32OK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'ok' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
if okay != 1 {
|
||||
desc.LastError = errors.New("not ok")
|
||||
return desc
|
||||
}
|
||||
case "passives":
|
||||
var err error
|
||||
desc.Passives, err = internal.StringSliceFromRawElement(element)
|
||||
if err != nil {
|
||||
desc.LastError = err
|
||||
return desc
|
||||
}
|
||||
case "passive":
|
||||
desc.Passive, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'passive' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "primary":
|
||||
primary, ok := element.Value().StringValueOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'primary' to be a string but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
desc.Primary = address.Address(primary)
|
||||
case "readOnly":
|
||||
desc.ReadOnly, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'readOnly' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "secondary":
|
||||
secondary, ok = element.Value().BooleanOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'secondary' to be a boolean but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "serviceId":
|
||||
oid, ok := element.Value().ObjectIDOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'serviceId' to be an ObjectId but it's a BSON %s", element.Value().Type)
|
||||
}
|
||||
desc.ServiceID = &oid
|
||||
case "setName":
|
||||
desc.SetName, ok = element.Value().StringValueOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'setName' to be a string but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
case "setVersion":
|
||||
i64, ok := element.Value().AsInt64OK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'setVersion' to be an integer but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
desc.SetVersion = uint32(i64)
|
||||
case "tags":
|
||||
m, err := decodeStringMap(element, "tags")
|
||||
if err != nil {
|
||||
desc.LastError = err
|
||||
return desc
|
||||
}
|
||||
desc.Tags = tag.NewTagSetFromMap(m)
|
||||
case "topologyVersion":
|
||||
doc, ok := element.Value().DocumentOK()
|
||||
if !ok {
|
||||
desc.LastError = fmt.Errorf("expected 'topologyVersion' to be a document but it's a BSON %s", element.Value().Type)
|
||||
return desc
|
||||
}
|
||||
|
||||
desc.TopologyVersion, err = NewTopologyVersion(doc)
|
||||
if err != nil {
|
||||
desc.LastError = err
|
||||
return desc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, host := range desc.Hosts {
|
||||
desc.Members = append(desc.Members, address.Address(host).Canonicalize())
|
||||
}
|
||||
|
||||
for _, passive := range desc.Passives {
|
||||
desc.Members = append(desc.Members, address.Address(passive).Canonicalize())
|
||||
}
|
||||
|
||||
for _, arbiter := range desc.Arbiters {
|
||||
desc.Members = append(desc.Members, address.Address(arbiter).Canonicalize())
|
||||
}
|
||||
|
||||
desc.Kind = Standalone
|
||||
|
||||
if isReplicaSet {
|
||||
desc.Kind = RSGhost
|
||||
} else if desc.SetName != "" {
|
||||
if isWritablePrimary {
|
||||
desc.Kind = RSPrimary
|
||||
} else if hidden {
|
||||
desc.Kind = RSMember
|
||||
} else if secondary {
|
||||
desc.Kind = RSSecondary
|
||||
} else if arbiterOnly {
|
||||
desc.Kind = RSArbiter
|
||||
} else {
|
||||
desc.Kind = RSMember
|
||||
}
|
||||
} else if msg == "isdbgrid" {
|
||||
desc.Kind = Mongos
|
||||
}
|
||||
|
||||
desc.WireVersion = &versionRange
|
||||
|
||||
return desc
|
||||
}
|
||||
|
||||
// NewDefaultServer creates a new unknown server description with the given address.
|
||||
func NewDefaultServer(addr address.Address) Server {
|
||||
return NewServerFromError(addr, nil, nil)
|
||||
}
|
||||
|
||||
// NewServerFromError creates a new unknown server description with the given parameters.
|
||||
func NewServerFromError(addr address.Address, err error, tv *TopologyVersion) Server {
|
||||
return Server{
|
||||
Addr: addr,
|
||||
LastError: err,
|
||||
Kind: Unknown,
|
||||
TopologyVersion: tv,
|
||||
}
|
||||
}
|
||||
|
||||
// SetAverageRTT sets the average round trip time for this server description.
|
||||
func (s Server) SetAverageRTT(rtt time.Duration) Server {
|
||||
s.AverageRTT = rtt
|
||||
s.AverageRTTSet = true
|
||||
return s
|
||||
}
|
||||
|
||||
// DataBearing returns true if the server is a data bearing server.
|
||||
func (s Server) DataBearing() bool {
|
||||
return s.Kind == RSPrimary ||
|
||||
s.Kind == RSSecondary ||
|
||||
s.Kind == Mongos ||
|
||||
s.Kind == Standalone
|
||||
}
|
||||
|
||||
// LoadBalanced returns true if the server is a load balancer or is behind a load balancer.
|
||||
func (s Server) LoadBalanced() bool {
|
||||
return s.Kind == LoadBalancer || s.ServiceID != nil
|
||||
}
|
||||
|
||||
// String implements the Stringer interface
|
||||
func (s Server) String() string {
|
||||
str := fmt.Sprintf("Addr: %s, Type: %s",
|
||||
s.Addr, s.Kind)
|
||||
if len(s.Tags) != 0 {
|
||||
str += fmt.Sprintf(", Tag sets: %s", s.Tags)
|
||||
}
|
||||
|
||||
if s.AverageRTTSet {
|
||||
str += fmt.Sprintf(", Average RTT: %d", s.AverageRTT)
|
||||
}
|
||||
|
||||
if s.LastError != nil {
|
||||
str += fmt.Sprintf(", Last error: %s", s.LastError)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func decodeStringMap(element bson.RawElement, name string) (map[string]string, error) {
|
||||
doc, ok := element.Value().DocumentOK()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected '%s' to be a document but it's a BSON %s", name, element.Value().Type)
|
||||
}
|
||||
elements, err := doc.Elements()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]string)
|
||||
for _, element := range elements {
|
||||
key := element.Key()
|
||||
value, ok := element.Value().StringValueOK()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected '%s' to be a document of strings, but found a BSON %s", name, element.Value().Type)
|
||||
}
|
||||
m[key] = value
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Equal compares two server descriptions and returns true if they are equal
|
||||
func (s Server) Equal(other Server) bool {
|
||||
if s.CanonicalAddr.String() != other.CanonicalAddr.String() {
|
||||
return false
|
||||
}
|
||||
|
||||
if !sliceStringEqual(s.Arbiters, other.Arbiters) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !sliceStringEqual(s.Hosts, other.Hosts) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !sliceStringEqual(s.Passives, other.Passives) {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.Primary != other.Primary {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.SetName != other.SetName {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.Kind != other.Kind {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.LastError != nil || other.LastError != nil {
|
||||
if s.LastError == nil || other.LastError == nil {
|
||||
return false
|
||||
}
|
||||
if s.LastError.Error() != other.LastError.Error() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !s.WireVersion.Equals(other.WireVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(s.Tags) != len(other.Tags) || !s.Tags.ContainsAll(other.Tags) {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.SetVersion != other.SetVersion {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.ElectionID != other.ElectionID {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.SessionTimeoutMinutes != other.SessionTimeoutMinutes {
|
||||
return false
|
||||
}
|
||||
|
||||
// If TopologyVersion is nil for both servers, CompareToIncoming will return -1 because it assumes that the
|
||||
// incoming response is newer. We want the descriptions to be considered equal in this case, though, so an
|
||||
// explicit check is required.
|
||||
if s.TopologyVersion == nil && other.TopologyVersion == nil {
|
||||
return true
|
||||
}
|
||||
return s.TopologyVersion.CompareToIncoming(other.TopologyVersion) == 0
|
||||
}
|
||||
|
||||
func sliceStringEqual(a []string, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
46
vendor/go.mongodb.org/mongo-driver/mongo/description/server_kind.go
generated
vendored
Normal file
46
vendor/go.mongodb.org/mongo-driver/mongo/description/server_kind.go
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) MongoDB, Inc. 2017-present.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
package description
|
||||
|
||||
// ServerKind represents the type of a single server in a topology.
|
||||
type ServerKind uint32
|
||||
|
||||
// These constants are the possible types of servers.
|
||||
const (
|
||||
Standalone ServerKind = 1
|
||||
RSMember ServerKind = 2
|
||||
RSPrimary ServerKind = 4 + RSMember
|
||||
RSSecondary ServerKind = 8 + RSMember
|
||||
RSArbiter ServerKind = 16 + RSMember
|
||||
RSGhost ServerKind = 32 + RSMember
|
||||
Mongos ServerKind = 256
|
||||
LoadBalancer ServerKind = 512
|
||||
)
|
||||
|
||||
// String returns a stringified version of the kind or "Unknown" if the kind is invalid.
|
||||
func (kind ServerKind) String() string {
|
||||
switch kind {
|
||||
case Standalone:
|
||||
return "Standalone"
|
||||
case RSMember:
|
||||
return "RSOther"
|
||||
case RSPrimary:
|
||||
return "RSPrimary"
|
||||
case RSSecondary:
|
||||
return "RSSecondary"
|
||||
case RSArbiter:
|
||||
return "RSArbiter"
|
||||
case RSGhost:
|
||||
return "RSGhost"
|
||||
case Mongos:
|
||||
return "Mongos"
|
||||
case LoadBalancer:
|
||||
return "LoadBalancer"
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
341
vendor/go.mongodb.org/mongo-driver/mongo/description/server_selector.go
generated
vendored
Normal file
341
vendor/go.mongodb.org/mongo-driver/mongo/description/server_selector.go
generated
vendored
Normal file
@@ -0,0 +1,341 @@
|
||||
// Copyright (C) MongoDB, Inc. 2017-present.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
package description
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
"go.mongodb.org/mongo-driver/tag"
|
||||
)
|
||||
|
||||
// ServerSelector is an interface implemented by types that can perform server selection given a topology description
|
||||
// and list of candidate servers. The selector should filter the provided candidates list and return a subset that
|
||||
// matches some criteria.
|
||||
type ServerSelector interface {
|
||||
SelectServer(Topology, []Server) ([]Server, error)
|
||||
}
|
||||
|
||||
// ServerSelectorFunc is a function that can be used as a ServerSelector.
|
||||
type ServerSelectorFunc func(Topology, []Server) ([]Server, error)
|
||||
|
||||
// SelectServer implements the ServerSelector interface.
|
||||
func (ssf ServerSelectorFunc) SelectServer(t Topology, s []Server) ([]Server, error) {
|
||||
return ssf(t, s)
|
||||
}
|
||||
|
||||
type compositeSelector struct {
|
||||
selectors []ServerSelector
|
||||
}
|
||||
|
||||
// CompositeSelector combines multiple selectors into a single selector by applying them in order to the candidates
|
||||
// list.
|
||||
//
|
||||
// For example, if the initial candidates list is [s0, s1, s2, s3] and two selectors are provided where the first
|
||||
// matches s0 and s1 and the second matches s1 and s2, the following would occur during server selection:
|
||||
//
|
||||
// 1. firstSelector([s0, s1, s2, s3]) -> [s0, s1]
|
||||
// 2. secondSelector([s0, s1]) -> [s1]
|
||||
//
|
||||
// The final list of candidates returned by the composite selector would be [s1].
|
||||
func CompositeSelector(selectors []ServerSelector) ServerSelector {
|
||||
return &compositeSelector{selectors: selectors}
|
||||
}
|
||||
|
||||
func (cs *compositeSelector) SelectServer(t Topology, candidates []Server) ([]Server, error) {
|
||||
var err error
|
||||
for _, sel := range cs.selectors {
|
||||
candidates, err = sel.SelectServer(t, candidates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
type latencySelector struct {
|
||||
latency time.Duration
|
||||
}
|
||||
|
||||
// LatencySelector creates a ServerSelector which selects servers based on their average RTT values.
|
||||
func LatencySelector(latency time.Duration) ServerSelector {
|
||||
return &latencySelector{latency: latency}
|
||||
}
|
||||
|
||||
func (ls *latencySelector) SelectServer(t Topology, candidates []Server) ([]Server, error) {
|
||||
if ls.latency < 0 {
|
||||
return candidates, nil
|
||||
}
|
||||
if t.Kind == LoadBalanced {
|
||||
// In LoadBalanced mode, there should only be one server in the topology and it must be selected.
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
switch len(candidates) {
|
||||
case 0, 1:
|
||||
return candidates, nil
|
||||
default:
|
||||
min := time.Duration(math.MaxInt64)
|
||||
for _, candidate := range candidates {
|
||||
if candidate.AverageRTTSet {
|
||||
if candidate.AverageRTT < min {
|
||||
min = candidate.AverageRTT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if min == math.MaxInt64 {
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
max := min + ls.latency
|
||||
|
||||
var result []Server
|
||||
for _, candidate := range candidates {
|
||||
if candidate.AverageRTTSet {
|
||||
if candidate.AverageRTT <= max {
|
||||
result = append(result, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSelector selects all the writable servers.
|
||||
func WriteSelector() ServerSelector {
|
||||
return ServerSelectorFunc(func(t Topology, candidates []Server) ([]Server, error) {
|
||||
switch t.Kind {
|
||||
case Single, LoadBalanced:
|
||||
return candidates, nil
|
||||
default:
|
||||
result := []Server{}
|
||||
for _, candidate := range candidates {
|
||||
switch candidate.Kind {
|
||||
case Mongos, RSPrimary, Standalone:
|
||||
result = append(result, candidate)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ReadPrefSelector selects servers based on the provided read preference.
|
||||
func ReadPrefSelector(rp *readpref.ReadPref) ServerSelector {
|
||||
return readPrefSelector(rp, false)
|
||||
}
|
||||
|
||||
// OutputAggregateSelector selects servers based on the provided read preference given that the underlying operation is
|
||||
// aggregate with an output stage.
|
||||
func OutputAggregateSelector(rp *readpref.ReadPref) ServerSelector {
|
||||
return readPrefSelector(rp, true)
|
||||
}
|
||||
|
||||
func readPrefSelector(rp *readpref.ReadPref, isOutputAggregate bool) ServerSelector {
|
||||
return ServerSelectorFunc(func(t Topology, candidates []Server) ([]Server, error) {
|
||||
if t.Kind == LoadBalanced {
|
||||
// In LoadBalanced mode, there should only be one server in the topology and it must be selected. We check
|
||||
// this before checking MaxStaleness support because there's no monitoring in this mode, so the candidate
|
||||
// server wouldn't have a wire version set, which would result in an error.
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
if _, set := rp.MaxStaleness(); set {
|
||||
for _, s := range candidates {
|
||||
if s.Kind != Unknown {
|
||||
if err := maxStalenessSupported(s.WireVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch t.Kind {
|
||||
case Single:
|
||||
return candidates, nil
|
||||
case ReplicaSetNoPrimary, ReplicaSetWithPrimary:
|
||||
return selectForReplicaSet(rp, isOutputAggregate, t, candidates)
|
||||
case Sharded:
|
||||
return selectByKind(candidates, Mongos), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
// maxStalenessSupported returns an error if the given server version does not support max staleness.
|
||||
func maxStalenessSupported(wireVersion *VersionRange) error {
|
||||
if wireVersion != nil && wireVersion.Max < 5 {
|
||||
return fmt.Errorf("max staleness is only supported for servers 3.4 or newer")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectForReplicaSet(rp *readpref.ReadPref, isOutputAggregate bool, t Topology, candidates []Server) ([]Server, error) {
|
||||
if err := verifyMaxStaleness(rp, t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If underlying operation is an aggregate with an output stage, only apply read preference
|
||||
// if all candidates are 5.0+. Otherwise, operate under primary read preference.
|
||||
if isOutputAggregate {
|
||||
for _, s := range candidates {
|
||||
if s.WireVersion.Max < 13 {
|
||||
return selectByKind(candidates, RSPrimary), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch rp.Mode() {
|
||||
case readpref.PrimaryMode:
|
||||
return selectByKind(candidates, RSPrimary), nil
|
||||
case readpref.PrimaryPreferredMode:
|
||||
selected := selectByKind(candidates, RSPrimary)
|
||||
|
||||
if len(selected) == 0 {
|
||||
selected = selectSecondaries(rp, candidates)
|
||||
return selectByTagSet(selected, rp.TagSets()), nil
|
||||
}
|
||||
|
||||
return selected, nil
|
||||
case readpref.SecondaryPreferredMode:
|
||||
selected := selectSecondaries(rp, candidates)
|
||||
selected = selectByTagSet(selected, rp.TagSets())
|
||||
if len(selected) > 0 {
|
||||
return selected, nil
|
||||
}
|
||||
return selectByKind(candidates, RSPrimary), nil
|
||||
case readpref.SecondaryMode:
|
||||
selected := selectSecondaries(rp, candidates)
|
||||
return selectByTagSet(selected, rp.TagSets()), nil
|
||||
case readpref.NearestMode:
|
||||
selected := selectByKind(candidates, RSPrimary)
|
||||
selected = append(selected, selectSecondaries(rp, candidates)...)
|
||||
return selectByTagSet(selected, rp.TagSets()), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported mode: %d", rp.Mode())
|
||||
}
|
||||
|
||||
func selectSecondaries(rp *readpref.ReadPref, candidates []Server) []Server {
|
||||
secondaries := selectByKind(candidates, RSSecondary)
|
||||
if len(secondaries) == 0 {
|
||||
return secondaries
|
||||
}
|
||||
if maxStaleness, set := rp.MaxStaleness(); set {
|
||||
primaries := selectByKind(candidates, RSPrimary)
|
||||
if len(primaries) == 0 {
|
||||
baseTime := secondaries[0].LastWriteTime
|
||||
for i := 1; i < len(secondaries); i++ {
|
||||
if secondaries[i].LastWriteTime.After(baseTime) {
|
||||
baseTime = secondaries[i].LastWriteTime
|
||||
}
|
||||
}
|
||||
|
||||
var selected []Server
|
||||
for _, secondary := range secondaries {
|
||||
estimatedStaleness := baseTime.Sub(secondary.LastWriteTime) + secondary.HeartbeatInterval
|
||||
if estimatedStaleness <= maxStaleness {
|
||||
selected = append(selected, secondary)
|
||||
}
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
primary := primaries[0]
|
||||
|
||||
var selected []Server
|
||||
for _, secondary := range secondaries {
|
||||
estimatedStaleness := secondary.LastUpdateTime.Sub(secondary.LastWriteTime) - primary.LastUpdateTime.Sub(primary.LastWriteTime) + secondary.HeartbeatInterval
|
||||
if estimatedStaleness <= maxStaleness {
|
||||
selected = append(selected, secondary)
|
||||
}
|
||||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
return secondaries
|
||||
}
|
||||
|
||||
func selectByTagSet(candidates []Server, tagSets []tag.Set) []Server {
|
||||
if len(tagSets) == 0 {
|
||||
return candidates
|
||||
}
|
||||
|
||||
for _, ts := range tagSets {
|
||||
// If this tag set is empty, we can take a fast path because the empty list is a subset of all tag sets, so
|
||||
// all candidate servers will be selected.
|
||||
if len(ts) == 0 {
|
||||
return candidates
|
||||
}
|
||||
|
||||
var results []Server
|
||||
for _, s := range candidates {
|
||||
// ts is non-empty, so only servers with a non-empty set of tags need to be checked.
|
||||
if len(s.Tags) > 0 && s.Tags.ContainsAll(ts) {
|
||||
results = append(results, s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) > 0 {
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
return []Server{}
|
||||
}
|
||||
|
||||
func selectByKind(candidates []Server, kind ServerKind) []Server {
|
||||
// Record the indices of viable candidates first and then append those to the returned slice
|
||||
// to avoid appending costly Server structs directly as an optimization.
|
||||
viableIndexes := make([]int, 0, len(candidates))
|
||||
for i, s := range candidates {
|
||||
if s.Kind == kind {
|
||||
viableIndexes = append(viableIndexes, i)
|
||||
}
|
||||
}
|
||||
result := make([]Server, len(viableIndexes))
|
||||
for i, idx := range viableIndexes {
|
||||
result[i] = candidates[idx]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func verifyMaxStaleness(rp *readpref.ReadPref, t Topology) error {
|
||||
maxStaleness, set := rp.MaxStaleness()
|
||||
if !set {
|
||||
return nil
|
||||
}
|
||||
|
||||
if maxStaleness < 90*time.Second {
|
||||
return fmt.Errorf("max staleness (%s) must be greater than or equal to 90s", maxStaleness)
|
||||
}
|
||||
|
||||
if len(t.Servers) < 1 {
|
||||
// Maybe we should return an error here instead?
|
||||
return nil
|
||||
}
|
||||
|
||||
// we'll assume all candidates have the same heartbeat interval.
|
||||
s := t.Servers[0]
|
||||
idleWritePeriod := 10 * time.Second
|
||||
|
||||
if maxStaleness < s.HeartbeatInterval+idleWritePeriod {
|
||||
return fmt.Errorf(
|
||||
"max staleness (%s) must be greater than or equal to the heartbeat interval (%s) plus idle write period (%s)",
|
||||
maxStaleness, s.HeartbeatInterval, idleWritePeriod,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
142
vendor/go.mongodb.org/mongo-driver/mongo/description/topology.go
generated
vendored
Normal file
142
vendor/go.mongodb.org/mongo-driver/mongo/description/topology.go
generated
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright (C) MongoDB, Inc. 2017-present.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
package description
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
// Topology contains information about a MongoDB cluster.
|
||||
type Topology struct {
|
||||
Servers []Server
|
||||
SetName string
|
||||
Kind TopologyKind
|
||||
SessionTimeoutMinutes uint32
|
||||
CompatibilityErr error
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (t Topology) String() string {
|
||||
var serversStr string
|
||||
for _, s := range t.Servers {
|
||||
serversStr += "{ " + s.String() + " }, "
|
||||
}
|
||||
return fmt.Sprintf("Type: %s, Servers: [%s]", t.Kind, serversStr)
|
||||
}
|
||||
|
||||
// Equal compares two topology descriptions and returns true if they are equal.
|
||||
func (t Topology) Equal(other Topology) bool {
|
||||
if t.Kind != other.Kind {
|
||||
return false
|
||||
}
|
||||
|
||||
topoServers := make(map[string]Server)
|
||||
for _, s := range t.Servers {
|
||||
topoServers[s.Addr.String()] = s
|
||||
}
|
||||
|
||||
otherServers := make(map[string]Server)
|
||||
for _, s := range other.Servers {
|
||||
otherServers[s.Addr.String()] = s
|
||||
}
|
||||
|
||||
if len(topoServers) != len(otherServers) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, server := range topoServers {
|
||||
otherServer := otherServers[server.Addr.String()]
|
||||
|
||||
if !server.Equal(otherServer) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// HasReadableServer returns true if the topology contains a server suitable for reading.
|
||||
//
|
||||
// If the Topology's kind is Single or Sharded, the mode parameter is ignored and the function contains true if any of
|
||||
// the servers in the Topology are of a known type.
|
||||
//
|
||||
// For replica sets, the function returns true if the cluster contains a server that matches the provided read
|
||||
// preference mode.
|
||||
func (t Topology) HasReadableServer(mode readpref.Mode) bool {
|
||||
switch t.Kind {
|
||||
case Single, Sharded:
|
||||
return hasAvailableServer(t.Servers, 0)
|
||||
case ReplicaSetWithPrimary:
|
||||
return hasAvailableServer(t.Servers, mode)
|
||||
case ReplicaSetNoPrimary, ReplicaSet:
|
||||
if mode == readpref.PrimaryMode {
|
||||
return false
|
||||
}
|
||||
// invalid read preference
|
||||
if !mode.IsValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasAvailableServer(t.Servers, mode)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasWritableServer returns true if a topology has a server available for writing.
|
||||
//
|
||||
// If the Topology's kind is Single or Sharded, this function returns true if any of the servers in the Topology are of
|
||||
// a known type.
|
||||
//
|
||||
// For replica sets, the function returns true if the replica set contains a primary.
|
||||
func (t Topology) HasWritableServer() bool {
|
||||
return t.HasReadableServer(readpref.PrimaryMode)
|
||||
}
|
||||
|
||||
// hasAvailableServer returns true if any servers are available based on the read preference.
|
||||
func hasAvailableServer(servers []Server, mode readpref.Mode) bool {
|
||||
switch mode {
|
||||
case readpref.PrimaryMode:
|
||||
for _, s := range servers {
|
||||
if s.Kind == RSPrimary {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case readpref.PrimaryPreferredMode, readpref.SecondaryPreferredMode, readpref.NearestMode:
|
||||
for _, s := range servers {
|
||||
if s.Kind == RSPrimary || s.Kind == RSSecondary {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case readpref.SecondaryMode:
|
||||
for _, s := range servers {
|
||||
if s.Kind == RSSecondary {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// read preference is not specified
|
||||
for _, s := range servers {
|
||||
switch s.Kind {
|
||||
case Standalone,
|
||||
RSMember,
|
||||
RSPrimary,
|
||||
RSSecondary,
|
||||
RSArbiter,
|
||||
RSGhost,
|
||||
Mongos:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
40
vendor/go.mongodb.org/mongo-driver/mongo/description/topology_kind.go
generated
vendored
Normal file
40
vendor/go.mongodb.org/mongo-driver/mongo/description/topology_kind.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (C) MongoDB, Inc. 2017-present.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
package description
|
||||
|
||||
// TopologyKind represents a specific topology configuration.
|
||||
type TopologyKind uint32
|
||||
|
||||
// These constants are the available topology configurations.
|
||||
const (
|
||||
Single TopologyKind = 1
|
||||
ReplicaSet TopologyKind = 2
|
||||
ReplicaSetNoPrimary TopologyKind = 4 + ReplicaSet
|
||||
ReplicaSetWithPrimary TopologyKind = 8 + ReplicaSet
|
||||
Sharded TopologyKind = 256
|
||||
LoadBalanced TopologyKind = 512
|
||||
)
|
||||
|
||||
// String implements the fmt.Stringer interface.
|
||||
func (kind TopologyKind) String() string {
|
||||
switch kind {
|
||||
case Single:
|
||||
return "Single"
|
||||
case ReplicaSet:
|
||||
return "ReplicaSet"
|
||||
case ReplicaSetNoPrimary:
|
||||
return "ReplicaSetNoPrimary"
|
||||
case ReplicaSetWithPrimary:
|
||||
return "ReplicaSetWithPrimary"
|
||||
case Sharded:
|
||||
return "Sharded"
|
||||
case LoadBalanced:
|
||||
return "LoadBalanced"
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
66
vendor/go.mongodb.org/mongo-driver/mongo/description/topology_version.go
generated
vendored
Normal file
66
vendor/go.mongodb.org/mongo-driver/mongo/description/topology_version.go
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (C) MongoDB, Inc. 2017-present.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
package description
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// TopologyVersion represents a software version.
|
||||
type TopologyVersion struct {
|
||||
ProcessID primitive.ObjectID
|
||||
Counter int64
|
||||
}
|
||||
|
||||
// NewTopologyVersion creates a TopologyVersion based on doc
|
||||
func NewTopologyVersion(doc bson.Raw) (*TopologyVersion, error) {
|
||||
elements, err := doc.Elements()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tv TopologyVersion
|
||||
var ok bool
|
||||
for _, element := range elements {
|
||||
switch element.Key() {
|
||||
case "processId":
|
||||
tv.ProcessID, ok = element.Value().ObjectIDOK()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected 'processId' to be a objectID but it's a BSON %s", element.Value().Type)
|
||||
}
|
||||
case "counter":
|
||||
tv.Counter, ok = element.Value().Int64OK()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected 'counter' to be an int64 but it's a BSON %s", element.Value().Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &tv, nil
|
||||
}
|
||||
|
||||
// CompareToIncoming compares the receiver, which represents the currently known TopologyVersion for a server, to an
|
||||
// incoming TopologyVersion extracted from a server command response.
|
||||
//
|
||||
// This returns -1 if the receiver version is less than the response, 0 if the versions are equal, and 1 if the
|
||||
// receiver version is greater than the response. This comparison is not commutative.
|
||||
func (tv *TopologyVersion) CompareToIncoming(responseTV *TopologyVersion) int {
|
||||
if tv == nil || responseTV == nil {
|
||||
return -1
|
||||
}
|
||||
if tv.ProcessID != responseTV.ProcessID {
|
||||
return -1
|
||||
}
|
||||
if tv.Counter == responseTV.Counter {
|
||||
return 0
|
||||
}
|
||||
if tv.Counter < responseTV.Counter {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
42
vendor/go.mongodb.org/mongo-driver/mongo/description/version_range.go
generated
vendored
Normal file
42
vendor/go.mongodb.org/mongo-driver/mongo/description/version_range.go
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) MongoDB, Inc. 2017-present.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
package description
|
||||
|
||||
import "fmt"
|
||||
|
||||
// VersionRange represents a range of versions.
|
||||
type VersionRange struct {
|
||||
Min int32
|
||||
Max int32
|
||||
}
|
||||
|
||||
// NewVersionRange creates a new VersionRange given a min and a max.
|
||||
func NewVersionRange(min, max int32) VersionRange {
|
||||
return VersionRange{Min: min, Max: max}
|
||||
}
|
||||
|
||||
// Includes returns a bool indicating whether the supplied integer is included
|
||||
// in the range.
|
||||
func (vr VersionRange) Includes(v int32) bool {
|
||||
return v >= vr.Min && v <= vr.Max
|
||||
}
|
||||
|
||||
// Equals returns a bool indicating whether the supplied VersionRange is equal.
|
||||
func (vr *VersionRange) Equals(other *VersionRange) bool {
|
||||
if vr == nil && other == nil {
|
||||
return true
|
||||
}
|
||||
if vr == nil || other == nil {
|
||||
return false
|
||||
}
|
||||
return vr.Min == other.Min && vr.Max == other.Max
|
||||
}
|
||||
|
||||
// String implements the fmt.Stringer interface.
|
||||
func (vr VersionRange) String() string {
|
||||
return fmt.Sprintf("[%d, %d]", vr.Min, vr.Max)
|
||||
}
|
Reference in New Issue
Block a user