Rion Williams created FLINK-40113:
-------------------------------------

             Summary: State restore does not validate max-parallelism agreement 
between operators sharing a vertex
                 Key: FLINK-40113
                 URL: https://issues.apache.org/jira/browse/FLINK-40113
             Project: Flink
          Issue Type: Improvement
          Components: Runtime / Checkpointing, Runtime / State Backends
            Reporter: Rion Williams


h2. Summary

[FLINK-31996|https://issues.apache.org/jira/projects/FLINK/issues/FLINK-31996] 
allowed operators with different max parallelism to be chained into a single 
vertex, and made it the default 
(`pipeline.operator-chaining.chain-operators-with-different-max-parallelism`). 
On restore, Flink validates that an operator's max parallelism matches the 
savepoint's and rejects an incompatible change with a clear error but that 
validation is per-operator against the vertex's single max parallelism. It does 
not verify that the operators now sharing a vertex have compatible max 
parallelism with each other. When they don't, the per-vertex reconciliation 
silently adopts one operator's value instead of rejecting.
h2. Current behavior
 - A single operator whose max parallelism changed since the savepoint → 
rejected: "{_}Max parallelism mismatch … changed in a non-compatible way."{_} 
(Correct.)
 - Multiple operators chained into one vertex with different recorded max 
parallelism into `StateAssignmentOperation#checkParallelismPreconditions` 
whichreconciles each against the shared vertex and adopts one value; operators 
whose recorded value differs are accepted under the adopted key-group count 
with {*}no equivalent check{*}.

h2. The gap

`checkParallelismPreconditions` checks each operator against the vertex's max 
parallelism (and `canRescaleMaxParallelism` correctly permits rescaling an 
auto-derived value), but nothing checks that the operators sharing the vertex 
agree with one another. 
[FLINK-31996|https://issues.apache.org/jira/projects/FLINK/issues/FLINK-31996] 
made this arrangement reachable by default; the restore-side validation was not 
extended to cover it. The single-operator path is guarded; the shared-vertex 
path is not.
h2. Why it matters

A keyed operator can then be restored under a key-group count different from 
the one its state was written with, so its state is mapped through a different 
`hash % maxParallelism` layout. The restore should reject this, consistent with 
how it already rejects the single-operator case, rather than proceeding.
h2. Proposed change (validation-only, low-risk)

Extend the restore validation to cover the chained-operators case FLINK-31996 
previously introduced: when operators sharing a vertex have incompatible max 
parallelism, fail with a clear error (mirroring the existing single-operator 
message) instead of silently adopting one. This only rejects a configuration 
that is {*}already restored incorrectly today{*}; behavior for valid jobs is 
unchanged.

Roughly, in the per-vertex reconciliation 
(`StateAssignmentOperation#checkParallelismPreconditions`) we would perform 
something _like_ the following:
{code:java}
// Operators assigned to one vertex must have been checkpointed with the same 
max parallelism,
// or a keyed operator would be restored under a different key-group count than 
its state was
// written with.
Set<Integer> maxParallelisms = operatorStates.stream()   // operators carrying 
keyed state
        .map(OperatorState::getMaxParallelism)
        .collect(Collectors.toSet());
if (maxParallelisms.size() > 1) {
    throw new IllegalStateException(String.format(
        "Operators chained into %s were checkpointed with different max 
parallelisms %s; "
        + "restoring would change a keyed operator's key-group count.",
        executionJobVertex, maxParallelisms));
}{code}
h2. Reproduction

With parallelism = 1 and using default config 
(`chain-operators-with-different-max-parallelism = true`):
{code:java}
DataStreamSource<Long> src = env.fromSequence(1, 200);   // chain head; max 
parallelism auto-derived (128)

// reinterpretAsKeyedStream puts a forward (chainable) edge before the keyed 
operator,
// so it can be chained as a NON-head.
DataStreamUtils.reinterpretAsKeyedStream(src, key -> key % 4)
        .process(new PerKeyCounter())        // keyed ValueState<Long> counter
        .uid("keyed")
        .setMaxParallelism(64)               // explicit — differs from the 
head's 128
        .addSink(...);
{code}
1. Run with `env.disableOperatorChaining()`, process some records, take a 
savepoint → the keyed operator is its own vertex with max parallelism 64; its 
state is written into 64 key groups.
2. Restore into the same job without disabling chaining → source and keyed 
operator chain into one vertex, which adopts the head's max parallelism 128; 
the operator's configured 64 is silently discarded.
3. The keyed state (written at 64 key groups) is read at 128 → per-key state 
isn't found and counters restart from zero. No exception is raised.

With `setMaxParallelism(256)` (larger than the head's 128) instead, the same 
setup fails the restore loudly with IllegalStateException: The key group must 
belong to the backend — same cause, opposite direction.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to