mirror of
https://gitee.com/milvus-io/milvus.git
synced 2024-12-01 19:39:21 +08:00
Add semver in session (#18768)
Signed-off-by: Congqi Xia <congqi.xia@zilliz.com> Signed-off-by: Congqi Xia <congqi.xia@zilliz.com>
This commit is contained in:
parent
909e46b6c2
commit
d5bb377bc7
1
go.mod
1
go.mod
@ -73,6 +73,7 @@ require (
|
||||
github.com/ardielle/ardielle-go v1.5.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/campoy/embedmd v1.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
|
2
go.sum
2
go.sum
@ -109,6 +109,8 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY
|
||||
github.com/bits-and-blooms/bloom/v3 v3.0.1 h1:Inlf0YXbgehxVjMPmCGv86iMCKMGPPrPSHtBF5yRHwA=
|
||||
github.com/bits-and-blooms/bloom/v3 v3.0.1/go.mod h1:MC8muvBzzPOFsrcdND/A7kU7kMhkqb9KI70JlZCP+C8=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
|
||||
|
10
internal/common/version.go
Normal file
10
internal/common/version.go
Normal file
@ -0,0 +1,10 @@
|
||||
package common
|
||||
|
||||
import "github.com/blang/semver/v4"
|
||||
|
||||
// Version current versiong for session
|
||||
var Version semver.Version
|
||||
|
||||
func init() {
|
||||
Version, _ = semver.Parse("2.2.0-pre+dev")
|
||||
}
|
@ -10,6 +10,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/milvus-io/milvus/internal/common"
|
||||
"github.com/milvus-io/milvus/internal/log"
|
||||
"github.com/milvus-io/milvus/internal/util/retry"
|
||||
"go.etcd.io/etcd/api/v3/mvccpb"
|
||||
@ -60,6 +62,7 @@ type Session struct {
|
||||
Address string `json:"Address,omitempty"`
|
||||
Exclusive bool `json:"Exclusive,omitempty"`
|
||||
TriggerKill bool
|
||||
Version semver.Version `json:"Version,omitempty"`
|
||||
|
||||
liveCh <-chan bool
|
||||
etcdCli *clientv3.Client
|
||||
@ -70,6 +73,58 @@ type Session struct {
|
||||
registered atomic.Value
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshal bytes to Session.
|
||||
func (s *Session) UnmarshalJSON(data []byte) error {
|
||||
var raw struct {
|
||||
ServerID int64 `json:"ServerID,omitempty"`
|
||||
ServerName string `json:"ServerName,omitempty"`
|
||||
Address string `json:"Address,omitempty"`
|
||||
Exclusive bool `json:"Exclusive,omitempty"`
|
||||
TriggerKill bool
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
err := json.Unmarshal(data, &raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if raw.Version != "" {
|
||||
s.Version, err = semver.Parse(raw.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.ServerID = raw.ServerID
|
||||
s.ServerName = raw.ServerName
|
||||
s.Address = raw.Address
|
||||
s.Exclusive = raw.Exclusive
|
||||
s.TriggerKill = raw.TriggerKill
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON marshals session to bytes.
|
||||
func (s *Session) MarshalJSON() ([]byte, error) {
|
||||
|
||||
verStr := s.Version.String()
|
||||
return json.Marshal(&struct {
|
||||
ServerID int64 `json:"ServerID,omitempty"`
|
||||
ServerName string `json:"ServerName,omitempty"`
|
||||
Address string `json:"Address,omitempty"`
|
||||
Exclusive bool `json:"Exclusive,omitempty"`
|
||||
TriggerKill bool
|
||||
Version string `json:"Version"`
|
||||
}{
|
||||
ServerID: s.ServerID,
|
||||
ServerName: s.ServerName,
|
||||
Address: s.Address,
|
||||
Exclusive: s.Exclusive,
|
||||
TriggerKill: s.TriggerKill,
|
||||
Version: verStr,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// NewSession is a helper to build Session object.
|
||||
// ServerID, ServerName, Address, Exclusive will be assigned after Init().
|
||||
// metaRoot is a path in etcd to save session information.
|
||||
@ -78,6 +133,7 @@ func NewSession(ctx context.Context, metaRoot string, client *clientv3.Client) *
|
||||
session := &Session{
|
||||
ctx: ctx,
|
||||
metaRoot: metaRoot,
|
||||
Version: common.Version,
|
||||
}
|
||||
|
||||
session.UpdateRegistered(false)
|
||||
@ -119,7 +175,7 @@ func (s *Session) Init(serverName, address string, exclusive bool, triggerKill b
|
||||
|
||||
// String makes Session struct able to be logged by zap
|
||||
func (s *Session) String() string {
|
||||
return fmt.Sprintf("Session:<ServerID: %d, ServerName: %s>", s.ServerID, s.ServerName)
|
||||
return fmt.Sprintf("Session:<ServerID: %d, ServerName: %s, Version: %s>", s.ServerID, s.ServerName, s.Version.String())
|
||||
}
|
||||
|
||||
// Register will process keepAliveResponse to keep alive with etcd.
|
||||
@ -304,6 +360,35 @@ func (s *Session) GetSessions(prefix string) (map[string]*Session, int64, error)
|
||||
return res, resp.Header.Revision, nil
|
||||
}
|
||||
|
||||
// GetSessionsWithVersionRange will get all sessions with provided prefix and version range in etcd.
|
||||
// Revision is returned for WatchServices to prevent missing events.
|
||||
func (s *Session) GetSessionsWithVersionRange(prefix string, r semver.Range) (map[string]*Session, int64, error) {
|
||||
res := make(map[string]*Session)
|
||||
key := path.Join(s.metaRoot, DefaultServiceRoot, prefix)
|
||||
resp, err := s.etcdCli.Get(s.ctx, key, clientv3.WithPrefix(),
|
||||
clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, kv := range resp.Kvs {
|
||||
session := &Session{}
|
||||
err = json.Unmarshal(kv.Value, session)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !r(session.Version) {
|
||||
log.Debug("Session version out of range", zap.String("version", session.Version.String()), zap.Int64("serverID", session.ServerID))
|
||||
continue
|
||||
}
|
||||
_, mapKey := path.Split(string(kv.Key))
|
||||
log.Debug("SessionUtil GetSessions ", zap.String("prefix", prefix),
|
||||
zap.String("key", mapKey),
|
||||
zap.String("address", session.Address))
|
||||
res[mapKey] = session
|
||||
}
|
||||
return res, resp.Header.Revision, nil
|
||||
}
|
||||
|
||||
// SessionEvent indicates the changes of other servers.
|
||||
// if a server is up, EventType is SessAddEvent.
|
||||
// if a server is down, EventType is SessDelEvent.
|
||||
@ -314,11 +399,12 @@ type SessionEvent struct {
|
||||
}
|
||||
|
||||
type sessionWatcher struct {
|
||||
s *Session
|
||||
rch clientv3.WatchChan
|
||||
eventCh chan *SessionEvent
|
||||
prefix string
|
||||
rewatch Rewatch
|
||||
s *Session
|
||||
rch clientv3.WatchChan
|
||||
eventCh chan *SessionEvent
|
||||
prefix string
|
||||
rewatch Rewatch
|
||||
validate func(*Session) bool
|
||||
}
|
||||
|
||||
func (w *sessionWatcher) start() {
|
||||
@ -348,11 +434,31 @@ func (w *sessionWatcher) start() {
|
||||
// If a server down, an event will be add to channel with eventType SessionDelType.
|
||||
func (s *Session) WatchServices(prefix string, revision int64, rewatch Rewatch) (eventChannel <-chan *SessionEvent) {
|
||||
w := &sessionWatcher{
|
||||
s: s,
|
||||
eventCh: make(chan *SessionEvent, 100),
|
||||
rch: s.etcdCli.Watch(s.ctx, path.Join(s.metaRoot, DefaultServiceRoot, prefix), clientv3.WithPrefix(), clientv3.WithPrevKV(), clientv3.WithRev(revision)),
|
||||
prefix: prefix,
|
||||
rewatch: rewatch,
|
||||
s: s,
|
||||
eventCh: make(chan *SessionEvent, 100),
|
||||
rch: s.etcdCli.Watch(s.ctx, path.Join(s.metaRoot, DefaultServiceRoot, prefix), clientv3.WithPrefix(), clientv3.WithPrevKV(), clientv3.WithRev(revision)),
|
||||
prefix: prefix,
|
||||
rewatch: rewatch,
|
||||
validate: func(s *Session) bool { return true },
|
||||
}
|
||||
w.start()
|
||||
return w.eventCh
|
||||
}
|
||||
|
||||
// WatchServicesWithVersionRange watches the service's up and down in etcd, and sends event toeventChannel.
|
||||
// Acts like WatchServices but with extra version range check.
|
||||
// prefix is a parameter to know which service to watch and can be obtained intypeutil.type.go.
|
||||
// revision is a etcd reversion to prevent missing key events and can be obtained in GetSessions.
|
||||
// If a server up, an event will be add to channel with eventType SessionAddType.
|
||||
// If a server down, an event will be add to channel with eventType SessionDelType.
|
||||
func (s *Session) WatchServicesWithVersionRange(prefix string, r semver.Range, revision int64, rewatch Rewatch) (eventChannel <-chan *SessionEvent) {
|
||||
w := &sessionWatcher{
|
||||
s: s,
|
||||
eventCh: make(chan *SessionEvent, 100),
|
||||
rch: s.etcdCli.Watch(s.ctx, path.Join(s.metaRoot, DefaultServiceRoot, prefix), clientv3.WithPrefix(), clientv3.WithPrevKV(), clientv3.WithRev(revision)),
|
||||
prefix: prefix,
|
||||
rewatch: rewatch,
|
||||
validate: func(s *Session) bool { return r(s.Version) },
|
||||
}
|
||||
w.start()
|
||||
return w.eventCh
|
||||
@ -379,6 +485,9 @@ func (w *sessionWatcher) handleWatchResponse(wresp clientv3.WatchResponse) {
|
||||
log.Error("watch services", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if !w.validate(session) {
|
||||
continue
|
||||
}
|
||||
eventType = SessionAddEvent
|
||||
case mvccpb.DELETE:
|
||||
log.Debug("watch services",
|
||||
@ -388,6 +497,9 @@ func (w *sessionWatcher) handleWatchResponse(wresp clientv3.WatchResponse) {
|
||||
log.Error("watch services", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if !w.validate(session) {
|
||||
continue
|
||||
}
|
||||
eventType = SessionDelEvent
|
||||
}
|
||||
log.Debug("WatchService", zap.Any("event type", eventType))
|
||||
|
@ -2,25 +2,35 @@ package sessionutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/milvus-io/milvus/internal/common"
|
||||
etcdkv "github.com/milvus-io/milvus/internal/kv/etcd"
|
||||
"github.com/milvus-io/milvus/internal/log"
|
||||
"github.com/milvus-io/milvus/internal/util/etcd"
|
||||
"github.com/milvus-io/milvus/internal/util/paramtable"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go.etcd.io/etcd/api/v3/mvccpb"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"go.etcd.io/etcd/server/v3/embed"
|
||||
"go.etcd.io/etcd/server/v3/etcdserver/api/v3client"
|
||||
)
|
||||
|
||||
var Params paramtable.BaseTable
|
||||
@ -230,10 +240,11 @@ func TestWatcherHandleWatchResp(t *testing.T) {
|
||||
|
||||
getWatcher := func(s *Session, rewatch Rewatch) *sessionWatcher {
|
||||
return &sessionWatcher{
|
||||
s: s,
|
||||
prefix: "test",
|
||||
rewatch: rewatch,
|
||||
eventCh: make(chan *SessionEvent, 10),
|
||||
s: s,
|
||||
prefix: "test",
|
||||
rewatch: rewatch,
|
||||
eventCh: make(chan *SessionEvent, 10),
|
||||
validate: func(*Session) bool { return true },
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,3 +407,206 @@ func TestSession_String(t *testing.T) {
|
||||
s := &Session{}
|
||||
log.Debug("log session", zap.Any("session", s))
|
||||
}
|
||||
|
||||
func TestSesssionMarshal(t *testing.T) {
|
||||
s := &Session{
|
||||
ServerID: 1,
|
||||
ServerName: "test",
|
||||
Address: "localhost",
|
||||
Version: common.Version,
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
s2 := &Session{}
|
||||
err = json.Unmarshal(bs, s2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.ServerID, s2.ServerID)
|
||||
assert.Equal(t, s.ServerName, s2.ServerName)
|
||||
assert.Equal(t, s.Address, s2.Address)
|
||||
assert.Equal(t, s.Version.String(), s2.Version.String())
|
||||
}
|
||||
|
||||
func TestSessionUnmarshal(t *testing.T) {
|
||||
t.Run("json failure", func(t *testing.T) {
|
||||
s := &Session{}
|
||||
err := json.Unmarshal([]byte("garbage"), s)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("version error", func(t *testing.T) {
|
||||
s := &Session{}
|
||||
err := json.Unmarshal([]byte(`{"Version": "a.b.c"}`), s)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
type SessionWithVersionSuite struct {
|
||||
suite.Suite
|
||||
tmpDir string
|
||||
etcdServer *embed.Etcd
|
||||
|
||||
metaRoot string
|
||||
serverName string
|
||||
sessions []*Session
|
||||
client *clientv3.Client
|
||||
}
|
||||
|
||||
// SetupSuite setup suite env
|
||||
func (suite *SessionWithVersionSuite) SetupSuite() {
|
||||
dir, err := ioutil.TempDir(os.TempDir(), "milvus_ut")
|
||||
suite.Require().NoError(err)
|
||||
suite.tmpDir = dir
|
||||
suite.T().Log("using tmp dir:", dir)
|
||||
|
||||
config := embed.NewConfig()
|
||||
|
||||
config.Dir = os.TempDir()
|
||||
config.LogLevel = "warn"
|
||||
config.LogOutputs = []string{"default"}
|
||||
u, err := url.Parse("http://localhost:0")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
config.LCUrls = []url.URL{*u}
|
||||
u, err = url.Parse("http://localhost:0")
|
||||
suite.Require().NoError(err)
|
||||
config.LPUrls = []url.URL{*u}
|
||||
|
||||
etcdServer, err := embed.StartEtcd(config)
|
||||
suite.Require().NoError(err)
|
||||
suite.etcdServer = etcdServer
|
||||
}
|
||||
|
||||
func (suite *SessionWithVersionSuite) TearDownSuite() {
|
||||
if suite.etcdServer != nil {
|
||||
suite.etcdServer.Close()
|
||||
}
|
||||
if suite.tmpDir != "" {
|
||||
os.RemoveAll(suite.tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *SessionWithVersionSuite) SetupTest() {
|
||||
client := v3client.New(suite.etcdServer.Server)
|
||||
suite.client = client
|
||||
|
||||
ctx := context.Background()
|
||||
suite.metaRoot = "sessionWithVersion"
|
||||
suite.serverName = "sessionComp"
|
||||
|
||||
s1 := NewSession(ctx, suite.metaRoot, client)
|
||||
s1.Version.Major, s1.Version.Minor, s1.Version.Patch = 0, 0, 0
|
||||
s1.Init(suite.serverName, "s1", false, false)
|
||||
s1.Register()
|
||||
|
||||
suite.sessions = append(suite.sessions, s1)
|
||||
|
||||
s2 := NewSession(ctx, suite.metaRoot, client)
|
||||
s2.Version.Major, s2.Version.Minor, s2.Version.Patch = 2, 1, 0
|
||||
s2.Init(suite.serverName, "s2", false, false)
|
||||
s2.Register()
|
||||
|
||||
suite.sessions = append(suite.sessions, s2)
|
||||
|
||||
s3 := NewSession(ctx, suite.metaRoot, client)
|
||||
s3.Version.Major, s3.Version.Minor, s3.Version.Patch = 2, 2, 0
|
||||
s3.Version.Build = []string{"dev"}
|
||||
s3.Init(suite.serverName, "s3", false, false)
|
||||
s3.Register()
|
||||
|
||||
suite.sessions = append(suite.sessions, s3)
|
||||
|
||||
}
|
||||
|
||||
func (suite *SessionWithVersionSuite) TearDownTest() {
|
||||
for _, s := range suite.sessions {
|
||||
s.Revoke(time.Second)
|
||||
}
|
||||
|
||||
suite.sessions = nil
|
||||
_, err := suite.client.Delete(context.Background(), suite.metaRoot, clientv3.WithPrefix())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
if suite.client != nil {
|
||||
suite.client.Close()
|
||||
suite.client = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *SessionWithVersionSuite) TestGetSessionsWithRangeVersion() {
|
||||
s := NewSession(context.Background(), suite.metaRoot, suite.client)
|
||||
|
||||
suite.Run(">1.0.0", func() {
|
||||
r, err := semver.ParseRange(">1.0.0")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
result, _, err := s.GetSessionsWithVersionRange(suite.serverName, r)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(2, len(result))
|
||||
})
|
||||
|
||||
suite.Run(">2.1.0", func() {
|
||||
r, err := semver.ParseRange(">2.1.0")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
result, _, err := s.GetSessionsWithVersionRange(suite.serverName, r)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(1, len(result))
|
||||
})
|
||||
|
||||
suite.Run(">=2.2.0", func() {
|
||||
r, err := semver.ParseRange(">=2.2.0")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
result, _, err := s.GetSessionsWithVersionRange(suite.serverName, r)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(0, len(result))
|
||||
})
|
||||
|
||||
suite.Run(">=0.0.0 with garbage", func() {
|
||||
ctx := context.Background()
|
||||
r, err := semver.ParseRange(">=0.0.0")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.client.Put(ctx, path.Join(suite.metaRoot, DefaultServiceRoot, suite.serverName, "garbage"), "garbage")
|
||||
suite.client.Put(ctx, path.Join(suite.metaRoot, DefaultServiceRoot, suite.serverName, "garbage_1"), `{"Version": "a.b.c"}`)
|
||||
|
||||
_, _, err = s.GetSessionsWithVersionRange(suite.serverName, r)
|
||||
suite.Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *SessionWithVersionSuite) TestWatchServicesWithVersionRange() {
|
||||
s := NewSession(context.Background(), suite.metaRoot, suite.client)
|
||||
|
||||
suite.Run(">1.0.0 <=2.1.0", func() {
|
||||
r, err := semver.ParseRange(">1.0.0 <=2.1.0")
|
||||
suite.Require().NoError(err)
|
||||
|
||||
_, rev, err := s.GetSessionsWithVersionRange(suite.serverName, r)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
ch := s.WatchServicesWithVersionRange(suite.serverName, r, rev, nil)
|
||||
|
||||
// remove all sessions
|
||||
go func() {
|
||||
for _, s := range suite.sessions {
|
||||
s.Revoke(time.Second)
|
||||
}
|
||||
}()
|
||||
|
||||
t := time.NewTimer(time.Second)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case evt := <-ch:
|
||||
suite.Equal(suite.sessions[1].ServerID, evt.Session.ServerID)
|
||||
case <-t.C:
|
||||
suite.Fail("no event received, failing")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSessionWithVersionRange(t *testing.T) {
|
||||
suite.Run(t, new(SessionWithVersionSuite))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user