-
Notifications
You must be signed in to change notification settings - Fork 90
Description
I think a recent change introduced a bug and a null pointer exception in BarrageUtil.java
The bug is triggered by unreleased code in my branch for the C++ client, so it's slightly awkward to repro.
Now, you might think "whatever you're talking about, that is probably the client's fault". However, please consider:
- any NPE on the server side is a bug, regardless of what the client is doing
- I think I know what the problem is
- I have a change explains what I think the solution should look like
On the server I have a table created this way
from deephaven import time_table
t1 = time_table("PT0:00:0.5").select(["Mod2 = ii % 2", "II = ii"]).group_by("Mod2")
When I try to subscribe to it from the client, I see this stack trace on the server:
re-server-1 | 2025-03-31T03:09:15.693Z | heduler-Concurrent-2 | ERROR | i.d.s.s.SessionService | Internal Error '080a7f7b-cb9d-4737-b93e-dd84941ae4f6' java.lang.NullPointerException: Cannot invoke "java.lang.Class.getComponentType()" because "componentType" is null
core-server-1 | at io.deephaven.extensions.barrage.util.BarrageUtil.arrowFieldFor(BarrageUtil.java:1028)
core-server-1 | at io.deephaven.extensions.barrage.util.BarrageUtil.arrowFieldFor(BarrageUtil.java:1027)
core-server-1 | at io.deephaven.extensions.barrage.util.BarrageUtil.lambda$columnDefinitionsToFields$5(BarrageUtil.java:621)
core-server-1 | at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
core-server-1 | at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708)
core-server-1 | at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
core-server-1 | at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
core-server-1 | at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
core-server-1 | at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
core-server-1 | at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
core-server-1 | at io.deephaven.extensions.barrage.util.BarrageUtil.makeSchema(BarrageUtil.java:468)
core-server-1 | at io.deephaven.extensions.barrage.util.BarrageUtil.makeTableSchemaPayload(BarrageUtil.java:453)
core-server-1 | at io.deephaven.server.barrage.BarrageMessageProducer.lambda$propagateSnapshotForSubscription$15(BarrageMessageProducer.java:1683)
core-server-1 | at io.deephaven.extensions.barrage.BarrageMessageWriterImpl$Factory.getSchemaView(BarrageMessageWriterImpl.java:92)
core-server-1 | at io.deephaven.server.barrage.BarrageMessageProducer.propagateSnapshotForSubscription(BarrageMessageProducer.java:1682)
core-server-1 | at io.deephaven.server.barrage.BarrageMessageProducer.updateSubscriptionsSnapshotAndPropagate(BarrageMessageProducer.java:1538)
core-server-1 | at io.deephaven.server.barrage.BarrageMessageProducer$UpdatePropagationJob.run(BarrageMessageProducer.java:1072)
core-server-1 | at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
core-server-1 | at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
core-server-1 | at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
core-server-1 | at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
core-server-1 | at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
core-server-1 | at io.deephaven.server.runner.scheduler.SchedulerModule$ThreadFactory.lambda$newThread$0(SchedulerModule.java:100)
core-server-1 | at org.jpy.PyLib.callAndReturnObject(Native Method)
core-server-1 | at org.jpy.PyObject.call(PyObject.java:444)
core-server-1 | at io.deephaven.server.console.python.DebuggingInitializer.lambda$createInitializer$0(DebuggingInitializer.java:46)
core-server-1 | at java.base/java.lang.Thread.run(Thread.java:1583)
The offending code is in BarrageUtil.arrowFieldFor(), which recursively calls itself if the type is an array or a Vector. I'll copy the whole method here
public static Field arrowFieldFor(
final String name,
final Class<?> type,
final Class<?> componentType,
final Map<String, String> metadata,
final boolean columnAsList) {
List<Field> children = Collections.emptyList();
final FieldType fieldType = arrowFieldTypeFor(type, metadata, columnAsList);
if (fieldType.getType().isComplex()) {
if (type.isArray() || Vector.class.isAssignableFrom(type)) {
children = Collections.singletonList(arrowFieldFor(
"", componentType, componentType.getComponentType(), Collections.emptyMap(), false));
} else {
throw Exceptions.statusRuntimeException(Code.INVALID_ARGUMENT,
"No default mapping for Arrow complex type: " + fieldType.getType());
}
}
return new Field(name, fieldType, children);
}
In the failing case, this method is getting an array of LongVector. So in the outer call, type=LongVector[] and componentType=LongVector
Where this goes wrong is when the method tries to make a recursive call. It calls itself recursively with componentType, componentType.getComponentType(). This feels OK until you note that Java's Type.getComponentType() returns null for any Type is not an array. It does not have any special support for Vector classes.
So, the nested call receives type=LongVector and componentType=null. This nested call wants to recurse again, but the call to componentType.getComponentType() fails with an NPE and here we are.
I think the correct fix is to handle the Vector case separately as in:
if (type.isArray()) {
children = Collections.singletonList(arrowFieldFor(
"", componentType, componentType.getComponentType(), Collections.emptyMap(), false));
} else if (Vector.class.isAssignableFrom(type)) {
return arrowFieldForVectorType(name, type, componentType, metadata);
} else ...