Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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 @@ -162,6 +162,26 @@ abstract class CompactibleFileStreamLog[T <: AnyRef : ClassTag](
batchAdded
}

/**
* Return the latest batch Id.
*
* This method is a complement of getLatest() - while metadata log file per batch tends to be
* small, it doesn't apply to the compacted log file. This method only checks for existence of
* file to avoid huge cost on reading and deserializing compacted log file.
*/
def getLatestBatchId(): Option[Long] = {
val batchIds = fileManager.list(metadataPath, batchFilesFilter)
.map(f => pathToBatchId(f.getPath))
.sorted(Ordering.Long.reverse)
for (batchId <- batchIds) {

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.

I just simply remove reading file here, but as we already get batch IDs from "listing" files, it may not even need to check for existence. It won't be the outstanding latency, though.

val batchMetadataFile = batchIdToPath(batchId)
if (fileManager.exists(batchMetadataFile)) {
return Some(batchId)
}
}
None
}

/**
* CompactibleFileStreamLog maintains logs by itself, and manual purging might break internal
* state, specifically which latest compaction batch is purged.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class FileStreamSink(
}

override def addBatch(batchId: Long, data: DataFrame): Unit = {
if (batchId <= fileLog.getLatest().map(_._1).getOrElse(-1L)) {

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.

Nice catch!

val latestBatchId = getLatest().map(_._1).getOrElse(-1L)

Can these two places also be optimized in this way?

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.

Yeah I think so. Nice finding. Thanks!

if (batchId <= fileLog.getLatestBatchId().getOrElse(-1L)) {
logInfo(s"Skipping already committed batch $batchId")
} else {
val committer = FileCommitProtocol.instantiate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@
package org.apache.spark.sql.execution.streaming

import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
import java.net.URI
import java.nio.charset.StandardCharsets.UTF_8
import java.util.concurrent.atomic.AtomicLong

import scala.collection.mutable
import scala.util.Random

import org.apache.hadoop.fs.{FSDataInputStream, Path, RawLocalFileSystem}

import org.apache.spark.SparkFunSuite
import org.apache.spark.sql.internal.SQLConf
Expand Down Expand Up @@ -240,6 +247,44 @@ class FileStreamSinkLogSuite extends SparkFunSuite with SharedSparkSession {
))
}

test("getLatestBatchId") {

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.

I don't add E2E test to simplify the test code, but if we prefer E2E than I'll try to add a new test to FileStreamSinkSuite.

withCountOpenLocalFileSystemAsLocalFileSystem {
val scheme = CountOpenLocalFileSystem.scheme
withSQLConf(SQLConf.FILE_SINK_LOG_COMPACT_INTERVAL.key -> "3") {
withTempDir { dir =>
val sinkLog = new FileStreamSinkLog(FileStreamSinkLog.VERSION, spark,
s"$scheme:///${dir.getCanonicalPath}")
for (batchId <- 0L to 2L) {
sinkLog.add(
batchId,
Array(newFakeSinkFileStatus("/a/b/" + batchId, FileStreamSinkLog.ADD_ACTION)))
}

def getCountForOpenOnMetadataFile(batchId: Long): Long = {
val path = sinkLog.batchIdToPath(batchId).toUri.getPath
CountOpenLocalFileSystem.pathToNumOpenCalled.get(path).map(_.get()).getOrElse(0L)
}

CountOpenLocalFileSystem.resetCount()

assert(sinkLog.getLatestBatchId() === Some(2L))
// getLatestBatchId doesn't open the latest metadata log file
(0L to 2L).foreach { batchId =>
assert(getCountForOpenOnMetadataFile(batchId) === 0L)
}

assert(sinkLog.getLatest().map(_._1).getOrElse(-1L) === 2L)
(0L to 1L).foreach { batchId =>
assert(getCountForOpenOnMetadataFile(batchId) === 0L)
}
// getLatest opens the latest metadata log file, which explains the needs on
// having "getLatestBatchId".
assert(getCountForOpenOnMetadataFile(2L) === 1L)
}
}
}
}

/**
* Create a fake SinkFileStatus using path and action. Most of tests don't care about other fields
* in SinkFileStatus.
Expand Down Expand Up @@ -267,4 +312,40 @@ class FileStreamSinkLogSuite extends SparkFunSuite with SharedSparkSession {
val log = new FileStreamSinkLog(FileStreamSinkLog.VERSION, spark, input.toString)
log.allFiles()
}

private def withCountOpenLocalFileSystemAsLocalFileSystem(body: => Unit): Unit = {

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.

The code regarding FileSystem I add here is very similar with what I add in #27620. When either one gets merged, I'll rebase and deduplicate it.

val optionKey = s"fs.${CountOpenLocalFileSystem.scheme}.impl"
val originClassForLocalFileSystem = spark.conf.getOption(optionKey)
try {
spark.conf.set(optionKey, classOf[CountOpenLocalFileSystem].getName)
body
} finally {
originClassForLocalFileSystem match {
case Some(fsClazz) => spark.conf.set(optionKey, fsClazz)
case _ => spark.conf.unset(optionKey)
}
}
}
}

class CountOpenLocalFileSystem extends RawLocalFileSystem {
import CountOpenLocalFileSystem._

override def getUri: URI = {
URI.create(s"$scheme:///")
}

override def open(f: Path, bufferSize: Int): FSDataInputStream = {
val path = f.toUri.getPath
val curVal = pathToNumOpenCalled.getOrElseUpdate(path, new AtomicLong(0))
curVal.incrementAndGet()
super.open(f, bufferSize)
}
}

object CountOpenLocalFileSystem {
val scheme = s"FileStreamSinkLogSuite${math.abs(Random.nextInt)}fs"
val pathToNumOpenCalled = new mutable.HashMap[String, AtomicLong]

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.

Some reset functionality would be good to make it re-usable. This would also make curCount disappear.

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.

Nit: could you use a thread-safe map in case someone may reuse this in a streaming query?

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.

Yeah, I feel it's safer to make the map be thread-safe. I thought it's a contradiction to assume because we allow "resetting" the count, but it can be handled with caution.


def resetCount(): Unit = pathToNumOpenCalled.clear()
}