Skip to content

fix(QTDI-2709): TCK processors should send generated records to next connector without buffer#1253

Open
wwang-talend wants to merge 23 commits into
masterfrom
wwang-talend/QTDI-2709-streaming
Open

fix(QTDI-2709): TCK processors should send generated records to next connector without buffer#1253
wwang-talend wants to merge 23 commits into
masterfrom
wwang-talend/QTDI-2709-streaming

Conversation

@wwang-talend

Copy link
Copy Markdown
Contributor

Requirements

  • Any code change adding any logic MUST be tested through a unit test executed with the default build
  • Any API addition MUST be done with a documentation update if relevant

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

  • this PR has been written with the help of GitHub Copilot or another generative AI tool

@wwang-talend wwang-talend left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
  • builderFactory is a protected static mutable field — risky with parallel test execution.

@wwang-talend

Copy link
Copy Markdown
Contributor Author

OutputIterator only for studio, only studio have that case, only need to document it that not suitable for beam env.

@wwang-talend wwang-talend left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@wwang-talend

Copy link
Copy Markdown
Contributor Author

Clarification on DataOutputFactory / JobImpl

The reviewer flagged DataOutputFactory (JobImpl.java:661-681) as another non-DI OutputFactory that would throw ClassCastException.

Analysis: JobImpl with DataOutputFactory is the local chain execution path used by the Job.builder() programmatic API (component-runtime-manager). It is NOT Studio DI and NOT Beam.

However, this API is only referenced in documentation examples — no production usage exists. The real Studio DI runtime uses OutputsHandler which correctly implements both OutputEmitter and OutputIterator.

The OutputIterator javadoc already clarifies it is supported in Studio DI runtime only. This is sufficient — adding adapter code in JobImpl.DataOutputFactory would be unnecessary complexity for a path that will not encounter OutputIterator processors in practice.

@wwang-talend
wwang-talend requested a review from thboileau July 13, 2026 08:33
@wwang-talend

wwang-talend commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use rec for instance ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@wwang-talend wwang-talend Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i revert it as "Record record" is right expression, here i not agree sonar.

@wwang-talend
wwang-talend requested a review from thboileau July 13, 2026 11:05
};
final String name = parameter.getAnnotation(Output.class).value();
if (OutputIterator.class == parameter.getType()) {
return (inputs, outputs) -> (OutputIterator) outputs.create(name);

@thboileau thboileau Jul 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@wwang-talend wwang-talend Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@wwang-talend
wwang-talend requested a review from thboileau July 14, 2026 03:22
thboileau
thboileau previously approved these changes Jul 14, 2026
@wwang-talend

Copy link
Copy Markdown
Contributor Author

about parallize, i not find issue for currernt design for studio part. But need to test

@wwang-talend
wwang-talend requested a review from thboileau July 20, 2026 10:27
* The drain loop on the Studio side uses {@code hasDataFor(name)} for correct
* per-connection checking in both modes:
*
* <pre>

@thboileau thboileau Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
								 */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wwang-talend wwang-talend Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@thboileau thboileau Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I want to say is that documenting outputsHandler.HasMoreData is right in class BaseIOHandler, not MultiOutputIterator.

@wwang-talend wwang-talend Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aha, i will correct the doc, thanks. TODO

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doc fixed

* @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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really wonder if we should drop this in another interface. It's another user case.

@wwang-talend wwang-talend Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that for the case which no need use TaggedOutput, like httpclient. And also sure httpclient can use TaggedOutput.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's why I prefer another interface, like SingleOutputIterator for instance

@wwang-talend wwang-talend Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@thboileau thboileau Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I understand. to me these are two different uses case that could be handled by tho distinct interfaces.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final BaseIOHandler.IO ref = connections.get(name);
final BaseIOHandler.IO ref = connections.get(getActualName(name));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i remove getActualName on purpose, as no meaning, and a equals method in a method called often.

@wwang-talend wwang-talend Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that just convert "default" to "FLOW", connector developer never use "default", that is a hiden thing in connector runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but maybe over consider performance

@thboileau thboileau Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonar-rnd

sonar-rnd Bot commented Jul 22, 2026

Copy link
Copy Markdown

Failed Quality Gate failed

  • 0.00% Coverage on New Code (is less than 80.00%)
  • 12 New Issues (is greater than 0)

Project ID: org.talend.sdk.component:component-runtime

View in SonarQube

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants