From 1590ff9a0d4988d132d9ecbd44c46f0ae6905505 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 16 Jun 2026 09:33:38 +0300 Subject: [PATCH] fix(testcontainers): wait for vshard storages to handshake before declaring cluster ready VshardClusterConfigurator#configure() previously stopped at router-up + vshard.router.bootstrap() + crud._VERSION. None of those verify that individual storages finished the vshard handshake, so a CRUD request right after configure() could fail with VHANDSHAKE_NOT_COMPLETE (vshard code 40). Add waitUntilVshardStoragesAreReady: polls vshard.router.info() until every replica is status='available' and info.bucket.unreachable == 0, 120s budget. --- CHANGELOG.md | 1 + .../vshard/VshardClusterConfigurator.java | 2 + .../vshard/VshardClusterContainer.java | 53 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c259d114..df23b953 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 876c7e2d..c75deae9 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 ba1e5ecd..481580d8 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");