fix(QTDI-2709): TCK processors should send generated records to next connector without buffer#1253
fix(QTDI-2709): TCK processors should send generated records to next connector without buffer#1253wwang-talend wants to merge 12 commits into
Conversation
wwang-talend
left a comment
There was a problem hiding this comment.
Code Review — PR #1253
🔴 Critical: ClassCastException in non-DI contexts
ProcessorImpl.buildProcessParamBuilder() and toOutputParamBuilder() cast outputs.create(name) to OutputIterator:
// ProcessorImpl.java:156
return (inputs, outputs) -> (OutputIterator) outputs.create(name);But OutputFactory.create() returns OutputEmitter. Only the DI/Studio OutputsHandler returns objects implementing both interfaces. Two other OutputFactory implementations do NOT implement OutputIterator:
BeamOutputFactory/BeamOutputEmitter(BaseProcessorFn.java:162-176)DataOutputFactory/OutputEmitterImpl(JobImpl.java:661-681)
Any processor using @Output OutputIterator<Record> in a Beam runner or via JobImpl chain will throw ClassCastException at runtime.
Suggested fix: Either update OutputFactory to provide OutputIterator support (e.g., a default method), or have ProcessorImpl wrap the OutputEmitter in an adapter, or update all OutputFactory implementations.
🟡 Design: Implicit contract not enforced by type system
The OutputFactory interface declares OutputEmitter create(String name). This PR adds an implicit requirement that returned objects also implement OutputIterator, but nothing enforces this. Future OutputFactory implementations will hit the same ClassCastException silently at runtime.
🟡 Raw types throughout
OutputEmitterWithIterator implements raw OutputEmitter and OutputIterator (no type parameters). setIterator() takes raw Iterator. This bypasses generic type safety and produces compiler warnings.
🟡 Silent exception swallowing
BaseIOHandler.IO.closeSource() catches all exceptions with an empty block:
} catch (final Exception e) {
// best effort cleanup
}Consider at minimum logging at DEBUG/TRACE level to avoid hiding bugs during iterator cleanup.
🟡 Test reliability concerns
- Memory-based assertions using
System.gc()+Runtime.freeMemory()are non-deterministic. The 5MB threshold could be flaky across JVMs, GC algorithms, or CI environments. builderFactoryis aprotected staticmutable field — risky with parallel test execution.
|
OutputIterator only for studio, only studio have that case, only need to document it that not suitable for beam env. |
wwang-talend
left a comment
There was a problem hiding this comment.
Response to AI Review
🔴 ClassCastException in non-DI contexts
Valid observation. However, this is by design — the OutputIterator pattern was built specifically for the Studio DI runtime to solve the single-threaded buffering problem.
As stated in the spec: "The impact of this pattern on Cloud is minimal. In the Cloud, processors are not TCK processors (usually dataprep functions)."
No real-world processor will use @Output OutputIterator<T> in a Beam pipeline. Adding adapter/fallback code in BeamOutputFactory / JobImpl would add unnecessary complexity for a use case that does not exist.
Action taken: Added explicit javadoc on OutputIterator documenting that it is Studio DI only and not supported in Beam runners.
🟡 Implicit contract not enforced by type system
Accepted trade-off. We control all OutputFactory implementations. The ModelVisitor validates processor declarations at plugin load time, so misuse surfaces early. A default method on OutputFactory would be cleaner but is a bigger change with no practical benefit given the Studio-only scope.
🟡 Raw types
Fair point. Will address in a follow-up cleanup.
🟡 Silent exception swallowing
Agreed. Will add logging in the catch block.
🟡 Test reliability
The memory test is intentionally designed as a manual/observable test — it prints memory stats so developers can see the delta. The 5MB threshold is generous (actual delta is ~1-2MB with iterator mode). The static builderFactory pattern is consistent with existing tests (DIBulkAutoChunkTest, DIBatchSimulationTest).
Clarification on
|
…e support limitations
|
sonar often report some small smell in tests, not worth to work on it even, should care on the major things we need to care thing, not that smell even in junit tests only. Every thing good smell, but bug appear. That is a kind of context go out like AI. |
| return null; | ||
| } else if (value instanceof javax.json.JsonValue) { | ||
| return jsonb.fromJson(value.toString(), ref.getType()); | ||
| } else if (value instanceof Record record) { |
| }; | ||
| final String name = parameter.getAnnotation(Output.class).value(); | ||
| if (OutputIterator.class == parameter.getType()) { | ||
| return (inputs, outputs) -> (OutputIterator) outputs.create(name); |
There was a problem hiding this comment.
That only works because the one concrete implementation, OutputsHandler.OutputEmitterWithIterator, happens to implement both interfaces. Nothing in OutputFactory's signature says a factory may hand back something iterator-capable.
One solution is to make the capability explicit in the OutputFactory : a default OutputIterator createIterator(String name) that throws a clear UnsupportedOperationException by default, except in the case of the OutputsHandler class.
The default implementation of createIterator allows retro-compatibility
public interface OutputFactory {
OutputEmitter create(String name);
default OutputIterator createIterator(String name) {
throw new UnsupportedOperationException();
}
}In OutputsHandler:
public OutputFactory asOutputFactory() {
return new OutputFactory() {
@Override
public OutputEmitter create(final String name) {
final BaseIOHandler.IO ref = connections.get(getActualName(name));
return value -> {
if (ref != null && value != null) {
ref.add(convert(value, ref));
}
};
}
@Override
public OutputIterator createIterator(final String name) {
final BaseIOHandler.IO ref = connections.get(getActualName(name));
return iterator -> {
if (ref == null) {
return;
}
ref.setSource(new Iterator() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Object next() {
return convert(iterator.next(), ref);
}
});
};
}
};
}What do you think about this proposal?
There was a problem hiding this comment.
Hi @thboileau , thanks for the review.
OutputFactory is a public interface, but not so public as only studio common javajet use it. So from the history regression risk for future view, i think your idea is ok, which add a new interface method.
My origin idea is not change interface, keep complex in implement.
So i will consider your advice as it look good, but give me some times to prove your idea is real better.
|
|
||
| private final Class<T> type; | ||
|
|
||
| private Iterator<T> source; |
There was a problem hiding this comment.
I feel we need more documentation here.
First, the IO class is shared between the InputsHandler and the OutputsHandler classes, and implements a "push-or-pull" behavior which is only used by the OutputsHandler. People may read InputsHandler and wonder why "source" is never used. We should document it.
Secondly, as we know that "push-or-pull" is actually exclusive, we could think of a better design that clearly separates these two behaviors, but I don't how, I'm not smart enough. When IO is created, there is no notion of "push" or "pull", just the name of the connection. So documentation would help here
What do you think about this?
There was a problem hiding this comment.
about refactor, no code compatiable risk for future as that is not so public to client user, we can do later. So agree document enough now.
… in component structure
| return type; | ||
| } | ||
|
|
||
| private void closeSource() { |
There was a problem hiding this comment.
the autoclose not work yet, except we call that .reset() method some where in studio common javajet, but in fact, not. So i may remove this part.
There was a problem hiding this comment.
for httpclient==>
every input record==> make M(bigger enough to use streaming feature here) output records
we may need to use this autoclose to release the resource for pre streaming object asap? not wait on @PreDestory close method.
But studio allow user not draw output line even allow. So for safe, better to close in @PreDestory too. Above one for "close asap", @PreDestory for "close finally even outside exception happen and so on"
There was a problem hiding this comment.
so keep it, not public and not make code refactor complex in future.
|
about parallize, i not find issue for currernt design for studio part. But need to test |
This reverts commit 25264bc.

Requirements
Why this PR is needed?
https://qlik-dev.atlassian.net/browse/QTDI-2709
What does this PR adds (design/code thoughts)?
https://qlik-dev.atlassian.net/browse/QTDI-2709
AI generated code
https://internal.qlik.dev/general/ways-of-working/code-reviews/#guidelines-for-ai-generated-code