Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check if server's and client's versions are compatible #65

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions qdrant/compare_versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package qdrant

import (
"context"
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
"unicode"
)

const fullVersionParts = 3
const reducedVersionParts = 2
const unknownVersion = "Unknown"

type Version struct {
Major int
Minor int
Rest string
}

func getServerVersion(clientConn *GrpcClient) string {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
healthCheckResult, err := clientConn.qdrant.HealthCheck(ctx, &HealthCheckRequest{})
if err != nil {
log.Printf("Unable to get server version: %v, server version defaults to `%s`", err, unknownVersion)
return unknownVersion
}
serverVersion := healthCheckResult.GetVersion()

return serverVersion
}

func removeLeadingNonNumeric(versionStr string) string {
return strings.TrimLeftFunc(versionStr, func(r rune) bool {
return !unicode.IsDigit(r)
})
}

// ParseVersion converts a version string "x.y.z" into a Version struct.
func ParseVersion(versionStr string) (*Version, error) {
cleanedVersionStr := removeLeadingNonNumeric(versionStr)
parts := strings.SplitN(cleanedVersionStr, ".", fullVersionParts)
if len(parts) < reducedVersionParts {
return nil, fmt.Errorf("unable to parse version, expected format: x.y.z, found: %s", cleanedVersionStr)
}

major, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("failed to parse major version: %w", err)
}

minor, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("failed to parse minor version: %w", err)
}

rest := ""
if len(parts) == fullVersionParts {
rest = parts[2]
}

return &Version{
Major: major,
Minor: minor,
Rest: rest,
}, nil
}

func IsCompatible(clientVersion, serverVersion *string) bool {
if *clientVersion == *serverVersion {
return true
}

parsedClientVersion, err := ParseVersion(*clientVersion)
if err != nil {
log.Printf("Unable to compare versions: %v", err)
return false
}
parsedServerVersion, err := ParseVersion(*serverVersion)
if err != nil {
log.Printf("Unable to compare versions: %v", err)
return false
}
majorDiff := int(math.Abs(float64(parsedClientVersion.Major - parsedServerVersion.Major)))
if majorDiff >= 1 {
return false
}
return int(math.Abs(float64(parsedClientVersion.Minor-parsedServerVersion.Minor))) <= 1
}
2 changes: 2 additions & 0 deletions qdrant/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type Config struct {
TLSConfig *tls.Config
// Additional gRPC options to use for the connection.
GrpcOptions []grpc.DialOption
// Whether to check compatibility between server's version and client's. Defaults to false.
SkipCompatibilityCheck bool
}

// Internal method.
Expand Down
22 changes: 20 additions & 2 deletions qdrant/grpc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package qdrant

import (
"fmt"
"log/slog"
"os/exec"
"strings"

Expand Down Expand Up @@ -47,7 +48,24 @@ func NewGrpcClient(config *Config) (*GrpcClient, error) {
return nil, err
}

return NewGrpcClientFromConn(conn), nil
newGrpcClientFromConn := NewGrpcClientFromConn(conn)

if !config.SkipCompatibilityCheck {
serverVersion := getServerVersion(newGrpcClientFromConn)
logger := slog.Default()
if serverVersion == unknownVersion {
logger.Warn("Failed to obtain server version. " +
"Unable to check client-server compatibility. " +
"Set SkipCompatibilityCheck=true to skip version check.")
} else if !IsCompatible(&clientVersion, &serverVersion) {
logger.Warn("Client version is not compatible with server version. "+
"Major versions should match and minor version difference must not exceed 1. "+
"Set SkipCompatibilityCheck=true to skip version check.",
"clientVersion", clientVersion, "serverVersion", serverVersion)
}
}

return newGrpcClientFromConn, nil
}

// Create a new gRPC client from existing connection.
Expand Down Expand Up @@ -96,7 +114,7 @@ func getClientVersion() string {
cmd := exec.Command("go", "list", "-m", "-f", "{{.Version}}", packageName)
output, err := cmd.Output()
if err != nil {
return "Unknown"
return unknownVersion
}
return strings.TrimSpace(string(output))
}
70 changes: 70 additions & 0 deletions qdrant_test/compare_versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package qdrant_test

import (
"testing"

"github.com/qdrant/go-client/qdrant"
)

func TestParseVersion(t *testing.T) {
tests := []struct {
input string
expected *qdrant.Version
hasError bool
}{
{"v1.2.3", &qdrant.Version{Major: 1, Minor: 2, Rest: "3"}, false},
{"1.2.3", &qdrant.Version{Major: 1, Minor: 2, Rest: "3"}, false},
{"v1.2", &qdrant.Version{Major: 1, Minor: 2, Rest: ""}, false},
{"1.2", &qdrant.Version{Major: 1, Minor: 2, Rest: ""}, false},
{"v1.2.3.4", &qdrant.Version{Major: 1, Minor: 2, Rest: "3.4"}, false},
{"", nil, true},
{"1", nil, true},
{"1.", nil, true},
{".1.", nil, true},
{"1.something.1", nil, true},
}

for _, test := range tests {
result, err := qdrant.ParseVersion(test.input)
if (err != nil) != test.hasError {
t.Errorf("ParseVersion(%q) error = %v, wantErr %v", test.input, err, test.hasError)
continue
}
if !test.hasError && result != nil && (result.Major != test.expected.Major ||
result.Minor != test.expected.Minor ||
result.Rest != test.expected.Rest) {
t.Errorf("ParseVersion(%q) = %v, want %v", test.input, result, test.expected)
}
}
}

func TestIsCompatible(t *testing.T) {
tests := []struct {
clientVersion string
serverVersion string
expected bool
}{
{"1.9.3.dev0", "2.8.1.dev12-something", false},
{"1.9", "2.8", false},
{"1", "2", false},
{"1", "1", true},
{"1.9.0", "2.9.0", false},
{"1.1.0", "1.2.9", true},
{"1.2.7", "1.1.8.dev0", true},
{"1.2.1", "1.2.29", true},
{"1.2.0", "1.2.0", true},
{"1.2.0", "1.4.0", false},
{"1.4.0", "1.2.0", false},
{"1.9.0", "3.7.0", false},
{"3.0.0", "1.0.0", false},
}

for _, test := range tests {
clientVersion := test.clientVersion
serverVersion := test.serverVersion
result := qdrant.IsCompatible(&clientVersion, &serverVersion)
if result != test.expected {
t.Errorf("IsCompatible(%q, %q) = %v, want %v", clientVersion, serverVersion, result, test.expected)
}
}
}
Loading