Skip to content

Commit b030057

Browse files
opencodedkasimovskiy
authored andcommitted
fix(testcontainers): wait for vshard storages to complete handshake before declaring cluster ready
VshardClusterConfigurator#configure() previously declared the cluster ready after three checks: router is up, vshard.router.bootstrap() returns cleanly, and crud._VERSION is reachable on the router. None of these verifies that individual storages have completed the vshard handshake during the initial rebalance; the router can answer 'bootstrap ok' while some storages are still in the VHANDSHAKE_NOT_COMPLETE (code 40) state, and a CRUD request that targets such a storage fails immediately. Observed in tests-crud-integration (3.5.0): TarantoolTemplateViaJavaConfigTest.testKVTemplateDeleteEntities -> CrudException: Failed to truncate for storage-002 VHANDSHAKE_NOT_COMPLETE, 'Handshake with storage-002-a have not been completed yet' Add a fourth readiness step: waitUntilVshardStoragesAreReady polls vshard.router.info() until every replica in every replicaset is in status='available' and there are no unreachable buckets, with a 120s budget. This guarantees that any subsequent CRUD request hits a fully handshaked storage. Verified locally on 3.5.0: - TarantoolCrudClientTest: 347/347 PASS (was failing with VHANDSHAKE_NOT_COMPLETE in CI) - TarantoolTemplateViaJavaConfigTest (spring-data27, the originally failing test): 35/35 PASS This is a testcontainers/infra change; it is added to the same PR as the pooling fixups because both were investigated together after the box-integration deadlocks were resolved.
1 parent c76d799 commit b030057

3 files changed

Lines changed: 56 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- 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.
1515
- Upgrade TQE to v3.5.0.
1616
- Extract `ObjectMapper` to a static field in the test `tdg.Utils` helper to avoid recreating it on every `sendUsers`/`getUsers` call.
17+
- 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.
1718

1819
### Documentation
1920

testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterConfigurator.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public void configure() {
4747
this.container.waitUntilVshardIsBootstrapped(
4848
VshardClusterContainer.TIMEOUT_VSHARD_BOOTSTRAP_IN_SECONDS);
4949
this.container.waitUntilCrudIsUp(VshardClusterContainer.TIMEOUT_CRUD_HEALTH_IN_SECONDS);
50+
this.container.waitUntilVshardStoragesAreReady(
51+
VshardClusterContainer.TIMEOUT_VSHARD_STORAGES_READY_IN_SECONDS);
5052
this.configured.set(true);
5153
}
5254

testcontainers/src/main/java/org/testcontainers/containers/cluster/vshard/VshardClusterContainer.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ public class VshardClusterContainer extends GenericContainer<VshardClusterContai
6565
protected static final String VSHARD_BOOTSTRAP_COMMAND =
6666
"return require('vshard').router.bootstrap({if_not_bootstrapped = true})";
6767
protected static final String ROUTER_HEALTH_COMMAND = "return box.info.status";
68+
protected static final String VSHARD_STORAGES_READY_COMMAND =
69+
"local info = require('vshard').router.info();"
70+
+ " for _, rs in pairs(info.replicasets or {}) do"
71+
+ " if rs.master == nil or rs.master == box.NULL then return false end;"
72+
+ " for _, r in pairs(rs.replicas or {}) do"
73+
+ " if r.status == nil or r.status ~= 'available' then return false end;"
74+
+ " end;"
75+
+ " end;"
76+
+ " if info.bucket and (info.bucket.unreachable or 0) > 0 then return false end;"
77+
+ " return true";
6878
private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());
6979

7080
public static final String ENV_TARANTOOL_VERSION = "TARANTOOL_VERSION";
@@ -82,6 +92,7 @@ public class VshardClusterContainer extends GenericContainer<VshardClusterContai
8292
protected static final int TIMEOUT_CRUD_HEALTH_IN_SECONDS = 60;
8393
protected static final int TIMEOUT_ROUTER_HEALTH_IN_SECONDS = 90;
8494
protected static final int TIMEOUT_VSHARD_BOOTSTRAP_IN_SECONDS = 90;
95+
protected static final int TIMEOUT_VSHARD_STORAGES_READY_IN_SECONDS = 120;
8596
protected static final int TIMEOUT_CONTAINER_START_IN_SECONDS = 600;
8697

8798
protected final String TARANTOOL_RUN_DIR;
@@ -557,6 +568,23 @@ protected void waitUntilVshardIsBootstrapped(int secondsToWait) {
557568
}
558569
}
559570

571+
/**
572+
* Waits until every vshard replica is in the {@code available} state (i.e. the vshard handshake
573+
* with each storage has completed) and there are no unreachable buckets. {@link
574+
* #vshardIsBootstrapped()} only verifies that {@code vshard.router.bootstrap} returned cleanly,
575+
* which does not preclude individual storages from still being in {@code VHANDSHAKE_NOT_COMPLETE}
576+
* (vshard code 40) during the initial rebalance. Issuing a CRUD request against such a cluster
577+
* fails immediately, so the readiness check must additionally inspect the per-replica status
578+
* surfaced by {@code vshard.router.info()}.
579+
*/
580+
protected void waitUntilVshardStoragesAreReady(int secondsToWait) {
581+
if (!waitUntilTrue(secondsToWait, this::vshardStoragesAreReady)) {
582+
throw new RuntimeException(
583+
"Timeout exceeded while waiting for vshard storages to complete handshake."
584+
+ " See the specific error in logs.");
585+
}
586+
}
587+
560588
protected boolean waitUntilTrue(int secondsToWait, Supplier<Boolean> waitFunc) {
561589
int secondsPassed = 0;
562590
boolean result = waitFunc.get();
@@ -629,6 +657,31 @@ protected boolean vshardIsBootstrapped() {
629657
}
630658
}
631659

660+
/**
661+
* Returns {@code true} when {@code vshard.router.info()} reports every replica as {@code
662+
* available} and {@code info.bucket.unreachable == 0}, i.e. the vshard handshake has completed
663+
* for all storages. See {@link #waitUntilVshardStoragesAreReady(int)} for rationale.
664+
*/
665+
protected boolean vshardStoragesAreReady() {
666+
try {
667+
List<?> result =
668+
TarantoolContainerClientHelper.executeCommandDecoded(
669+
this, VSHARD_STORAGES_READY_COMMAND, null);
670+
if (result.isEmpty()) {
671+
logger().warn("Vshard storages readiness probe returned an empty response");
672+
return false;
673+
}
674+
boolean ready = Boolean.TRUE.equals(result.get(0));
675+
if (!ready) {
676+
logger().warn("Vshard storages are not handshaked yet");
677+
}
678+
return ready;
679+
} catch (Exception e) {
680+
logger().warn("Vshard storages readiness probe failed: {}", e.getMessage());
681+
return false;
682+
}
683+
}
684+
632685
protected String getFileName(String filePath) {
633686
if (filePath == null || filePath.isBlank()) {
634687
throw new IllegalArgumentException("File path must not be null or empty");

0 commit comments

Comments
 (0)