Saturday, June 27, 2015

Today I learned... doing things the hard way when languages and OSes are glitchy

So I have some code that works just fine... most of the time. But there has been some strange stuff that doesn't make any sense and isn't easily reproducible.

Few examples I encountered recently were Scala's parallel HashMap and file renaming in Java.

Scala has a nice parallel HashMap that allows you to do gets, puts, and updates concurrently, and it handles the synchronization for you... except when it doesn't.

I put a tuple into a ParHashMap, which has no delete operations on it in my whole code, and later when I went to get that value, it wasn't there.

On another instance, when I went to put a tuple into a ParHashMap, I got an ArrayIndexOutOfBoundsException.

I replaced my ParHashMap with a single-threaded HashMap and did my own synchronization around it, and I haven't seen either problem since...


Another issue was renaming a file in Windows using Java. File.renameTo was working just fine, but all of a sudden I came across one occurrence where the file appeared to have been copied instead of renamed. So I changed my code to delete the source file after the rename, if it was still there. But then I got an exception that the source file was already in use, even though I had previously closed my writer that was operating on that file.

I ended up writing the following method to replace what should be a simple one-line file rename:

def rename(filePath: String, targetPath: String): Unit = {
    RetryHelper.retry(10, Seq(classOf[java.nio.file.FileSystemException])){
      val sourceFile = new File(filePath)
      val targetFile = new File(targetPath)
      if (!targetFile.exists) {
        Files.move(sourceFile.toPath, targetFile.toPath)
      }
      if (sourceFile.exists) {
        sourceFile.delete()
      }
    }(Thread.sleep(50))
}

That's right, the only way I could get it to work every single time, without fail (so far!) was to delete the source file after the "move" (in case it behaved as a copy for some reason), and to wrap that in a retry block that executes up to 10 times and catches FileSystemExceptions with a short sleep before retrying. Yikes!

For reference, here is my retry helper, which I discussed in a previous post but which has been revised since, as it now takes in a code block defining what to do in the exception handling block when catching one of the specified exceptions.

import scala.util.control.Exception._ // Motivation for this object is described in the following blog posts:// http://googlyadventures.blogspot.com/2015/05/what-else-i-learned-yesterday.html// http://googlyadventures.blogspot.com/2015/06/today-i-learned-doing-things-hard-way.html /** * Helper methods to retry code with configurable retry count and exceptions to handle */object RetryHelper { /** * Retry any block of code up to a max number of times, optionally specifying the types of exceptions to retry * and code to run when handling one of the retryable exception types. * * @param maxTries Number of times to try before throwing the exception. * @param exceptionTypesToRetry Types of exceptions to retry. Defaults to single-element sequence containing classOf[RuntimeException] * @param codeToRetry Block of code to try * @param handlingCode Block of code to run if there is a catchable exception * @tparam T Return type of block of code to try * @return Return value of block of code to try (else exception will be thrown if it failed all tries) */ def retry[T](maxTries: Int, exceptionTypesToRetry: Seq[Class[_ <: Throwable]] = Seq(classOf[RuntimeException])) (codeToRetry: => T) (handlingCode: Throwable => Unit = _ => ()): T = { var result: Option[T] = None var left = maxTries while (!result.isDefined) { left = left - 1 // try/catch{case...} doesn't seem to support dynamic exception types, so using handling block instead. handling(exceptionTypesToRetry: _*) .by(ex => { if (left <= 0) { throw ex } else { handlingCode(ex) } }).apply({ result = Option(codeToRetry) }) } result.get } }

No comments:

Post a Comment