Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 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 @@ -46,6 +46,7 @@ class SparkPlanner(
Window ::
JoinSelection ::
InMemoryScans ::
Scripts ::
BasicOperators :: Nil)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import org.apache.spark.sql.execution.command._
import org.apache.spark.sql.execution.exchange.ShuffleExchangeExec
import org.apache.spark.sql.execution.joins.{BuildLeft, BuildRight, BuildSide}
import org.apache.spark.sql.execution.python._
import org.apache.spark.sql.execution.script.{ScriptTransformationExec, ScriptTransformIOSchema}
import org.apache.spark.sql.execution.streaming._
import org.apache.spark.sql.execution.streaming.sources.MemoryPlan
import org.apache.spark.sql.internal.SQLConf
Expand Down Expand Up @@ -649,6 +650,20 @@ abstract class SparkStrategies extends QueryPlanner[SparkPlan] {
}
}

object Scripts extends Strategy {
def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {
case logical.ScriptTransformation(input, script, output, child, ioschema) =>
ScriptTransformationExec(
input,
script,
output,
planLater(child),
ScriptTransformIOSchema(ioschema)
) :: Nil
case _ => Nil
}
}

object BasicOperators extends Strategy {
def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {
case d: DataWritingCommand => DataWritingCommandExec(d, planLater(d.query)) :: Nil
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* 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.script

import java.io._
import java.nio.charset.StandardCharsets
import java.sql.Date

import scala.collection.JavaConverters._
import scala.util.control.NonFatal

import org.apache.hadoop.conf.Configuration

import org.apache.spark.TaskContext
import org.apache.spark.sql.catalyst.{CatalystTypeConverters, InternalRow}
import org.apache.spark.sql.catalyst.expressions._
import org.apache.spark.sql.catalyst.plans.logical.ScriptInputOutputSchema
import org.apache.spark.sql.catalyst.plans.physical.Partitioning
import org.apache.spark.sql.catalyst.util.{DateFormatter, DateTimeUtils, TimestampFormatter}
import org.apache.spark.sql.catalyst.util.DateTimeUtils.SQLTimestamp
import org.apache.spark.sql.execution._
import org.apache.spark.sql.types.{DataType, DateType, TimestampType}
import org.apache.spark.util.{CircularBuffer, RedirectThread}

/**
* Transforms the input by forking and running the specified script.
*
* @param input the set of expression that should be passed to the script.
* @param script the command that should be executed.
* @param output the attributes that are produced by the script.
*/
case class ScriptTransformationExec(
input: Seq[Expression],
script: String,
output: Seq[Attribute],
child: SparkPlan,
ioschema: ScriptTransformIOSchema)
extends ScriptTransformBase {

override def producedAttributes: AttributeSet = outputSet -- inputSet

override def outputPartitioning: Partitioning = child.outputPartitioning

override def processIterator(inputIterator: Iterator[InternalRow], hadoopConf: Configuration)
: Iterator[InternalRow] = {
val cmd = List("/bin/bash", "-c", script)
val builder = new ProcessBuilder(cmd.asJava)

val proc = builder.start()
val inputStream = proc.getInputStream
val outputStream = proc.getOutputStream
val errorStream = proc.getErrorStream

// In order to avoid deadlocks, we need to consume the error output of the child process.
// To avoid issues caused by large error output, we use a circular buffer to limit the amount
// of error output that we retain. See SPARK-7862 for more discussion of the deadlock / hang
// that motivates this.
val stderrBuffer = new CircularBuffer(2048)
new RedirectThread(
errorStream,
stderrBuffer,
"Thread-ScriptTransformation-STDERR-Consumer").start()

val outputProjection = new InterpretedProjection(input, child.output)

// This new thread will consume the ScriptTransformation's input rows and write them to the
// external process. That process's output will be read by this current thread.
val writerThread = new ScriptTransformationWriterThread(
inputIterator.map(outputProjection),
input.map(_.dataType),
ioschema,
outputStream,
proc,
stderrBuffer,
TaskContext.get(),
hadoopConf
)

val reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
val outputIterator: Iterator[InternalRow] = new Iterator[InternalRow] {
var curLine: String = null
val mutableRow = new SpecificInternalRow(output.map(_.dataType))
Comment thread
HyukjinKwon marked this conversation as resolved.
Outdated

override def hasNext: Boolean = {
try {
if (curLine == null) {
curLine = reader.readLine()
if (curLine == null) {
checkFailureAndPropagate(writerThread, null, proc, stderrBuffer)
return false
}
}
true
} catch {
case NonFatal(e) =>
// If this exception is due to abrupt / unclean termination of `proc`,
// then detect it and propagate a better exception message for end users
checkFailureAndPropagate(writerThread, e, proc, stderrBuffer)

throw e
}
}

override def next(): InternalRow = {
if (!hasNext) {
throw new NoSuchElementException
}
val prevLine = curLine
curLine = reader.readLine()
if (!ioschema.isSchemaLess) {
new GenericInternalRow(
prevLine.split(ioschema.outputRowFormatMap("TOK_TABLEROWFORMATFIELD"))
.map(CatalystTypeConverters.convertToCatalyst))
} else {
new GenericInternalRow(
prevLine.split(ioschema.outputRowFormatMap("TOK_TABLEROWFORMATFIELD"), 2)
.map(CatalystTypeConverters.convertToCatalyst))
}
}
}

writerThread.start()

outputIterator
}
}

private class ScriptTransformationWriterThread(
iter: Iterator[InternalRow],
inputSchema: Seq[DataType],
ioschema: ScriptTransformIOSchema,
outputStream: OutputStream,
proc: Process,
stderrBuffer: CircularBuffer,
taskContext: TaskContext,
conf: Configuration)
extends ScriptTransformationWriterThreadBase(
iter,
inputSchema,
outputStream,
proc,
stderrBuffer,
taskContext,
conf) {

setDaemon(true)

protected val lineDelimiter = ioschema.inputRowFormatMap("TOK_TABLEROWFORMATLINES")
protected val fieldDelimiter = ioschema.inputRowFormatMap("TOK_TABLEROWFORMATFIELD")

override def processRows(): Unit = {
val len = inputSchema.length
iter.foreach { row =>
val data = if (len == 0) {
ioschema.inputRowFormatMap("TOK_TABLEROWFORMATLINES")
} else {
val sb = new StringBuilder
sb.append(row.get(0, inputSchema(0)))
var i = 1
while (i < len) {
sb.append(ioschema.inputRowFormatMap("TOK_TABLEROWFORMATFIELD"))
val columnType = inputSchema(i)
val fieldValue = row.get(i, columnType)
val fieldStringValue = columnType match {
case _: DateType =>
val dateFormatter = DateFormatter(DateTimeUtils.defaultTimeZone.toZoneId)
dateFormatter.format(fieldValue.asInstanceOf[Int])
case _: TimestampType =>
DateTimeUtils.timestampToString(
TimestampFormatter.getFractionFormatter(DateTimeUtils.defaultTimeZone.toZoneId),
fieldValue.asInstanceOf[SQLTimestamp])

@maropu maropu Jun 9, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two questions I have here;

  • Why do we need the special handling for date/timestamp here? It seems the original one (w/ the null serde case) simply outputs rows as strings though:
    if (inputSerde == null) {
    iter.foreach { row =>
    val data = if (len == 0) {
    ioschema.inputRowFormatMap("TOK_TABLEROWFORMATLINES")
    } else {
    val sb = new StringBuilder
    sb.append(row.get(0, inputSchema(0)))
    var i = 1
    while (i < len) {
    sb.append(ioschema.inputRowFormatMap("TOK_TABLEROWFORMATFIELD"))
    sb.append(row.get(i, inputSchema(i)))
    i += 1
    }
    sb.append(ioschema.inputRowFormatMap("TOK_TABLEROWFORMATLINES"))
    sb.toString()
    }
    outputStream.write(data.getBytes(StandardCharsets.UTF_8))
    }
  • Do you have any plan to support a custom serde in the Spark-native implementation?

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.

  • Why do we need the special handling for date/timestamp here? It seems the original one (w/ the null serde case) simply outputs rows as strings though:

For this question, if we remove code about handle DataTyle/TimestampType, for test case SPARK-25990: TRANSFORM should handle different data types correctly will get below error, since #14279

[info] - SPARK-25990: TRANSFORM should handle different data types correctly *** FAILED *** (4 seconds, 997 milliseconds)
[info]   Results do not match for Spark plan:
[info]    ScriptTransformation [a#19, b#20, c#21, d#22, e#23, f#24], python /Users/angerszhu/Documents/project/AngersZhu/spark/sql/core/target/scala-2.12/test-classes/test_script.py, [a#31, b#32, c#33, d#34, e#35, f#36], org.apache.spark.sql.execution.script.ScriptTransformIOSchema@1ad5a29c
[info]   +- Project [_1#6 AS a#19, _2#7 AS b#20, _3#8 AS c#21, _4#9 AS d#22, _5#10 AS e#23, _6#11 AS f#24]
[info]      +- LocalTableScan [_1#6, _2#7, _3#8, _4#9, _5#10, _6#11]
[info]
[info]
[info]    == Results ==
[info]    !== Expected Answer - 3 ==                                           == Actual Answer - 3 ==
[info]   ![1,1,1.0,1.000000000000000000,1970-01-01 08:00:00.001,2015-05-21]   [1,1,1.0,1.000000000000000000,1000,16576]
[info]   ![2,2,2.0,2.000000000000000000,1970-01-01 08:00:00.002,2015-05-22]   [2,2,2.0,2.000000000000000000,2000,16577]
[info]   ![3,3,3.0,3.000000000000000000,1970-01-01 08:00:00.003,2015-05-23]   [3,3,3.0,3.000000000000000000,3000,16578] (SparkPlanTest.scala:95)
[

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.

  • Why do we need the special handling for date/timestamp here? It seems the original one (w/ the null serde case) simply outputs rows as strings though:

For this part, maybe I should make a new jira as a sub-task of this?

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.

  • Do you have any plan to support a custom serde in the Spark-native implementation?

We are discusses about this, if we need to still keep serde, if need, I will do this work.

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.

@maropu
Remove logical about handle DataType/TimestampType and add a new sub-task, fix this after this PR

case _ =>
fieldValue.toString
}
sb.append(fieldStringValue)
i += 1
}
sb.append(ioschema.inputRowFormatMap("TOK_TABLEROWFORMATLINES"))
sb.toString()
}
outputStream.write(data.getBytes(StandardCharsets.UTF_8))
}
}
}

object ScriptTransformIOSchema {
def apply(input: ScriptInputOutputSchema): ScriptTransformIOSchema = {
new ScriptTransformIOSchema(
input.inputRowFormat,
input.outputRowFormat,
input.inputSerdeClass,
input.outputSerdeClass,
input.inputSerdeProps,
input.outputSerdeProps,
input.recordReaderClass,
input.recordWriterClass,
input.schemaLess)
}
}

/**
* The wrapper class of Hive input and output schema properties
*/
private[sql] class ScriptTransformIOSchema (
inputRowFormat: Seq[(String, String)],
outputRowFormat: Seq[(String, String)],
inputSerdeClass: Option[String],
outputSerdeClass: Option[String],
inputSerdeProps: Seq[(String, String)],
outputSerdeProps: Seq[(String, String)],
recordReaderClass: Option[String],
recordWriterClass: Option[String],
schemaLess: Boolean) extends Serializable {

protected val defaultFormat = Map(
("TOK_TABLEROWFORMATFIELD", "\t"),
("TOK_TABLEROWFORMATLINES", "\n")
)

val inputRowFormatMap = inputRowFormat.toMap.withDefault((k) => defaultFormat(k))
val outputRowFormatMap = outputRowFormat.toMap.withDefault((k) => defaultFormat(k))

def isSchemaLess: Boolean = schemaLess
}
Loading