You are viewing a plain text version of this content. The canonical link for it is here.
Posted to reviews@spark.apache.org by tdas <gi...@git.apache.org> on 2017/09/18 23:46:48 UTC

[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join

GitHub user tdas opened a pull request:

    https://github.com/apache/spark/pull/19271

    [SPARK-22053][SS] Stream-stream inner join

    ## What changes were proposed in this pull request?
    
    This PR implements stream-stream inner join using a two-way symmetric hash join. At a high level, we want to do the following.
    1. For each stream, we maintain the past rows as state in State Store.
      - For each joining key, there can be multiple rows that have been received.
      - So, we have to effectively maintain a key-to-list-of-values multimap as state for each stream.
    2. In each batch, for each input row in each stream
      - Look up the other streams state to see if there are matching rows, and output them if they satisfy the joining condition
      - Add the input row to corresponding stream’s state.
      - If the data has a timestamp/window column with watermark, then we will use that to calculate the threshold for keys that are required to buffered for future matches and drop the rest from the state.
    
    Cleaning up old unnecessary state rows depends completely on whether watermark has been defined and what are join conditions. We definitely want to support state clean up two types of queries that are likely to be common.
    
    - Queries to time range conditions - E.g. `SELECT * FROM leftTable, rightTable ON leftKey = rightKey AND leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR`
    - Queries with windows as the matching key - E.g. `SELECT * FROM leftTable, rightTable ON leftKey = rightKey AND window(leftTime, "1 hour") = window(rightTime, "1 hour")` (pseudo-SQL)
    
    Refer to the scaladocs on the `StreamingSymmetricHashJoinExec` class for more implementation details.
    
    Besides the implementation of stream-stream inner join SparkPlan. Some additional changes are
    - Allowed inner join in append mode in UnsupportedOperationChecker
    - Prevented stream-stream join on an empty batch dataframe to be collapsed by the optimizer
    
    ## How was this patch tested?
    - New tests in StreamingJoinSuite
    - Updated tests UnsupportedOperationSuite


You can merge this pull request into a Git repository by running:

    $ git pull https://github.com/tdas/spark SPARK-22053

Alternatively you can review and apply these changes as the patch at:

    https://github.com/apache/spark/pull/19271.patch

To close this pull request, make a commit to your master/trunk branch
with (at least) the following in the commit message:

    This closes #19271
    
----
commit 747c8da89d257618bbbfa51b70144911933366e3
Author: Tathagata Das <ta...@gmail.com>
Date:   2017-09-18T22:30:46Z

    Stream stream inner join first commit

commit 9edaa580edc6e599b01033190a9ef383ed001ae9
Author: Tathagata Das <ta...@gmail.com>
Date:   2017-09-18T23:40:34Z

    Updated test

----


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #82004 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/82004/testReport)** for PR 19271 at commit [`c01654c`](https://github.com/apache/spark/commit/c01654c58e4537ed4bb2b648a5939edb0e92b7e3).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139810069
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    +        stored right input, and also stores all the right input. It also generates all rows from
    +        matching new left input with new right input, since the new left input has become stored
    +        by that point. This tiny asymmetry is necessary to avoid doubling.
    +    */
    +    val leftOutputIter = leftSideJoiner.joinWithOtherSide(rightSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withLeft(inputRow).withRight(matchedRow)
    +    }
    +    val rightOutputIter = rightSideJoiner.joinWithOtherSide(leftSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withRight(inputRow).withLeft(matchedRow)
    --- End diff --
    
    nit: don't know the semantics of `joinedRow`, so this may be fine, but for consistency reasons, would be nice to keep
    `joinedRow.withLeft(matchedRow).withRight(inputRow)`
    
    I suddenly had to think well, will we put the columns out of order otherwise?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140050228
  
    --- Diff: sql/core/src/test/scala/org/apache/spark/sql/streaming/StreamingJoinSuite.scala ---
    @@ -0,0 +1,585 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.streaming
    +
    +import java.util.UUID
    +
    +import scala.util.Random
    +
    +import org.apache.hadoop.conf.Configuration
    +import org.scalatest.BeforeAndAfter
    +
    +import org.apache.spark.scheduler.ExecutorCacheTaskLocation
    +import org.apache.spark.sql.SparkSession
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, AttributeSet, BoundReference, Expression, GenericInternalRow, LessThanOrEqual, Literal, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.{GeneratePredicate}
    +import org.apache.spark.sql.catalyst.plans.logical.{EventTimeWatermark, Filter}
    +import org.apache.spark.sql.execution.LogicalRDD
    +import org.apache.spark.sql.execution.streaming.{MemoryStream, StatefulOperatorStateInfo, StreamingSymmetricHashJoinHelper}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper.LeftSide
    +import org.apache.spark.sql.execution.streaming.state.{StateStore, StateStoreConf, StateStoreProviderId, SymmetricHashJoinStateManager}
    +import org.apache.spark.sql.functions._
    +import org.apache.spark.sql.types._
    +import org.apache.spark.util.Utils
    +
    +
    +class StreamingJoinSuite extends StreamTest with StateStoreMetricsTest with BeforeAndAfter {
    +
    +  before {
    +    SparkSession.setActiveSession(spark)  // set this before force initializing 'joinExec'
    +    spark.streams.stateStoreCoordinator   // initialize the lazy coordinator
    +  }
    +
    +  after {
    +    StateStore.stop()
    +  }
    +
    +  import testImplicits._
    +
    +  test("SymmetricHashJoinStateManager - all operations") {
    --- End diff --
    
    I would put this test in a separate suite, and test operations individually. Right now it's a massive test with a bunch of helper functions defined within it. It takes 50 lines of preparation to actually start the "test"


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #82010 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/82010/testReport)** for PR 19271 at commit [`5198ef3`](https://github.com/apache/spark/commit/5198ef3d2bf38ce827f0fd5a026724f0825dd7dd).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139816392
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in milliseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              val millisPerMonth = CalendarInterval.MICROS_PER_DAY / 1000 * 31
    +              val intervalMillis = calendarInterval.milliseconds +
    +                millisPerMonth * calendarInterval.months
    +              Literal(intervalMillis.toDouble)
    +            case DoubleType => Multiply(lit, Literal(1000.0))
    +            case _: NumericType | _: TimestampType =>
    +              Multiply(Cast(lit, DoubleType), Literal(1000.0))
    --- End diff --
    
    the current implementation actually doesn't allow joins at microsecond precision, which some use cases may require.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81958 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81958/testReport)** for PR 19271 at commit [`8e13959`](https://github.com/apache/spark/commit/8e139594ce164a18fa54df680afbc213691da081).
     * This patch **fails Spark unit tests**.
     * This patch merges cleanly.
     * This patch adds no public classes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #82004 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/82004/testReport)** for PR 19271 at commit [`c01654c`](https://github.com/apache/spark/commit/c01654c58e4537ed4bb2b648a5939edb0e92b7e3).
     * This patch passes all tests.
     * This patch merges cleanly.
     * This patch adds no public classes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139841490
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    --- End diff --
    
    I get that state gets discarded every batch, but what I don't understand is if `StateValueWatermark(B) < StateKeyWatermark(B)` what happens


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by zsxwing <gi...@git.apache.org>.
Github user zsxwing commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140050970
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/SymmetricHashJoinStateManager.scala ---
    @@ -0,0 +1,405 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming.state
    +
    +import scala.reflect.ClassTag
    +
    +import org.apache.hadoop.conf.Configuration
    +
    +import org.apache.spark.{Partition, SparkContext, TaskContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, BindReferences, Expression, LessThanOrEqual, Literal, SpecificInternalRow, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.Predicate
    +import org.apache.spark.sql.execution.streaming.{StatefulOperatorStateInfo, StreamingSymmetricHashJoinExec}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.types.{LongType, StructField, StructType}
    +import org.apache.spark.util.NextIterator
    +
    +/**
    + * Helper class to manage state required by a single side of [[StreamingSymmetricHashJoinExec]].
    + * The interface of this class is basically that of a multi-map:
    + * - Get: Returns an iterator of multiple values for given key
    + * - Append: Append a new value to the given key
    + * - Remove Data by predicate: Drop any state using a predicate condition on keys or values
    + *
    + * @param joinSide          Defines the join side
    + * @param inputValueAttributes   Attributes of the input row which will be stored as value
    + * @param joinKeys          Expressions to find the join key that will be used to key the value rows
    + * @param stateInfo           Information about how to retrieve the correct version of state
    + * @param storeConf           Configuration for the state store.
    + * @param hadoopConf          Hadoop configuration for reading state data from storage
    + *
    + * Internally, the key -> multiple values is stored in two [[StateStore]]s.
    + * - Store 1 ([[KeyToNumValuesStore]]) maintains mapping between key -> number of values
    + * - Store 2 ([[KeyWithIndexToValueStore]]) maintains mapping between (key, index) -> value
    + * - Put:   update count in KeyToNumValuesStore,
    + *          insert new (key, count) -> value in KeyWithIndexToValueStore
    + * - Get:   read count from KeyToNumValuesStore,
    + *          read each of the n values in KeyWithIndexToValueStore
    + * - Remove state by predicate on keys:
    + *          scan all keys in KeyToNumValuesStore to find keys that do match the predicate,
    + *          delete from key from KeyToNumValuesStore, delete values in KeyWithIndexToValueStore
    + * - Remove state by condition on values:
    + *          scan all [(key, index) -> value] in KeyWithIndexToValueStore to find values that match
    + *          the predicate, delete corresponding (key, indexToDelete) from KeyWithIndexToValueStore
    + *          by overwriting with the value of (key, maxIndex), and removing [(key, maxIndex),
    + *          decrement corresponding num values in KeyToNumValuesStore
    + */
    +class SymmetricHashJoinStateManager(
    +    val joinSide: JoinSide,
    +    val inputValueAttributes: Seq[Attribute],
    +    joinKeys: Seq[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    storeConf: StateStoreConf,
    +    hadoopConf: Configuration) extends Logging {
    +
    +  import SymmetricHashJoinStateManager._
    +
    +  // Clean up any state store resources if necessary at the end of the task
    +  Option(TaskContext.get()).foreach { _.addTaskCompletionListener { _ => abortIfNeeded() } }
    --- End diff --
    
    The task completion listener may be called without initializing `keyToNumValues` or `keyWithIndexToValue`. It's better to avoid NPE in this case.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139833312
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    --- End diff --
    
    removed the other one. not needed. copied the docs to this location.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140051116
  
    --- Diff: sql/core/src/test/scala/org/apache/spark/sql/streaming/StreamingJoinSuite.scala ---
    @@ -0,0 +1,585 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.streaming
    +
    +import java.util.UUID
    +
    +import scala.util.Random
    +
    +import org.apache.hadoop.conf.Configuration
    +import org.scalatest.BeforeAndAfter
    +
    +import org.apache.spark.scheduler.ExecutorCacheTaskLocation
    +import org.apache.spark.sql.SparkSession
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, AttributeSet, BoundReference, Expression, GenericInternalRow, LessThanOrEqual, Literal, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.{GeneratePredicate}
    +import org.apache.spark.sql.catalyst.plans.logical.{EventTimeWatermark, Filter}
    +import org.apache.spark.sql.execution.LogicalRDD
    +import org.apache.spark.sql.execution.streaming.{MemoryStream, StatefulOperatorStateInfo, StreamingSymmetricHashJoinHelper}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper.LeftSide
    +import org.apache.spark.sql.execution.streaming.state.{StateStore, StateStoreConf, StateStoreProviderId, SymmetricHashJoinStateManager}
    +import org.apache.spark.sql.functions._
    +import org.apache.spark.sql.types._
    +import org.apache.spark.util.Utils
    +
    +
    +class StreamingJoinSuite extends StreamTest with StateStoreMetricsTest with BeforeAndAfter {
    +
    +  before {
    +    SparkSession.setActiveSession(spark)  // set this before force initializing 'joinExec'
    +    spark.streams.stateStoreCoordinator   // initialize the lazy coordinator
    +  }
    +
    +  after {
    +    StateStore.stop()
    +  }
    +
    +  import testImplicits._
    +
    +  test("SymmetricHashJoinStateManager - all operations") {
    +    val watermarkMetadata = new MetadataBuilder().putLong(EventTimeWatermark.delayKey, 10).build()
    +    val inputValueSchema = new StructType()
    +      .add(StructField("time", IntegerType, metadata = watermarkMetadata))
    +      .add(StructField("value", BooleanType))
    +    val inputValueAttribs = inputValueSchema.toAttributes
    +    val inputValueAttribWithWatermark = inputValueAttribs(0)
    +    val joinKeyExprs = Seq[Expression](Literal(false), inputValueAttribWithWatermark, Literal(10.0))
    +
    +    val inputValueGen = UnsafeProjection.create(inputValueAttribs.map(_.dataType).toArray)
    +    val joinKeyGen = UnsafeProjection.create(joinKeyExprs.map(_.dataType).toArray)
    +
    +    def toInputValue(i: Int): UnsafeRow = {
    +      inputValueGen.apply(new GenericInternalRow(Array[Any](i, false)))
    +    }
    +
    +    def toJoinKeyRow(i: Int): UnsafeRow = {
    +      joinKeyGen.apply(new GenericInternalRow(Array[Any](false, i, 10.0)))
    +    }
    +
    +    def toKeyInt(joinKeyRow: UnsafeRow): Int = joinKeyRow.getInt(1)
    +
    +    def toValueInt(inputValueRow: UnsafeRow): Int = inputValueRow.getInt(0)
    +
    +    withJoinStateManager(inputValueAttribs, joinKeyExprs) { manager =>
    +      def append(key: Int, value: Int): Unit = {
    +        manager.append(toJoinKeyRow(key), toInputValue(value))
    +      }
    +
    +      def get(key: Int): Seq[Int] = manager.get(toJoinKeyRow(key)).map(toValueInt).toSeq.sorted
    +
    +      /** Remove keys (and corresponding values) where `time <= threshold` */
    +      def removeByKey(threshold: Long): Unit = {
    +        val expr =
    +          LessThanOrEqual(
    +            BoundReference(
    +              1, inputValueAttribWithWatermark.dataType, inputValueAttribWithWatermark.nullable),
    +            Literal(threshold))
    +        manager.removeByKeyCondition(GeneratePredicate.generate(expr).eval _)
    +      }
    +
    +      /** Remove values where `time <= threshold` */
    +      def removeByValue(watermark: Long): Unit = {
    +        val expr = LessThanOrEqual(inputValueAttribWithWatermark, Literal(watermark))
    +        manager.removeByValueCondition(
    +          GeneratePredicate.generate(expr, inputValueAttribs).eval _)
    +      }
    +
    +      def numRows: Long = {
    +        manager.metrics.numKeys
    +      }
    +
    +      assert(get(20) === Seq.empty)     // initially empty
    +      append(20, 2)
    +      assert(get(20) === Seq(2))        // should first value correctly
    +      assert(numRows === 1)
    +
    +      append(20, 3)
    +      assert(get(20) === Seq(2, 3))     // should append new values
    +      append(20, 3)
    +      assert(get(20) === Seq(2, 3, 3))  // should append another copy if same value added again
    +      assert(numRows === 3)
    +
    +      assert(get(30) === Seq.empty)
    +      append(30, 1)
    +      assert(get(30) === Seq(1))
    +      assert(get(20) === Seq(2, 3, 3))  // add another key-value should not affect existing ones
    +      assert(numRows === 4)
    +
    +      removeByKey(25)
    +      assert(get(20) === Seq.empty)
    +      assert(get(30) === Seq(1))        // should remove 20, not 30
    +      assert(numRows === 1)
    +
    +      removeByKey(30)
    +      assert(get(30) === Seq.empty)     // should remove 30
    +      assert(numRows === 0)
    +
    +      def appendAndTest(key: Int, values: Int*): Unit = {
    +        values.foreach { value => append(key, value)}
    +        require(get(key) === values)
    +      }
    +
    +      appendAndTest(40, 100, 200, 300)
    +      appendAndTest(50, 125)
    +      appendAndTest(60, 275)              // prepare for testing removeByValue
    +      assert(numRows === 5)
    +
    +      removeByValue(125)
    +      assert(get(40) === Seq(200, 300))
    +      assert(get(50) === Seq.empty)
    +      assert(get(60) === Seq(275))        // should remove only some values, not all
    +      assert(numRows === 3)
    +
    +      append(40, 50)
    +      assert(get(40) === Seq(50, 200, 300))
    +      assert(numRows === 4)
    +
    +      removeByValue(200)
    +      assert(get(40) === Seq(300))
    +      assert(get(60) === Seq(275))        // should remove only some values, not all
    +      assert(numRows === 2)
    +
    +      removeByValue(300)
    +      assert(get(40) === Seq.empty)
    +      assert(get(60) === Seq.empty)       // should remove all values now
    +      assert(numRows === 0)
    +    }
    +  }
    +
    +  test("stream stream inner join on non-time column") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF.select('value as "key", ('value * 2) as "leftValue")
    +    val df2 = input2.toDF.select('value as "key", ('value * 3) as "rightValue")
    +    val joined = df1.join(df2, "key")
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckAnswer(),
    +      AddData(input2, 1, 10),       // 1 arrived on input1 first, then input2, should join
    +      CheckLastBatch((1, 2, 3)),
    +      AddData(input1, 10),          // 10 arrived on input2 first, then input1, should join
    +      CheckLastBatch((10, 20, 30)),
    +      AddData(input2, 1),           // another 1 in input2 should join with 1 input1
    +      CheckLastBatch((1, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 1), // multiple 1s should be kept in state causing multiple (1, 2, 3)
    +      CheckLastBatch((1, 2, 3), (1, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 100),
    +      AddData(input2, 100),
    +      CheckLastBatch((100, 200, 300))
    +    )
    +  }
    +
    +
    +  test("stream stream inner join on windows - without watermark") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp", ('value * 2) as "leftValue")
    +      .select('key, window('timestamp, "10 second"), 'leftValue)
    +
    +    val df2 = input2.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp",
    +        ('value * 3) as "rightValue")
    +      .select('key, window('timestamp, "10 second"), 'rightValue)
    +
    +    val joined = df1.join(df2, Seq("key", "window"))
    +      .select('key, $"window.end".cast("long"), 'leftValue, 'rightValue)
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckLastBatch(),
    +      AddData(input2, 1),
    +      CheckLastBatch((1, 10, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 25),
    +      CheckLastBatch(),
    +      StopStream,
    +      StartStream(),
    +      AddData(input2, 25),
    +      CheckLastBatch((25, 30, 50, 75)),
    +      AddData(input1, 1),
    +      CheckLastBatch((1, 10, 2, 3)),      // State for 1 still around as there is not watermark
    --- End diff --
    
    nit: `there is no watermark`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139831092
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    --- End diff --
    
    done


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139895913
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinHelper.scala ---
    @@ -0,0 +1,407 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.reflect.ClassTag
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.{Partition, SparkContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, AttributeSet, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.execution.streaming.state.{StateStoreCoordinatorRef, StateStoreProvider, StateStoreProviderId}
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]]. See that object for more details.
    + */
    +object StreamingSymmetricHashJoinHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate {
    +    def expr: Expression
    +    def desc: String
    +    override def toString: String = s"$desc: $expr"
    +  }
    +  /** Predicate for watermark on state keys */
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression)
    +    extends JoinStateWatermarkPredicate {
    +    def desc: String = "key predicate"
    +  }
    +  /** Predicate for watermark on state values */
    +  case class JoinStateValueWatermarkPredicate(expr: Expression)
    +    extends JoinStateWatermarkPredicate {
    +    def desc: String = "value predicate"
    +  }
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None) {
    +    override def toString(): String = {
    +      s"state cleanup [ left ${left.map(_.toString).getOrElse("= null")}, " +
    +        s"right ${right.map(_.toString).getOrElse("")} ]"
    +    }
    +  }
    +
    +  /** Get the predicates defining the state watermarks for both sides of the join */
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +
    +
    +    // Join keys of both sides generate rows of the same fields, that is, same sequence of data
    +    // types. If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +    // then it will never consider joining keys that are < state key watermark (i.e. event time
    +    // watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +    // there has to be an equivalent column (i.e., timestamp). And any right side data that has the
    +    // timestamp < watermark will not match will not match with left side data, as the left side get
    +    // filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +    // left side keys effectively causes the timestamp on the right side to have a watermark.
    +    // We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +    // right timestamp in the right keys.
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 in the StreamingSymmetricHashJoinExec docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate.apply _)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 in the StreamingSymmetricHashJoinExec docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = AttributeSet(oneSideInputAttributes),
    +          attributesWithEventWatermark = AttributeSet(otherSideInputAttributes),
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate.apply _)
    +
    +      } else {
    +        None
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   * This function is supposed to make best-effort attempt to get the state watermark. If there is
    +   * any error, it will return None.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds, is possible.
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: AttributeSet,
    +      attributesWithEventWatermark: AttributeSet,
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    // If condition or event time watermark is not provided, then cannot calculate state watermark
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    // If there is not watermark attribute, then cannot define state watermark
    +    if (!attributesWithEventWatermark.exists(_.metadata.contains(delayKey))) return None
    +    val attributesInCondition =
    +      AttributeSet(joinCondition.get.collect { case a: AttributeReference => a })
    +    if (
    +      attributesInCondition.filter { attributesToFindStateWatemarkFor.contains(_)}.size > 1 ||
    +      attributesInCondition.filter { attributesWithEventWatermark.contains(_)}.size > 1) {
    +      // If more than attributes present in condition from one side, then it cannot be solved
    +      return None
    +    }
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatermarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +
    +      // The generated the state watermark cleanup expression is inclusive of the state watermark.
    +      // If state watermark is W, all state where timestamp <= W will be cleaned up.
    +      // Now when the canonicalized join condition solves to leftTime >= W, we dont want to clean
    +      // up leftTime <= W. Rather we should clean up leftTime <= W - 1. Hence the -1 below.
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatermarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: AttributeSet,
    +      attributesWithEventWatermark: AttributeSet,
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a @ AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a @ AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && metadata.contains(delayKey) =>
    +          Multiply(Literal(eventWatermark.get.toDouble), Literal(1000.0))
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some((Double2double(constraintValue) / 1000.0).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Cast(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in microseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              if (calendarInterval.months > 0) {
    +                invalid = true
    +                logWarning(
    +                  s"Failed to extract state value watermark from condition $exprToCollectFrom " +
    +                    s"as imprecise intervals like months and years cannot be used for precise" +
    +                    s"watermark calculation")
    +                Literal(0.0)
    +              } else {
    +                Literal(calendarInterval.microseconds.toDouble)
    +              }
    +            case DoubleType =>
    +              Multiply(lit, Literal(1000000.0))
    --- End diff --
    
    how do you know that they provided the double in `millisecond` or `second` precision?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139830996
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    --- End diff --
    
    Its technically `eventTimeWatermark(streamA) + interval of Stream A = StateValueWatermark(StreamB)`. State gets discarded in every batch.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81972 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81972/testReport)** for PR 19271 at commit [`2ed62d8`](https://github.com/apache/spark/commit/2ed62d82373446dbbb4da5ee8e5af32c5571aef0).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139833470
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81972 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81972/testReport)** for PR 19271 at commit [`2ed62d8`](https://github.com/apache/spark/commit/2ed62d82373446dbbb4da5ee8e5af32c5571aef0).
     * This patch **fails due to an unknown error code, -9**.
     * This patch merges cleanly.
     * This patch adds the following public classes _(experimental)_:
      * `  sealed trait JoinStateWatermarkPredicate `
      * `  case class JoinStateKeyWatermarkPredicate(expr: Expression)`
      * `  case class JoinStateValueWatermarkPredicate(expr: Expression)`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139835816
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    --- End diff --
    
    added this as docs.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    @brkyvz @zsxwing thank you for reviewing. I have merged it to master! 


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140062015
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/IncrementalExecution.scala ---
    @@ -114,6 +115,16 @@ class IncrementalExecution(
               stateInfo = Some(nextStatefulOperationStateInfo),
               batchTimestampMs = Some(offsetSeqMetadata.batchTimestampMs),
               eventTimeWatermark = Some(offsetSeqMetadata.batchWatermarkMs))
    +
    +      case j @ StreamingSymmetricHashJoinExec(lKeys, rKeys, _, cond, _, _, _, left, right) =>
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139832036
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139834356
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    --- End diff --
    
    the generated the state watermark cleanup expression is inclusive of the state watermark. That is if state watermark is W, all state where timestamp <= W will be cleaned up. 
    Now when the canonicalized join condition solves to leftTime >= W, then I dont want to clean up <= W. Rather I choose to cleanup <= W-1.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test FAILed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/81986/
    Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139812580
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    --- End diff --
    
    specify which class docs


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139830633
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    @brkyvz I dont splitting this PR now into 2 PRs is a good idea. The reason I did this together is that both features are intricately tied together and the watermark stuff required completely refactoring the former. Splitting it NOW will take an exorbitant amount of useless work to split and then merge it again later. 
    



---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #82002 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/82002/testReport)** for PR 19271 at commit [`9e3be26`](https://github.com/apache/spark/commit/9e3be269a28ae19f2a1cf285aa8c834276642ee4).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139806969
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    --- End diff --
    
    `require`?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81957 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81957/testReport)** for PR 19271 at commit [`8a551d7`](https://github.com/apache/spark/commit/8a551d7fc045fd633cc15c14b27133a13eacb727).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by zsxwing <gi...@git.apache.org>.
Github user zsxwing commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    LGTM pending tests


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #82011 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/82011/testReport)** for PR 19271 at commit [`d8116d0`](https://github.com/apache/spark/commit/d8116d0577bba516320c976debba9eb708fe2ce2).
     * This patch passes all tests.
     * This patch merges cleanly.
     * This patch adds no public classes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #82011 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/82011/testReport)** for PR 19271 at commit [`d8116d0`](https://github.com/apache/spark/commit/d8116d0577bba516320c976debba9eb708fe2ce2).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139892683
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,344 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, BindReferences, Expression, JoinedRow, Literal, NamedExpression, PreciseTimestampConversion, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.sql.types.{LongType, TimestampType}
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and the new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range conditions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + *
    + * @param leftKeys  Expression to generate key rows for joining from left input
    + * @param rightKeys Expression to generate key rows for joining from right input
    + * @param joinType  Type of join (inner, left outer, etc.)
    + * @param condition Optional, additional condition to filter output of the equi-join
    + * @param stateInfo Version information required to read join state (buffered rows)
    + * @param eventTimeWatermark Watermark of input event, same for both sides
    + * @param stateWatermarkPredicates Predicates for removal of state, see
    + *                                 [[JoinStateWatermarkPredicates]]
    + * @param left      Left child plan
    + * @param right     Right child plan
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  require(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  require(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    if (stateInfo.isEmpty) {
    +      throw new IllegalStateException(s"Cannot execute join as state info was not specified\n$this")
    +    }
    +
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(
    +      LeftSide, left.output, leftKeys, leftInputIter, stateWatermarkPredicates.left)
    +    val rightSideJoiner = new OneSideHashJoiner(
    +      RightSide, right.output, rightKeys, rightInputIter, stateWatermarkPredicates.right)
    +
    +    //  Join one side input using the other side's buffered/state rows. Here is how it is done.
    +    //
    +    //  - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +    //    stored right input, and also stores all the left input
    +    //
    +    //  - `rightJoiner.joinWith(leftJoiner)` generates all rows from matching new right input with
    +    //    stored right input, and also stores all the right input. It also generates all rows from
    --- End diff --
    
    `stored left input`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    LGTM: Would love to see the following things a bit more buttoned up in a follow up PR:
    
    1. Tests on join + aggregation and aggregation + join
    2. A better, safer, more understandable handling of [this line](https://github.com/apache/spark/pull/19271/files#diff-8e18a76135ded4a7998d28013c02b1a2R182). Getting the watermark out of a window function is too fragile at the moment, and I think it could fail if you would put had a structure such as:
    ```
    timestamps: struct {
       eventTime,
       ingestTime,
       processingTime
    }
    ```
    and you had set a watermark on `timestamps.eventTime`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81907 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81907/testReport)** for PR 19271 at commit [`94b63fb`](https://github.com/apache/spark/commit/94b63fb4082c98ff4f2e67e0759e98f46a3537a5).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139832253
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    +        stored right input, and also stores all the right input. It also generates all rows from
    +        matching new left input with new right input, since the new left input has become stored
    +        by that point. This tiny asymmetry is necessary to avoid doubling.
    +    */
    +    val leftOutputIter = leftSideJoiner.joinWithOtherSide(rightSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withLeft(inputRow).withRight(matchedRow)
    +    }
    +    val rightOutputIter = rightSideJoiner.joinWithOtherSide(leftSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withRight(inputRow).withLeft(matchedRow)
    --- End diff --
    
    changed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139832320
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    +        stored right input, and also stores all the right input. It also generates all rows from
    +        matching new left input with new right input, since the new left input has become stored
    +        by that point. This tiny asymmetry is necessary to avoid doubling.
    +    */
    +    val leftOutputIter = leftSideJoiner.joinWithOtherSide(rightSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withLeft(inputRow).withRight(matchedRow)
    +    }
    +    val rightOutputIter = rightSideJoiner.joinWithOtherSide(leftSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withRight(inputRow).withLeft(matchedRow)
    +    }
    +
    +    // Filter the joined rows based on the given condition.
    +    val outputFilterFunction = if (condition.isDefined) {
    +      newPredicate(condition.get, left.output ++ right.output).eval _
    +    } else {
    +      (_: InternalRow) => true
    +    }
    +    val filteredOutputIter =
    +      (leftOutputIter ++ rightOutputIter).filter(outputFilterFunction).map { row =>
    +        numOutputRows += 1
    +        row
    +      }
    +
    +    // Function to remove old state after all the input has been consumed and output generated
    +    def onOutputCompletion = {
    +      allUpdatesTimeMs += math.max(NANOSECONDS.toMillis(System.nanoTime - updateStartTimeNs), 0)
    +
    +      // Remove old state if needed
    +      allRemovalsTimeMs += timeTakenMs {
    +        leftSideJoiner.removeOldState(stateWatermarkPredicates.left)
    +        rightSideJoiner.removeOldState(stateWatermarkPredicates.right)
    +      }
    +
    +      // Commit all state changes and update state store metrics
    +      commitTimeMs += timeTakenMs {
    +        val leftSideMetrics = leftSideJoiner.commitStateAndGetMetrics()
    +        val rightSideMetrics = rightSideJoiner.commitStateAndGetMetrics()
    +        val stateStoreMetrics = StateStoreMetrics.combine(Seq(leftSideMetrics, rightSideMetrics))
    +        numUpdatedStateRows +=
    +          (leftSideJoiner.numUpdatedStateRows + rightSideJoiner.numUpdatedStateRows)
    +        numTotalStateRows += stateStoreMetrics.numKeys
    +        stateMemory += stateStoreMetrics.memoryUsedBytes
    +        stateStoreMetrics.customMetrics.foreach { case (metric, value) =>
    +          longMetric(metric.name) += value
    +        }
    +      }
    +    }
    +
    +    CompletionIterator[InternalRow, Iterator[InternalRow]](filteredOutputIter, onOutputCompletion)
    +  }
    +
    +  /**
    +   * Internal helper class to consume input rows, generate join output rows using other sides
    +   * buffered state rows, and finally clean up this sides buffered state rows
    +   */
    +  private class OneSideHashJoiner(
    +      joinSide: JoinSide,
    +      inputAttributes: Seq[Attribute],
    +      joinKeys: Seq[Expression],
    +      inputIter: Iterator[InternalRow]) {
    +
    +    private val joinStateManager = new SymmetricHashJoinStateManager(
    +      joinSide, inputAttributes, joinKeys, stateInfo,
    +      storeConf, hadoopConfBcast.value.value, newPredicate _)
    +    private val keyGenerator = UnsafeProjection.create(joinKeys, inputAttributes)
    +
    +    var numUpdatedStateRows = 0
    +
    +    /**
    +     * Generate joined rows by consuming input from this side, and matching it with the buffered
    +     * rows (i.e. state) of the other side.
    +     * @param otherSideJoiner   Joined of the other side
    +     * @param generateJoinedRow Function to generate the joined row from the
    +     *                          input row from this side and the matched row from the other side
    +     */
    +    def joinWithOtherSide(
    --- End diff --
    
    agreed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139832660
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    +        stored right input, and also stores all the right input. It also generates all rows from
    +        matching new left input with new right input, since the new left input has become stored
    +        by that point. This tiny asymmetry is necessary to avoid doubling.
    +    */
    +    val leftOutputIter = leftSideJoiner.joinWithOtherSide(rightSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withLeft(inputRow).withRight(matchedRow)
    +    }
    +    val rightOutputIter = rightSideJoiner.joinWithOtherSide(leftSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withRight(inputRow).withLeft(matchedRow)
    +    }
    +
    +    // Filter the joined rows based on the given condition.
    +    val outputFilterFunction = if (condition.isDefined) {
    +      newPredicate(condition.get, left.output ++ right.output).eval _
    +    } else {
    +      (_: InternalRow) => true
    +    }
    +    val filteredOutputIter =
    +      (leftOutputIter ++ rightOutputIter).filter(outputFilterFunction).map { row =>
    +        numOutputRows += 1
    +        row
    +      }
    +
    +    // Function to remove old state after all the input has been consumed and output generated
    +    def onOutputCompletion = {
    +      allUpdatesTimeMs += math.max(NANOSECONDS.toMillis(System.nanoTime - updateStartTimeNs), 0)
    +
    +      // Remove old state if needed
    +      allRemovalsTimeMs += timeTakenMs {
    +        leftSideJoiner.removeOldState(stateWatermarkPredicates.left)
    +        rightSideJoiner.removeOldState(stateWatermarkPredicates.right)
    +      }
    +
    +      // Commit all state changes and update state store metrics
    +      commitTimeMs += timeTakenMs {
    +        val leftSideMetrics = leftSideJoiner.commitStateAndGetMetrics()
    +        val rightSideMetrics = rightSideJoiner.commitStateAndGetMetrics()
    +        val stateStoreMetrics = StateStoreMetrics.combine(Seq(leftSideMetrics, rightSideMetrics))
    +        numUpdatedStateRows +=
    +          (leftSideJoiner.numUpdatedStateRows + rightSideJoiner.numUpdatedStateRows)
    +        numTotalStateRows += stateStoreMetrics.numKeys
    +        stateMemory += stateStoreMetrics.memoryUsedBytes
    +        stateStoreMetrics.customMetrics.foreach { case (metric, value) =>
    +          longMetric(metric.name) += value
    +        }
    +      }
    +    }
    +
    +    CompletionIterator[InternalRow, Iterator[InternalRow]](filteredOutputIter, onOutputCompletion)
    +  }
    +
    +  /**
    +   * Internal helper class to consume input rows, generate join output rows using other sides
    +   * buffered state rows, and finally clean up this sides buffered state rows
    +   */
    +  private class OneSideHashJoiner(
    +      joinSide: JoinSide,
    +      inputAttributes: Seq[Attribute],
    +      joinKeys: Seq[Expression],
    +      inputIter: Iterator[InternalRow]) {
    +
    +    private val joinStateManager = new SymmetricHashJoinStateManager(
    +      joinSide, inputAttributes, joinKeys, stateInfo,
    +      storeConf, hadoopConfBcast.value.value, newPredicate _)
    +    private val keyGenerator = UnsafeProjection.create(joinKeys, inputAttributes)
    +
    +    var numUpdatedStateRows = 0
    +
    +    /**
    +     * Generate joined rows by consuming input from this side, and matching it with the buffered
    +     * rows (i.e. state) of the other side.
    +     * @param otherSideJoiner   Joined of the other side
    +     * @param generateJoinedRow Function to generate the joined row from the
    +     *                          input row from this side and the matched row from the other side
    +     */
    +    def joinWithOtherSide(
    +        otherSideJoiner: OneSideHashJoiner)(
    +        generateJoinedRow: (UnsafeRow, UnsafeRow) => JoinedRow): Iterator[InternalRow] = {
    +
    +      val watermarkAttribute = inputAttributes.find(_.metadata.contains(delayKey))
    +      val nonLateRows =
    +        WatermarkSupport.watermarkExpression(watermarkAttribute, eventTimeWatermark) match {
    +          case Some(watermarkExpr) =>
    +            val predicate = newPredicate(watermarkExpr, inputAttributes)
    +            inputIter.filter { row => !predicate.eval(row) }
    +          case None =>
    +            inputIter
    +        }
    +
    +      nonLateRows.flatMap { row =>
    +        val thisRow = row.asInstanceOf[UnsafeRow]
    +        val key = keyGenerator(thisRow)
    +        joinStateManager.append(key, thisRow)
    +        numUpdatedStateRows += 2
    --- End diff --
    
    changed already. earlier i was counting all the rows in the state stores, and so every added row would be 2 updated rows in the 2 state stores (count updated, and row added). I have changed it already. I report the count of the number of buffered rows stored (i.e. number of values in the key+index state store). Hence this increment is 1 now. 


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139832926
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    +        stored right input, and also stores all the right input. It also generates all rows from
    +        matching new left input with new right input, since the new left input has become stored
    +        by that point. This tiny asymmetry is necessary to avoid doubling.
    +    */
    +    val leftOutputIter = leftSideJoiner.joinWithOtherSide(rightSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withLeft(inputRow).withRight(matchedRow)
    +    }
    +    val rightOutputIter = rightSideJoiner.joinWithOtherSide(leftSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withRight(inputRow).withLeft(matchedRow)
    +    }
    +
    +    // Filter the joined rows based on the given condition.
    +    val outputFilterFunction = if (condition.isDefined) {
    +      newPredicate(condition.get, left.output ++ right.output).eval _
    +    } else {
    +      (_: InternalRow) => true
    +    }
    +    val filteredOutputIter =
    +      (leftOutputIter ++ rightOutputIter).filter(outputFilterFunction).map { row =>
    +        numOutputRows += 1
    +        row
    +      }
    +
    +    // Function to remove old state after all the input has been consumed and output generated
    +    def onOutputCompletion = {
    +      allUpdatesTimeMs += math.max(NANOSECONDS.toMillis(System.nanoTime - updateStartTimeNs), 0)
    +
    +      // Remove old state if needed
    +      allRemovalsTimeMs += timeTakenMs {
    +        leftSideJoiner.removeOldState(stateWatermarkPredicates.left)
    +        rightSideJoiner.removeOldState(stateWatermarkPredicates.right)
    +      }
    +
    +      // Commit all state changes and update state store metrics
    +      commitTimeMs += timeTakenMs {
    +        val leftSideMetrics = leftSideJoiner.commitStateAndGetMetrics()
    +        val rightSideMetrics = rightSideJoiner.commitStateAndGetMetrics()
    +        val stateStoreMetrics = StateStoreMetrics.combine(Seq(leftSideMetrics, rightSideMetrics))
    +        numUpdatedStateRows +=
    +          (leftSideJoiner.numUpdatedStateRows + rightSideJoiner.numUpdatedStateRows)
    +        numTotalStateRows += stateStoreMetrics.numKeys
    +        stateMemory += stateStoreMetrics.memoryUsedBytes
    +        stateStoreMetrics.customMetrics.foreach { case (metric, value) =>
    +          longMetric(metric.name) += value
    +        }
    +      }
    +    }
    +
    +    CompletionIterator[InternalRow, Iterator[InternalRow]](filteredOutputIter, onOutputCompletion)
    +  }
    +
    +  /**
    +   * Internal helper class to consume input rows, generate join output rows using other sides
    +   * buffered state rows, and finally clean up this sides buffered state rows
    +   */
    +  private class OneSideHashJoiner(
    +      joinSide: JoinSide,
    +      inputAttributes: Seq[Attribute],
    +      joinKeys: Seq[Expression],
    +      inputIter: Iterator[InternalRow]) {
    +
    +    private val joinStateManager = new SymmetricHashJoinStateManager(
    +      joinSide, inputAttributes, joinKeys, stateInfo,
    +      storeConf, hadoopConfBcast.value.value, newPredicate _)
    +    private val keyGenerator = UnsafeProjection.create(joinKeys, inputAttributes)
    +
    +    var numUpdatedStateRows = 0
    +
    +    /**
    +     * Generate joined rows by consuming input from this side, and matching it with the buffered
    +     * rows (i.e. state) of the other side.
    +     * @param otherSideJoiner   Joined of the other side
    +     * @param generateJoinedRow Function to generate the joined row from the
    +     *                          input row from this side and the matched row from the other side
    +     */
    +    def joinWithOtherSide(
    +        otherSideJoiner: OneSideHashJoiner)(
    +        generateJoinedRow: (UnsafeRow, UnsafeRow) => JoinedRow): Iterator[InternalRow] = {
    +
    +      val watermarkAttribute = inputAttributes.find(_.metadata.contains(delayKey))
    +      val nonLateRows =
    +        WatermarkSupport.watermarkExpression(watermarkAttribute, eventTimeWatermark) match {
    +          case Some(watermarkExpr) =>
    +            val predicate = newPredicate(watermarkExpr, inputAttributes)
    +            inputIter.filter { row => !predicate.eval(row) }
    +          case None =>
    +            inputIter
    +        }
    +
    +      nonLateRows.flatMap { row =>
    +        val thisRow = row.asInstanceOf[UnsafeRow]
    +        val key = keyGenerator(thisRow)
    +        joinStateManager.append(key, thisRow)
    +        numUpdatedStateRows += 2
    +        otherSideJoiner.joinStateManager.get(key).map { thatRow =>
    +          generateJoinedRow(thisRow, thatRow)
    +        }
    +      }
    +    }
    +
    +    /** Remove old buffered state rows using watermarks for state keys and values */
    +    def removeOldState(predicate: Option[JoinStateWatermarkPredicate]): Unit = {
    +      predicate match {
    +        case Some(JoinStateKeyWatermarkPredicate(expr)) =>
    +          joinStateManager.removeByPRedicateOnKeys(expr)
    --- End diff --
    
    refactored and renamed completely. now it just takes a pure function, not expression. I moved the predicate code generation to outside the state manager (remove the generatePredicate function in the constructor)


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    @brkyvz 
    1. we can add this test later.
    2. as discussed offline, this method is only to generate the expression and is not responsible for checking whether the expression has watermarks. instead its the operator's responsibility to check and provide the right attribute on which the watermark expression should be defined.



---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by zsxwing <gi...@git.apache.org>.
Github user zsxwing commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140044239
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/StateStore.scala ---
    @@ -27,9 +27,10 @@ import scala.util.control.NonFatal
     import org.apache.hadoop.conf.Configuration
     import org.apache.hadoop.fs.Path
     
    -import org.apache.spark.{SparkContext, SparkEnv}
    +import org.apache.spark.{SparkContext, SparkEnv, TaskContext}
    --- End diff --
    
    nit: `TaskContext` is not used


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit closed the pull request at:

    https://github.com/apache/spark/pull/19271


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139811883
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    +        stored right input, and also stores all the right input. It also generates all rows from
    +        matching new left input with new right input, since the new left input has become stored
    +        by that point. This tiny asymmetry is necessary to avoid doubling.
    +    */
    +    val leftOutputIter = leftSideJoiner.joinWithOtherSide(rightSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withLeft(inputRow).withRight(matchedRow)
    +    }
    +    val rightOutputIter = rightSideJoiner.joinWithOtherSide(leftSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withRight(inputRow).withLeft(matchedRow)
    +    }
    +
    +    // Filter the joined rows based on the given condition.
    +    val outputFilterFunction = if (condition.isDefined) {
    +      newPredicate(condition.get, left.output ++ right.output).eval _
    +    } else {
    +      (_: InternalRow) => true
    +    }
    +    val filteredOutputIter =
    +      (leftOutputIter ++ rightOutputIter).filter(outputFilterFunction).map { row =>
    +        numOutputRows += 1
    +        row
    +      }
    +
    +    // Function to remove old state after all the input has been consumed and output generated
    +    def onOutputCompletion = {
    +      allUpdatesTimeMs += math.max(NANOSECONDS.toMillis(System.nanoTime - updateStartTimeNs), 0)
    +
    +      // Remove old state if needed
    +      allRemovalsTimeMs += timeTakenMs {
    +        leftSideJoiner.removeOldState(stateWatermarkPredicates.left)
    +        rightSideJoiner.removeOldState(stateWatermarkPredicates.right)
    +      }
    +
    +      // Commit all state changes and update state store metrics
    +      commitTimeMs += timeTakenMs {
    +        val leftSideMetrics = leftSideJoiner.commitStateAndGetMetrics()
    +        val rightSideMetrics = rightSideJoiner.commitStateAndGetMetrics()
    +        val stateStoreMetrics = StateStoreMetrics.combine(Seq(leftSideMetrics, rightSideMetrics))
    +        numUpdatedStateRows +=
    +          (leftSideJoiner.numUpdatedStateRows + rightSideJoiner.numUpdatedStateRows)
    +        numTotalStateRows += stateStoreMetrics.numKeys
    +        stateMemory += stateStoreMetrics.memoryUsedBytes
    +        stateStoreMetrics.customMetrics.foreach { case (metric, value) =>
    +          longMetric(metric.name) += value
    +        }
    +      }
    +    }
    +
    +    CompletionIterator[InternalRow, Iterator[InternalRow]](filteredOutputIter, onOutputCompletion)
    +  }
    +
    +  /**
    +   * Internal helper class to consume input rows, generate join output rows using other sides
    +   * buffered state rows, and finally clean up this sides buffered state rows
    +   */
    +  private class OneSideHashJoiner(
    +      joinSide: JoinSide,
    +      inputAttributes: Seq[Attribute],
    +      joinKeys: Seq[Expression],
    +      inputIter: Iterator[InternalRow]) {
    +
    +    private val joinStateManager = new SymmetricHashJoinStateManager(
    +      joinSide, inputAttributes, joinKeys, stateInfo,
    +      storeConf, hadoopConfBcast.value.value, newPredicate _)
    +    private val keyGenerator = UnsafeProjection.create(joinKeys, inputAttributes)
    +
    +    var numUpdatedStateRows = 0
    +
    +    /**
    +     * Generate joined rows by consuming input from this side, and matching it with the buffered
    +     * rows (i.e. state) of the other side.
    +     * @param otherSideJoiner   Joined of the other side
    +     * @param generateJoinedRow Function to generate the joined row from the
    +     *                          input row from this side and the matched row from the other side
    +     */
    +    def joinWithOtherSide(
    +        otherSideJoiner: OneSideHashJoiner)(
    +        generateJoinedRow: (UnsafeRow, UnsafeRow) => JoinedRow): Iterator[InternalRow] = {
    +
    +      val watermarkAttribute = inputAttributes.find(_.metadata.contains(delayKey))
    +      val nonLateRows =
    +        WatermarkSupport.watermarkExpression(watermarkAttribute, eventTimeWatermark) match {
    +          case Some(watermarkExpr) =>
    +            val predicate = newPredicate(watermarkExpr, inputAttributes)
    +            inputIter.filter { row => !predicate.eval(row) }
    +          case None =>
    +            inputIter
    +        }
    +
    +      nonLateRows.flatMap { row =>
    +        val thisRow = row.asInstanceOf[UnsafeRow]
    +        val key = keyGenerator(thisRow)
    +        joinStateManager.append(key, thisRow)
    +        numUpdatedStateRows += 2
    +        otherSideJoiner.joinStateManager.get(key).map { thatRow =>
    +          generateJoinedRow(thisRow, thatRow)
    +        }
    +      }
    +    }
    +
    +    /** Remove old buffered state rows using watermarks for state keys and values */
    +    def removeOldState(predicate: Option[JoinStateWatermarkPredicate]): Unit = {
    +      predicate match {
    +        case Some(JoinStateKeyWatermarkPredicate(expr)) =>
    +          joinStateManager.removeByPRedicateOnKeys(expr)
    --- End diff --
    
    `removeByPredicateOnKeys`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139852081
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in milliseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              val millisPerMonth = CalendarInterval.MICROS_PER_DAY / 1000 * 31
    +              val intervalMillis = calendarInterval.milliseconds +
    +                millisPerMonth * calendarInterval.months
    +              Literal(intervalMillis.toDouble)
    +            case DoubleType => Multiply(lit, Literal(1000.0))
    +            case _: NumericType | _: TimestampType =>
    +              Multiply(Cast(lit, DoubleType), Literal(1000.0))
    --- End diff --
    
    updated the watermark extraction to calculate everything in microseconds.
    though, event time is tracked at millisecond level, so i am not sure whether this helps much.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139815599
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in milliseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    --- End diff --
    
    fancy stuff!


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81986 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81986/testReport)** for PR 19271 at commit [`2ed62d8`](https://github.com/apache/spark/commit/2ed62d82373446dbbb4da5ee8e5af32c5571aef0).
     * This patch **fails Spark unit tests**.
     * This patch merges cleanly.
     * This patch adds the following public classes _(experimental)_:
      * `  sealed trait JoinStateWatermarkPredicate `
      * `  case class JoinStateKeyWatermarkPredicate(expr: Expression)`
      * `  case class JoinStateValueWatermarkPredicate(expr: Expression)`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81986 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81986/testReport)** for PR 19271 at commit [`2ed62d8`](https://github.com/apache/spark/commit/2ed62d82373446dbbb4da5ee8e5af32c5571aef0).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139808731
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    --- End diff --
    
    how does this behave if I union two streams with the same schema, but one stream has a watermark delay of 25 minutes whereas the other one has a delay of 5 minutes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139807021
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    --- End diff --
    
    ditto


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test FAILed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/81913/
    Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139806701
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    --- End diff --
    
    I'm a bit confused by this paragraph.
    Let's say I have two streams:
     - Stream A -> 5 minute watermark
     - Stream B -> 10 minute watermark
    
    As I understand from the paragraph, the `StateKeyWatermark(StreamA) + interval` of Stream A is the `StateValueWatermark` of Stream B. But I'm a bit confused on when state gets discarded based on these values.



---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139815159
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    --- End diff --
    
    ditto


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139814640
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    --- End diff --
    
    nit: `a @ Attribute`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test FAILed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/81957/
    Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139833518
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test PASSed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139830591
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81907 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81907/testReport)** for PR 19271 at commit [`94b63fb`](https://github.com/apache/spark/commit/94b63fb4082c98ff4f2e67e0759e98f46a3537a5).
     * This patch **fails Spark unit tests**.
     * This patch merges cleanly.
     * This patch adds the following public classes _(experimental)_:
      * `  sealed trait JoinStateWatermarkPredicate `


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140061998
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,344 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, BindReferences, Expression, JoinedRow, Literal, NamedExpression, PreciseTimestampConversion, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.sql.types.{LongType, TimestampType}
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and the new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range conditions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + *
    + * @param leftKeys  Expression to generate key rows for joining from left input
    + * @param rightKeys Expression to generate key rows for joining from right input
    + * @param joinType  Type of join (inner, left outer, etc.)
    + * @param condition Optional, additional condition to filter output of the equi-join
    + * @param stateInfo Version information required to read join state (buffered rows)
    + * @param eventTimeWatermark Watermark of input event, same for both sides
    + * @param stateWatermarkPredicates Predicates for removal of state, see
    + *                                 [[JoinStateWatermarkPredicates]]
    + * @param left      Left child plan
    + * @param right     Right child plan
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  require(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  require(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    if (stateInfo.isEmpty) {
    +      throw new IllegalStateException(s"Cannot execute join as state info was not specified\n$this")
    +    }
    +
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(
    +      LeftSide, left.output, leftKeys, leftInputIter, stateWatermarkPredicates.left)
    +    val rightSideJoiner = new OneSideHashJoiner(
    +      RightSide, right.output, rightKeys, rightInputIter, stateWatermarkPredicates.right)
    +
    +    //  Join one side input using the other side's buffered/state rows. Here is how it is done.
    +    //
    +    //  - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +    //    stored right input, and also stores all the left input
    +    //
    +    //  - `rightJoiner.joinWith(leftJoiner)` generates all rows from matching new right input with
    +    //    stored right input, and also stores all the right input. It also generates all rows from
    --- End diff --
    
    oops. fixed


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test FAILed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/81900/
    Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81957 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81957/testReport)** for PR 19271 at commit [`8a551d7`](https://github.com/apache/spark/commit/8a551d7fc045fd633cc15c14b27133a13eacb727).
     * This patch **fails Spark unit tests**.
     * This patch merges cleanly.
     * This patch adds no public classes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81900 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81900/testReport)** for PR 19271 at commit [`9edaa58`](https://github.com/apache/spark/commit/9edaa580edc6e599b01033190a9ef383ed001ae9).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81913 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81913/testReport)** for PR 19271 at commit [`9004d46`](https://github.com/apache/spark/commit/9004d46a70ddb785f687d4a2726fe73719553ae3).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139892790
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,344 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, BindReferences, Expression, JoinedRow, Literal, NamedExpression, PreciseTimestampConversion, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.sql.types.{LongType, TimestampType}
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and the new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range conditions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + *
    + * @param leftKeys  Expression to generate key rows for joining from left input
    + * @param rightKeys Expression to generate key rows for joining from right input
    + * @param joinType  Type of join (inner, left outer, etc.)
    + * @param condition Optional, additional condition to filter output of the equi-join
    + * @param stateInfo Version information required to read join state (buffered rows)
    + * @param eventTimeWatermark Watermark of input event, same for both sides
    + * @param stateWatermarkPredicates Predicates for removal of state, see
    + *                                 [[JoinStateWatermarkPredicates]]
    + * @param left      Left child plan
    + * @param right     Right child plan
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  require(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  require(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    if (stateInfo.isEmpty) {
    +      throw new IllegalStateException(s"Cannot execute join as state info was not specified\n$this")
    +    }
    +
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(
    +      LeftSide, left.output, leftKeys, leftInputIter, stateWatermarkPredicates.left)
    +    val rightSideJoiner = new OneSideHashJoiner(
    +      RightSide, right.output, rightKeys, rightInputIter, stateWatermarkPredicates.right)
    +
    +    //  Join one side input using the other side's buffered/state rows. Here is how it is done.
    +    //
    +    //  - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +    //    stored right input, and also stores all the left input
    +    //
    +    //  - `rightJoiner.joinWith(leftJoiner)` generates all rows from matching new right input with
    +    //    stored right input, and also stores all the right input. It also generates all rows from
    +    //    matching new left input with new right input, since the new left input has become stored
    +    //    by that point. This tiny asymmetry is necessary to avoid doubling.
    --- End diff --
    
    avoid `duplication` sounds better maybe? up to you


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139831209
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139813588
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    --- End diff --
    
    where can the `joinCondition` be `None`?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139812315
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    --- End diff --
    
    I see this code is duplicated above. Maybe you can make it a function?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140062127
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinHelper.scala ---
    @@ -0,0 +1,407 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.reflect.ClassTag
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.{Partition, SparkContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, AttributeSet, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.execution.streaming.state.{StateStoreCoordinatorRef, StateStoreProvider, StateStoreProviderId}
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]]. See that object for more details.
    + */
    +object StreamingSymmetricHashJoinHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate {
    +    def expr: Expression
    +    def desc: String
    +    override def toString: String = s"$desc: $expr"
    +  }
    +  /** Predicate for watermark on state keys */
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression)
    +    extends JoinStateWatermarkPredicate {
    +    def desc: String = "key predicate"
    +  }
    +  /** Predicate for watermark on state values */
    +  case class JoinStateValueWatermarkPredicate(expr: Expression)
    +    extends JoinStateWatermarkPredicate {
    +    def desc: String = "value predicate"
    +  }
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None) {
    +    override def toString(): String = {
    +      s"state cleanup [ left ${left.map(_.toString).getOrElse("= null")}, " +
    +        s"right ${right.map(_.toString).getOrElse("")} ]"
    +    }
    +  }
    +
    +  /** Get the predicates defining the state watermarks for both sides of the join */
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +
    +
    +    // Join keys of both sides generate rows of the same fields, that is, same sequence of data
    +    // types. If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +    // then it will never consider joining keys that are < state key watermark (i.e. event time
    +    // watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +    // there has to be an equivalent column (i.e., timestamp). And any right side data that has the
    +    // timestamp < watermark will not match will not match with left side data, as the left side get
    +    // filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +    // left side keys effectively causes the timestamp on the right side to have a watermark.
    +    // We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +    // right timestamp in the right keys.
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 in the StreamingSymmetricHashJoinExec docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate.apply _)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 in the StreamingSymmetricHashJoinExec docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = AttributeSet(oneSideInputAttributes),
    +          attributesWithEventWatermark = AttributeSet(otherSideInputAttributes),
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate.apply _)
    +
    +      } else {
    +        None
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   * This function is supposed to make best-effort attempt to get the state watermark. If there is
    +   * any error, it will return None.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds, is possible.
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: AttributeSet,
    +      attributesWithEventWatermark: AttributeSet,
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    // If condition or event time watermark is not provided, then cannot calculate state watermark
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    // If there is not watermark attribute, then cannot define state watermark
    +    if (!attributesWithEventWatermark.exists(_.metadata.contains(delayKey))) return None
    +    val attributesInCondition =
    +      AttributeSet(joinCondition.get.collect { case a: AttributeReference => a })
    +    if (
    +      attributesInCondition.filter { attributesToFindStateWatemarkFor.contains(_)}.size > 1 ||
    +      attributesInCondition.filter { attributesWithEventWatermark.contains(_)}.size > 1) {
    +      // If more than attributes present in condition from one side, then it cannot be solved
    +      return None
    +    }
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatermarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +
    +      // The generated the state watermark cleanup expression is inclusive of the state watermark.
    +      // If state watermark is W, all state where timestamp <= W will be cleaned up.
    +      // Now when the canonicalized join condition solves to leftTime >= W, we dont want to clean
    +      // up leftTime <= W. Rather we should clean up leftTime <= W - 1. Hence the -1 below.
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatermarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: AttributeSet,
    +      attributesWithEventWatermark: AttributeSet,
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a @ AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a @ AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && metadata.contains(delayKey) =>
    +          Multiply(Literal(eventWatermark.get.toDouble), Literal(1000.0))
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some((Double2double(constraintValue) / 1000.0).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Cast(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in microseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              if (calendarInterval.months > 0) {
    +                invalid = true
    +                logWarning(
    +                  s"Failed to extract state value watermark from condition $exprToCollectFrom " +
    +                    s"as imprecise intervals like months and years cannot be used for precise" +
    +                    s"watermark calculation")
    +                Literal(0.0)
    +              } else {
    +                Literal(calendarInterval.microseconds.toDouble)
    +              }
    +            case DoubleType =>
    +              Multiply(lit, Literal(1000000.0))
    +            case _: NumericType | _: TimestampType =>
    +              Multiply(Cast(lit, DoubleType), Literal(1000000.0))
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test PASSed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/82010/
    Test PASSed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139835427
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in milliseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              val millisPerMonth = CalendarInterval.MICROS_PER_DAY / 1000 * 31
    +              val intervalMillis = calendarInterval.milliseconds +
    +                millisPerMonth * calendarInterval.months
    +              Literal(intervalMillis.toDouble)
    +            case DoubleType => Multiply(lit, Literal(1000.0))
    +            case _: NumericType | _: TimestampType =>
    +              Multiply(Cast(lit, DoubleType), Literal(1000.0))
    --- End diff --
    
    if you are joining by equating timestamps, then it will work because its in the keys. the code path is used only when the time ranges are given, which is any way imprecise.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #82002 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/82002/testReport)** for PR 19271 at commit [`9e3be26`](https://github.com/apache/spark/commit/9e3be269a28ae19f2a1cf285aa8c834276642ee4).
     * This patch passes all tests.
     * This patch merges cleanly.
     * This patch adds no public classes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139804087
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    --- End diff --
    
    nit: `with the new data`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by zsxwing <gi...@git.apache.org>.
Github user zsxwing commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140055717
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/SymmetricHashJoinStateManager.scala ---
    @@ -0,0 +1,403 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming.state
    +
    +import scala.reflect.ClassTag
    +
    +import org.apache.hadoop.conf.Configuration
    +
    +import org.apache.spark.{Partition, SparkContext, TaskContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, BindReferences, Expression, LessThanOrEqual, Literal, SpecificInternalRow, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.Predicate
    +import org.apache.spark.sql.execution.streaming.{StatefulOperatorStateInfo, StreamingSymmetricHashJoinExec}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.types.{LongType, StructField, StructType, TimestampType}
    +import org.apache.spark.util.NextIterator
    +
    +/**
    + * Helper class to manage state required by a single side of [[StreamingSymmetricHashJoinExec]].
    + * The interface of this class is basically that of a multi-map:
    + * - Get: Returns an iterator of multiple values for given key
    + * - Append: Append a new value to the given key
    + * - Remove Data by predicate: Drop any state using a predicate condition on keys or values
    + *
    + * @param joinSide              Defines the join side
    + * @param inputValueAttributes  Attributes of the input row which will be stored as value
    + * @param joinKeys              Expressions to generate rows that will be used to key the value rows
    + * @param stateInfo             Information about how to retrieve the correct version of state
    + * @param storeConf             Configuration for the state store.
    + * @param hadoopConf            Hadoop configuration for reading state data from storage
    + *
    + * Internally, the key -> multiple values is stored in two [[StateStore]]s.
    + * - Store 1 ([[KeyToNumValuesStore]]) maintains mapping between key -> number of values
    + * - Store 2 ([[KeyWithIndexToValueStore]]) maintains mapping between (key, index) -> value
    + * - Put:   update count in KeyToNumValuesStore,
    + *          insert new (key, count) -> value in KeyWithIndexToValueStore
    + * - Get:   read count from KeyToNumValuesStore,
    + *          read each of the n values in KeyWithIndexToValueStore
    + * - Remove state by predicate on keys:
    + *          scan all keys in KeyToNumValuesStore to find keys that do match the predicate,
    + *          delete from key from KeyToNumValuesStore, delete values in KeyWithIndexToValueStore
    + * - Remove state by condition on values:
    + *          scan all [(key, index) -> value] in KeyWithIndexToValueStore to find values that match
    + *          the predicate, delete corresponding (key, indexToDelete) from KeyWithIndexToValueStore
    + *          by overwriting with the value of (key, maxIndex), and removing [(key, maxIndex),
    + *          decrement corresponding num values in KeyToNumValuesStore
    + */
    +class SymmetricHashJoinStateManager(
    +    val joinSide: JoinSide,
    +    inputValueAttributes: Seq[Attribute],
    +    joinKeys: Seq[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    storeConf: StateStoreConf,
    +    hadoopConf: Configuration) extends Logging {
    +
    +  import SymmetricHashJoinStateManager._
    +
    +  // Clean up any state store resources if necessary at the end of the task
    +  Option(TaskContext.get()).foreach { _.addTaskCompletionListener { _ => abortIfNeeded() } }
    +
    +  /*
    +  =====================================================
    +                  Public methods
    +  =====================================================
    +   */
    +
    +  /** Get all the values of a key */
    +  def get(key: UnsafeRow): Iterator[UnsafeRow] = {
    +    val numValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.getAll(key, numValues)
    +  }
    +
    +  /** Append a new value to the key */
    +  def append(key: UnsafeRow, value: UnsafeRow): Unit = {
    +    val numExistingValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.put(key, numExistingValues, value)
    +    keyToNumValues.put(key, numExistingValues + 1)
    +  }
    +
    +  /**
    +   * Remove using a predicate on keys. See class docs for more context and implement details.
    +   */
    +  def removeByKeyCondition(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      if (condition(keyToNumValue.key)) {
    +        keyToNumValues.remove(keyToNumValue.key)
    +        keyWithIndexToValue.removeAllValues(keyToNumValue.key, keyToNumValue.numValue)
    +      }
    +    }
    +  }
    +
    +  /**
    +   * Remove using a predicate on values. See class docs for more context and implementation details.
    +   */
    +  def removeByValueCondition(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +    var numValues: Long = 0L
    --- End diff --
    
    nit: could you declare them inside the while loop? Then it's easy to tell that they will not be used cross keys.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139811702
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    +        stored right input, and also stores all the right input. It also generates all rows from
    +        matching new left input with new right input, since the new left input has become stored
    +        by that point. This tiny asymmetry is necessary to avoid doubling.
    +    */
    +    val leftOutputIter = leftSideJoiner.joinWithOtherSide(rightSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withLeft(inputRow).withRight(matchedRow)
    +    }
    +    val rightOutputIter = rightSideJoiner.joinWithOtherSide(leftSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withRight(inputRow).withLeft(matchedRow)
    +    }
    +
    +    // Filter the joined rows based on the given condition.
    +    val outputFilterFunction = if (condition.isDefined) {
    +      newPredicate(condition.get, left.output ++ right.output).eval _
    +    } else {
    +      (_: InternalRow) => true
    +    }
    +    val filteredOutputIter =
    +      (leftOutputIter ++ rightOutputIter).filter(outputFilterFunction).map { row =>
    +        numOutputRows += 1
    +        row
    +      }
    +
    +    // Function to remove old state after all the input has been consumed and output generated
    +    def onOutputCompletion = {
    +      allUpdatesTimeMs += math.max(NANOSECONDS.toMillis(System.nanoTime - updateStartTimeNs), 0)
    +
    +      // Remove old state if needed
    +      allRemovalsTimeMs += timeTakenMs {
    +        leftSideJoiner.removeOldState(stateWatermarkPredicates.left)
    +        rightSideJoiner.removeOldState(stateWatermarkPredicates.right)
    +      }
    +
    +      // Commit all state changes and update state store metrics
    +      commitTimeMs += timeTakenMs {
    +        val leftSideMetrics = leftSideJoiner.commitStateAndGetMetrics()
    +        val rightSideMetrics = rightSideJoiner.commitStateAndGetMetrics()
    +        val stateStoreMetrics = StateStoreMetrics.combine(Seq(leftSideMetrics, rightSideMetrics))
    +        numUpdatedStateRows +=
    +          (leftSideJoiner.numUpdatedStateRows + rightSideJoiner.numUpdatedStateRows)
    +        numTotalStateRows += stateStoreMetrics.numKeys
    +        stateMemory += stateStoreMetrics.memoryUsedBytes
    +        stateStoreMetrics.customMetrics.foreach { case (metric, value) =>
    +          longMetric(metric.name) += value
    +        }
    +      }
    +    }
    +
    +    CompletionIterator[InternalRow, Iterator[InternalRow]](filteredOutputIter, onOutputCompletion)
    +  }
    +
    +  /**
    +   * Internal helper class to consume input rows, generate join output rows using other sides
    +   * buffered state rows, and finally clean up this sides buffered state rows
    +   */
    +  private class OneSideHashJoiner(
    +      joinSide: JoinSide,
    +      inputAttributes: Seq[Attribute],
    +      joinKeys: Seq[Expression],
    +      inputIter: Iterator[InternalRow]) {
    +
    +    private val joinStateManager = new SymmetricHashJoinStateManager(
    +      joinSide, inputAttributes, joinKeys, stateInfo,
    +      storeConf, hadoopConfBcast.value.value, newPredicate _)
    +    private val keyGenerator = UnsafeProjection.create(joinKeys, inputAttributes)
    +
    +    var numUpdatedStateRows = 0
    +
    +    /**
    +     * Generate joined rows by consuming input from this side, and matching it with the buffered
    +     * rows (i.e. state) of the other side.
    +     * @param otherSideJoiner   Joined of the other side
    +     * @param generateJoinedRow Function to generate the joined row from the
    +     *                          input row from this side and the matched row from the other side
    +     */
    +    def joinWithOtherSide(
    +        otherSideJoiner: OneSideHashJoiner)(
    +        generateJoinedRow: (UnsafeRow, UnsafeRow) => JoinedRow): Iterator[InternalRow] = {
    +
    +      val watermarkAttribute = inputAttributes.find(_.metadata.contains(delayKey))
    +      val nonLateRows =
    +        WatermarkSupport.watermarkExpression(watermarkAttribute, eventTimeWatermark) match {
    +          case Some(watermarkExpr) =>
    +            val predicate = newPredicate(watermarkExpr, inputAttributes)
    +            inputIter.filter { row => !predicate.eval(row) }
    +          case None =>
    +            inputIter
    +        }
    +
    +      nonLateRows.flatMap { row =>
    +        val thisRow = row.asInstanceOf[UnsafeRow]
    +        val key = keyGenerator(thisRow)
    +        joinStateManager.append(key, thisRow)
    +        numUpdatedStateRows += 2
    --- End diff --
    
    why 2?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139816206
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in milliseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              val millisPerMonth = CalendarInterval.MICROS_PER_DAY / 1000 * 31
    +              val intervalMillis = calendarInterval.milliseconds +
    +                millisPerMonth * calendarInterval.months
    +              Literal(intervalMillis.toDouble)
    +            case DoubleType => Multiply(lit, Literal(1000.0))
    +            case _: NumericType | _: TimestampType =>
    +              Multiply(Cast(lit, DoubleType), Literal(1000.0))
    --- End diff --
    
    for timestamp, you can use `PreciseTimestamp` to get microsecond precision btw


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139804436
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    --- End diff --
    
    nit: `say each join side`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #82010 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/82010/testReport)** for PR 19271 at commit [`5198ef3`](https://github.com/apache/spark/commit/5198ef3d2bf38ce827f0fd5a026724f0825dd7dd).
     * This patch passes all tests.
     * This patch merges cleanly.
     * This patch adds no public classes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test PASSed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/82004/
    Test PASSed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test FAILed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/81958/
    Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81913 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81913/testReport)** for PR 19271 at commit [`9004d46`](https://github.com/apache/spark/commit/9004d46a70ddb785f687d4a2726fe73719553ae3).
     * This patch **fails Spark unit tests**.
     * This patch merges cleanly.
     * This patch adds no public classes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140063861
  
    --- Diff: sql/core/src/test/scala/org/apache/spark/sql/streaming/StreamingJoinSuite.scala ---
    @@ -0,0 +1,585 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.streaming
    +
    +import java.util.UUID
    +
    +import scala.util.Random
    +
    +import org.apache.hadoop.conf.Configuration
    +import org.scalatest.BeforeAndAfter
    +
    +import org.apache.spark.scheduler.ExecutorCacheTaskLocation
    +import org.apache.spark.sql.SparkSession
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, AttributeSet, BoundReference, Expression, GenericInternalRow, LessThanOrEqual, Literal, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.{GeneratePredicate}
    +import org.apache.spark.sql.catalyst.plans.logical.{EventTimeWatermark, Filter}
    +import org.apache.spark.sql.execution.LogicalRDD
    +import org.apache.spark.sql.execution.streaming.{MemoryStream, StatefulOperatorStateInfo, StreamingSymmetricHashJoinHelper}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper.LeftSide
    +import org.apache.spark.sql.execution.streaming.state.{StateStore, StateStoreConf, StateStoreProviderId, SymmetricHashJoinStateManager}
    +import org.apache.spark.sql.functions._
    +import org.apache.spark.sql.types._
    +import org.apache.spark.util.Utils
    +
    +
    +class StreamingJoinSuite extends StreamTest with StateStoreMetricsTest with BeforeAndAfter {
    +
    +  before {
    +    SparkSession.setActiveSession(spark)  // set this before force initializing 'joinExec'
    +    spark.streams.stateStoreCoordinator   // initialize the lazy coordinator
    +  }
    +
    +  after {
    +    StateStore.stop()
    +  }
    +
    +  import testImplicits._
    +
    +  test("SymmetricHashJoinStateManager - all operations") {
    +    val watermarkMetadata = new MetadataBuilder().putLong(EventTimeWatermark.delayKey, 10).build()
    +    val inputValueSchema = new StructType()
    +      .add(StructField("time", IntegerType, metadata = watermarkMetadata))
    +      .add(StructField("value", BooleanType))
    +    val inputValueAttribs = inputValueSchema.toAttributes
    +    val inputValueAttribWithWatermark = inputValueAttribs(0)
    +    val joinKeyExprs = Seq[Expression](Literal(false), inputValueAttribWithWatermark, Literal(10.0))
    +
    +    val inputValueGen = UnsafeProjection.create(inputValueAttribs.map(_.dataType).toArray)
    +    val joinKeyGen = UnsafeProjection.create(joinKeyExprs.map(_.dataType).toArray)
    +
    +    def toInputValue(i: Int): UnsafeRow = {
    +      inputValueGen.apply(new GenericInternalRow(Array[Any](i, false)))
    +    }
    +
    +    def toJoinKeyRow(i: Int): UnsafeRow = {
    +      joinKeyGen.apply(new GenericInternalRow(Array[Any](false, i, 10.0)))
    +    }
    +
    +    def toKeyInt(joinKeyRow: UnsafeRow): Int = joinKeyRow.getInt(1)
    +
    +    def toValueInt(inputValueRow: UnsafeRow): Int = inputValueRow.getInt(0)
    +
    +    withJoinStateManager(inputValueAttribs, joinKeyExprs) { manager =>
    +      def append(key: Int, value: Int): Unit = {
    +        manager.append(toJoinKeyRow(key), toInputValue(value))
    +      }
    +
    +      def get(key: Int): Seq[Int] = manager.get(toJoinKeyRow(key)).map(toValueInt).toSeq.sorted
    +
    +      /** Remove keys (and corresponding values) where `time <= threshold` */
    +      def removeByKey(threshold: Long): Unit = {
    +        val expr =
    +          LessThanOrEqual(
    +            BoundReference(
    +              1, inputValueAttribWithWatermark.dataType, inputValueAttribWithWatermark.nullable),
    +            Literal(threshold))
    +        manager.removeByKeyCondition(GeneratePredicate.generate(expr).eval _)
    +      }
    +
    +      /** Remove values where `time <= threshold` */
    +      def removeByValue(watermark: Long): Unit = {
    +        val expr = LessThanOrEqual(inputValueAttribWithWatermark, Literal(watermark))
    +        manager.removeByValueCondition(
    +          GeneratePredicate.generate(expr, inputValueAttribs).eval _)
    +      }
    +
    +      def numRows: Long = {
    +        manager.metrics.numKeys
    +      }
    +
    +      assert(get(20) === Seq.empty)     // initially empty
    +      append(20, 2)
    +      assert(get(20) === Seq(2))        // should first value correctly
    +      assert(numRows === 1)
    +
    +      append(20, 3)
    +      assert(get(20) === Seq(2, 3))     // should append new values
    +      append(20, 3)
    +      assert(get(20) === Seq(2, 3, 3))  // should append another copy if same value added again
    +      assert(numRows === 3)
    +
    +      assert(get(30) === Seq.empty)
    +      append(30, 1)
    +      assert(get(30) === Seq(1))
    +      assert(get(20) === Seq(2, 3, 3))  // add another key-value should not affect existing ones
    +      assert(numRows === 4)
    +
    +      removeByKey(25)
    +      assert(get(20) === Seq.empty)
    +      assert(get(30) === Seq(1))        // should remove 20, not 30
    +      assert(numRows === 1)
    +
    +      removeByKey(30)
    +      assert(get(30) === Seq.empty)     // should remove 30
    +      assert(numRows === 0)
    +
    +      def appendAndTest(key: Int, values: Int*): Unit = {
    +        values.foreach { value => append(key, value)}
    +        require(get(key) === values)
    +      }
    +
    +      appendAndTest(40, 100, 200, 300)
    +      appendAndTest(50, 125)
    +      appendAndTest(60, 275)              // prepare for testing removeByValue
    +      assert(numRows === 5)
    +
    +      removeByValue(125)
    +      assert(get(40) === Seq(200, 300))
    +      assert(get(50) === Seq.empty)
    +      assert(get(60) === Seq(275))        // should remove only some values, not all
    +      assert(numRows === 3)
    +
    +      append(40, 50)
    +      assert(get(40) === Seq(50, 200, 300))
    +      assert(numRows === 4)
    +
    +      removeByValue(200)
    +      assert(get(40) === Seq(300))
    +      assert(get(60) === Seq(275))        // should remove only some values, not all
    +      assert(numRows === 2)
    +
    +      removeByValue(300)
    +      assert(get(40) === Seq.empty)
    +      assert(get(60) === Seq.empty)       // should remove all values now
    +      assert(numRows === 0)
    +    }
    +  }
    +
    +  test("stream stream inner join on non-time column") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF.select('value as "key", ('value * 2) as "leftValue")
    +    val df2 = input2.toDF.select('value as "key", ('value * 3) as "rightValue")
    +    val joined = df1.join(df2, "key")
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckAnswer(),
    +      AddData(input2, 1, 10),       // 1 arrived on input1 first, then input2, should join
    +      CheckLastBatch((1, 2, 3)),
    +      AddData(input1, 10),          // 10 arrived on input2 first, then input1, should join
    +      CheckLastBatch((10, 20, 30)),
    +      AddData(input2, 1),           // another 1 in input2 should join with 1 input1
    +      CheckLastBatch((1, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 1), // multiple 1s should be kept in state causing multiple (1, 2, 3)
    +      CheckLastBatch((1, 2, 3), (1, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 100),
    +      AddData(input2, 100),
    +      CheckLastBatch((100, 200, 300))
    +    )
    +  }
    +
    +
    +  test("stream stream inner join on windows - without watermark") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp", ('value * 2) as "leftValue")
    +      .select('key, window('timestamp, "10 second"), 'leftValue)
    +
    +    val df2 = input2.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp",
    +        ('value * 3) as "rightValue")
    +      .select('key, window('timestamp, "10 second"), 'rightValue)
    +
    +    val joined = df1.join(df2, Seq("key", "window"))
    +      .select('key, $"window.end".cast("long"), 'leftValue, 'rightValue)
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckLastBatch(),
    +      AddData(input2, 1),
    +      CheckLastBatch((1, 10, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 25),
    +      CheckLastBatch(),
    +      StopStream,
    +      StartStream(),
    +      AddData(input2, 25),
    +      CheckLastBatch((25, 30, 50, 75)),
    +      AddData(input1, 1),
    +      CheckLastBatch((1, 10, 2, 3)),      // State for 1 still around as there is not watermark
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 5),
    +      CheckLastBatch(),
    +      AddData(input2, 5),
    +      CheckLastBatch((5, 10, 10, 15))     // No filter by any watermark
    +    )
    +  }
    +
    +  test("stream stream inner join on windows - with watermark") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp", ('value * 2) as "leftValue")
    +      .withWatermark("timestamp", "10 seconds")
    +      .select('key, window('timestamp, "10 second"), 'leftValue)
    +
    +    val df2 = input2.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp",
    +        ('value * 3) as "rightValue")
    +      .select('key, window('timestamp, "10 second"), 'rightValue)
    +
    +    val joined = df1.join(df2, Seq("key", "window"))
    +      .select('key, $"window.end".cast("long"), 'leftValue, 'rightValue)
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckAnswer(),
    +      assertNumStateRows(total = 1, updated = 1),
    --- End diff --
    
    We are not counting the number of rows for the counts as it is confusing to do so. The whole point of this metric is to convey the number of rows effectively stored as the state, and should not depend on implementation details such as keeping the counts separately.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139812115
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    +        stored right input, and also stores all the right input. It also generates all rows from
    +        matching new left input with new right input, since the new left input has become stored
    +        by that point. This tiny asymmetry is necessary to avoid doubling.
    +    */
    +    val leftOutputIter = leftSideJoiner.joinWithOtherSide(rightSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withLeft(inputRow).withRight(matchedRow)
    +    }
    +    val rightOutputIter = rightSideJoiner.joinWithOtherSide(leftSideJoiner) {
    +      (inputRow: UnsafeRow, matchedRow: UnsafeRow) =>
    +        joinedRow.withRight(inputRow).withLeft(matchedRow)
    +    }
    +
    +    // Filter the joined rows based on the given condition.
    +    val outputFilterFunction = if (condition.isDefined) {
    +      newPredicate(condition.get, left.output ++ right.output).eval _
    +    } else {
    +      (_: InternalRow) => true
    +    }
    +    val filteredOutputIter =
    +      (leftOutputIter ++ rightOutputIter).filter(outputFilterFunction).map { row =>
    +        numOutputRows += 1
    +        row
    +      }
    +
    +    // Function to remove old state after all the input has been consumed and output generated
    +    def onOutputCompletion = {
    +      allUpdatesTimeMs += math.max(NANOSECONDS.toMillis(System.nanoTime - updateStartTimeNs), 0)
    +
    +      // Remove old state if needed
    +      allRemovalsTimeMs += timeTakenMs {
    +        leftSideJoiner.removeOldState(stateWatermarkPredicates.left)
    +        rightSideJoiner.removeOldState(stateWatermarkPredicates.right)
    +      }
    +
    +      // Commit all state changes and update state store metrics
    +      commitTimeMs += timeTakenMs {
    +        val leftSideMetrics = leftSideJoiner.commitStateAndGetMetrics()
    +        val rightSideMetrics = rightSideJoiner.commitStateAndGetMetrics()
    +        val stateStoreMetrics = StateStoreMetrics.combine(Seq(leftSideMetrics, rightSideMetrics))
    +        numUpdatedStateRows +=
    +          (leftSideJoiner.numUpdatedStateRows + rightSideJoiner.numUpdatedStateRows)
    +        numTotalStateRows += stateStoreMetrics.numKeys
    +        stateMemory += stateStoreMetrics.memoryUsedBytes
    +        stateStoreMetrics.customMetrics.foreach { case (metric, value) =>
    +          longMetric(metric.name) += value
    +        }
    +      }
    +    }
    +
    +    CompletionIterator[InternalRow, Iterator[InternalRow]](filteredOutputIter, onOutputCompletion)
    +  }
    +
    +  /**
    +   * Internal helper class to consume input rows, generate join output rows using other sides
    +   * buffered state rows, and finally clean up this sides buffered state rows
    +   */
    +  private class OneSideHashJoiner(
    +      joinSide: JoinSide,
    +      inputAttributes: Seq[Attribute],
    +      joinKeys: Seq[Expression],
    +      inputIter: Iterator[InternalRow]) {
    +
    +    private val joinStateManager = new SymmetricHashJoinStateManager(
    +      joinSide, inputAttributes, joinKeys, stateInfo,
    +      storeConf, hadoopConfBcast.value.value, newPredicate _)
    +    private val keyGenerator = UnsafeProjection.create(joinKeys, inputAttributes)
    +
    +    var numUpdatedStateRows = 0
    +
    +    /**
    +     * Generate joined rows by consuming input from this side, and matching it with the buffered
    +     * rows (i.e. state) of the other side.
    +     * @param otherSideJoiner   Joined of the other side
    +     * @param generateJoinedRow Function to generate the joined row from the
    +     *                          input row from this side and the matched row from the other side
    +     */
    +    def joinWithOtherSide(
    --- End diff --
    
    `storeAndJoinWith` seems to provide the best context about what this method is doing


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140052754
  
    --- Diff: sql/core/src/test/scala/org/apache/spark/sql/streaming/StreamingJoinSuite.scala ---
    @@ -0,0 +1,585 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.streaming
    +
    +import java.util.UUID
    +
    +import scala.util.Random
    +
    +import org.apache.hadoop.conf.Configuration
    +import org.scalatest.BeforeAndAfter
    +
    +import org.apache.spark.scheduler.ExecutorCacheTaskLocation
    +import org.apache.spark.sql.SparkSession
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, AttributeSet, BoundReference, Expression, GenericInternalRow, LessThanOrEqual, Literal, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.{GeneratePredicate}
    +import org.apache.spark.sql.catalyst.plans.logical.{EventTimeWatermark, Filter}
    +import org.apache.spark.sql.execution.LogicalRDD
    +import org.apache.spark.sql.execution.streaming.{MemoryStream, StatefulOperatorStateInfo, StreamingSymmetricHashJoinHelper}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper.LeftSide
    +import org.apache.spark.sql.execution.streaming.state.{StateStore, StateStoreConf, StateStoreProviderId, SymmetricHashJoinStateManager}
    +import org.apache.spark.sql.functions._
    +import org.apache.spark.sql.types._
    +import org.apache.spark.util.Utils
    +
    +
    +class StreamingJoinSuite extends StreamTest with StateStoreMetricsTest with BeforeAndAfter {
    +
    +  before {
    +    SparkSession.setActiveSession(spark)  // set this before force initializing 'joinExec'
    +    spark.streams.stateStoreCoordinator   // initialize the lazy coordinator
    +  }
    +
    +  after {
    +    StateStore.stop()
    +  }
    +
    +  import testImplicits._
    +
    +  test("SymmetricHashJoinStateManager - all operations") {
    +    val watermarkMetadata = new MetadataBuilder().putLong(EventTimeWatermark.delayKey, 10).build()
    +    val inputValueSchema = new StructType()
    +      .add(StructField("time", IntegerType, metadata = watermarkMetadata))
    +      .add(StructField("value", BooleanType))
    +    val inputValueAttribs = inputValueSchema.toAttributes
    +    val inputValueAttribWithWatermark = inputValueAttribs(0)
    +    val joinKeyExprs = Seq[Expression](Literal(false), inputValueAttribWithWatermark, Literal(10.0))
    +
    +    val inputValueGen = UnsafeProjection.create(inputValueAttribs.map(_.dataType).toArray)
    +    val joinKeyGen = UnsafeProjection.create(joinKeyExprs.map(_.dataType).toArray)
    +
    +    def toInputValue(i: Int): UnsafeRow = {
    +      inputValueGen.apply(new GenericInternalRow(Array[Any](i, false)))
    +    }
    +
    +    def toJoinKeyRow(i: Int): UnsafeRow = {
    +      joinKeyGen.apply(new GenericInternalRow(Array[Any](false, i, 10.0)))
    +    }
    +
    +    def toKeyInt(joinKeyRow: UnsafeRow): Int = joinKeyRow.getInt(1)
    +
    +    def toValueInt(inputValueRow: UnsafeRow): Int = inputValueRow.getInt(0)
    +
    +    withJoinStateManager(inputValueAttribs, joinKeyExprs) { manager =>
    +      def append(key: Int, value: Int): Unit = {
    +        manager.append(toJoinKeyRow(key), toInputValue(value))
    +      }
    +
    +      def get(key: Int): Seq[Int] = manager.get(toJoinKeyRow(key)).map(toValueInt).toSeq.sorted
    +
    +      /** Remove keys (and corresponding values) where `time <= threshold` */
    +      def removeByKey(threshold: Long): Unit = {
    +        val expr =
    +          LessThanOrEqual(
    +            BoundReference(
    +              1, inputValueAttribWithWatermark.dataType, inputValueAttribWithWatermark.nullable),
    +            Literal(threshold))
    +        manager.removeByKeyCondition(GeneratePredicate.generate(expr).eval _)
    +      }
    +
    +      /** Remove values where `time <= threshold` */
    +      def removeByValue(watermark: Long): Unit = {
    +        val expr = LessThanOrEqual(inputValueAttribWithWatermark, Literal(watermark))
    +        manager.removeByValueCondition(
    +          GeneratePredicate.generate(expr, inputValueAttribs).eval _)
    +      }
    +
    +      def numRows: Long = {
    +        manager.metrics.numKeys
    +      }
    +
    +      assert(get(20) === Seq.empty)     // initially empty
    +      append(20, 2)
    +      assert(get(20) === Seq(2))        // should first value correctly
    +      assert(numRows === 1)
    +
    +      append(20, 3)
    +      assert(get(20) === Seq(2, 3))     // should append new values
    +      append(20, 3)
    +      assert(get(20) === Seq(2, 3, 3))  // should append another copy if same value added again
    +      assert(numRows === 3)
    +
    +      assert(get(30) === Seq.empty)
    +      append(30, 1)
    +      assert(get(30) === Seq(1))
    +      assert(get(20) === Seq(2, 3, 3))  // add another key-value should not affect existing ones
    +      assert(numRows === 4)
    +
    +      removeByKey(25)
    +      assert(get(20) === Seq.empty)
    +      assert(get(30) === Seq(1))        // should remove 20, not 30
    +      assert(numRows === 1)
    +
    +      removeByKey(30)
    +      assert(get(30) === Seq.empty)     // should remove 30
    +      assert(numRows === 0)
    +
    +      def appendAndTest(key: Int, values: Int*): Unit = {
    +        values.foreach { value => append(key, value)}
    +        require(get(key) === values)
    +      }
    +
    +      appendAndTest(40, 100, 200, 300)
    +      appendAndTest(50, 125)
    +      appendAndTest(60, 275)              // prepare for testing removeByValue
    +      assert(numRows === 5)
    +
    +      removeByValue(125)
    +      assert(get(40) === Seq(200, 300))
    +      assert(get(50) === Seq.empty)
    +      assert(get(60) === Seq(275))        // should remove only some values, not all
    +      assert(numRows === 3)
    +
    +      append(40, 50)
    +      assert(get(40) === Seq(50, 200, 300))
    +      assert(numRows === 4)
    +
    +      removeByValue(200)
    +      assert(get(40) === Seq(300))
    +      assert(get(60) === Seq(275))        // should remove only some values, not all
    +      assert(numRows === 2)
    +
    +      removeByValue(300)
    +      assert(get(40) === Seq.empty)
    +      assert(get(60) === Seq.empty)       // should remove all values now
    +      assert(numRows === 0)
    +    }
    +  }
    +
    +  test("stream stream inner join on non-time column") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF.select('value as "key", ('value * 2) as "leftValue")
    +    val df2 = input2.toDF.select('value as "key", ('value * 3) as "rightValue")
    +    val joined = df1.join(df2, "key")
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckAnswer(),
    +      AddData(input2, 1, 10),       // 1 arrived on input1 first, then input2, should join
    +      CheckLastBatch((1, 2, 3)),
    +      AddData(input1, 10),          // 10 arrived on input2 first, then input1, should join
    +      CheckLastBatch((10, 20, 30)),
    +      AddData(input2, 1),           // another 1 in input2 should join with 1 input1
    +      CheckLastBatch((1, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 1), // multiple 1s should be kept in state causing multiple (1, 2, 3)
    +      CheckLastBatch((1, 2, 3), (1, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 100),
    +      AddData(input2, 100),
    +      CheckLastBatch((100, 200, 300))
    +    )
    +  }
    +
    +
    +  test("stream stream inner join on windows - without watermark") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp", ('value * 2) as "leftValue")
    +      .select('key, window('timestamp, "10 second"), 'leftValue)
    +
    +    val df2 = input2.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp",
    +        ('value * 3) as "rightValue")
    +      .select('key, window('timestamp, "10 second"), 'rightValue)
    +
    +    val joined = df1.join(df2, Seq("key", "window"))
    +      .select('key, $"window.end".cast("long"), 'leftValue, 'rightValue)
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckLastBatch(),
    +      AddData(input2, 1),
    +      CheckLastBatch((1, 10, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 25),
    +      CheckLastBatch(),
    +      StopStream,
    +      StartStream(),
    +      AddData(input2, 25),
    +      CheckLastBatch((25, 30, 50, 75)),
    +      AddData(input1, 1),
    +      CheckLastBatch((1, 10, 2, 3)),      // State for 1 still around as there is not watermark
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 5),
    +      CheckLastBatch(),
    +      AddData(input2, 5),
    +      CheckLastBatch((5, 10, 10, 15))     // No filter by any watermark
    +    )
    +  }
    +
    +  test("stream stream inner join on windows - with watermark") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp", ('value * 2) as "leftValue")
    +      .withWatermark("timestamp", "10 seconds")
    +      .select('key, window('timestamp, "10 second"), 'leftValue)
    +
    +    val df2 = input2.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp",
    +        ('value * 3) as "rightValue")
    +      .select('key, window('timestamp, "10 second"), 'rightValue)
    +
    +    val joined = df1.join(df2, Seq("key", "window"))
    +      .select('key, $"window.end".cast("long"), 'leftValue, 'rightValue)
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckAnswer(),
    +      assertNumStateRows(total = 1, updated = 1),
    --- End diff --
    
    why is there only one state row, shouldn't there be 1 for the count, 1 for the value?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139896338
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinHelper.scala ---
    @@ -0,0 +1,407 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.reflect.ClassTag
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.{Partition, SparkContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, AttributeSet, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.execution.streaming.state.{StateStoreCoordinatorRef, StateStoreProvider, StateStoreProviderId}
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]]. See that object for more details.
    + */
    +object StreamingSymmetricHashJoinHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate {
    +    def expr: Expression
    +    def desc: String
    +    override def toString: String = s"$desc: $expr"
    +  }
    +  /** Predicate for watermark on state keys */
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression)
    +    extends JoinStateWatermarkPredicate {
    +    def desc: String = "key predicate"
    +  }
    +  /** Predicate for watermark on state values */
    +  case class JoinStateValueWatermarkPredicate(expr: Expression)
    +    extends JoinStateWatermarkPredicate {
    +    def desc: String = "value predicate"
    +  }
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None) {
    +    override def toString(): String = {
    +      s"state cleanup [ left ${left.map(_.toString).getOrElse("= null")}, " +
    +        s"right ${right.map(_.toString).getOrElse("")} ]"
    +    }
    +  }
    +
    +  /** Get the predicates defining the state watermarks for both sides of the join */
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +
    +
    +    // Join keys of both sides generate rows of the same fields, that is, same sequence of data
    +    // types. If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +    // then it will never consider joining keys that are < state key watermark (i.e. event time
    +    // watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +    // there has to be an equivalent column (i.e., timestamp). And any right side data that has the
    +    // timestamp < watermark will not match will not match with left side data, as the left side get
    +    // filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +    // left side keys effectively causes the timestamp on the right side to have a watermark.
    +    // We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +    // right timestamp in the right keys.
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 in the StreamingSymmetricHashJoinExec docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate.apply _)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 in the StreamingSymmetricHashJoinExec docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = AttributeSet(oneSideInputAttributes),
    +          attributesWithEventWatermark = AttributeSet(otherSideInputAttributes),
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate.apply _)
    +
    +      } else {
    +        None
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   * This function is supposed to make best-effort attempt to get the state watermark. If there is
    +   * any error, it will return None.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds, is possible.
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: AttributeSet,
    +      attributesWithEventWatermark: AttributeSet,
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    // If condition or event time watermark is not provided, then cannot calculate state watermark
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    // If there is not watermark attribute, then cannot define state watermark
    +    if (!attributesWithEventWatermark.exists(_.metadata.contains(delayKey))) return None
    +    val attributesInCondition =
    +      AttributeSet(joinCondition.get.collect { case a: AttributeReference => a })
    +    if (
    +      attributesInCondition.filter { attributesToFindStateWatemarkFor.contains(_)}.size > 1 ||
    +      attributesInCondition.filter { attributesWithEventWatermark.contains(_)}.size > 1) {
    +      // If more than attributes present in condition from one side, then it cannot be solved
    +      return None
    +    }
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatermarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +
    +      // The generated the state watermark cleanup expression is inclusive of the state watermark.
    +      // If state watermark is W, all state where timestamp <= W will be cleaned up.
    +      // Now when the canonicalized join condition solves to leftTime >= W, we dont want to clean
    +      // up leftTime <= W. Rather we should clean up leftTime <= W - 1. Hence the -1 below.
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatermarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: AttributeSet,
    +      attributesWithEventWatermark: AttributeSet,
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a @ AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a @ AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && metadata.contains(delayKey) =>
    +          Multiply(Literal(eventWatermark.get.toDouble), Literal(1000.0))
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some((Double2double(constraintValue) / 1000.0).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Cast(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in microseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              if (calendarInterval.months > 0) {
    +                invalid = true
    +                logWarning(
    +                  s"Failed to extract state value watermark from condition $exprToCollectFrom " +
    +                    s"as imprecise intervals like months and years cannot be used for precise" +
    +                    s"watermark calculation")
    +                Literal(0.0)
    +              } else {
    +                Literal(calendarInterval.microseconds.toDouble)
    +              }
    +            case DoubleType =>
    +              Multiply(lit, Literal(1000000.0))
    +            case _: NumericType | _: TimestampType =>
    +              Multiply(Cast(lit, DoubleType), Literal(1000000.0))
    --- End diff --
    
    you may use `PreciseTimestamp` to get the microsecond value of `TimestampType`. If you cast it first to `DoubleType`, you will lose precision.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139844415
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/SymmetricHashJoinStateManager.scala ---
    @@ -0,0 +1,405 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming.state
    +
    +import scala.reflect.ClassTag
    +
    +import org.apache.hadoop.conf.Configuration
    +
    +import org.apache.spark.{Partition, SparkContext, TaskContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, BindReferences, Expression, LessThanOrEqual, Literal, SpecificInternalRow, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.Predicate
    +import org.apache.spark.sql.execution.streaming.{StatefulOperatorStateInfo, StreamingSymmetricHashJoinExec}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.types.{LongType, StructField, StructType}
    +import org.apache.spark.util.NextIterator
    +
    +/**
    + * Helper class to manage state required by a single side of [[StreamingSymmetricHashJoinExec]].
    + * The interface of this class is basically that of a multi-map:
    + * - Get: Returns an iterator of multiple values for given key
    + * - Append: Append a new value to the given key
    + * - Remove Data by predicate: Drop any state using a predicate condition on keys or values
    + *
    + * @param joinSide          Defines the join side
    --- End diff --
    
    nit: why the weird indentation


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140062092
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinHelper.scala ---
    @@ -0,0 +1,407 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.reflect.ClassTag
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.{Partition, SparkContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, AttributeSet, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.execution.streaming.state.{StateStoreCoordinatorRef, StateStoreProvider, StateStoreProviderId}
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]]. See that object for more details.
    + */
    +object StreamingSymmetricHashJoinHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate {
    +    def expr: Expression
    +    def desc: String
    +    override def toString: String = s"$desc: $expr"
    +  }
    +  /** Predicate for watermark on state keys */
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression)
    +    extends JoinStateWatermarkPredicate {
    +    def desc: String = "key predicate"
    +  }
    +  /** Predicate for watermark on state values */
    +  case class JoinStateValueWatermarkPredicate(expr: Expression)
    +    extends JoinStateWatermarkPredicate {
    +    def desc: String = "value predicate"
    +  }
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None) {
    +    override def toString(): String = {
    +      s"state cleanup [ left ${left.map(_.toString).getOrElse("= null")}, " +
    +        s"right ${right.map(_.toString).getOrElse("")} ]"
    +    }
    +  }
    +
    +  /** Get the predicates defining the state watermarks for both sides of the join */
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +
    +
    +    // Join keys of both sides generate rows of the same fields, that is, same sequence of data
    +    // types. If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +    // then it will never consider joining keys that are < state key watermark (i.e. event time
    +    // watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +    // there has to be an equivalent column (i.e., timestamp). And any right side data that has the
    +    // timestamp < watermark will not match will not match with left side data, as the left side get
    +    // filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +    // left side keys effectively causes the timestamp on the right side to have a watermark.
    +    // We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +    // right timestamp in the right keys.
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 in the StreamingSymmetricHashJoinExec docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate.apply _)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 in the StreamingSymmetricHashJoinExec docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = AttributeSet(oneSideInputAttributes),
    +          attributesWithEventWatermark = AttributeSet(otherSideInputAttributes),
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate.apply _)
    +
    +      } else {
    +        None
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   * This function is supposed to make best-effort attempt to get the state watermark. If there is
    +   * any error, it will return None.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds, is possible.
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: AttributeSet,
    +      attributesWithEventWatermark: AttributeSet,
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    // If condition or event time watermark is not provided, then cannot calculate state watermark
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    // If there is not watermark attribute, then cannot define state watermark
    +    if (!attributesWithEventWatermark.exists(_.metadata.contains(delayKey))) return None
    +    val attributesInCondition =
    +      AttributeSet(joinCondition.get.collect { case a: AttributeReference => a })
    +    if (
    +      attributesInCondition.filter { attributesToFindStateWatemarkFor.contains(_)}.size > 1 ||
    +      attributesInCondition.filter { attributesWithEventWatermark.contains(_)}.size > 1) {
    +      // If more than attributes present in condition from one side, then it cannot be solved
    +      return None
    +    }
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatermarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +
    +      // The generated the state watermark cleanup expression is inclusive of the state watermark.
    +      // If state watermark is W, all state where timestamp <= W will be cleaned up.
    +      // Now when the canonicalized join condition solves to leftTime >= W, we dont want to clean
    +      // up leftTime <= W. Rather we should clean up leftTime <= W - 1. Hence the -1 below.
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatermarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: AttributeSet,
    +      attributesWithEventWatermark: AttributeSet,
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a @ AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a @ AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && metadata.contains(delayKey) =>
    +          Multiply(Literal(eventWatermark.get.toDouble), Literal(1000.0))
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some((Double2double(constraintValue) / 1000.0).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Cast(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in microseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              if (calendarInterval.months > 0) {
    +                invalid = true
    +                logWarning(
    +                  s"Failed to extract state value watermark from condition $exprToCollectFrom " +
    +                    s"as imprecise intervals like months and years cannot be used for precise" +
    +                    s"watermark calculation")
    +                Literal(0.0)
    +              } else {
    +                Literal(calendarInterval.microseconds.toDouble)
    +              }
    +            case DoubleType =>
    +              Multiply(lit, Literal(1000000.0))
    --- End diff --
    
    Standard conversion/casting between timestamp type to any numeric type is done as seconds. 


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139835014
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in milliseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              val millisPerMonth = CalendarInterval.MICROS_PER_DAY / 1000 * 31
    --- End diff --
    
    I think there is a problem with months. Overestimation (similar to what we do for watermark delay specific in months) is not correct in this case. It should not throw analysis exception though as this is fundamentally a best-effort extraction. 


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    jenkins retest this please.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139831879
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    --- End diff --
    
    That's a separate problem with unions. I am not sure what we do wither watermarks when we join. It could be that we dont preserve the watermark metadata. Out of scope for this PR.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139803500
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/IncrementalExecution.scala ---
    @@ -114,6 +115,16 @@ class IncrementalExecution(
               stateInfo = Some(nextStatefulOperationStateInfo),
               batchTimestampMs = Some(offsetSeqMetadata.batchTimestampMs),
               eventTimeWatermark = Some(offsetSeqMetadata.batchWatermarkMs))
    +
    +      case j @ StreamingSymmetricHashJoinExec(lKeys, rKeys, _, cond, _, _, _, left, right) =>
    +        j.copy(
    +          stateInfo = Some(nextStatefulOperationStateInfo),
    --- End diff --
    
    So, I think this may not be robust. At each trigger, we create a new IncrementalExecution, so the `statefulOperatorId` we hope gets incremented in a deterministic manner.
    I can imagine adding things to the Optimizer in the future which may move an `EquiJoin` before an aggregation.
    In this case, the state store id's of the aggregation and join may switch. I'm not sure if we're protected against that somehow, so just wanted to bring it up.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140062031
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/SymmetricHashJoinStateManager.scala ---
    @@ -0,0 +1,405 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming.state
    +
    +import scala.reflect.ClassTag
    +
    +import org.apache.hadoop.conf.Configuration
    +
    +import org.apache.spark.{Partition, SparkContext, TaskContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, BindReferences, Expression, LessThanOrEqual, Literal, SpecificInternalRow, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.Predicate
    +import org.apache.spark.sql.execution.streaming.{StatefulOperatorStateInfo, StreamingSymmetricHashJoinExec}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.types.{LongType, StructField, StructType}
    +import org.apache.spark.util.NextIterator
    +
    +/**
    + * Helper class to manage state required by a single side of [[StreamingSymmetricHashJoinExec]].
    + * The interface of this class is basically that of a multi-map:
    + * - Get: Returns an iterator of multiple values for given key
    + * - Append: Append a new value to the given key
    + * - Remove Data by predicate: Drop any state using a predicate condition on keys or values
    + *
    + * @param joinSide          Defines the join side
    + * @param inputValueAttributes   Attributes of the input row which will be stored as value
    + * @param joinKeys          Expressions to find the join key that will be used to key the value rows
    + * @param stateInfo           Information about how to retrieve the correct version of state
    + * @param storeConf           Configuration for the state store.
    + * @param hadoopConf          Hadoop configuration for reading state data from storage
    + *
    + * Internally, the key -> multiple values is stored in two [[StateStore]]s.
    + * - Store 1 ([[KeyToNumValuesStore]]) maintains mapping between key -> number of values
    + * - Store 2 ([[KeyWithIndexToValueStore]]) maintains mapping between (key, index) -> value
    + * - Put:   update count in KeyToNumValuesStore,
    + *          insert new (key, count) -> value in KeyWithIndexToValueStore
    + * - Get:   read count from KeyToNumValuesStore,
    + *          read each of the n values in KeyWithIndexToValueStore
    + * - Remove state by predicate on keys:
    + *          scan all keys in KeyToNumValuesStore to find keys that do match the predicate,
    + *          delete from key from KeyToNumValuesStore, delete values in KeyWithIndexToValueStore
    + * - Remove state by condition on values:
    + *          scan all [(key, index) -> value] in KeyWithIndexToValueStore to find values that match
    + *          the predicate, delete corresponding (key, indexToDelete) from KeyWithIndexToValueStore
    + *          by overwriting with the value of (key, maxIndex), and removing [(key, maxIndex),
    + *          decrement corresponding num values in KeyToNumValuesStore
    + */
    +class SymmetricHashJoinStateManager(
    +    val joinSide: JoinSide,
    +    val inputValueAttributes: Seq[Attribute],
    +    joinKeys: Seq[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    storeConf: StateStoreConf,
    +    hadoopConf: Configuration) extends Logging {
    +
    +  import SymmetricHashJoinStateManager._
    +
    +  // Clean up any state store resources if necessary at the end of the task
    +  Option(TaskContext.get()).foreach { _.addTaskCompletionListener { _ => abortIfNeeded() } }
    +
    +  /*
    +  =====================================================
    +                  Public methods
    +  =====================================================
    +   */
    +
    +  /** Get all the values of a key */
    +  def get(key: UnsafeRow): Iterator[UnsafeRow] = {
    +    val numValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.getAll(key, numValues)
    +  }
    +
    +  /** Append a new value to the key */
    +  def append(key: UnsafeRow, value: UnsafeRow): Unit = {
    +    val numExistingValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.put(key, numExistingValues, value)
    +    keyToNumValues.put(key, numExistingValues + 1)
    +  }
    +
    +  /**
    +   * Remove using a predicate on keys. See class docs for more context and implement details.
    +   */
    +  def removeByKeyCondition(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      if (condition(keyToNumValue.key)) {
    +        keyToNumValues.remove(keyToNumValue.key)
    +        keyWithIndexToValue.removeAllValues(keyToNumValue.key, keyToNumValue.numValue)
    +      }
    +    }
    +  }
    +
    +  /**
    +   * Remove using a predicate on values. See class docs for more context and implementation details.
    +   */
    +  def removeByPredicateOnValues(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +
    +    var numValues: Long = 0L
    +    var index: Long = 0L
    +    var valueRemoved = false
    +    var valueForIndex: UnsafeRow = null
    +
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      val key = keyToNumValue.key
    +
    +      numValues = keyToNumValue.numValue
    +      index = 0L
    +      valueRemoved = false
    +      valueForIndex = null
    +
    +      while (index < numValues) {
    +        if (valueForIndex == null) {
    +          valueForIndex = keyWithIndexToValue.get(key, index)
    +        }
    +        if (condition(valueForIndex)) {
    +          if (numValues > 1) {
    +            val valueAtMaxIndex = keyWithIndexToValue.get(key, numValues - 1)
    +            keyWithIndexToValue.put(key, index, valueAtMaxIndex)
    +            keyWithIndexToValue.remove(key, numValues - 1)
    +            valueForIndex = valueAtMaxIndex
    +          } else {
    +            keyWithIndexToValue.remove(key, 0)
    +            valueForIndex = null
    +          }
    +          numValues -= 1
    +          valueRemoved = true
    +        } else {
    +          valueForIndex = null
    +          index += 1
    +        }
    +      }
    +      if (valueRemoved) {
    +        if (numValues >= 1) {
    +          keyToNumValues.put(key, numValues)
    +        } else {
    +          keyToNumValues.remove(key)
    +        }
    +      }
    +    }
    +  }
    +
    +  def iterator(): Iterator[UnsafeRowPair] = {
    +    val pair = new UnsafeRowPair()
    +    keyWithIndexToValue.iterator.map { x =>
    +      pair.withRows(x.key, x.value)
    +    }
    +  }
    +
    +  /** Commit all the changes to all the state stores */
    +  def commit(): Unit = {
    +    keyToNumValues.commit()
    +    keyWithIndexToValue.commit()
    +  }
    +
    +  /** Abort any changes to the state stores if needed */
    +  def abortIfNeeded(): Unit = {
    +    keyWithIndexToValue.abortIfNeeded()
    +    keyWithIndexToValue.abortIfNeeded()
    --- End diff --
    
    good catch!


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test PASSed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/82002/
    Test PASSed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139813860
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    --- End diff --
    
    why `-1`?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139806782
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    --- End diff --
    
    nit: `conditions`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139830482
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/IncrementalExecution.scala ---
    @@ -114,6 +115,16 @@ class IncrementalExecution(
               stateInfo = Some(nextStatefulOperationStateInfo),
               batchTimestampMs = Some(offsetSeqMetadata.batchTimestampMs),
               eventTimeWatermark = Some(offsetSeqMetadata.batchWatermarkMs))
    +
    +      case j @ StreamingSymmetricHashJoinExec(lKeys, rKeys, _, cond, _, _, _, left, right) =>
    +        j.copy(
    +          stateInfo = Some(nextStatefulOperationStateInfo),
    --- End diff --
    
    Whatever optimization takes place, the same optimizations will occur in EVERY batch. So if aggregation is pushed below join, then all the batches will have that. 
    
    What we have to guard against is cost-based optimization that can reorder things differently in different batches. That is, why I have disabled cost-based join optimization. And when adding such optimizations in the future, we have to be cautious for the streaming case and disable them.
    
    Also, this is a general concern with other stateful ops as well, not something that this PR would address.



---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139815407
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    --- End diff --
    
    nit: `Cast`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test FAILed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/81907/
    Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139841304
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/IncrementalExecution.scala ---
    @@ -114,6 +115,16 @@ class IncrementalExecution(
               stateInfo = Some(nextStatefulOperationStateInfo),
               batchTimestampMs = Some(offsetSeqMetadata.batchTimestampMs),
               eventTimeWatermark = Some(offsetSeqMetadata.batchWatermarkMs))
    +
    +      case j @ StreamingSymmetricHashJoinExec(lKeys, rKeys, _, cond, _, _, _, left, right) =>
    +        j.copy(
    +          stateInfo = Some(nextStatefulOperationStateInfo),
    --- End diff --
    
    That is the case **if** you don't change Spark versions. A more recent Spark version may include new optimizer rules that may change the ordering.
    Just something to think about. Would be nice to add a test with aggregation + join and join + aggregation.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139814268
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    --- End diff --
    
    if the above line is `-1`, then I would have assumed this should be `+1`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81900 has finished](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81900/testReport)** for PR 19271 at commit [`9edaa58`](https://github.com/apache/spark/commit/9edaa580edc6e599b01033190a9ef383ed001ae9).
     * This patch **fails Spark unit tests**.
     * This patch merges cleanly.
     * This patch adds no public classes.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by SparkQA <gi...@git.apache.org>.
Github user SparkQA commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    **[Test build #81958 has started](https://amplab.cs.berkeley.edu/jenkins/job/SparkPullRequestBuilder/81958/testReport)** for PR 19271 at commit [`8e13959`](https://github.com/apache/spark/commit/8e139594ce164a18fa54df680afbc213691da081).


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140062022
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/SymmetricHashJoinStateManager.scala ---
    @@ -0,0 +1,403 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming.state
    +
    +import scala.reflect.ClassTag
    +
    +import org.apache.hadoop.conf.Configuration
    +
    +import org.apache.spark.{Partition, SparkContext, TaskContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, BindReferences, Expression, LessThanOrEqual, Literal, SpecificInternalRow, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.Predicate
    +import org.apache.spark.sql.execution.streaming.{StatefulOperatorStateInfo, StreamingSymmetricHashJoinExec}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.types.{LongType, StructField, StructType}
    +import org.apache.spark.util.NextIterator
    +
    +/**
    + * Helper class to manage state required by a single side of [[StreamingSymmetricHashJoinExec]].
    + * The interface of this class is basically that of a multi-map:
    + * - Get: Returns an iterator of multiple values for given key
    + * - Append: Append a new value to the given key
    + * - Remove Data by predicate: Drop any state using a predicate condition on keys or values
    + *
    + * @param joinSide              Defines the join side
    + * @param inputValueAttributes  Attributes of the input row which will be stored as value
    + * @param joinKeys              Expressions to generate rows that will be used to key the value rows
    + * @param stateInfo             Information about how to retrieve the correct version of state
    + * @param storeConf             Configuration for the state store.
    + * @param hadoopConf            Hadoop configuration for reading state data from storage
    + *
    + * Internally, the key -> multiple values is stored in two [[StateStore]]s.
    + * - Store 1 ([[KeyToNumValuesStore]]) maintains mapping between key -> number of values
    + * - Store 2 ([[KeyWithIndexToValueStore]]) maintains mapping between (key, index) -> value
    + * - Put:   update count in KeyToNumValuesStore,
    + *          insert new (key, count) -> value in KeyWithIndexToValueStore
    + * - Get:   read count from KeyToNumValuesStore,
    + *          read each of the n values in KeyWithIndexToValueStore
    + * - Remove state by predicate on keys:
    + *          scan all keys in KeyToNumValuesStore to find keys that do match the predicate,
    + *          delete from key from KeyToNumValuesStore, delete values in KeyWithIndexToValueStore
    + * - Remove state by condition on values:
    + *          scan all [(key, index) -> value] in KeyWithIndexToValueStore to find values that match
    + *          the predicate, delete corresponding (key, indexToDelete) from KeyWithIndexToValueStore
    + *          by overwriting with the value of (key, maxIndex), and removing [(key, maxIndex),
    + *          decrement corresponding num values in KeyToNumValuesStore
    + */
    +class SymmetricHashJoinStateManager(
    +    val joinSide: JoinSide,
    +    inputValueAttributes: Seq[Attribute],
    +    joinKeys: Seq[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    storeConf: StateStoreConf,
    +    hadoopConf: Configuration) extends Logging {
    +
    +  import SymmetricHashJoinStateManager._
    +
    +  // Clean up any state store resources if necessary at the end of the task
    +  Option(TaskContext.get()).foreach { _.addTaskCompletionListener { _ => abortIfNeeded() } }
    +
    +  /*
    +  =====================================================
    +                  Public methods
    +  =====================================================
    +   */
    +
    +  /** Get all the values of a key */
    +  def get(key: UnsafeRow): Iterator[UnsafeRow] = {
    +    val numValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.getAll(key, numValues)
    +  }
    +
    +  /** Append a new value to the key */
    +  def append(key: UnsafeRow, value: UnsafeRow): Unit = {
    +    val numExistingValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.put(key, numExistingValues, value)
    +    keyToNumValues.put(key, numExistingValues + 1)
    +  }
    +
    +  /**
    +   * Remove using a predicate on keys. See class docs for more context and implement details.
    +   */
    +  def removeByKeyCondition(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      if (condition(keyToNumValue.key)) {
    +        keyToNumValues.remove(keyToNumValue.key)
    +        keyWithIndexToValue.removeAllValues(keyToNumValue.key, keyToNumValue.numValue)
    +      }
    +    }
    +  }
    +
    +  /**
    +   * Remove using a predicate on values. See class docs for more context and implementation details.
    +   */
    +  def removeByValueCondition(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +    var numValues: Long = 0L
    +    var index: Long = 0L
    +    var valueRemoved = false
    +    var valueForIndex: UnsafeRow = null
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      val key = keyToNumValue.key
    +
    +      numValues = keyToNumValue.numValue
    +      index = 0L
    +      valueRemoved = false
    +      valueForIndex = null
    +
    +      while (index < numValues) {
    +        if (valueForIndex == null) {
    +          valueForIndex = keyWithIndexToValue.get(key, index)
    +        }
    +        if (condition(valueForIndex)) {
    +          if (numValues > 1) {
    +            val valueAtMaxIndex = keyWithIndexToValue.get(key, numValues - 1)
    +            keyWithIndexToValue.put(key, index, valueAtMaxIndex)
    +            keyWithIndexToValue.remove(key, numValues - 1)
    +            valueForIndex = valueAtMaxIndex
    +          } else {
    +            keyWithIndexToValue.remove(key, 0)
    +            valueForIndex = null
    +          }
    +          numValues -= 1
    +          valueRemoved = true
    +        } else {
    +          valueForIndex = null
    +          index += 1
    +        }
    +      }
    +      if (valueRemoved) {
    +        if (numValues >= 1) {
    +          keyToNumValues.put(key, numValues)
    +        } else {
    +          keyToNumValues.remove(key)
    +        }
    +      }
    +    }
    +  }
    +
    +  def iterator(): Iterator[UnsafeRowPair] = {
    +    val pair = new UnsafeRowPair()
    +    keyWithIndexToValue.iterator.map { x =>
    +      pair.withRows(x.key, x.value)
    +    }
    +  }
    +
    +  /** Commit all the changes to all the state stores */
    +  def commit(): Unit = {
    +    keyToNumValues.commit()
    +    keyWithIndexToValue.commit()
    +  }
    +
    +  /** Abort any changes to the state stores if needed */
    +  def abortIfNeeded(): Unit = {
    +    keyWithIndexToValue.abortIfNeeded()
    +    keyWithIndexToValue.abortIfNeeded()
    +  }
    +
    +  /** Get the combined metrics of all the state stores */
    +  def metrics: StateStoreMetrics = {
    +    val keyToNumValuesMetrics = keyToNumValues.metrics
    +    val keyWithIndexToValueMetrics = keyWithIndexToValue.metrics
    +    def newDesc(desc: String): String = s"${joinSide.toString.toUpperCase}: $desc"
    +
    +    StateStoreMetrics(
    +      keyWithIndexToValueMetrics.numKeys,       // represent each buffered row only once
    +      keyToNumValuesMetrics.memoryUsedBytes + keyWithIndexToValueMetrics.memoryUsedBytes,
    +      keyWithIndexToValueMetrics.customMetrics.map {
    +        case (s @ StateStoreCustomSizeMetric(_, desc), value) =>
    +          s.copy(desc = newDesc(desc)) -> value
    +        case (s @ StateStoreCustomTimingMetric(_, desc), value) =>
    +          s.copy(desc = newDesc(desc)) -> value
    +      }
    +    )
    +  }
    +
    +  /*
    +  =====================================================
    +            Private methods and inner classes
    +  =====================================================
    +   */
    +
    +  private val keySchema = StructType(
    +    joinKeys.zipWithIndex.map { case (k, i) => StructField(s"field$i", k.dataType, k.nullable) })
    +  private val keyAttributes = keySchema.toAttributes
    +  private val keyToNumValues = new KeyToNumValuesStore()
    +  private val keyWithIndexToValue = new KeyWithIndexToValueStore()
    +
    +  /** Helper trait for invoking common functionalities of a state store. */
    +  private abstract class StateStoreHandler(stateStoreType: StateStoreType) extends Logging {
    +
    +    /** StateStore that the subclasses of this class is going to operate on */
    +    protected def stateStore: StateStore
    +
    +    def commit(): Unit = {
    +      stateStore.commit()
    +      logDebug("Committed, metrics = " + stateStore.metrics)
    +    }
    +
    +    def abortIfNeeded(): Unit = {
    +      if (!stateStore.hasCommitted) {
    +        logInfo(s"Aborted store ${stateStore.id}")
    +        stateStore.abort()
    +      }
    +    }
    +
    +    def metrics: StateStoreMetrics = stateStore.metrics
    +
    +    /** Get the StateStore with the given schema */
    +    protected def getStateStore(keySchema: StructType, valueSchema: StructType): StateStore = {
    +      val storeProviderId = StateStoreProviderId(
    +        stateInfo.get, TaskContext.getPartitionId(), getStateStoreName(joinSide, stateStoreType))
    +      val store = StateStore.get(
    +        storeProviderId, keySchema, valueSchema, None,
    +        stateInfo.get.storeVersion, storeConf, hadoopConf)
    +      logInfo(s"Loaded store ${store.id}")
    +      store
    +    }
    +  }
    +
    +  /**
    +   * Helper class for representing data returned by [[KeyWithIndexToValueStore]].
    +   * Designed for object reuse.
    +   */
    +  private case class KeyAndNumValues(var key: UnsafeRow = null, var numValue: Long = 0) {
    +    def withNew(newKey: UnsafeRow, newNumValues: Long): this.type = {
    +      this.key = newKey
    +      this.numValue = newNumValues
    +      this
    +    }
    +  }
    +
    +
    +  /** A wrapper around a [[StateStore]] that stores [key -> number of values]. */
    +  private class KeyToNumValuesStore extends StateStoreHandler(KeyToNumValuesType) {
    +    private val longValueSchema = new StructType().add("value", "long")
    +    private val longToUnsafeRow = UnsafeProjection.create(longValueSchema)
    +    private val valueRow = longToUnsafeRow(new SpecificInternalRow(longValueSchema))
    +    protected val stateStore: StateStore = getStateStore(keySchema, longValueSchema)
    +
    +    /** Get the number of values the key has */
    +    def get(key: UnsafeRow): Long = {
    +      val longValueRow = stateStore.get(key)
    +      if (longValueRow != null) longValueRow.getLong(0) else 0L
    +    }
    +
    +    /** Set the number of values the key has */
    +    def put(key: UnsafeRow, numValues: Long): Unit = {
    +      require(numValues > 0)
    +      valueRow.setLong(0, numValues)
    +      stateStore.put(key, valueRow)
    +    }
    +
    +    def remove(key: UnsafeRow): Unit = {
    +      stateStore.remove(key)
    +    }
    +
    +    def iterator: Iterator[KeyAndNumValues] = {
    +      val keyAndNumValues = new KeyAndNumValues()
    +      stateStore.getRange(None, None).map { case pair =>
    +        keyAndNumValues.withNew(pair.key, pair.value.getLong(0))
    +      }
    +    }
    +  }
    +
    +  /**
    +   * Helper class for representing data returned by [[KeyWithIndexToValueStore]].
    +   * Designed for object reuse.
    +   */
    +  private case class KeyWithIndexAndValue(
    +    var key: UnsafeRow = null, var valueIndex: Long = -1, var value: UnsafeRow = null) {
    +    def withNew(newKey: UnsafeRow, newIndex: Long, newValue: UnsafeRow): this.type = {
    +      this.key = newKey
    +      this.valueIndex = newIndex
    +      this.value = newValue
    +      this
    +    }
    +  }
    +
    +  /** A wrapper around a [[StateStore]] that stores [(key, index) -> value]. */
    +  private class KeyWithIndexToValueStore extends StateStoreHandler(KeyWithIndexToValuesType) {
    +    private val keyWithIndexExprs = keyAttributes :+ Literal(1L)
    +    private val keyWithIndexSchema = StructType(keySchema.fields).add("index", LongType)
    --- End diff --
    
    changed


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139809419
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    --- End diff --
    
    nit: do we prefer `//` over `/* ... */`? I generally see 
    ```
    //
    //
    //
    ```


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139812749
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    --- End diff --
    
    nit: remove empty line


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139831615
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    --- End diff --
    
    this is the broadcast variable, this should NOT be transient. You want to serialize the broadcast, so the executor fetches the actual data using that serialized-deserialized reference.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test PASSed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test PASSed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/82011/
    Test PASSed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test PASSed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139891727
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/IncrementalExecution.scala ---
    @@ -114,6 +115,16 @@ class IncrementalExecution(
               stateInfo = Some(nextStatefulOperationStateInfo),
               batchTimestampMs = Some(offsetSeqMetadata.batchTimestampMs),
               eventTimeWatermark = Some(offsetSeqMetadata.batchWatermarkMs))
    +
    +      case j @ StreamingSymmetricHashJoinExec(lKeys, rKeys, _, cond, _, _, _, left, right) =>
    --- End diff --
    
    nit: when there are so many values you're using out of `StreamingSymmetricHashJoinExec`, this piece of code may be easier to maintain if you just use `j:  StreamingSymmetricHashJoinExec` instead. Otherwise, if we add one more field to this thing, then we're going to hit 100 chars per line scalastyle issue.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139832098
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    --- End diff --
    
    good catch. done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by zsxwing <gi...@git.apache.org>.
Github user zsxwing commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140042806
  
    --- Diff: sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/UnsupportedOperationChecker.scala ---
    @@ -222,16 +222,17 @@ object UnsupportedOperationChecker {
               joinType match {
     
                 case _: InnerLike =>
    -              if (left.isStreaming && right.isStreaming) {
    -                throwError("Inner join between two streaming DataFrames/Datasets is not supported")
    +              if (left.isStreaming && right.isStreaming &&
    --- End diff --
    
    What if the join is not equality join? UnsupportedOperationChecker should disallow this case?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139833753
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    --- End diff --
    
    When the join is like `select * from A, B where A.x = B.x`. The `A.x` and `B.x` becomes the join keys, and there is no other condition needed. 


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139895235
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/statefulOperators.scala ---
    @@ -179,6 +167,31 @@ trait WatermarkSupport extends UnaryExecNode {
       }
     }
     
    +object WatermarkSupport {
    +
    +  /** Generate an expression on given attributes that matches data older than the watermark */
    +  def watermarkExpression(
    +      optionalWatermarkExpression: Option[Expression],
    +      optionalWatermarkMs: Option[Long]): Option[Expression] = {
    +    if (optionalWatermarkExpression.isEmpty || optionalWatermarkMs.isEmpty) return None
    +
    +    val watermarkAttribute = optionalWatermarkExpression.get
    +    // If we are evicting based on a window, use the end of the window.  Otherwise just
    +    // use the attribute itself.
    +    val evictionExpression =
    +      if (watermarkAttribute.dataType.isInstanceOf[StructType]) {
    --- End diff --
    
    Is this for the output of `window`? It seems very fragile at the moment. Can we add a special metadata that specifies that it would in fact be the output of a `window` expression? That would allow us in the future to check that people actually perform windowed joins instead of arbitrary joins, etc.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139831226
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    --- End diff --
    
    done.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140017131
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/SymmetricHashJoinStateManager.scala ---
    @@ -0,0 +1,403 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming.state
    +
    +import scala.reflect.ClassTag
    +
    +import org.apache.hadoop.conf.Configuration
    +
    +import org.apache.spark.{Partition, SparkContext, TaskContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, BindReferences, Expression, LessThanOrEqual, Literal, SpecificInternalRow, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.Predicate
    +import org.apache.spark.sql.execution.streaming.{StatefulOperatorStateInfo, StreamingSymmetricHashJoinExec}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.types.{LongType, StructField, StructType}
    +import org.apache.spark.util.NextIterator
    +
    +/**
    + * Helper class to manage state required by a single side of [[StreamingSymmetricHashJoinExec]].
    + * The interface of this class is basically that of a multi-map:
    + * - Get: Returns an iterator of multiple values for given key
    + * - Append: Append a new value to the given key
    + * - Remove Data by predicate: Drop any state using a predicate condition on keys or values
    + *
    + * @param joinSide              Defines the join side
    + * @param inputValueAttributes  Attributes of the input row which will be stored as value
    + * @param joinKeys              Expressions to generate rows that will be used to key the value rows
    + * @param stateInfo             Information about how to retrieve the correct version of state
    + * @param storeConf             Configuration for the state store.
    + * @param hadoopConf            Hadoop configuration for reading state data from storage
    + *
    + * Internally, the key -> multiple values is stored in two [[StateStore]]s.
    + * - Store 1 ([[KeyToNumValuesStore]]) maintains mapping between key -> number of values
    + * - Store 2 ([[KeyWithIndexToValueStore]]) maintains mapping between (key, index) -> value
    + * - Put:   update count in KeyToNumValuesStore,
    + *          insert new (key, count) -> value in KeyWithIndexToValueStore
    + * - Get:   read count from KeyToNumValuesStore,
    + *          read each of the n values in KeyWithIndexToValueStore
    + * - Remove state by predicate on keys:
    + *          scan all keys in KeyToNumValuesStore to find keys that do match the predicate,
    + *          delete from key from KeyToNumValuesStore, delete values in KeyWithIndexToValueStore
    + * - Remove state by condition on values:
    + *          scan all [(key, index) -> value] in KeyWithIndexToValueStore to find values that match
    + *          the predicate, delete corresponding (key, indexToDelete) from KeyWithIndexToValueStore
    + *          by overwriting with the value of (key, maxIndex), and removing [(key, maxIndex),
    + *          decrement corresponding num values in KeyToNumValuesStore
    + */
    +class SymmetricHashJoinStateManager(
    +    val joinSide: JoinSide,
    +    inputValueAttributes: Seq[Attribute],
    +    joinKeys: Seq[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    storeConf: StateStoreConf,
    +    hadoopConf: Configuration) extends Logging {
    +
    +  import SymmetricHashJoinStateManager._
    +
    +  // Clean up any state store resources if necessary at the end of the task
    +  Option(TaskContext.get()).foreach { _.addTaskCompletionListener { _ => abortIfNeeded() } }
    +
    +  /*
    +  =====================================================
    +                  Public methods
    +  =====================================================
    +   */
    +
    +  /** Get all the values of a key */
    +  def get(key: UnsafeRow): Iterator[UnsafeRow] = {
    +    val numValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.getAll(key, numValues)
    +  }
    +
    +  /** Append a new value to the key */
    +  def append(key: UnsafeRow, value: UnsafeRow): Unit = {
    +    val numExistingValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.put(key, numExistingValues, value)
    +    keyToNumValues.put(key, numExistingValues + 1)
    +  }
    +
    +  /**
    +   * Remove using a predicate on keys. See class docs for more context and implement details.
    +   */
    +  def removeByKeyCondition(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      if (condition(keyToNumValue.key)) {
    +        keyToNumValues.remove(keyToNumValue.key)
    +        keyWithIndexToValue.removeAllValues(keyToNumValue.key, keyToNumValue.numValue)
    +      }
    +    }
    +  }
    +
    +  /**
    +   * Remove using a predicate on values. See class docs for more context and implementation details.
    +   */
    +  def removeByValueCondition(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +    var numValues: Long = 0L
    +    var index: Long = 0L
    +    var valueRemoved = false
    +    var valueForIndex: UnsafeRow = null
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      val key = keyToNumValue.key
    +
    +      numValues = keyToNumValue.numValue
    +      index = 0L
    +      valueRemoved = false
    +      valueForIndex = null
    +
    +      while (index < numValues) {
    +        if (valueForIndex == null) {
    +          valueForIndex = keyWithIndexToValue.get(key, index)
    +        }
    +        if (condition(valueForIndex)) {
    +          if (numValues > 1) {
    +            val valueAtMaxIndex = keyWithIndexToValue.get(key, numValues - 1)
    +            keyWithIndexToValue.put(key, index, valueAtMaxIndex)
    +            keyWithIndexToValue.remove(key, numValues - 1)
    +            valueForIndex = valueAtMaxIndex
    +          } else {
    +            keyWithIndexToValue.remove(key, 0)
    +            valueForIndex = null
    +          }
    +          numValues -= 1
    +          valueRemoved = true
    +        } else {
    +          valueForIndex = null
    +          index += 1
    +        }
    +      }
    +      if (valueRemoved) {
    +        if (numValues >= 1) {
    +          keyToNumValues.put(key, numValues)
    +        } else {
    +          keyToNumValues.remove(key)
    +        }
    +      }
    +    }
    +  }
    +
    +  def iterator(): Iterator[UnsafeRowPair] = {
    +    val pair = new UnsafeRowPair()
    +    keyWithIndexToValue.iterator.map { x =>
    +      pair.withRows(x.key, x.value)
    +    }
    +  }
    +
    +  /** Commit all the changes to all the state stores */
    +  def commit(): Unit = {
    +    keyToNumValues.commit()
    +    keyWithIndexToValue.commit()
    +  }
    +
    +  /** Abort any changes to the state stores if needed */
    +  def abortIfNeeded(): Unit = {
    +    keyWithIndexToValue.abortIfNeeded()
    +    keyWithIndexToValue.abortIfNeeded()
    +  }
    +
    +  /** Get the combined metrics of all the state stores */
    +  def metrics: StateStoreMetrics = {
    +    val keyToNumValuesMetrics = keyToNumValues.metrics
    +    val keyWithIndexToValueMetrics = keyWithIndexToValue.metrics
    +    def newDesc(desc: String): String = s"${joinSide.toString.toUpperCase}: $desc"
    +
    +    StateStoreMetrics(
    +      keyWithIndexToValueMetrics.numKeys,       // represent each buffered row only once
    +      keyToNumValuesMetrics.memoryUsedBytes + keyWithIndexToValueMetrics.memoryUsedBytes,
    +      keyWithIndexToValueMetrics.customMetrics.map {
    +        case (s @ StateStoreCustomSizeMetric(_, desc), value) =>
    +          s.copy(desc = newDesc(desc)) -> value
    +        case (s @ StateStoreCustomTimingMetric(_, desc), value) =>
    +          s.copy(desc = newDesc(desc)) -> value
    +      }
    +    )
    +  }
    +
    +  /*
    +  =====================================================
    +            Private methods and inner classes
    +  =====================================================
    +   */
    +
    +  private val keySchema = StructType(
    +    joinKeys.zipWithIndex.map { case (k, i) => StructField(s"field$i", k.dataType, k.nullable) })
    +  private val keyAttributes = keySchema.toAttributes
    +  private val keyToNumValues = new KeyToNumValuesStore()
    +  private val keyWithIndexToValue = new KeyWithIndexToValueStore()
    +
    +  /** Helper trait for invoking common functionalities of a state store. */
    +  private abstract class StateStoreHandler(stateStoreType: StateStoreType) extends Logging {
    +
    +    /** StateStore that the subclasses of this class is going to operate on */
    +    protected def stateStore: StateStore
    +
    +    def commit(): Unit = {
    +      stateStore.commit()
    +      logDebug("Committed, metrics = " + stateStore.metrics)
    +    }
    +
    +    def abortIfNeeded(): Unit = {
    +      if (!stateStore.hasCommitted) {
    +        logInfo(s"Aborted store ${stateStore.id}")
    +        stateStore.abort()
    +      }
    +    }
    +
    +    def metrics: StateStoreMetrics = stateStore.metrics
    +
    +    /** Get the StateStore with the given schema */
    +    protected def getStateStore(keySchema: StructType, valueSchema: StructType): StateStore = {
    +      val storeProviderId = StateStoreProviderId(
    +        stateInfo.get, TaskContext.getPartitionId(), getStateStoreName(joinSide, stateStoreType))
    +      val store = StateStore.get(
    +        storeProviderId, keySchema, valueSchema, None,
    +        stateInfo.get.storeVersion, storeConf, hadoopConf)
    +      logInfo(s"Loaded store ${store.id}")
    +      store
    +    }
    +  }
    +
    +  /**
    +   * Helper class for representing data returned by [[KeyWithIndexToValueStore]].
    +   * Designed for object reuse.
    +   */
    +  private case class KeyAndNumValues(var key: UnsafeRow = null, var numValue: Long = 0) {
    +    def withNew(newKey: UnsafeRow, newNumValues: Long): this.type = {
    +      this.key = newKey
    +      this.numValue = newNumValues
    +      this
    +    }
    +  }
    +
    +
    +  /** A wrapper around a [[StateStore]] that stores [key -> number of values]. */
    +  private class KeyToNumValuesStore extends StateStoreHandler(KeyToNumValuesType) {
    +    private val longValueSchema = new StructType().add("value", "long")
    +    private val longToUnsafeRow = UnsafeProjection.create(longValueSchema)
    +    private val valueRow = longToUnsafeRow(new SpecificInternalRow(longValueSchema))
    +    protected val stateStore: StateStore = getStateStore(keySchema, longValueSchema)
    +
    +    /** Get the number of values the key has */
    +    def get(key: UnsafeRow): Long = {
    +      val longValueRow = stateStore.get(key)
    +      if (longValueRow != null) longValueRow.getLong(0) else 0L
    +    }
    +
    +    /** Set the number of values the key has */
    +    def put(key: UnsafeRow, numValues: Long): Unit = {
    +      require(numValues > 0)
    +      valueRow.setLong(0, numValues)
    +      stateStore.put(key, valueRow)
    +    }
    +
    +    def remove(key: UnsafeRow): Unit = {
    +      stateStore.remove(key)
    +    }
    +
    +    def iterator: Iterator[KeyAndNumValues] = {
    +      val keyAndNumValues = new KeyAndNumValues()
    +      stateStore.getRange(None, None).map { case pair =>
    +        keyAndNumValues.withNew(pair.key, pair.value.getLong(0))
    +      }
    +    }
    +  }
    +
    +  /**
    +   * Helper class for representing data returned by [[KeyWithIndexToValueStore]].
    +   * Designed for object reuse.
    +   */
    +  private case class KeyWithIndexAndValue(
    +    var key: UnsafeRow = null, var valueIndex: Long = -1, var value: UnsafeRow = null) {
    +    def withNew(newKey: UnsafeRow, newIndex: Long, newValue: UnsafeRow): this.type = {
    +      this.key = newKey
    +      this.valueIndex = newIndex
    +      this.value = newValue
    +      this
    +    }
    +  }
    +
    +  /** A wrapper around a [[StateStore]] that stores [(key, index) -> value]. */
    +  private class KeyWithIndexToValueStore extends StateStoreHandler(KeyWithIndexToValuesType) {
    +    private val keyWithIndexExprs = keyAttributes :+ Literal(1L)
    +    private val keyWithIndexSchema = StructType(keySchema.fields).add("index", LongType)
    --- End diff --
    
    can't you just call `keySchema.add(..)`?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139852354
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in milliseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              val millisPerMonth = CalendarInterval.MICROS_PER_DAY / 1000 * 31
    --- End diff --
    
    Updated to throw warning when month is present.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139809689
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    +    new SerializableConfiguration(SessionState.newHadoopConf(
    +      sparkContext.hadoopConfiguration, sqlContext.conf)))
    +
    +  /**
    +   * Join keys of both sides generate rows of the same fields, that is, same sequence of data types.
    +   * If one side (say left side) has a column (say timestmap) that has a watermark on it,
    +   * then it will never consider joining keys that are < state key watermark (i.e. event time
    +   * watermark). On the other side (i.e. right side), even if there is no watermark defined,
    +   * there has to be an equivalent column (i.e. timestamp). And any right side data that has the
    +   * timestamp < watermark will not match will not match with left side data, as the left side get
    +   * filtered with the explicitly defined watermark. So, the watermark in timestamp column in
    +   * left side keys effectively causes the timestamp on the right side to have a watermark.
    +   * We will use the ordinal of the left timestamp in the left keys to find the corresponding
    +   * right timestamp in the right keys.
    +   */
    +  private val joinKeyOrdinalForWatermark: Option[Int] = {
    +    leftKeys.zipWithIndex.collectFirst {
    +      case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +    } orElse {
    +      rightKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      }
    +    }
    +  }
    +
    +  override def requiredChildDistribution: Seq[Distribution] =
    +    ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
    +
    +  override def output: Seq[Attribute] = left.output ++ right.output
    +
    +  override def outputPartitioning: Partitioning = joinType match {
    +    case _: InnerLike =>
    +      PartitioningCollection(Seq(left.outputPartitioning, right.outputPartitioning))
    +    case x =>
    +      throw new IllegalArgumentException(
    +        s"${getClass.getSimpleName} should not take $x as the JoinType")
    +  }
    +
    +  protected override def doExecute(): RDD[InternalRow] = {
    +    val stateStoreCoord = sqlContext.sessionState.streamingQueryManager.stateStoreCoordinator
    +    val stateStoreNames = SymmetricHashJoinStateManager.allStateStoreNames(LeftSide, RightSide)
    +    left.execute().stateStoreAwareZipPartitions(
    +      right.execute(), stateInfo.get, stateStoreNames, stateStoreCoord)(processPartitions)
    +  }
    +
    +  private def processPartitions(
    +      leftInputIter: Iterator[InternalRow],
    +      rightInputIter: Iterator[InternalRow]): Iterator[InternalRow] = {
    +    val numOutputRows = longMetric("numOutputRows")
    +    val numUpdatedStateRows = longMetric("numUpdatedStateRows")
    +    val numTotalStateRows = longMetric("numTotalStateRows")
    +    val allUpdatesTimeMs = longMetric("allUpdatesTimeMs")
    +    val allRemovalsTimeMs = longMetric("allRemovalsTimeMs")
    +    val commitTimeMs = longMetric("commitTimeMs")
    +    val stateMemory = longMetric("stateMemory")
    +
    +    val updateStartTimeNs = System.nanoTime
    +    val joinedRow = new JoinedRow
    +
    +    val leftSideJoiner = new OneSideHashJoiner(LeftSide, left.output, leftKeys, leftInputIter)
    +    val rightSideJoiner = new OneSideHashJoiner(RightSide, right.output, rightKeys, rightInputIter)
    +
    +    /*
    +      Join one side input using the other side's buffered/state rows. Here is how it is done.
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new left input with
    +        stored right input, and also stores all the left input
    +
    +      - `leftJoiner.joinWith(rightJoiner)` generates all rows from matching new right input with
    --- End diff --
    
    `rightJoiner.joinWith(leftJoiner)`?


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139844541
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/SymmetricHashJoinStateManager.scala ---
    @@ -0,0 +1,405 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming.state
    +
    +import scala.reflect.ClassTag
    +
    +import org.apache.hadoop.conf.Configuration
    +
    +import org.apache.spark.{Partition, SparkContext, TaskContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, BindReferences, Expression, LessThanOrEqual, Literal, SpecificInternalRow, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.Predicate
    +import org.apache.spark.sql.execution.streaming.{StatefulOperatorStateInfo, StreamingSymmetricHashJoinExec}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.types.{LongType, StructField, StructType}
    +import org.apache.spark.util.NextIterator
    +
    +/**
    + * Helper class to manage state required by a single side of [[StreamingSymmetricHashJoinExec]].
    + * The interface of this class is basically that of a multi-map:
    + * - Get: Returns an iterator of multiple values for given key
    + * - Append: Append a new value to the given key
    + * - Remove Data by predicate: Drop any state using a predicate condition on keys or values
    + *
    + * @param joinSide          Defines the join side
    + * @param inputValueAttributes   Attributes of the input row which will be stored as value
    + * @param joinKeys          Expressions to find the join key that will be used to key the value rows
    + * @param stateInfo           Information about how to retrieve the correct version of state
    + * @param storeConf           Configuration for the state store.
    + * @param hadoopConf          Hadoop configuration for reading state data from storage
    + *
    + * Internally, the key -> multiple values is stored in two [[StateStore]]s.
    + * - Store 1 ([[KeyToNumValuesStore]]) maintains mapping between key -> number of values
    + * - Store 2 ([[KeyWithIndexToValueStore]]) maintains mapping between (key, index) -> value
    + * - Put:   update count in KeyToNumValuesStore,
    + *          insert new (key, count) -> value in KeyWithIndexToValueStore
    + * - Get:   read count from KeyToNumValuesStore,
    + *          read each of the n values in KeyWithIndexToValueStore
    + * - Remove state by predicate on keys:
    + *          scan all keys in KeyToNumValuesStore to find keys that do match the predicate,
    + *          delete from key from KeyToNumValuesStore, delete values in KeyWithIndexToValueStore
    + * - Remove state by condition on values:
    + *          scan all [(key, index) -> value] in KeyWithIndexToValueStore to find values that match
    + *          the predicate, delete corresponding (key, indexToDelete) from KeyWithIndexToValueStore
    + *          by overwriting with the value of (key, maxIndex), and removing [(key, maxIndex),
    + *          decrement corresponding num values in KeyToNumValuesStore
    + */
    +class SymmetricHashJoinStateManager(
    +    val joinSide: JoinSide,
    +    val inputValueAttributes: Seq[Attribute],
    +    joinKeys: Seq[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    storeConf: StateStoreConf,
    +    hadoopConf: Configuration) extends Logging {
    +
    +  import SymmetricHashJoinStateManager._
    +
    +  // Clean up any state store resources if necessary at the end of the task
    +  Option(TaskContext.get()).foreach { _.addTaskCompletionListener { _ => abortIfNeeded() } }
    --- End diff --
    
    +1


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139815966
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    +     *    Cast(AttributeReference, Double), UnaryMinus(Cast(AttributeReference, Double))
    +     *    Multiply(Literal), UnaryMinus(Multiply(Literal))
    +     *    Multiply(Cast(Literal)), UnaryMinus(Multiple(Cast(Literal)))
    +     *
    +     * Note:
    +     * - If term needs to be negated for making it a commutative term,
    +     *   then it will be wrapped in UnaryMinus(...)
    +     * - Each terms will be representing timestamp value or time interval in milliseconds,
    +     *   typed as doubles.
    +     */
    +    def collect(expr: Expression, negate: Boolean): Seq[Expression] = {
    +      expr match {
    +        case Add(left, right) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case Subtract(left, right) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case TimeAdd(left, right, _) =>
    +          collect(left, negate) ++ collect(right, negate)
    +        case TimeSub(left, right, _) =>
    +          collect(left, negate) ++ collect(right, !negate)
    +        case UnaryMinus(child) =>
    +          collect(child, !negate)
    +        case CheckOverflow(child, _) =>
    +          collect(child, negate)
    +        case Cast(child, dataType, _) =>
    +          dataType match {
    +            case _: NumericType | _: TimestampType => collect(child, negate)
    +            case _ =>
    +              invalid = true
    +              Seq.empty
    +          }
    +        case a: AttributeReference =>
    +          val castedRef = if (a.dataType != DoubleType) Cast(a, DoubleType) else a
    +          Seq(negateIfNeeded(castedRef, negate))
    +        case lit: Literal =>
    +          // If literal of type calendar interval, then explicitly convert to millis
    +          // Convert other number like literal to doubles representing millis (by x1000)
    +          val castedLit = lit.dataType match {
    +            case CalendarIntervalType =>
    +              val calendarInterval = lit.value.asInstanceOf[CalendarInterval]
    +              val millisPerMonth = CalendarInterval.MICROS_PER_DAY / 1000 * 31
    --- End diff --
    
    I don't think 31 is a standard anywhere. I would prefer if we just throw an AnalysisException here if we find months.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139850114
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/SymmetricHashJoinStateManager.scala ---
    @@ -0,0 +1,405 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming.state
    +
    +import scala.reflect.ClassTag
    +
    +import org.apache.hadoop.conf.Configuration
    +
    +import org.apache.spark.{Partition, SparkContext, TaskContext}
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.rdd.{RDD, ZippedPartitionsRDD2}
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, BindReferences, Expression, LessThanOrEqual, Literal, SpecificInternalRow, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.Predicate
    +import org.apache.spark.sql.execution.streaming.{StatefulOperatorStateInfo, StreamingSymmetricHashJoinExec}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper._
    +import org.apache.spark.sql.types.{LongType, StructField, StructType}
    +import org.apache.spark.util.NextIterator
    +
    +/**
    + * Helper class to manage state required by a single side of [[StreamingSymmetricHashJoinExec]].
    + * The interface of this class is basically that of a multi-map:
    + * - Get: Returns an iterator of multiple values for given key
    + * - Append: Append a new value to the given key
    + * - Remove Data by predicate: Drop any state using a predicate condition on keys or values
    + *
    + * @param joinSide          Defines the join side
    + * @param inputValueAttributes   Attributes of the input row which will be stored as value
    + * @param joinKeys          Expressions to find the join key that will be used to key the value rows
    + * @param stateInfo           Information about how to retrieve the correct version of state
    + * @param storeConf           Configuration for the state store.
    + * @param hadoopConf          Hadoop configuration for reading state data from storage
    + *
    + * Internally, the key -> multiple values is stored in two [[StateStore]]s.
    + * - Store 1 ([[KeyToNumValuesStore]]) maintains mapping between key -> number of values
    + * - Store 2 ([[KeyWithIndexToValueStore]]) maintains mapping between (key, index) -> value
    + * - Put:   update count in KeyToNumValuesStore,
    + *          insert new (key, count) -> value in KeyWithIndexToValueStore
    + * - Get:   read count from KeyToNumValuesStore,
    + *          read each of the n values in KeyWithIndexToValueStore
    + * - Remove state by predicate on keys:
    + *          scan all keys in KeyToNumValuesStore to find keys that do match the predicate,
    + *          delete from key from KeyToNumValuesStore, delete values in KeyWithIndexToValueStore
    + * - Remove state by condition on values:
    + *          scan all [(key, index) -> value] in KeyWithIndexToValueStore to find values that match
    + *          the predicate, delete corresponding (key, indexToDelete) from KeyWithIndexToValueStore
    + *          by overwriting with the value of (key, maxIndex), and removing [(key, maxIndex),
    + *          decrement corresponding num values in KeyToNumValuesStore
    + */
    +class SymmetricHashJoinStateManager(
    +    val joinSide: JoinSide,
    +    val inputValueAttributes: Seq[Attribute],
    +    joinKeys: Seq[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    storeConf: StateStoreConf,
    +    hadoopConf: Configuration) extends Logging {
    +
    +  import SymmetricHashJoinStateManager._
    +
    +  // Clean up any state store resources if necessary at the end of the task
    +  Option(TaskContext.get()).foreach { _.addTaskCompletionListener { _ => abortIfNeeded() } }
    +
    +  /*
    +  =====================================================
    +                  Public methods
    +  =====================================================
    +   */
    +
    +  /** Get all the values of a key */
    +  def get(key: UnsafeRow): Iterator[UnsafeRow] = {
    +    val numValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.getAll(key, numValues)
    +  }
    +
    +  /** Append a new value to the key */
    +  def append(key: UnsafeRow, value: UnsafeRow): Unit = {
    +    val numExistingValues = keyToNumValues.get(key)
    +    keyWithIndexToValue.put(key, numExistingValues, value)
    +    keyToNumValues.put(key, numExistingValues + 1)
    +  }
    +
    +  /**
    +   * Remove using a predicate on keys. See class docs for more context and implement details.
    +   */
    +  def removeByKeyCondition(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      if (condition(keyToNumValue.key)) {
    +        keyToNumValues.remove(keyToNumValue.key)
    +        keyWithIndexToValue.removeAllValues(keyToNumValue.key, keyToNumValue.numValue)
    +      }
    +    }
    +  }
    +
    +  /**
    +   * Remove using a predicate on values. See class docs for more context and implementation details.
    +   */
    +  def removeByPredicateOnValues(condition: UnsafeRow => Boolean): Unit = {
    +    val allKeyToNumValues = keyToNumValues.iterator
    +
    +    var numValues: Long = 0L
    +    var index: Long = 0L
    +    var valueRemoved = false
    +    var valueForIndex: UnsafeRow = null
    +
    +
    +    while (allKeyToNumValues.hasNext) {
    +      val keyToNumValue = allKeyToNumValues.next
    +      val key = keyToNumValue.key
    +
    +      numValues = keyToNumValue.numValue
    +      index = 0L
    +      valueRemoved = false
    +      valueForIndex = null
    +
    +      while (index < numValues) {
    +        if (valueForIndex == null) {
    +          valueForIndex = keyWithIndexToValue.get(key, index)
    +        }
    +        if (condition(valueForIndex)) {
    +          if (numValues > 1) {
    +            val valueAtMaxIndex = keyWithIndexToValue.get(key, numValues - 1)
    +            keyWithIndexToValue.put(key, index, valueAtMaxIndex)
    +            keyWithIndexToValue.remove(key, numValues - 1)
    +            valueForIndex = valueAtMaxIndex
    +          } else {
    +            keyWithIndexToValue.remove(key, 0)
    +            valueForIndex = null
    +          }
    +          numValues -= 1
    +          valueRemoved = true
    +        } else {
    +          valueForIndex = null
    +          index += 1
    +        }
    +      }
    +      if (valueRemoved) {
    +        if (numValues >= 1) {
    +          keyToNumValues.put(key, numValues)
    +        } else {
    +          keyToNumValues.remove(key)
    +        }
    +      }
    +    }
    +  }
    +
    +  def iterator(): Iterator[UnsafeRowPair] = {
    +    val pair = new UnsafeRowPair()
    +    keyWithIndexToValue.iterator.map { x =>
    +      pair.withRows(x.key, x.value)
    +    }
    +  }
    +
    +  /** Commit all the changes to all the state stores */
    +  def commit(): Unit = {
    +    keyToNumValues.commit()
    +    keyWithIndexToValue.commit()
    +  }
    +
    +  /** Abort any changes to the state stores if needed */
    +  def abortIfNeeded(): Unit = {
    +    keyWithIndexToValue.abortIfNeeded()
    +    keyWithIndexToValue.abortIfNeeded()
    --- End diff --
    
    you're aborting twice on the same state store. `keyToNumValues`


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by tdas <gi...@git.apache.org>.
Github user tdas commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139834609
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExecHelper.scala ---
    @@ -0,0 +1,303 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import scala.util.control.NonFatal
    +
    +import org.apache.spark.internal.Logging
    +import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, AttributeReference, BoundReference, Cast, CheckOverflow, Expression, ExpressionSet, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Literal, Multiply, NamedExpression, PredicateHelper, Subtract, TimeAdd, TimeSub, UnaryMinus}
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.execution.streaming.WatermarkSupport.watermarkExpression
    +import org.apache.spark.sql.types._
    +import org.apache.spark.unsafe.types.CalendarInterval
    +
    +
    +/**
    + * Helper object for [[StreamingSymmetricHashJoinExec]].
    + */
    +object StreamingSymmetricHashJoinExecHelper extends PredicateHelper with Logging {
    +
    +  sealed trait JoinSide
    +  case object LeftSide extends JoinSide { override def toString(): String = "left" }
    +  case object RightSide extends JoinSide { override def toString(): String = "right" }
    +
    +  sealed trait JoinStateWatermarkPredicate
    +  case class JoinStateKeyWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +  case class JoinStateValueWatermarkPredicate(expr: Expression) extends JoinStateWatermarkPredicate
    +
    +  case class JoinStateWatermarkPredicates(
    +    left: Option[JoinStateWatermarkPredicate] = None,
    +    right: Option[JoinStateWatermarkPredicate] = None)
    +
    +  def getStateWatermarkPredicates(
    +      leftAttributes: Seq[Attribute],
    +      rightAttributes: Seq[Attribute],
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      condition: Option[Expression],
    +      eventTimeWatermark: Option[Long]): JoinStateWatermarkPredicates = {
    +    val joinKeyOrdinalForWatermark: Option[Int] = {
    +      leftKeys.zipWithIndex.collectFirst {
    +        case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +      } orElse {
    +        rightKeys.zipWithIndex.collectFirst {
    +          case (ne: NamedExpression, index) if ne.metadata.contains(delayKey) => index
    +        }
    +      }
    +    }
    +
    +    def getOneSideStateWatermarkPredicate(
    +        oneSideInputAttributes: Seq[Attribute],
    +        oneSideJoinKeys: Seq[Expression],
    +        otherSideInputAttributes: Seq[Attribute]): Option[JoinStateWatermarkPredicate] = {
    +      val isWatermarkDefinedOnInput = oneSideInputAttributes.exists(_.metadata.contains(delayKey))
    +      val isWatermarkDefinedOnJoinKey = joinKeyOrdinalForWatermark.isDefined
    +
    +      if (isWatermarkDefinedOnJoinKey) { // case 1 and 3 explained in the class docs
    +        val keyExprWithWatermark = BoundReference(
    +          joinKeyOrdinalForWatermark.get,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).dataType,
    +          oneSideJoinKeys(joinKeyOrdinalForWatermark.get).nullable)
    +        val expr = watermarkExpression(Some(keyExprWithWatermark), eventTimeWatermark)
    +        expr.map(JoinStateKeyWatermarkPredicate)
    +
    +      } else if (isWatermarkDefinedOnInput) { // case 2 explained in the class docs
    +        val stateValueWatermark = getStateValueWatermark(
    +          attributesToFindStateWatemarkFor = oneSideInputAttributes,
    +          attributesWithEventWatermark = otherSideInputAttributes,
    +          condition,
    +          eventTimeWatermark)
    +        val inputAttributeWithWatermark = oneSideInputAttributes.find(_.metadata.contains(delayKey))
    +        val expr = watermarkExpression(inputAttributeWithWatermark, stateValueWatermark)
    +        expr.map(JoinStateValueWatermarkPredicate)
    +
    +      } else {
    +        None
    +
    +      }
    +    }
    +
    +    val leftStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(leftAttributes, leftKeys, rightAttributes)
    +    val rightStateWatermarkPredicate =
    +      getOneSideStateWatermarkPredicate(rightAttributes, rightKeys, leftAttributes)
    +    JoinStateWatermarkPredicates(leftStateWatermarkPredicate, rightStateWatermarkPredicate)
    +  }
    +
    +  /**
    +   * Get state value watermark (see [[StreamingSymmetricHashJoinExec]] for context about it)
    +   * given the join condition and the event time watermark. This is how it works.
    +   * - The condition is split into conjunctive predicates, and we find the predicates of the
    +   *   form `leftTime + c1 < rightTime + c2`   (or <=, >, >=).
    +   * - We canoncalize the predicate and solve it with the event time watermark value to find the
    +   *  value of the state watermark.
    +   *
    +   * @param attributesToFindStateWatemarkFor attributes of the side whose state watermark
    +   *                                         is to be calculated
    +   * @param attributesWithEventWatermark  attributes of the other side which has a watermark column
    +   * @param joinCondition                 join condition
    +   * @param eventWatermark                watermark defined on the input event data
    +   * @return state value watermark in milliseconds
    +   */
    +  def getStateValueWatermark(
    +      attributesToFindStateWatemarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      joinCondition: Option[Expression],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +    if (joinCondition.isEmpty || eventWatermark.isEmpty) return None
    +
    +    def getStateWatermarkSafely(l: Expression, r: Expression): Option[Long] = {
    +      try {
    +        getStateWatemarkFromLessThenPredicate(
    +          l, r, attributesToFindStateWatemarkFor, attributesWithEventWatermark, eventWatermark)
    +      } catch {
    +        case NonFatal(e) =>
    +          logWarning(s"Error trying to extract state constraint from condition $joinCondition", e)
    +          None
    +      }
    +    }
    +
    +    val allStateWatermarks = splitConjunctivePredicates(joinCondition.get).flatMap { predicate =>
    +      val stateWatermark = predicate match {
    +        case LessThan(l, r) => getStateWatermarkSafely(l, r)
    +        case LessThanOrEqual(l, r) => getStateWatermarkSafely(l, r).map(_ - 1)
    +        case GreaterThan(l, r) => getStateWatermarkSafely(r, l)
    +        case GreaterThanOrEqual(l, r) => getStateWatermarkSafely(r, l).map(_ - 1)
    +        case _ => None
    +      }
    +      if (stateWatermark.nonEmpty) {
    +        logInfo(s"Condition $joinCondition generated watermark constraint = ${stateWatermark.get}")
    +      }
    +      stateWatermark
    +    }
    +    allStateWatermarks.reduceOption((x, y) => Math.min(x, y))
    +  }
    +
    +  /**
    +   * Extract constraint from conditions. For example: if we want to find the constraint for
    +   * leftTime using the watermark on the rightTime. Example:
    +   *
    +   * Input:                 rightTime-with-watermark + c1 < leftTime + c2
    +   * Canonical form:        rightTime-with-watermark + c1 + (-c2) + (-leftTime) < 0
    +   * Solving for rightTime: rightTime-with-watermark + c1 + (-c2) < leftTime
    +   * With watermark value:  watermark-value + c1 + (-c2) < leftTime
    +   */
    +  private def getStateWatemarkFromLessThenPredicate(
    +      leftExpr: Expression,
    +      rightExpr: Expression,
    +      attributesToFindStateWatermarkFor: Seq[Attribute],
    +      attributesWithEventWatermark: Seq[Attribute],
    +      eventWatermark: Option[Long]): Option[Long] = {
    +
    +    def containsAttributeToFindStateConstraintFor(e: Expression): Boolean = {
    +      e.collectLeaves().collectFirst {
    +        case a@AttributeReference(_, TimestampType, _, _)
    +          if attributesToFindStateWatermarkFor.contains(a) => a
    +      }.nonEmpty
    +    }
    +
    +    // Canonicalization step 1: convert to (rightTime-with-watermark + c1) - (leftTime + c2) < 0
    +    val allOnLeftExpr = Subtract(leftExpr, rightExpr)
    +    logDebug(s"All on Left:\n${allOnLeftExpr.treeString(true)}\n${allOnLeftExpr.asCode}")
    +
    +    // Canonicalization step 2: extract commutative terms
    +    //    rightTime-with-watermark, c1, -leftTime, -c2
    +    val terms = ExpressionSet(collectTerms(allOnLeftExpr))
    +    logDebug("Terms extracted from join condition:\n\t" + terms.mkString("\n\t"))
    +
    +    // Find the term that has leftTime (i.e. the one present in attributesToFindConstraintFor
    +    val constraintTerms = terms.filter(containsAttributeToFindStateConstraintFor)
    +
    +    // Verify there is only one correct constraint term and of the correct type
    +    if (constraintTerms.size > 1) {
    +      logWarning("Failed to extract state constraint terms: multiple time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    if (constraintTerms.isEmpty) {
    +      logDebug("Failed to extract state constraint terms: no time terms in condition\n\t" +
    +        terms.mkString("\n\t"))
    +      return None
    +    }
    +    val constraintTerm = constraintTerms.head
    +    if (constraintTerm.collectFirst { case u: UnaryMinus => u }.isEmpty) {
    +      // Incorrect condition. We want the constraint term in canonical form to be `-leftTime`
    +      // so that resolve for it as `-leftTime + watermark + c < 0` ==> `watermark + c < leftTime`.
    +      // Now, if the original conditions is `rightTime-with-watermark > leftTime` and watermark
    +      // condition is `rightTime-with-watermark > watermarkValue`, then no constraint about
    +      // `leftTime` can be inferred. In this case, after canonicalization and collection of terms,
    +      // the constraintTerm would be `leftTime` and not `-leftTime`. Hence, we return None.
    +      return None
    +    }
    +
    +    // Replace watermark attribute with watermark value, and generate the resolved expression
    +    // from the other terms. That is,
    +    // rightTime-with-watermark, c1, -c2  =>  watermark, c1, -c2  =>  watermark + c1 + (-c2)
    +    logDebug(s"Constraint term from join condition:\t$constraintTerm")
    +    val exprWithWatermarkSubstituted = (terms - constraintTerm).map { term =>
    +      term.transform {
    +        case a@AttributeReference(_, TimestampType, _, metadata)
    +          if attributesWithEventWatermark.contains(a) && a.metadata.contains(delayKey) =>
    +          Literal(eventWatermark.get)
    +      }
    +    }.reduceLeft(Add)
    +
    +    // Calculate the constraint value
    +    logInfo(s"Final expression to evaluate constraint:\t$exprWithWatermarkSubstituted")
    +    val constraintValue = exprWithWatermarkSubstituted.eval().asInstanceOf[java.lang.Double]
    +    Some(Double2double(constraintValue).toLong)
    +  }
    +
    +  /**
    +   * Collect all the terms present in an expression after converting it into the form
    +   * a + b + c + d where each term be either an attribute or a literal casted to long,
    +   * optionally wrapped in a unary minus.
    +   */
    +  private def collectTerms(exprToCollectFrom: Expression): Seq[Expression] = {
    +    var invalid = false
    +
    +    /** Wrap a term with UnaryMinus if its needs to be negated. */
    +    def negateIfNeeded(expr: Expression, minus: Boolean): Expression = {
    +      if (minus) UnaryMinus(expr) else expr
    +    }
    +
    +    /**
    +     * Recursively split the expression into its leaf terms contains attributes or literals.
    +     * Returns terms only of the forms:
    +     *    Csat(AttributeReference), UnaryMinus(Cast(AttributeReference)),
    --- End diff --
    
    done. lol. "cats" would have been funnier.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Test FAILed.
    Refer to this link for build results (access rights to CI server needed): 
    https://amplab.cs.berkeley.edu/jenkins//job/SparkPullRequestBuilder/81972/
    Test FAILed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r139807880
  
    --- Diff: sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StreamingSymmetricHashJoinExec.scala ---
    @@ -0,0 +1,330 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.execution.streaming
    +
    +import java.util.concurrent.TimeUnit.NANOSECONDS
    +
    +import org.apache.spark.rdd.RDD
    +import org.apache.spark.sql.catalyst.InternalRow
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, JoinedRow, NamedExpression, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.plans._
    +import org.apache.spark.sql.catalyst.plans.logical.EventTimeWatermark._
    +import org.apache.spark.sql.catalyst.plans.physical._
    +import org.apache.spark.sql.execution.{BinaryExecNode, SparkPlan}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinExecHelper._
    +import org.apache.spark.sql.execution.streaming.state._
    +import org.apache.spark.sql.internal.SessionState
    +import org.apache.spark.util.{CompletionIterator, SerializableConfiguration}
    +
    +
    +/**
    + * Performs stream-stream join using symmetric hash join algorithm. It works as follows.
    + *
    + *                             /-----------------------\
    + *   left side input --------->|    left side state    |------\
    + *                             \-----------------------/      |
    + *                                                            |--------> joined output
    + *                             /-----------------------\      |
    + *   right side input -------->|    right side state   |------/
    + *                             \-----------------------/
    + *
    + * Each join side buffers past input rows as streaming state so that the past input can be joined
    + * with future input on the other side. This buffer state is effectively a multi-map:
    + *    equi-join key -> list of past input rows received with the join key
    + *
    + * For each input row in each side, the following operations take place.
    + * - Calculate join key from the row.
    + * - Use the join key to append the row to the buffer state of the side that the row came from.
    + * - Find past buffered values for the key from the other side. For each such value, emit the
    + *   "joined row" (left-row, right-row)
    + * - Apply the optional condition to filter the joined rows as the final output.
    + *
    + * If a timestamp column with event time watermark is present in the join keys or in the input
    + * data, then the it uses the watermark figure out which rows in the buffer will not join with
    + * and new data, and therefore can be discarded. Depending on the provided query conditions, we
    + * can define thresholds on both state key (i.e. joining keys) and state value (i.e. input rows).
    + * There are three kinds of queries possible regarding this as explained below.
    + * Assume that watermark has been defined on both `leftTime` and `rightTime` columns used below.
    + *
    + * 1. When timestamp/time-window + watermark is in the join keys. Example (pseudo-SQL):
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        window(leftTime, "1 hour") = window(rightTime, "1 hour")    // 1hr tumbling windows
    + *
    + *    In this case, this operator will join rows newer than watermark which fall in the same
    + *    1 hour window. Say the event-time watermark is "12:34" (both left and right input).
    + *    Then input rows can only have time > 12:34. Hence, they can only join with buffered rows
    + *    where window >= 12:00 - 1:00 and all buffered rows with join window < 12:00 can be
    + *    discarded. In other words, the operator will discard all state where
    + *    window in state key (i.e. join key) < event time watermark. This threshold is called
    + *    State Key Watermark.
    + *
    + * 2. When timestamp range conditions are provided (no time/window + watermark in join keys). E.g.
    + *
    + *      SELECT * FROM leftTable, rightTable
    + *      ON
    + *        leftKey = rightKey AND
    + *        leftTime > rightTime - INTERVAL 8 MINUTES AND leftTime < rightTime + INTERVAL 1 HOUR
    + *
    + *   In this case, the event-time watermark and the BETWEEN condition can be used to calculate a
    + *   state watermark, i.e., time threshold for the state rows that can be discarded.
    + *   For example, say the each join side has a time column, named "leftTime" and
    + *   "rightTime", and there is a join condition "leftTime > rightTime - 8 min".
    + *   While processing, say the watermark on right input is "12:34". This means that from henceforth,
    + *   only right inputs rows with "rightTime > 12:34" will be processed, and any older rows will be
    + *   considered as "too late" and therefore dropped. Then, the left side buffer only needs
    + *   to keep rows where "leftTime > rightTime - 8 min > 12:34 - 8m > 12:26".
    + *   That is, the left state watermark is 12:26, and any rows older than that can be dropped from
    + *   the state. In other words, the operator will discard all state where
    + *   timestamp in state value (input rows) < state watermark. This threshold is called
    + *   State Value Watermark (to distinguish from the state key watermark).
    + *
    + *   Note:
    + *   - The event watermark value of one side is used to calculate the
    + *     state watermark of the other side. That is, a condition ~ "leftTime > rightTime + X" with
    + *     right side event watermark is used to calculate the left side state watermark. Conversely,
    + *     a condition ~ "left < rightTime + Y" with left side event watermark is used to calculate
    + *     right side state watermark.
    + *   - Depending on the conditions, the state watermark maybe different for the left and right
    + *     side. In the above example, leftTime > 12:26 AND rightTime > 12:34 - 1 hour = 11:34.
    + *   - State can be dropped from BOTH sides only when there are conditions of the above forms that
    + *     define time bounds on timestamp in both directions.
    + *
    + * 3. When both window in join key and time range condiions are present, case 1 + 2.
    + *    In this case, since window equality is a stricter condition than the time range, we can
    + *    use the the State Key Watermark = event time watermark to discard state (similar to case 1).
    + */
    +case class StreamingSymmetricHashJoinExec(
    +    leftKeys: Seq[Expression],
    +    rightKeys: Seq[Expression],
    +    joinType: JoinType,
    +    condition: Option[Expression],
    +    stateInfo: Option[StatefulOperatorStateInfo],
    +    eventTimeWatermark: Option[Long],
    +    stateWatermarkPredicates: JoinStateWatermarkPredicates,
    +    left: SparkPlan,
    +    right: SparkPlan) extends SparkPlan with BinaryExecNode with StateStoreWriter {
    +
    +  def this(
    +      leftKeys: Seq[Expression],
    +      rightKeys: Seq[Expression],
    +      joinType: JoinType,
    +      condition: Option[Expression],
    +      left: SparkPlan,
    +      right: SparkPlan) = {
    +    this(
    +      leftKeys, rightKeys, joinType, condition, stateInfo = None, eventTimeWatermark = None,
    +      stateWatermarkPredicates = JoinStateWatermarkPredicates(), left, right)
    +  }
    +
    +  assert(joinType == Inner, s"${getClass.getSimpleName} should not take $joinType as the JoinType")
    +  assert(leftKeys.map(_.dataType) == rightKeys.map(_.dataType))
    +
    +  private val storeConf = new StateStoreConf(sqlContext.conf)
    +  private val hadoopConfBcast = sparkContext.broadcast(
    --- End diff --
    
    this may need to be `transient`? Sometimes we serialize and send `SparkPlan`s to executors


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark pull request #19271: [SPARK-22053][SS] Stream-stream inner join in App...

Posted by brkyvz <gi...@git.apache.org>.
Github user brkyvz commented on a diff in the pull request:

    https://github.com/apache/spark/pull/19271#discussion_r140053073
  
    --- Diff: sql/core/src/test/scala/org/apache/spark/sql/streaming/StreamingJoinSuite.scala ---
    @@ -0,0 +1,585 @@
    +/*
    + * Licensed to the Apache Software Foundation (ASF) under one or more
    + * contributor license agreements.  See the NOTICE file distributed with
    + * this work for additional information regarding copyright ownership.
    + * The ASF licenses this file to You under the Apache License, Version 2.0
    + * (the "License"); you may not use this file except in compliance with
    + * the License.  You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.apache.spark.sql.streaming
    +
    +import java.util.UUID
    +
    +import scala.util.Random
    +
    +import org.apache.hadoop.conf.Configuration
    +import org.scalatest.BeforeAndAfter
    +
    +import org.apache.spark.scheduler.ExecutorCacheTaskLocation
    +import org.apache.spark.sql.SparkSession
    +import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference, AttributeSet, BoundReference, Expression, GenericInternalRow, LessThanOrEqual, Literal, UnsafeProjection, UnsafeRow}
    +import org.apache.spark.sql.catalyst.expressions.codegen.{GeneratePredicate}
    +import org.apache.spark.sql.catalyst.plans.logical.{EventTimeWatermark, Filter}
    +import org.apache.spark.sql.execution.LogicalRDD
    +import org.apache.spark.sql.execution.streaming.{MemoryStream, StatefulOperatorStateInfo, StreamingSymmetricHashJoinHelper}
    +import org.apache.spark.sql.execution.streaming.StreamingSymmetricHashJoinHelper.LeftSide
    +import org.apache.spark.sql.execution.streaming.state.{StateStore, StateStoreConf, StateStoreProviderId, SymmetricHashJoinStateManager}
    +import org.apache.spark.sql.functions._
    +import org.apache.spark.sql.types._
    +import org.apache.spark.util.Utils
    +
    +
    +class StreamingJoinSuite extends StreamTest with StateStoreMetricsTest with BeforeAndAfter {
    +
    +  before {
    +    SparkSession.setActiveSession(spark)  // set this before force initializing 'joinExec'
    +    spark.streams.stateStoreCoordinator   // initialize the lazy coordinator
    +  }
    +
    +  after {
    +    StateStore.stop()
    +  }
    +
    +  import testImplicits._
    +
    +  test("SymmetricHashJoinStateManager - all operations") {
    +    val watermarkMetadata = new MetadataBuilder().putLong(EventTimeWatermark.delayKey, 10).build()
    +    val inputValueSchema = new StructType()
    +      .add(StructField("time", IntegerType, metadata = watermarkMetadata))
    +      .add(StructField("value", BooleanType))
    +    val inputValueAttribs = inputValueSchema.toAttributes
    +    val inputValueAttribWithWatermark = inputValueAttribs(0)
    +    val joinKeyExprs = Seq[Expression](Literal(false), inputValueAttribWithWatermark, Literal(10.0))
    +
    +    val inputValueGen = UnsafeProjection.create(inputValueAttribs.map(_.dataType).toArray)
    +    val joinKeyGen = UnsafeProjection.create(joinKeyExprs.map(_.dataType).toArray)
    +
    +    def toInputValue(i: Int): UnsafeRow = {
    +      inputValueGen.apply(new GenericInternalRow(Array[Any](i, false)))
    +    }
    +
    +    def toJoinKeyRow(i: Int): UnsafeRow = {
    +      joinKeyGen.apply(new GenericInternalRow(Array[Any](false, i, 10.0)))
    +    }
    +
    +    def toKeyInt(joinKeyRow: UnsafeRow): Int = joinKeyRow.getInt(1)
    +
    +    def toValueInt(inputValueRow: UnsafeRow): Int = inputValueRow.getInt(0)
    +
    +    withJoinStateManager(inputValueAttribs, joinKeyExprs) { manager =>
    +      def append(key: Int, value: Int): Unit = {
    +        manager.append(toJoinKeyRow(key), toInputValue(value))
    +      }
    +
    +      def get(key: Int): Seq[Int] = manager.get(toJoinKeyRow(key)).map(toValueInt).toSeq.sorted
    +
    +      /** Remove keys (and corresponding values) where `time <= threshold` */
    +      def removeByKey(threshold: Long): Unit = {
    +        val expr =
    +          LessThanOrEqual(
    +            BoundReference(
    +              1, inputValueAttribWithWatermark.dataType, inputValueAttribWithWatermark.nullable),
    +            Literal(threshold))
    +        manager.removeByKeyCondition(GeneratePredicate.generate(expr).eval _)
    +      }
    +
    +      /** Remove values where `time <= threshold` */
    +      def removeByValue(watermark: Long): Unit = {
    +        val expr = LessThanOrEqual(inputValueAttribWithWatermark, Literal(watermark))
    +        manager.removeByValueCondition(
    +          GeneratePredicate.generate(expr, inputValueAttribs).eval _)
    +      }
    +
    +      def numRows: Long = {
    +        manager.metrics.numKeys
    +      }
    +
    +      assert(get(20) === Seq.empty)     // initially empty
    +      append(20, 2)
    +      assert(get(20) === Seq(2))        // should first value correctly
    +      assert(numRows === 1)
    +
    +      append(20, 3)
    +      assert(get(20) === Seq(2, 3))     // should append new values
    +      append(20, 3)
    +      assert(get(20) === Seq(2, 3, 3))  // should append another copy if same value added again
    +      assert(numRows === 3)
    +
    +      assert(get(30) === Seq.empty)
    +      append(30, 1)
    +      assert(get(30) === Seq(1))
    +      assert(get(20) === Seq(2, 3, 3))  // add another key-value should not affect existing ones
    +      assert(numRows === 4)
    +
    +      removeByKey(25)
    +      assert(get(20) === Seq.empty)
    +      assert(get(30) === Seq(1))        // should remove 20, not 30
    +      assert(numRows === 1)
    +
    +      removeByKey(30)
    +      assert(get(30) === Seq.empty)     // should remove 30
    +      assert(numRows === 0)
    +
    +      def appendAndTest(key: Int, values: Int*): Unit = {
    +        values.foreach { value => append(key, value)}
    +        require(get(key) === values)
    +      }
    +
    +      appendAndTest(40, 100, 200, 300)
    +      appendAndTest(50, 125)
    +      appendAndTest(60, 275)              // prepare for testing removeByValue
    +      assert(numRows === 5)
    +
    +      removeByValue(125)
    +      assert(get(40) === Seq(200, 300))
    +      assert(get(50) === Seq.empty)
    +      assert(get(60) === Seq(275))        // should remove only some values, not all
    +      assert(numRows === 3)
    +
    +      append(40, 50)
    +      assert(get(40) === Seq(50, 200, 300))
    +      assert(numRows === 4)
    +
    +      removeByValue(200)
    +      assert(get(40) === Seq(300))
    +      assert(get(60) === Seq(275))        // should remove only some values, not all
    +      assert(numRows === 2)
    +
    +      removeByValue(300)
    +      assert(get(40) === Seq.empty)
    +      assert(get(60) === Seq.empty)       // should remove all values now
    +      assert(numRows === 0)
    +    }
    +  }
    +
    +  test("stream stream inner join on non-time column") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF.select('value as "key", ('value * 2) as "leftValue")
    +    val df2 = input2.toDF.select('value as "key", ('value * 3) as "rightValue")
    +    val joined = df1.join(df2, "key")
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckAnswer(),
    +      AddData(input2, 1, 10),       // 1 arrived on input1 first, then input2, should join
    +      CheckLastBatch((1, 2, 3)),
    +      AddData(input1, 10),          // 10 arrived on input2 first, then input1, should join
    +      CheckLastBatch((10, 20, 30)),
    +      AddData(input2, 1),           // another 1 in input2 should join with 1 input1
    +      CheckLastBatch((1, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 1), // multiple 1s should be kept in state causing multiple (1, 2, 3)
    +      CheckLastBatch((1, 2, 3), (1, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 100),
    +      AddData(input2, 100),
    +      CheckLastBatch((100, 200, 300))
    +    )
    +  }
    +
    +
    +  test("stream stream inner join on windows - without watermark") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp", ('value * 2) as "leftValue")
    +      .select('key, window('timestamp, "10 second"), 'leftValue)
    +
    +    val df2 = input2.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp",
    +        ('value * 3) as "rightValue")
    +      .select('key, window('timestamp, "10 second"), 'rightValue)
    +
    +    val joined = df1.join(df2, Seq("key", "window"))
    +      .select('key, $"window.end".cast("long"), 'leftValue, 'rightValue)
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckLastBatch(),
    +      AddData(input2, 1),
    +      CheckLastBatch((1, 10, 2, 3)),
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 25),
    +      CheckLastBatch(),
    +      StopStream,
    +      StartStream(),
    +      AddData(input2, 25),
    +      CheckLastBatch((25, 30, 50, 75)),
    +      AddData(input1, 1),
    +      CheckLastBatch((1, 10, 2, 3)),      // State for 1 still around as there is not watermark
    +      StopStream,
    +      StartStream(),
    +      AddData(input1, 5),
    +      CheckLastBatch(),
    +      AddData(input2, 5),
    +      CheckLastBatch((5, 10, 10, 15))     // No filter by any watermark
    +    )
    +  }
    +
    +  test("stream stream inner join on windows - with watermark") {
    +    val input1 = MemoryStream[Int]
    +    val input2 = MemoryStream[Int]
    +
    +    val df1 = input1.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp", ('value * 2) as "leftValue")
    +      .withWatermark("timestamp", "10 seconds")
    +      .select('key, window('timestamp, "10 second"), 'leftValue)
    +
    +    val df2 = input2.toDF
    +      .select('value as "key", 'value.cast("timestamp") as "timestamp",
    +        ('value * 3) as "rightValue")
    +      .select('key, window('timestamp, "10 second"), 'rightValue)
    +
    +    val joined = df1.join(df2, Seq("key", "window"))
    +      .select('key, $"window.end".cast("long"), 'leftValue, 'rightValue)
    +
    +    testStream(joined)(
    +      AddData(input1, 1),
    +      CheckAnswer(),
    +      assertNumStateRows(total = 1, updated = 1),
    +
    +      AddData(input2, 1),
    +      CheckLastBatch((1, 10, 2, 3)),
    +      assertNumStateRows(total = 2, updated = 1),
    +      StopStream,
    +      StartStream(),
    +
    +      AddData(input1, 25),
    +      CheckLastBatch(), // since there is only 1 watermark operator, the watermark should be 15
    +      assertNumStateRows(total = 3, updated = 1),
    +
    +      AddData(input2, 25),
    +      CheckLastBatch((25, 30, 50, 75)), // watermark = 15 should remove 2 rows having window=[0,10]
    +      assertNumStateRows(total = 2, updated = 1),
    +      StopStream,
    +      StartStream(),
    +
    +      AddData(input2, 1),
    --- End diff --
    
    can you add this right before stopping the stream as well, just so that we make sure that cleanup happens all the time, not only after restarting a stream


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org


[GitHub] spark issue #19271: [SPARK-22053][SS] Stream-stream inner join in Append Mod...

Posted by AmplabJenkins <gi...@git.apache.org>.
Github user AmplabJenkins commented on the issue:

    https://github.com/apache/spark/pull/19271
  
    Merged build finished. Test PASSed.


---

---------------------------------------------------------------------
To unsubscribe, e-mail: reviews-unsubscribe@spark.apache.org
For additional commands, e-mail: reviews-help@spark.apache.org