client-v2: a self-referential collection in a Dynamic column hangs (array/List) or throws StackOverflowError (Map) instead of a clean exception
Mantenedores costumam responder em até 1 dia
Ninguém assumiu esta issue ainda.
Avaliação
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Facilidade para iniciantes
- 62/100
Direção de pesquisa
Start in client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java, reading valueToColumnForDynamicType and listValue2Column. Reproduce with SerializerUtils.valueToColumnForDynamicType using the cyclic and contrast values, then verify the client insert reproduction against a Dynamic column. Done means cyclic arrays, lists, and maps fail fast with a declared client exception while nested and shared non-cyclic values still work.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
Describe the bug
A value that contains itself (e.g. Object[] a = new Object[1]; a[0] = a;) is not rejected when the client infers the type for a Dynamic column. ClickHouse cannot store a cyclic value, so the insert must fail — but instead of a ClientException, the client either spins forever or throws a bare StackOverflowError:
Value written to Dynamic |
Result |
|---|---|
Object[] a; a[0] = a |
hangs (infinite loop, allocating; no timeout, no exception) |
List<Object> l; l.add(l) |
hangs |
Map<String,Object> m; m.put("self", m) |
java.lang.StackOverflowError |
new Object[]{new Object[]{1,2}} (nested, no cycle) |
OK — inserted (contrast case) |
Object[] x = {1}; new Object[]{x, x} (shared, no cycle) |
OK — Array(Array(Int32)) (contrast case) |
The array/List case is the worse of the two: the call never returns, the calling thread is stuck in a tight loop, and a StringBuilder grows by "Array()" on every iteration, so the JVM heads toward OutOfMemoryError. The insert cannot be bounded with a query timeout because the serialization happens before anything is sent.
ClickHouse server version
Verified against a live server: 26.9.7.9 (HTTP, client-v2). The hang/StackOverflowError happens client-side during serialization, so the server version is not relevant to the failure.
Reproduction
End-to-end through the public insert API (client-v2, reusing the existing test POJO com.clickhouse.client.insert.PojoWithDynamic, which has Object any/Object nullableAny mapped to Dynamic). Each insert runs on a watchdog thread so the hang does not stall the test run:
package com.clickhouse.client.internal;
import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.command.CommandSettings;
import com.clickhouse.client.insert.PojoWithDynamic;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
public class CyclicDynamicE2ETest {
@Test(groups = {"integration"})
public void insertCyclicDynamic() throws Exception {
try (Client client = new Client.Builder()
.addEndpoint("http://localhost:8123")
.setUsername("default").setPassword("")
.compressServerResponse(false).compressClientRequest(false)
.build()) {
CommandSettings cs = new CommandSettings();
cs.serverSetting("allow_experimental_dynamic_type", "1");
client.execute("DROP TABLE IF EXISTS cyclic_dyn_test", cs).get();
client.execute("CREATE TABLE cyclic_dyn_test (rowId Int32, any Dynamic, nullableAny Dynamic) ENGINE = Memory", cs).get();
client.register(PojoWithDynamic.class, client.getTableSchema("cyclic_dyn_test"));
Object[] selfArray = new Object[1];
selfArray[0] = selfArray; // the array contains itself
Map<String, Object> selfMap = new HashMap<>();
selfMap.put("self", selfMap); // the map contains itself
probe(client, "nested array, no cycle (contrast)",
new PojoWithDynamic(3, new Object[]{new Object[]{1, 2}}, null));
probe(client, "self-referential Object[]", new PojoWithDynamic(1, selfArray, null));
probe(client, "self-referential Map", new PojoWithDynamic(2, selfMap, null));
}
}
private static void probe(Client client, String label, PojoWithDynamic pojo) throws Exception {
AtomicReference<String> result = new AtomicReference<>("NO_RESULT");
Thread t = new Thread(() -> {
try {
client.insert("cyclic_dyn_test", Arrays.asList(pojo)).get();
result.set("OK - inserted");
} catch (Throwable e) {
Throwable root = e;
while (root.getCause() != null) root = root.getCause();
result.set("THREW " + e.getClass().getName() + " / root " + root.getClass().getName() + ": " + root.getMessage());
}
});
t.setDaemon(true);
t.start();
t.join(15_000);
System.out.println("### " + label + " -> " + (t.isAlive() ? "HUNG (still running after 15s)" : result.get()));
}
}
Actual output:
### nested array, no cycle (contrast) -> OK - inserted
### self-referential Object[] -> HUNG (still running after 15s)
### self-referential Map -> THREW java.lang.StackOverflowError / root java.lang.StackOverflowError: null
Expected: all three cyclic values fail fast with a ClientException (or another declared client exception) describing the circular reference; the contrast case keeps working.
The same is visible without a server by calling the inference entry point directly — SerializerUtils.valueToColumnForDynamicType(value), each call on a 10s watchdog thread:
### Object[] containing itself -> HUNG (still running after 10s)
### List containing itself -> HUNG (still running after 10s)
### Map containing itself -> THREW java.lang.StackOverflowError: null
### nested no cycle -> RETURNED v Array(Array(Int32))
### shared no cycle -> RETURNED v Array(Array(Int32))
Root cause
client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/SerializerUtils.java — no code path that descends into a container value tracks the values it has already entered:
valueToColumnForDynamicType(SerializerUtils.java:230): theMapbranch (:279-283) calls itself on the first entry's key and value. Form.put("self", m)that is unbounded recursion →StackOverflowError.listValue2Column(SerializerUtils.java:311) walks arrays/lists with an explicitStackinstead of recursion, so it does not overflow — it never terminates. A nested container is pushed back withdepth + 1(:342-343); a self-referential array is pushed again on every pop,maxDepthkeeps increasing, andtypeStr.insert(insertPos, "Array()")(:331) grows the type string without bound. Net effect: a spinning thread plus steadily growing allocation.
This is the Java counterpart of ClickHouse/clickhouse-cs#651 — same defect class, different symptom: the C# client aborts the process with a stack overflow, here the array/List case hangs and the Map case throws a bare Error.
Suggested fix
Bound the descent into container values in SerializerUtils:
- Thread a depth counter (or a reference-identity "visited" set, as the C# client already does for POCO fields) through
valueToColumnForDynamicType'sMapbranch and throughlistValue2Column's stack entries, and throw aClientExceptionwhen the limit is exceeded or a value is re-entered.System.Text.Json's limit of 64 is a reasonable reference point for a depth cap — far above any realistic nesting, so scalar and shallow values pay nothing. - A visited set gives the better message ("circular reference detected"); a depth cap is cheaper on this hot path. Either must keep the contrast cases working: nested containers without a cycle (
new Object[]{new Object[]{1,2}}) and one instance referenced more than once without a cycle (new Object[]{x, x}) — so a visited set has to drop an entry when its scope ends.
Note that listValue2Column needs its own guard even though it is iterative: a limit there is what turns the hang into an exception.
Link
Relayed from ClickHouse/clickhouse-cs#651 — https://github.com/ClickHouse/clickhouse-cs/issues/651
Central tracking issue: ClickHouse/integrations-ai-playground#534
- Linguagem predominante
- Java
- Estrelas
- 1.6k
- Forks
- 649
- Merge médio
- 1d 20h
- PRs com merge (30d)
- 39
Preparar o ambiente
- Sem Dockerfile nem arquivo Docker Compose
- Tem um modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de ClickHouse/clickhouse-java
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 76/100
ClickHouse/clickhouse-java#3349 ·
Mantenedores costumam responder em até 1 dia
-
bug jdbc-v2
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 85/100
ClickHouse/clickhouse-java#3347 ·
Mantenedores costumam responder em até 1 dia
-
bug jdbc-v2
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 82/100
ClickHouse/clickhouse-java#3326 ·
Mantenedores costumam responder em até 1 dia
-
[client-v2] Enum8/Enum16 insert accepts a non-member integer and uses Enum.ordinal(), silently storing values that are not enum membersTalvez já em andamento @jaya232 assumiu há 6 dias. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
ClickHouse/clickhouse-java#3304 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
bug jdbc-v2
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 70/100
ClickHouse/clickhouse-java#3214 ·
Mantenedores costumam responder em até 1 dia
Todas as issues de ClickHouse/clickhouse-java
Issues semelhantes
-
bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
dekobon/big-code-analysis#1913 ·
Mantenedores costumam responder em até 1 dia
-
bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
Mantenedores costumam responder em até 1 dia
-
bug:unconfirmed
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 68/100
UniversalMediaServer/UniversalMediaServer#6473 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 1/5 Menos de uma hora Facilidade para iniciantes 82/100
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 70/100
Mantenedores costumam responder em até 1 dia