diff --git a/CHANGELOG.md b/CHANGELOG.md index c259d11..df23b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add constructor/builder parameters to supply the initial Lua script as a string or as a file path, and optional additional script paths copied into the container data directory (`Tarantool2Container`, `CartridgeClusterContainer`, `VshardClusterContainer`); simplify bundled `server.lua` accordingly. - Upgrade TQE to v3.5.0. - Extract `ObjectMapper` to a static field in the test `tdg.Utils` helper to avoid recreating it on every `sendUsers`/`getUsers` call. +- Wait for vshard storages to complete the handshake (`vshard.router.info()` reports every replica as `available` and no unreachable buckets) before declaring a `VshardClusterContainer` ready, preventing intermittent `VHANDSHAKE_NOT_COMPLETE` CRUD failures right after bootstrap. ### Documentation diff --git a/testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterConfigurator.java b/testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterConfigurator.java index 876c7e2..c75deae 100644 --- a/testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterConfigurator.java +++ b/testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterConfigurator.java @@ -47,6 +47,8 @@ public void configure() { this.container.waitUntilVshardIsBootstrapped( VshardClusterContainer.TIMEOUT_VSHARD_BOOTSTRAP_IN_SECONDS); this.container.waitUntilCrudIsUp(VshardClusterContainer.TIMEOUT_CRUD_HEALTH_IN_SECONDS); + this.container.waitUntilVshardStoragesAreReady( + VshardClusterContainer.TIMEOUT_VSHARD_STORAGES_READY_IN_SECONDS); this.configured.set(true); } diff --git a/testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterContainer.java b/testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterContainer.java index ba1e5ec..481580d 100644 --- a/testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterContainer.java +++ b/testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterContainer.java @@ -65,6 +65,16 @@ public class VshardClusterContainer extends GenericContainer 0 then return false end;" + + " return true"; private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); public static final String ENV_TARANTOOL_VERSION = "TARANTOOL_VERSION"; @@ -82,6 +92,7 @@ public class VshardClusterContainer extends GenericContainer waitFunc) { int secondsPassed = 0; boolean result = waitFunc.get(); @@ -629,6 +657,31 @@ protected boolean vshardIsBootstrapped() { } } + /** + * Returns {@code true} when {@code vshard.router.info()} reports every replica as {@code + * available} and {@code info.bucket.unreachable == 0}, i.e. the vshard handshake has completed + * for all storages. See {@link #waitUntilVshardStoragesAreReady(int)} for rationale. + */ + protected boolean vshardStoragesAreReady() { + try { + List result = + TarantoolContainerClientHelper.executeCommandDecoded( + this, VSHARD_STORAGES_READY_COMMAND, null); + if (result.isEmpty()) { + logger().warn("Vshard storages readiness probe returned an empty response"); + return false; + } + boolean ready = Boolean.TRUE.equals(result.get(0)); + if (!ready) { + logger().warn("Vshard storages are not handshaked yet"); + } + return ready; + } catch (Exception e) { + logger().warn("Vshard storages readiness probe failed: {}", e.getMessage()); + return false; + } + } + protected String getFileName(String filePath) { if (filePath == null || filePath.isBlank()) { throw new IllegalArgumentException("File path must not be null or empty");