Skip to content

Commit 82584c0

Browse files
authored
Add version check on client init (#61)
* Add version check on client init * Use logger.warn * Fix texts * Address review
1 parent 593a397 commit 82584c0

File tree

4 files changed

+207
-11
lines changed

4 files changed

+207
-11
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ dependencies {
100100

101101
testImplementation "io.grpc:grpc-testing:${grpcVersion}"
102102
testImplementation "org.junit.jupiter:junit-jupiter-api:${jUnitVersion}"
103+
testImplementation "org.junit.jupiter:junit-jupiter-params:${jUnitVersion}"
103104
testImplementation "org.mockito:mockito-core:3.4.0"
104105
testImplementation "org.slf4j:slf4j-nop:${slf4jVersion}"
105106
testImplementation "org.testcontainers:qdrant:${testcontainersVersion}"

src/main/java/io/qdrant/client/QdrantGrpcClient.java

+67-11
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
import io.grpc.Deadline;
55
import io.grpc.ManagedChannel;
66
import io.grpc.ManagedChannelBuilder;
7-
import io.qdrant.client.grpc.CollectionsGrpc;
7+
import io.qdrant.client.grpc.*;
88
import io.qdrant.client.grpc.CollectionsGrpc.CollectionsFutureStub;
9-
import io.qdrant.client.grpc.PointsGrpc;
109
import io.qdrant.client.grpc.PointsGrpc.PointsFutureStub;
11-
import io.qdrant.client.grpc.QdrantGrpc;
1210
import io.qdrant.client.grpc.QdrantGrpc.QdrantFutureStub;
13-
import io.qdrant.client.grpc.SnapshotsGrpc;
1411
import io.qdrant.client.grpc.SnapshotsGrpc.SnapshotsFutureStub;
1512
import java.time.Duration;
1613
import java.util.concurrent.TimeUnit;
@@ -45,7 +42,7 @@ public class QdrantGrpcClient implements AutoCloseable {
4542
* @return a new instance of {@link Builder}
4643
*/
4744
public static Builder newBuilder(ManagedChannel channel) {
48-
return new Builder(channel, false);
45+
return new Builder(channel, false, true);
4946
}
5047

5148
/**
@@ -56,7 +53,21 @@ public static Builder newBuilder(ManagedChannel channel) {
5653
* @return a new instance of {@link Builder}
5754
*/
5855
public static Builder newBuilder(ManagedChannel channel, boolean shutdownChannelOnClose) {
59-
return new Builder(channel, shutdownChannelOnClose);
56+
return new Builder(channel, shutdownChannelOnClose, true);
57+
}
58+
59+
/**
60+
* Creates a new builder to build a client.
61+
*
62+
* @param channel The channel for communication.
63+
* @param shutdownChannelOnClose Whether the channel is shutdown on client close.
64+
* @param checkCompatibility Whether to check compatibility between client's and server's
65+
* versions.
66+
* @return a new instance of {@link Builder}
67+
*/
68+
public static Builder newBuilder(
69+
ManagedChannel channel, boolean shutdownChannelOnClose, boolean checkCompatibility) {
70+
return new Builder(channel, shutdownChannelOnClose, checkCompatibility);
6071
}
6172

6273
/**
@@ -66,7 +77,7 @@ public static Builder newBuilder(ManagedChannel channel, boolean shutdownChannel
6677
* @return a new instance of {@link Builder}
6778
*/
6879
public static Builder newBuilder(String host) {
69-
return new Builder(host, 6334, true);
80+
return new Builder(host, 6334, true, true);
7081
}
7182

7283
/**
@@ -77,7 +88,7 @@ public static Builder newBuilder(String host) {
7788
* @return a new instance of {@link Builder}
7889
*/
7990
public static Builder newBuilder(String host, int port) {
80-
return new Builder(host, port, true);
91+
return new Builder(host, port, true, true);
8192
}
8293

8394
/**
@@ -90,7 +101,23 @@ public static Builder newBuilder(String host, int port) {
90101
* @return a new instance of {@link Builder}
91102
*/
92103
public static Builder newBuilder(String host, int port, boolean useTransportLayerSecurity) {
93-
return new Builder(host, port, useTransportLayerSecurity);
104+
return new Builder(host, port, useTransportLayerSecurity, true);
105+
}
106+
107+
/**
108+
* Creates a new builder to build a client.
109+
*
110+
* @param host The host to connect to.
111+
* @param port The port to connect to.
112+
* @param useTransportLayerSecurity Whether the client uses Transport Layer Security (TLS) to
113+
* secure communications. Running without TLS should only be used for testing purposes.
114+
* @param checkCompatibility Whether to check compatibility between client's and server's
115+
* versions.
116+
* @return a new instance of {@link Builder}
117+
*/
118+
public static Builder newBuilder(
119+
String host, int port, boolean useTransportLayerSecurity, boolean checkCompatibility) {
120+
return new Builder(host, port, useTransportLayerSecurity, checkCompatibility);
94121
}
95122

96123
/**
@@ -168,17 +195,24 @@ public static class Builder {
168195
@Nullable private CallCredentials callCredentials;
169196
@Nullable private Duration timeout;
170197

171-
Builder(ManagedChannel channel, boolean shutdownChannelOnClose) {
198+
Builder(ManagedChannel channel, boolean shutdownChannelOnClose, boolean checkCompatibility) {
172199
this.channel = channel;
173200
this.shutdownChannelOnClose = shutdownChannelOnClose;
201+
String clientVersion = Builder.class.getPackage().getImplementationVersion();
202+
if (checkCompatibility) {
203+
checkVersionsCompatibility(clientVersion);
204+
}
174205
}
175206

176-
Builder(String host, int port, boolean useTransportLayerSecurity) {
207+
Builder(String host, int port, boolean useTransportLayerSecurity, boolean checkCompatibility) {
177208
String clientVersion = Builder.class.getPackage().getImplementationVersion();
178209
String javaVersion = System.getProperty("java.version");
179210
String userAgent = "java-client/" + clientVersion + " java/" + javaVersion;
180211
this.channel = createChannel(host, port, useTransportLayerSecurity, userAgent);
181212
this.shutdownChannelOnClose = true;
213+
if (checkCompatibility) {
214+
checkVersionsCompatibility(clientVersion);
215+
}
182216
}
183217

184218
/**
@@ -238,5 +272,27 @@ private static ManagedChannel createChannel(
238272

239273
return channelBuilder.build();
240274
}
275+
276+
private void checkVersionsCompatibility(String clientVersion) {
277+
try {
278+
String serverVersion =
279+
QdrantGrpc.newBlockingStub(this.channel)
280+
.healthCheck(QdrantOuterClass.HealthCheckRequest.getDefaultInstance())
281+
.getVersion();
282+
if (!VersionsCompatibilityChecker.isCompatible(clientVersion, serverVersion)) {
283+
String logMessage =
284+
"Qdrant client version "
285+
+ clientVersion
286+
+ " is incompatible with server version "
287+
+ serverVersion
288+
+ ". Major versions should match and minor version difference must not exceed 1. "
289+
+ "Set checkCompatibility=false to skip version check.";
290+
logger.warn(logMessage);
291+
}
292+
} catch (Exception e) {
293+
logger.warn(
294+
"Failed to obtain server version. Unable to check client-server compatibility. Set checkCompatibility=false to skip version check.");
295+
}
296+
}
241297
}
242298
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.qdrant.client;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
class Version {
7+
private final int major;
8+
private final int minor;
9+
10+
public Version(int major, int minor) {
11+
this.major = major;
12+
this.minor = minor;
13+
}
14+
15+
public int getMajor() {
16+
return major;
17+
}
18+
19+
public int getMinor() {
20+
return minor;
21+
}
22+
}
23+
24+
/** Utility class to check compatibility between server's and client's versions. */
25+
public class VersionsCompatibilityChecker {
26+
private static final Logger logger = LoggerFactory.getLogger(VersionsCompatibilityChecker.class);
27+
28+
/** Default constructor. */
29+
public VersionsCompatibilityChecker() {}
30+
31+
private static Version parseVersion(String version) throws IllegalArgumentException {
32+
if (version.isEmpty()) {
33+
throw new IllegalArgumentException("Version is None");
34+
}
35+
36+
try {
37+
String[] parts = version.split("\\.");
38+
int major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0;
39+
int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
40+
41+
return new Version(major, minor);
42+
} catch (Exception e) {
43+
throw new IllegalArgumentException(
44+
"Unable to parse version, expected format: x.y[.z], found: " + version, e);
45+
}
46+
}
47+
48+
/**
49+
* Compares server's and client's versions.
50+
*
51+
* @param clientVersion The client's version.
52+
* @param serverVersion The server's version.
53+
* @return True if the versions are compatible, false otherwise.
54+
*/
55+
public static boolean isCompatible(String clientVersion, String serverVersion) {
56+
try {
57+
Version client = parseVersion(clientVersion);
58+
Version server = parseVersion(serverVersion);
59+
60+
if (client.getMajor() != server.getMajor()) return false;
61+
return Math.abs(client.getMinor() - server.getMinor()) <= 1;
62+
63+
} catch (IllegalArgumentException e) {
64+
logger.warn("Version comparison failed: {}", e.getMessage());
65+
return false;
66+
}
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.qdrant.client;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import java.lang.reflect.InvocationTargetException;
7+
import java.lang.reflect.Method;
8+
import java.util.stream.Stream;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.MethodSource;
11+
12+
public class VersionsCompatibilityCheckerTest {
13+
private static Stream<Object[]> validVersionProvider() {
14+
return Stream.of(
15+
new Object[] {"1.2.3", 1, 2},
16+
new Object[] {"1.2.3-alpha", 1, 2},
17+
new Object[] {"1.2", 1, 2},
18+
new Object[] {"1", 1, 0},
19+
new Object[] {"1.", 1, 0});
20+
}
21+
22+
@ParameterizedTest
23+
@MethodSource("validVersionProvider")
24+
public void testParseVersion_validVersion(String versionStr, int expectedMajor, int expectedMinor)
25+
throws Exception {
26+
Method method =
27+
VersionsCompatibilityChecker.class.getDeclaredMethod("parseVersion", String.class);
28+
method.setAccessible(true);
29+
Version version = (Version) method.invoke(null, versionStr);
30+
assertEquals(expectedMajor, version.getMajor());
31+
assertEquals(expectedMinor, version.getMinor());
32+
}
33+
34+
private static Stream<String> invalidVersionProvider() {
35+
return Stream.of("v1.12.0", "", ".1", ".1.", "1.null.1", "null.0.1", null);
36+
}
37+
38+
@ParameterizedTest
39+
@MethodSource("invalidVersionProvider")
40+
public void testParseVersion_invalidVersion(String versionStr) throws Exception {
41+
Method method =
42+
VersionsCompatibilityChecker.class.getDeclaredMethod("parseVersion", String.class);
43+
method.setAccessible(true);
44+
assertThrows(InvocationTargetException.class, () -> method.invoke(null, versionStr));
45+
}
46+
47+
private static Stream<Object[]> versionCompatibilityProvider() {
48+
return Stream.of(
49+
new Object[] {"1.9.3.dev0", "2.8.1.dev12-something", false},
50+
new Object[] {"1.9", "2.8", false},
51+
new Object[] {"1", "2", false},
52+
new Object[] {"1.9.0", "2.9.0", false},
53+
new Object[] {"1.1.0", "1.2.9", true},
54+
new Object[] {"1.2.7", "1.1.8.dev0", true},
55+
new Object[] {"1.2.1", "1.2.29", true},
56+
new Object[] {"1.2.0", "1.2.0", true},
57+
new Object[] {"1.2.0", "1.4.0", false},
58+
new Object[] {"1.4.0", "1.2.0", false},
59+
new Object[] {"1.9.0", "3.7.0", false},
60+
new Object[] {"3.0.0", "1.0.0", false},
61+
new Object[] {"", "1.0.0", false},
62+
new Object[] {"1.0.0", "", false},
63+
new Object[] {"", "", false});
64+
}
65+
66+
@ParameterizedTest
67+
@MethodSource("versionCompatibilityProvider")
68+
public void testIsCompatible(String clientVersion, String serverVersion, boolean expected) {
69+
assertEquals(expected, VersionsCompatibilityChecker.isCompatible(clientVersion, serverVersion));
70+
}
71+
}

0 commit comments

Comments
 (0)