[ 
https://issues.apache.org/jira/browse/SPARK-58904?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Jungtaek Lim updated SPARK-58904:
---------------------------------
    Description: 
h3. Description

For stream-stream joins with event-time range conditions, the analyzer may 
accept a one-sided watermark because it can derive a state-value watermark for 
the opposite input from the join condition.

Execution does not always implement that accepted plan. 
{{getOneSideStateWatermarkPredicate}} only creates a state-value eviction 
predicate when the state side itself contains an attribute marked with 
watermark metadata. When only the opposite input is
watermarked, no predicate is created for the state that should be evicted.

This affects the following directions:
||Join type||Watermarked input||State that should be evicted||Effect||
|Left outer|Right|Left|Unmatched left rows are never emitted|
|Left semi|Right|Left|Never-matched left rows remain indefinitely|
|Right outer|Left|Right|Unmatched right rows are never emitted|
|Full outer|Right|Left|Unmatched left rows are never emitted|
|Full outer|Left|Right|Unmatched right rows are never emitted|

For full outer joins, one-sided watermarking is intentional: it should finalize 
unmatched rows from the opposite side. A watermark on both inputs is needed to 
finalize both directions and clean up both state stores. The bug is that the 
range-condition
implementation does not honor the supported one-sided case.

The state-key watermark path, where event time or a window is part of the 
equi-join keys, is not covered by this issue.
h3. Reproducer

The following test uses a right-side watermark and a range condition. Once the 
right watermark passes the latest possible matching time for the left row, the 
left outer join should emit that row with a null right side.
{code:java}
  test("left outer range join with only right watermark evicts left state") {
    val leftInput = MemoryStream[(Int, Int)]
    val rightInput = MemoryStream[(Int, Int)]

    val left = leftInput.toDF().toDF("leftKey", "time")
      .select(
        $"leftKey",
        timestamp_seconds($"time").as("leftTime"))

    val right = rightInput.toDF().toDF("rightKey", "time")
      .select(
        $"rightKey",
        timestamp_seconds($"time").as("rightTime"))
      .withWatermark("rightTime", "10 seconds")

    val joined = left.join(
      right,
      expr(
        """
          |leftKey = rightKey AND
          |leftTime BETWEEN
          |  rightTime - INTERVAL 5 SECONDS AND
          |  rightTime + INTERVAL 5 SECONDS
        """.stripMargin),
      "left_outer")
      .select(
        $"leftKey",
        $"rightKey",
        $"leftTime".cast("long"),
        $"rightTime".cast("long"))

    testStream(joined)(
      AddData(leftInput, (1, 5)),
      CheckNewAnswer(),

      // Advances the right watermark to 20.
      AddData(rightInput, (2, 30)),
      CheckNewAnswer(),

      // Runs another batch using watermark 20. A future right row that
      // could match leftTime=5 would have rightTime in [0, 10], so the
      // left row can now be finalized.
      AddData(rightInput, (2, 40)),

      // Fails because the query produces no output.
      CheckNewAnswer(Row(1, null, 5, null))
    )
  }
  {code}
Changing the join type to {{full_outer}} reproduces the same missing 
unmatched-left output. Swapping the inputs and using {{right_outer}} reproduces 
the symmetric problem.
h3. Expected behavior

For an accepted range join, execution should construct the state-value eviction 
predicate implied by the opposite input's watermark and the range condition.

In the reproducer, the left row should be emitted as:
{noformat}
  Row(1, null, 5, null)
  {noformat}
h3. Actual behavior

The query passes analysis, but no left-state eviction predicate is created. The 
left row remains in state and its unmatched outer result is never emitted.

For left semi joins, the equivalent failure causes never-matched left rows to 
accumulate rather than causing missing output.
h3. Notes

[PR #58238|https://github.com/apache/spark/pull/58238] attempted a broader 
analyzer restriction but was closed for further design review. The fix should 
preserve the documented one-sided watermark semantics or explicitly redefine 
them; analysis and execution must agree.

  was:
  h3. Description

  For stream-stream joins with event-time range conditions, the analyzer may 
accept a one-sided watermark because it can derive a state-value watermark for 
the opposite input from the join condition.

  Execution does not always implement that accepted plan. 
{{getOneSideStateWatermarkPredicate}} only creates a state-value eviction 
predicate when the state side itself contains an attribute marked with 
watermark metadata. When only the opposite input is
  watermarked, no predicate is created for the state that should be evicted.

  This affects the following directions:

  || Join type || Watermarked input || State that should be evicted || Effect ||
  | Left outer | Right | Left | Unmatched left rows are never emitted |
  | Left semi | Right | Left | Never-matched left rows remain indefinitely |
  | Right outer | Left | Right | Unmatched right rows are never emitted |
  | Full outer | Right | Left | Unmatched left rows are never emitted |
  | Full outer | Left | Right | Unmatched right rows are never emitted |

  For full outer joins, one-sided watermarking is intentional: it should 
finalize unmatched rows from the opposite side. A watermark on both inputs is 
needed to finalize both directions and clean up both state stores. The bug is 
that the range-condition
  implementation does not honor the supported one-sided case.

  The state-key watermark path, where event time or a window is part of the 
equi-join keys, is not covered by this issue.

  h3. Reproducer

  The following test uses a right-side watermark and a range condition. Once 
the right watermark passes the latest possible matching time for the left row, 
the left outer join should emit that row with a null right side.

  {code}
  test("left outer range join with only right watermark evicts left state") {
    val leftInput = MemoryStream[(Int, Int)]
    val rightInput = MemoryStream[(Int, Int)]

    val left = leftInput.toDF().toDF("leftKey", "time")
      .select(
        $"leftKey",
        timestamp_seconds($"time").as("leftTime"))

    val right = rightInput.toDF().toDF("rightKey", "time")
      .select(
        $"rightKey",
        timestamp_seconds($"time").as("rightTime"))
      .withWatermark("rightTime", "10 seconds")

    val joined = left.join(
      right,
      expr(
        """
          |leftKey = rightKey AND
          |leftTime BETWEEN
          |  rightTime - INTERVAL 5 SECONDS AND
          |  rightTime + INTERVAL 5 SECONDS
        """.stripMargin),
      "left_outer")
      .select(
        $"leftKey",
        $"rightKey",
        $"leftTime".cast("long"),
        $"rightTime".cast("long"))

    testStream(joined)(
      AddData(leftInput, (1, 5)),
      CheckNewAnswer(),

      // Advances the right watermark to 20.
      AddData(rightInput, (2, 30)),
      CheckNewAnswer(),

      // Runs another batch using watermark 20. A future right row that
      // could match leftTime=5 would have rightTime in [0, 10], so the
      // left row can now be finalized.
      AddData(rightInput, (2, 40)),

      // Fails because the query produces no output.
      CheckNewAnswer(Row(1, null, 5, null))
    )
  }
  {code}

  Changing the join type to {{full_outer}} reproduces the same missing 
unmatched-left output. Swapping the inputs and using {{right_outer}} reproduces 
the symmetric problem.

  h3. Expected behavior

  For an accepted range join, execution should construct the state-value 
eviction predicate implied by the opposite input's watermark and the range 
condition.

  In the reproducer, the left row should be emitted as:

  {noformat}
  Row(1, null, 5, null)
  {noformat}

  h3. Actual behavior

  The query passes analysis, but no left-state eviction predicate is created. 
The left row remains in state and its unmatched outer result is never emitted.

  For left semi joins, the equivalent failure causes never-matched left rows to 
accumulate rather than causing missing output.

  h3. Notes

  [PR #58238|https://github.com/apache/spark/pull/58238] attempted a broader 
analyzer restriction but was closed for further design review. The fix should 
preserve the documented one-sided watermark semantics or explicitly redefine 
them; analysis and execution must
  agree.


> Enforce correct watermark placement for stream-stream left semi and left 
> outer joins
> ------------------------------------------------------------------------------------
>
>                 Key: SPARK-58904
>                 URL: https://issues.apache.org/jira/browse/SPARK-58904
>             Project: Spark
>          Issue Type: Bug
>          Components: Structured Streaming
>    Affects Versions: 4.3.0
>            Reporter: Ganesha S
>            Priority: Critical
>              Labels: correctness
>
> h3. Description
> For stream-stream joins with event-time range conditions, the analyzer may 
> accept a one-sided watermark because it can derive a state-value watermark 
> for the opposite input from the join condition.
> Execution does not always implement that accepted plan. 
> {{getOneSideStateWatermarkPredicate}} only creates a state-value eviction 
> predicate when the state side itself contains an attribute marked with 
> watermark metadata. When only the opposite input is
> watermarked, no predicate is created for the state that should be evicted.
> This affects the following directions:
> ||Join type||Watermarked input||State that should be evicted||Effect||
> |Left outer|Right|Left|Unmatched left rows are never emitted|
> |Left semi|Right|Left|Never-matched left rows remain indefinitely|
> |Right outer|Left|Right|Unmatched right rows are never emitted|
> |Full outer|Right|Left|Unmatched left rows are never emitted|
> |Full outer|Left|Right|Unmatched right rows are never emitted|
> For full outer joins, one-sided watermarking is intentional: it should 
> finalize unmatched rows from the opposite side. A watermark on both inputs is 
> needed to finalize both directions and clean up both state stores. The bug is 
> that the range-condition
> implementation does not honor the supported one-sided case.
> The state-key watermark path, where event time or a window is part of the 
> equi-join keys, is not covered by this issue.
> h3. Reproducer
> The following test uses a right-side watermark and a range condition. Once 
> the right watermark passes the latest possible matching time for the left 
> row, the left outer join should emit that row with a null right side.
> {code:java}
>   test("left outer range join with only right watermark evicts left state") {
>     val leftInput = MemoryStream[(Int, Int)]
>     val rightInput = MemoryStream[(Int, Int)]
>     val left = leftInput.toDF().toDF("leftKey", "time")
>       .select(
>         $"leftKey",
>         timestamp_seconds($"time").as("leftTime"))
>     val right = rightInput.toDF().toDF("rightKey", "time")
>       .select(
>         $"rightKey",
>         timestamp_seconds($"time").as("rightTime"))
>       .withWatermark("rightTime", "10 seconds")
>     val joined = left.join(
>       right,
>       expr(
>         """
>           |leftKey = rightKey AND
>           |leftTime BETWEEN
>           |  rightTime - INTERVAL 5 SECONDS AND
>           |  rightTime + INTERVAL 5 SECONDS
>         """.stripMargin),
>       "left_outer")
>       .select(
>         $"leftKey",
>         $"rightKey",
>         $"leftTime".cast("long"),
>         $"rightTime".cast("long"))
>     testStream(joined)(
>       AddData(leftInput, (1, 5)),
>       CheckNewAnswer(),
>       // Advances the right watermark to 20.
>       AddData(rightInput, (2, 30)),
>       CheckNewAnswer(),
>       // Runs another batch using watermark 20. A future right row that
>       // could match leftTime=5 would have rightTime in [0, 10], so the
>       // left row can now be finalized.
>       AddData(rightInput, (2, 40)),
>       // Fails because the query produces no output.
>       CheckNewAnswer(Row(1, null, 5, null))
>     )
>   }
>   {code}
> Changing the join type to {{full_outer}} reproduces the same missing 
> unmatched-left output. Swapping the inputs and using {{right_outer}} 
> reproduces the symmetric problem.
> h3. Expected behavior
> For an accepted range join, execution should construct the state-value 
> eviction predicate implied by the opposite input's watermark and the range 
> condition.
> In the reproducer, the left row should be emitted as:
> {noformat}
>   Row(1, null, 5, null)
>   {noformat}
> h3. Actual behavior
> The query passes analysis, but no left-state eviction predicate is created. 
> The left row remains in state and its unmatched outer result is never emitted.
> For left semi joins, the equivalent failure causes never-matched left rows to 
> accumulate rather than causing missing output.
> h3. Notes
> [PR #58238|https://github.com/apache/spark/pull/58238] attempted a broader 
> analyzer restriction but was closed for further design review. The fix should 
> preserve the documented one-sided watermark semantics or explicitly redefine 
> them; analysis and execution must agree.



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

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to