Skip to content

Commit

Permalink
Check if server's and client's versions are compatible (#65)
Browse files Browse the repository at this point in the history
* Check if server's and client's versions are compatible

* Use slog

* Address review
  • Loading branch information
tellet-q authored Jan 2, 2025
1 parent 036252c commit 8ed317f
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 2 deletions.
89 changes: 89 additions & 0 deletions qdrant/compare_versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package qdrant

import (
"context"
"fmt"
"log/slog"
"strconv"
"strings"
"time"
"unicode"
)

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

type Version struct {
Major int
Minor int
}

func getServerVersion(clientConn *GrpcClient) string {
logger := slog.Default()
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
healthCheckResult, err := clientConn.qdrant.HealthCheck(ctx, &HealthCheckRequest{})
if err != nil {
logger.Warn("Unable to get server version, use default", "err", err, "default", 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)
}

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

func IsCompatible(clientVersion, serverVersion string) bool {
if clientVersion == serverVersion {
return true
}
logger := slog.Default()
client, err := ParseVersion(clientVersion)
if err != nil {
logger.Warn("Unable to compare versions", "err", err)
return false
}

server, err := ParseVersion(serverVersion)
if err != nil {
logger.Warn("Unable to compare versions", "err", err)
return false
}

if client.Major != server.Major {
return false
}

diff := client.Minor - server.Minor
return diff <= 1 && diff >= -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))
}
69 changes: 69 additions & 0 deletions qdrant_test/compare_versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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}, false},
{"1.2.3", &qdrant.Version{Major: 1, Minor: 2}, false},
{"v1.2", &qdrant.Version{Major: 1, Minor: 2}, false},
{"1.2", &qdrant.Version{Major: 1, Minor: 2}, false},
{"v1.2.3.4", &qdrant.Version{Major: 1, Minor: 2}, 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) {
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)
}
}
}

0 comments on commit 8ed317f

Please sign in to comment.