Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.apache.spark.sql.catalyst.expressions.{Attribute, UnsafeRow}
import org.apache.spark.sql.catalyst.plans.logical.{LeafNode, LogicalPlan, Statistics}
import org.apache.spark.sql.catalyst.plans.logical.statsEstimation.EstimationUtils
import org.apache.spark.sql.catalyst.streaming.InternalOutputModes._
import org.apache.spark.sql.sources.v2.DataSourceOptions
import org.apache.spark.sql.sources.v2.reader.{InputPartition, InputPartitionReader, SupportsScanUnsafeRow}
import org.apache.spark.sql.sources.v2.reader.streaming.{MicroBatchReader, Offset => OffsetV2}
import org.apache.spark.sql.streaming.OutputMode
Expand Down Expand Up @@ -228,19 +229,45 @@ trait MemorySinkBase extends BaseStreamingSink {
def latestBatchId: Option[Long]
}

/**
* Companion object to MemorySinkBase.
*/
object MemorySinkBase {
val MAX_MEMORY_SINK_ROWS = "maxMemorySinkRows"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maxRows is sufficient

val MAX_MEMORY_SINK_ROWS_DEFAULT = -1

/**
* Gets the max number of rows a MemorySink should store. This number is based on the memory
* sink row limit if it is set. If not, there is no limit.
* @param options Options for writing from which we get the max rows option
* @return The maximum number of rows a memorySink should store, or None for no limit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

need to update docs

*/
def getMemorySinkCapacity(options: DataSourceOptions): Option[Int] = {
val maxRows = options.getInt(MAX_MEMORY_SINK_ROWS, MAX_MEMORY_SINK_ROWS_DEFAULT)
if (maxRows >= 0) Some(maxRows) else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you want to do if (maxRows >= 0) maxRows else Int.MaxValue - 10
We can't exceed runtime array max size anyway

}
}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: remove extra line

/**
* A sink that stores the results in memory. This [[Sink]] is primarily intended for use in unit
* tests and does not provide durability.
*/
class MemorySink(val schema: StructType, outputMode: OutputMode) extends Sink
with MemorySinkBase with Logging {
class MemorySink(val schema: StructType, outputMode: OutputMode, options: DataSourceOptions)
extends Sink with MemorySinkBase with Logging {

private case class AddedData(batchId: Long, data: Array[Row])

/** An order list of batches that have been written to this [[Sink]]. */
@GuardedBy("this")
private val batches = new ArrayBuffer[AddedData]()

/** The number of rows in this MemorySink. */
private var numRows = 0

/** The capacity in rows of this sink. */
val sinkCapacity: Option[Int] = MemorySinkBase.getMemorySinkCapacity(options)

/** Returns all rows that are stored in this [[Sink]]. */
def allData: Seq[Row] = synchronized {
batches.flatMap(_.data)
Expand Down Expand Up @@ -273,14 +300,26 @@ class MemorySink(val schema: StructType, outputMode: OutputMode) extends Sink
logDebug(s"Committing batch $batchId to $this")
outputMode match {
case Append | Update =>
val rows = AddedData(batchId, data.collect())
synchronized { batches += rows }
var rowsToAdd = data.collect()
synchronized {
if (sinkCapacity.isDefined) {
rowsToAdd = truncateRowsIfNeeded(rowsToAdd, sinkCapacity.get - numRows, batchId)
}
val rows = AddedData(batchId, rowsToAdd)
batches += rows
numRows += rowsToAdd.length
}

case Complete =>
val rows = AddedData(batchId, data.collect())
var rowsToAdd = data.collect()
synchronized {
if (sinkCapacity.isDefined) {
rowsToAdd = truncateRowsIfNeeded(rowsToAdd, sinkCapacity.get, batchId)
}
val rows = AddedData(batchId, rowsToAdd)
batches.clear()
batches += rows
numRows = rowsToAdd.length
}

case _ =>
Expand All @@ -294,6 +333,16 @@ class MemorySink(val schema: StructType, outputMode: OutputMode) extends Sink

def clear(): Unit = synchronized {
batches.clear()
numRows = 0
}

private def truncateRowsIfNeeded(rows: Array[Row], maxRows: Int, batchId: Long): Array[Row] = {

@jose-torres jose-torres Jun 14, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I'd document that maxRows is the remaining row capacity, not the maximum row limit defined in the options. I got confused for a minute here.

if (rows.length > maxRows) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also adding a check here to make sure maxRows >= 0. It shouldn't ever be negative, but doesn't hurt to safeguard.

logWarning(s"Truncating batch $batchId to $maxRows rows")
rows.take(maxRows)
} else {
rows
}
}

override def toString(): String = "MemorySink"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class MemorySinkV2 extends DataSourceV2 with StreamWriteSupport with MemorySinkB
schema: StructType,
mode: OutputMode,
options: DataSourceOptions): StreamWriter = {
new MemoryStreamWriter(this, mode)
new MemoryStreamWriter(this, mode, options)
}

private case class AddedData(batchId: Long, data: Array[Row])
Expand All @@ -55,6 +55,9 @@ class MemorySinkV2 extends DataSourceV2 with StreamWriteSupport with MemorySinkB
@GuardedBy("this")
private val batches = new ArrayBuffer[AddedData]()

/** The number of rows in this MemorySink. */
private var numRows = 0

/** Returns all rows that are stored in this [[Sink]]. */
def allData: Seq[Row] = synchronized {
batches.flatMap(_.data)
Expand All @@ -81,22 +84,35 @@ class MemorySinkV2 extends DataSourceV2 with StreamWriteSupport with MemorySinkB
}.mkString("\n")
}

def write(batchId: Long, outputMode: OutputMode, newRows: Array[Row]): Unit = {
def write(batchId: Long, outputMode: OutputMode, newRows: Array[Row], sinkCapacity: Option[Int])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: our style is more like

  def write(
    batchId: Long,
    outputMode: OutputMode,
    newRows: Array[Row],
    sinkCapacity: Option[Int]): Unit = {

: Unit = {
val notCommitted = synchronized {
latestBatchId.isEmpty || batchId > latestBatchId.get
}
if (notCommitted) {
logDebug(s"Committing batch $batchId to $this")
outputMode match {
case Append | Update =>
val rows = AddedData(batchId, newRows)
synchronized { batches += rows }
synchronized {
var rowsToAdd = newRows
if (sinkCapacity.isDefined) {
rowsToAdd = truncateRowsIfNeeded(rowsToAdd, sinkCapacity.get - numRows, batchId)
}
val rows = AddedData(batchId, rowsToAdd)
batches += rows
numRows += rowsToAdd.length
}

case Complete =>
val rows = AddedData(batchId, newRows)
synchronized {
var rowsToAdd = newRows
if (sinkCapacity.isDefined) {
rowsToAdd = truncateRowsIfNeeded(rowsToAdd, sinkCapacity.get, batchId)
}
val rows = AddedData(batchId, rowsToAdd)
batches.clear()
batches += rows
numRows = rowsToAdd.length
}

case _ =>
Expand All @@ -110,40 +126,61 @@ class MemorySinkV2 extends DataSourceV2 with StreamWriteSupport with MemorySinkB

def clear(): Unit = synchronized {
batches.clear()
numRows = 0
}

private def truncateRowsIfNeeded(rows: Array[Row], maxRows: Int, batchId: Long): Array[Row] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can this go in MemorySinkBase?

if (rows.length > maxRows) {
logWarning(s"Truncating batch $batchId to $maxRows rows")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How does take behave with negative rows? Printing a warning message with negative values may be weird. I would also include the sink limit in the warning.

rows.take(maxRows)
} else {
rows
}
}

override def toString(): String = "MemorySinkV2"
}

case class MemoryWriterCommitMessage(partition: Int, data: Seq[Row]) extends WriterCommitMessage {}

class MemoryWriter(sink: MemorySinkV2, batchId: Long, outputMode: OutputMode)
class MemoryWriter(
sink: MemorySinkV2,
batchId: Long,
outputMode: OutputMode,
options: DataSourceOptions)
extends DataSourceWriter with Logging {

val sinkCapacity: Option[Int] = MemorySinkBase.getMemorySinkCapacity(options)

override def createWriterFactory: MemoryWriterFactory = MemoryWriterFactory(outputMode)

def commit(messages: Array[WriterCommitMessage]): Unit = {
val newRows = messages.flatMap {
case message: MemoryWriterCommitMessage => message.data
}
sink.write(batchId, outputMode, newRows)
sink.write(batchId, outputMode, newRows, sinkCapacity)
}

override def abort(messages: Array[WriterCommitMessage]): Unit = {
// Don't accept any of the new input.
}
}

class MemoryStreamWriter(val sink: MemorySinkV2, outputMode: OutputMode)
class MemoryStreamWriter(
val sink: MemorySinkV2,
outputMode: OutputMode,
options: DataSourceOptions)
extends StreamWriter {

val sinkCapacity: Option[Int] = MemorySinkBase.getMemorySinkCapacity(options)

override def createWriterFactory: MemoryWriterFactory = MemoryWriterFactory(outputMode)

override def commit(epochId: Long, messages: Array[WriterCommitMessage]): Unit = {
val newRows = messages.flatMap {
case message: MemoryWriterCommitMessage => message.data
}
sink.write(epochId, outputMode, newRows)
sink.write(epochId, outputMode, newRows, sinkCapacity)
}

override def abort(epochId: Long, messages: Array[WriterCommitMessage]): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import org.apache.spark.sql.execution.datasources.DataSource
import org.apache.spark.sql.execution.streaming._
import org.apache.spark.sql.execution.streaming.continuous.ContinuousTrigger
import org.apache.spark.sql.execution.streaming.sources.{ForeachWriterProvider, MemoryPlanV2, MemorySinkV2}
import org.apache.spark.sql.sources.v2.StreamWriteSupport
import org.apache.spark.sql.sources.v2.{DataSourceOptions, StreamWriteSupport}

/**
* Interface used to write a streaming `Dataset` to external storage systems (e.g. file systems,
Expand Down Expand Up @@ -249,7 +249,7 @@ final class DataStreamWriter[T] private[sql](ds: Dataset[T]) {
val r = Dataset.ofRows(df.sparkSession, new MemoryPlanV2(s, df.schema.toAttributes))
(s, r)
case _ =>
val s = new MemorySink(df.schema, outputMode)
val s = new MemorySink(df.schema, outputMode, new DataSourceOptions(extraOptions.asJava))
val r = Dataset.ofRows(df.sparkSession, new MemoryPlan(s))
(s, r)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@

package org.apache.spark.sql.execution.streaming

import scala.collection.JavaConverters._
import scala.language.implicitConversions

import org.scalatest.BeforeAndAfter

import org.apache.spark.sql._
import org.apache.spark.sql.sources.v2.DataSourceOptions
import org.apache.spark.sql.streaming.{OutputMode, StreamTest}
import org.apache.spark.sql.types.{IntegerType, StructField, StructType}
import org.apache.spark.util.Utils
Expand All @@ -36,7 +38,7 @@ class MemorySinkSuite extends StreamTest with BeforeAndAfter {

test("directly add data in Append output mode") {
implicit val schema = new StructType().add(new StructField("value", IntegerType))
val sink = new MemorySink(schema, OutputMode.Append)
val sink = new MemorySink(schema, OutputMode.Append, DataSourceOptions.empty())

// Before adding data, check output
assert(sink.latestBatchId === None)
Expand Down Expand Up @@ -68,9 +70,35 @@ class MemorySinkSuite extends StreamTest with BeforeAndAfter {
checkAnswer(sink.allData, 1 to 9)
}

test("directly add data in Append output mode with row limit") {
implicit val schema = new StructType().add(new StructField("value", IntegerType))

var optionsMap = new scala.collection.mutable.HashMap[String, String]
optionsMap.put(MemorySinkBase.MAX_MEMORY_SINK_ROWS, 5.toString())
var options = new DataSourceOptions(optionsMap.toMap.asJava)
val sink = new MemorySink(schema, OutputMode.Append, options)

// Before adding data, check output
assert(sink.latestBatchId === None)
checkAnswer(sink.latestBatchData, Seq.empty)
checkAnswer(sink.allData, Seq.empty)

// Add batch 0 and check outputs
sink.addBatch(0, 1 to 3)
assert(sink.latestBatchId === Some(0))
checkAnswer(sink.latestBatchData, 1 to 3)
checkAnswer(sink.allData, 1 to 3)

// Add batch 1 and check outputs
sink.addBatch(1, 4 to 6)
assert(sink.latestBatchId === Some(1))
checkAnswer(sink.latestBatchData, 4 to 5)
checkAnswer(sink.allData, 1 to 5) // new data should not go over the limit
}

test("directly add data in Update output mode") {
implicit val schema = new StructType().add(new StructField("value", IntegerType))
val sink = new MemorySink(schema, OutputMode.Update)
val sink = new MemorySink(schema, OutputMode.Update, DataSourceOptions.empty())

// Before adding data, check output
assert(sink.latestBatchId === None)
Expand Down Expand Up @@ -104,7 +132,7 @@ class MemorySinkSuite extends StreamTest with BeforeAndAfter {

test("directly add data in Complete output mode") {
implicit val schema = new StructType().add(new StructField("value", IntegerType))
val sink = new MemorySink(schema, OutputMode.Complete)
val sink = new MemorySink(schema, OutputMode.Complete, DataSourceOptions.empty())

// Before adding data, check output
assert(sink.latestBatchId === None)
Expand Down Expand Up @@ -136,6 +164,32 @@ class MemorySinkSuite extends StreamTest with BeforeAndAfter {
checkAnswer(sink.allData, 7 to 9)
}

test("directly add data in Complete output mode with row limit") {
implicit val schema = new StructType().add(new StructField("value", IntegerType))

var optionsMap = new scala.collection.mutable.HashMap[String, String]
optionsMap.put(MemorySinkBase.MAX_MEMORY_SINK_ROWS, 5.toString())
var options = new DataSourceOptions(optionsMap.toMap.asJava)
val sink = new MemorySink(schema, OutputMode.Complete, options)

// Before adding data, check output
assert(sink.latestBatchId === None)
checkAnswer(sink.latestBatchData, Seq.empty)
checkAnswer(sink.allData, Seq.empty)

// Add batch 0 and check outputs
sink.addBatch(0, 1 to 3)
assert(sink.latestBatchId === Some(0))
checkAnswer(sink.latestBatchData, 1 to 3)
checkAnswer(sink.allData, 1 to 3)

// Add batch 1 and check outputs
sink.addBatch(1, 4 to 10)
assert(sink.latestBatchId === Some(1))
checkAnswer(sink.latestBatchData, 4 to 8)
checkAnswer(sink.allData, 4 to 8) // new data should replace old data
}


test("registering as a table in Append output mode") {
val input = MemoryStream[Int]
Expand Down Expand Up @@ -211,7 +265,7 @@ class MemorySinkSuite extends StreamTest with BeforeAndAfter {

test("MemoryPlan statistics") {
implicit val schema = new StructType().add(new StructField("value", IntegerType))
val sink = new MemorySink(schema, OutputMode.Append)
val sink = new MemorySink(schema, OutputMode.Append, DataSourceOptions.empty())
val plan = new MemoryPlan(sink)

// Before adding data, check output
Expand Down
Loading