-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-32148][SS] Fix stream-stream join issue on missing to copy reused unsafe row #28975
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
82e5a76
be34258
e2201ef
1c011ab
fb63d7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -451,10 +451,25 @@ class SymmetricHashJoinStateManager( | |
| } | ||
|
|
||
| private trait KeyWithIndexToValueRowConverter { | ||
| /** Defines the schema of the value row (the value side of K-V in state store). */ | ||
| def valueAttributes: Seq[Attribute] | ||
|
|
||
| /** | ||
| * Convert the value row to (actual value, match) pair. | ||
| * | ||
| * NOTE: implementations should ensure the result row is NOT reused during execution, as | ||
| * caller may use the value to store without copy(). | ||
|
cloud-fan marked this conversation as resolved.
Outdated
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to update the comment. It's not because of storing, but the caller side updates the row.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I get it. The problem occurs when caller reads the value lately and there's "another" interaction with the method in the middle of. I agree the sentence in the source code comment is not clear as well though. Would it be better if we can rephrase as "... during execution, so that caller can safely read the value in any time" ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SGTM. I was referring to #28975 (comment) |
||
| */ | ||
| def convertValue(value: UnsafeRow): ValueAndMatchPair | ||
|
|
||
| /** | ||
| * Build the value row from (actual value, match) pair. This is expected to be called just | ||
| * before storing to the state store. | ||
| * | ||
| * NOTE: depending on the implementation, the result row "may" be reused during execution | ||
| * (to avoid initialization of object), so the caller should ensure that the logic doesn't | ||
| * affect by such behavior. Call copy() against the result row if needed. | ||
| */ | ||
| def convertToValueRow(value: UnsafeRow, matched: Boolean): UnsafeRow | ||
| } | ||
|
|
||
|
|
@@ -493,7 +508,7 @@ class SymmetricHashJoinStateManager( | |
|
|
||
| override def convertValue(value: UnsafeRow): ValueAndMatchPair = { | ||
| if (value != null) { | ||
| ValueAndMatchPair(valueRowGenerator(value), | ||
| ValueAndMatchPair(valueRowGenerator(value).copy(), | ||
| value.getBoolean(indexOrdinalInValueWithMatchedRow)) | ||
| } else { | ||
| null | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| package org.apache.spark.sql.streaming | ||
|
|
||
| import java.io.File | ||
| import java.sql.Timestamp | ||
| import java.util.{Locale, UUID} | ||
|
|
||
| import scala.util.Random | ||
|
|
@@ -996,4 +997,47 @@ class StreamingOuterJoinSuite extends StreamTest with StateStoreMetricsTest with | |
| ) | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-32148 stream-stream join regression on Spark 3.0.0") { | ||
| val input1 = MemoryStream[(Timestamp, String, String)] | ||
| val df1 = input1.toDF | ||
| .selectExpr("_1 as eventTime", "_2 as id", "_3 as comment") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any specific reason why not use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess it's pretty much simpler and more readable than |
||
| .withWatermark(s"eventTime", "2 minutes") | ||
|
|
||
| val input2 = MemoryStream[(Timestamp, String, String)] | ||
| val df2 = input2.toDF | ||
| .selectExpr("_1 as eventTime", "_2 as id", "_3 as name") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here as well. |
||
| .withWatermark(s"eventTime", "4 minutes") | ||
|
|
||
| val joined = df1.as("left") | ||
| .join(df2.as("right"), | ||
| expr(s""" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why string interpolation needed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah that's not necessary. Will remove. |
||
| |left.id = right.id AND left.eventTime BETWEEN | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: indent
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The indentation of """ looks vary on the codebase, and I can find same indentation on the codebase. |
||
| | right.eventTime - INTERVAL 30 seconds AND | ||
| | right.eventTime + INTERVAL 30 seconds | ||
| """.stripMargin), | ||
| joinType = "leftOuter") | ||
|
|
||
| val inputDataForInput1 = Seq( | ||
| (Timestamp.valueOf("2020-01-01 00:00:00"), "abc", "has no join partner"), | ||
| (Timestamp.valueOf("2020-01-02 00:00:00"), "abc", "joined with A"), | ||
| (Timestamp.valueOf("2020-01-02 01:00:00"), "abc", "joined with B")) | ||
|
|
||
| val inputDataForInput2 = Seq( | ||
| (Timestamp.valueOf("2020-01-02 00:00:10"), "abc", "A"), | ||
| (Timestamp.valueOf("2020-01-02 00:59:59"), "abc", "B"), | ||
| (Timestamp.valueOf("2020-01-02 02:00:00"), "abc", "C")) | ||
|
|
||
| val expectedOutput = Seq( | ||
| (Timestamp.valueOf("2020-01-01 00:00:00"), "abc", "has no join partner", null, null, null), | ||
| (Timestamp.valueOf("2020-01-02 00:00:00"), "abc", "joined with A", | ||
| Timestamp.valueOf("2020-01-02 00:00:10"), "abc", "A"), | ||
| (Timestamp.valueOf("2020-01-02 01:00:00"), "abc", "joined with B", | ||
| Timestamp.valueOf("2020-01-02 00:59:59"), "abc", "B")) | ||
|
|
||
| testStream(joined)( | ||
| MultiAddData((input1, inputDataForInput1), (input2, inputDataForInput2)), | ||
| CheckNewAnswer(expectedOutput.head, expectedOutput.tail: _*) | ||
| ) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to clarify: this comment is not related to the bug and just to document an existing assumption?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes right.
TBH I suspected this first and crafted a patch including the part with new iterator explicitly runs the logic after evaluating innerOutputIter, and later realized current logic already dealt with this properly, because removeOldState() doesn't materialize the candidates and evaluate lazily. This patch contains minimal change.
Worth to mention how it works for someone who may need to touch here.