@@ -29,7 +29,7 @@ import org.apache.spark._
2929import org .apache .spark .sql .catalyst .util .quietly
3030import org .apache .spark .sql .execution .streaming .CreateAtomicTestManager
3131import org .apache .spark .sql .internal .SQLConf
32- import org .apache .spark .util .Utils
32+ import org .apache .spark .util .{ ThreadUtils , Utils }
3333
3434class RocksDBSuite extends SparkFunSuite {
3535
@@ -102,6 +102,72 @@ class RocksDBSuite extends SparkFunSuite {
102102 }
103103 }
104104
105+ test(" RocksDB: cleanup old files" ) {
106+ val remoteDir = Utils .createTempDir().toString
107+ val conf = RocksDBConf ().copy(compactOnCommit = true , minVersionsToRetain = 10 )
108+
109+ def versionsPresent : Seq [Long ] = {
110+ remoteDir.listFiles.filter(_.getName.endsWith(" .zip" ))
111+ .map(_.getName.stripSuffix(" .zip" ))
112+ .map(_.toLong)
113+ .sorted
114+ }
115+
116+ withDB(remoteDir, conf = conf) { db =>
117+ // Generate versions without cleaning up
118+ for (version <- 1 to 50 ) {
119+ db.put(version.toString, version.toString) // update "1" -> "1", "2" -> "2", ...
120+ db.commit()
121+ }
122+
123+ // Clean up and verify version files and SST files were deleted
124+ require(versionsPresent === (1L to 50L ))
125+ val sstDir = new File (remoteDir, " SSTs" )
126+ val numSstFiles = listFiles(sstDir).length
127+ db.cleanup()
128+ assert(versionsPresent === (41L to 50L ))
129+ assert(listFiles(sstDir).length < numSstFiles)
130+
131+ // Verify data in retained vesions.
132+ versionsPresent.foreach { version =>
133+ db.load(version)
134+ val data = db.iterator().map(toStr).toSet
135+ assert(data === (1L to version).map(_.toString).map(x => x -> x).toSet)
136+ }
137+ }
138+ }
139+
140+ test(" RocksDB: handle commit failures and aborts" ) {
141+ val hadoopConf = new Configuration ()
142+ hadoopConf.set(
143+ SQLConf .STREAMING_CHECKPOINT_FILE_MANAGER_CLASS .parent.key,
144+ classOf [CreateAtomicTestManager ].getName)
145+ val remoteDir = Utils .createTempDir().getAbsolutePath
146+ val conf = RocksDBConf ().copy(compactOnCommit = true )
147+ withDB(remoteDir, conf = conf, hadoopConf = hadoopConf) { db =>
148+ // Disable failure of output stream and generate versions
149+ CreateAtomicTestManager .shouldFailInCreateAtomic = false
150+ for (version <- 1 to 10 ) {
151+ db.put(version.toString, version.toString) // update "1" -> "1", "2" -> "2", ...
152+ db.commit()
153+ }
154+ val version10Data = (1L to 10 ).map(_.toString).map(x => x -> x).toSet
155+
156+ // Fail commit for next version and verify that reloading resets the files
157+ CreateAtomicTestManager .shouldFailInCreateAtomic = true
158+ db.put(" 11" , " 11" )
159+ intercept[IOException ] { quietly { db.commit() } }
160+ assert(db.load(10 ).iterator().map(toStr).toSet === version10Data)
161+ CreateAtomicTestManager .shouldFailInCreateAtomic = false
162+
163+ // Abort commit for next version and verify that reloading resets the files
164+ db.load(10 )
165+ db.put(" 11" , " 11" )
166+ db.rollback()
167+ assert(db.load(10 ).iterator().map(toStr).toSet === version10Data)
168+ }
169+ }
170+
105171 test(" RocksDBFileManager: upload only new immutable files" ) {
106172 withTempDir { dir =>
107173 val dfsRootDir = dir.getAbsolutePath
@@ -207,6 +273,133 @@ class RocksDBSuite extends SparkFunSuite {
207273 }
208274 }
209275
276+ test(" disallow concurrent updates to the same RocksDB instance" ) {
277+ quietly {
278+ withDB(
279+ Utils .createTempDir().toString,
280+ conf = RocksDBConf ().copy(lockAcquireTimeoutMs = 20 )) { db =>
281+ // DB has been loaded so current thread has alread acquired the lock on the RocksDB instance
282+
283+ db.load(0 ) // Current thread should be able to load again
284+
285+ // Another thread should not be able to load while current thread is using it
286+ val ex = intercept[IllegalStateException ] {
287+ ThreadUtils .runInNewThread(" concurrent-test-thread-1" ) { db.load(0 ) }
288+ }
289+ // Assert that the error message contains the stack trace
290+ assert(ex.getMessage.contains(" Thread holding the lock has trace:" ))
291+ assert(ex.getMessage.contains(" runInNewThread" ))
292+
293+ // Commit should release the instance allowing other threads to load new version
294+ db.commit()
295+ ThreadUtils .runInNewThread(" concurrent-test-thread-2" ) {
296+ db.load(1 )
297+ db.commit()
298+ }
299+
300+ // Another thread should not be able to load while current thread is using it
301+ db.load(2 )
302+ intercept[IllegalStateException ] {
303+ ThreadUtils .runInNewThread(" concurrent-test-thread-2" ) { db.load(2 ) }
304+ }
305+
306+ // Rollback should release the instance allowing other threads to load new version
307+ db.rollback()
308+ ThreadUtils .runInNewThread(" concurrent-test-thread-3" ) {
309+ db.load(1 )
310+ db.commit()
311+ }
312+ }
313+ }
314+ }
315+
316+ test(" ensure concurrent access lock is released after Spark task completes" ) {
317+ val conf = new SparkConf ().setAppName(" test" ).setMaster(" local" )
318+ val sc = new SparkContext (conf)
319+
320+ try {
321+ RocksDBSuite .withSingletonDB {
322+ // Load a RocksDB instance, that is, get a lock inside a task and then fail
323+ quietly {
324+ intercept[Exception ] {
325+ sc.makeRDD[Int ](1 to 1 , 1 ).map { i =>
326+ RocksDBSuite .singleton.load(0 )
327+ throw new Exception (" fail this task to test lock release" )
328+ }.count()
329+ }
330+ }
331+
332+ // Test whether you can load again, that is, will it successfully lock again
333+ RocksDBSuite .singleton.load(0 )
334+ }
335+ } finally {
336+ sc.stop()
337+ }
338+ }
339+
340+ test(" ensure that concurrent update and cleanup consistent versions" ) {
341+ quietly {
342+ val numThreads = 20
343+ val numUpdatesInEachThread = 20
344+ val remoteDir = Utils .createTempDir().toString
345+ @ volatile var exception : Exception = null
346+ val updatingThreads = Array .fill(numThreads) {
347+ new Thread () {
348+ override def run (): Unit = {
349+ try {
350+ for (version <- 0 to numUpdatesInEachThread) {
351+ withDB(
352+ remoteDir,
353+ version = version) { db =>
354+ val prevValue = Option (toStr(db.get(" a" ))).getOrElse(" 0" ).toInt
355+ db.put(" a" , (prevValue + 1 ).toString)
356+ db.commit()
357+ }
358+ }
359+ } catch {
360+ case e : Exception =>
361+ val newException = new Exception (s " ThreadId ${this .getId} failed " , e)
362+ if (exception != null ) {
363+ exception = newException
364+ }
365+ throw e
366+ }
367+ }
368+ }
369+ }
370+ val cleaningThread = new Thread () {
371+ override def run (): Unit = {
372+ try {
373+ withDB(remoteDir, conf = RocksDBConf ().copy(compactOnCommit = true )) { db =>
374+ while (! this .isInterrupted) {
375+ db.cleanup()
376+ Thread .sleep(1 )
377+ }
378+ }
379+ } catch {
380+ case e : Exception =>
381+ val newException = new Exception (s " ThreadId ${this .getId} failed " , e)
382+ if (exception != null ) {
383+ exception = newException
384+ }
385+ throw e
386+ }
387+ }
388+ }
389+ updatingThreads.foreach(_.start())
390+ cleaningThread.start()
391+ updatingThreads.foreach(_.join())
392+ cleaningThread.interrupt()
393+ cleaningThread.join()
394+ if (exception != null ) {
395+ fail(exception)
396+ }
397+ withDB(remoteDir, numUpdatesInEachThread) { db =>
398+ assert(toStr(db.get(" a" )) === numUpdatesInEachThread.toString)
399+ }
400+ }
401+ }
402+
210403 test(" checkpoint metadata serde roundtrip" ) {
211404 def checkJsonRoundtrip (metadata : RocksDBCheckpointMetadata , json : String ): Unit = {
212405 assert(metadata.json == json)
@@ -304,3 +497,24 @@ class RocksDBSuite extends SparkFunSuite {
304497
305498 def listFiles (file : String ): Seq [File ] = listFiles(new File (file))
306499}
500+
501+ object RocksDBSuite {
502+ @ volatile var singleton : RocksDB = _
503+
504+ def withSingletonDB [T ](func : => T ): T = {
505+ try {
506+ singleton = new RocksDB (
507+ dfsRootDir = Utils .createTempDir().getAbsolutePath,
508+ conf = RocksDBConf ().copy(compactOnCommit = false , minVersionsToRetain = 100 ),
509+ hadoopConf = new Configuration (),
510+ loggingId = s " [Thread- ${Thread .currentThread.getId}] " )
511+
512+ func
513+ } finally {
514+ if (singleton != null ) {
515+ singleton.close()
516+ singleton = null
517+ }
518+ }
519+ }
520+ }
0 commit comments