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 23 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) { |
There was a problem hiding this comment.
i revert it as "Record record" is right expression, here i not agree sonar.
| }; | ||
| 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.
… in component structure
|
about parallize, i not find issue for currernt design for studio part. But need to test |
This reverts commit 25264bc.
…r in documentation and tests
…and improve split streaming tests
…related processing logic
…multiple output values
| * The drain loop on the Studio side uses {@code hasDataFor(name)} for correct | ||
| * per-connection checking in both modes: | ||
| * | ||
| * <pre> |
There was a problem hiding this comment.
I think that this part does not relate directly to the MultiIterator.
It's part of the API of BaseIOHandler class, can be implemented based on the current state of BaseIOHandler (ie.: with only the support of OutputEmitter). It's a nice replacement of the if (row2 != null) { that we can find in the generated code.
while (outputHandler_tDateFormatValidatorValidator_1.hasMoreData()) {
nbLineOutput_tDateFormatValidatorValidator_1++;
row2 = outputHandler_tDateFormatValidatorValidator_1.getValue("FLOW");
row3 = outputHandler_tDateFormatValidatorValidator_1.getValue("reject");
/**
* [tDateFormatValidatorValidator_1 process_data_begin ] stop
*/
// Start of branch "row2"
if (row2 != null) {
/**
* [tLogRow_1 main ] start
*/
There was a problem hiding this comment.
There was a problem hiding this comment.
about this code generation, the patterns :
pattern 1: one input record come ==> dispatch output record, go output 1(main) or go output 2(reject) or output xxx, but only go unique output x.
pattern 2: any, group or not group, or one by one ==> output x records to output 1(main), output y to output 2(reject), z to output 3...,x, y, z value even no relationship with input records number
About pattern 1, the code generation above is right.
About pattern 2, the code generation above is strange.
TODO desc more
There was a problem hiding this comment.
What I want to say is that documenting outputsHandler.HasMoreData is right in class BaseIOHandler, not MultiOutputIterator.
There was a problem hiding this comment.
aha, i will correct the doc, thanks. TODO
| * @param outputName the output connection name (e.g. {@code "MAIN"}, {@code "REJECT"}) | ||
| * @param iterator the lazy iterator producing records for that connection | ||
| */ | ||
| void setIterator(String outputName, Iterator<T> iterator); |
There was a problem hiding this comment.
I really wonder if we should drop this in another interface. It's another user case.
There was a problem hiding this comment.
that for the case which no need use TaggedOutput, like httpclient. And also sure httpclient can use TaggedOutput.
There was a problem hiding this comment.
yes, that's why I prefer another interface, like SingleOutputIterator for instance
There was a problem hiding this comment.
image one day:
no input==>httpclient ==> main
==> reject
==> xxxx
a number input==>httpclient ==> x number main
==> y reject
==> z xxxx
a value no relationship with x + y + z and so on.
MultiOutputIterator support one channel too. But SingleOutputIterator don't support multi channel.
There was a problem hiding this comment.
yes, I understand. to me these are two different uses case that could be handled by tho distinct interfaces.
There was a problem hiding this comment.
nb: let's start with your solution. I propose to discuss the topic with Yves and Manu when they will be back.
| } | ||
| final TaggedOutput<T> tagged = iterator.next(); | ||
| final String name = tagged.getOutputName(); | ||
| final BaseIOHandler.IO ref = connections.get(name); |
There was a problem hiding this comment.
| final BaseIOHandler.IO ref = connections.get(name); | |
| final BaseIOHandler.IO ref = connections.get(getActualName(name)); |
There was a problem hiding this comment.
i remove getActualName on purpose, as no meaning, and a equals method in a method called often.
There was a problem hiding this comment.
that just convert "default" to "FLOW", connector developer never use "default", that is a hiden thing in connector runtime.
There was a problem hiding this comment.
but maybe over consider performance
There was a problem hiding this comment.
in this case, shouldn't we drop the other references to getActualName ? Either we keep getActualName or we drop it. we can't be in the middle because it makes maintenance difficult.
… enhanced output handling

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