All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ebiznext.comet.utils.FileLock.scala Maven / Gradle / Ivy

There is a newer version: 0.2.6
Show newest version
package com.ebiznext.comet.utils

import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.{Semaphore, TimeUnit, TimeoutException}

import com.ebiznext.comet.schema.handlers.StorageHandler
import com.typesafe.scalalogging.StrictLogging
import org.apache.hadoop.fs.Path

import scala.util.{Failure, Success, Try}

/** HDFS does not have a file locking mechanism.
  * We implement it here in the following way
  * - A file is locked when it's modification time is less than 5000ms than the current time
  * - If the modification time is older tahn the current time of more than 5000ms, it is considered unlocked.
  * - The owner of a lock spawn a thread that update the modification every 5s to keep the lock
  * - The process willing to get the lock check every 5s second for the modification time of the file to gain the lock.
  * - When a process gain the lock, it deletes the file first and tries to create a new one, this make sure that of
  * two process gaining the lock, only one will be able to recreate it after deletion.
  *
  * To gain the lock, call [[tryLock]], to release it, call `release()`.
  * If you are going to use the lock and release it within the same call stack frame, please consider
  * calling `tryExclusively()` (as in exclusive(delay) { action }`) instead.
  *
  * @param path Lock File path
  * @param storageHandler Filesystem Handler
  */
class FileLock(path: Path, storageHandler: StorageHandler) extends StrictLogging {
  def checkinPeriod: Long = storageHandler.lockAcquisitionPollTime.toMillis
  def refreshPeriod: Long = storageHandler.lockRefreshPollTime.toMillis

  private val fileWatcher = new FileLock.LockWatcher(path, storageHandler, refreshPeriod)

  /** Try to perform an operation while holding a lock exclusively
    * @param timeoutInMillis number of milliseconds during which the calling process will try to get the lock before it time out. -1 means no timeout
    * @return the result of the operation (if successful) when locked is acquired, Failure otherwise
    *
    * the lock is guaranteed freed upon normal or exceptional exit from this function.
    */
  def tryExclusively[T](timeoutInMillis: Long = -1L)(op: => T): Try[T] = {
    Try(doExclusively(timeoutInMillis)(op))
  }

  /** Try to perform an operation while holding a lock exclusively
    * @param timeoutInMillis number of milliseconds during which the calling process will try to get the lock before it
    *                        times out. -1 means no timeout
    * @return the result of the operation (if successful) when locked is acquired
    * throws TimeoutException if the lock could not be acquired within timeoutInMillis, or any exception thrown by op
    *                          if the lock was acquired but the operation failed
    *
    * the lock is guaranteed freed upon normal or exceptional exit from this function.
    */
  def doExclusively[T](timeoutInMillis: Long = -1L)(op: => T): T = {
    if (tryLock(timeoutInMillis)) {
      try {
        op
      } finally {
        release()
      }
    } else {
      throw new TimeoutException(
        s"Failed to obtain lock on file $path waited (millis) $timeoutInMillis"
      )
    }
  }

  /** Try to gain the lock during timeoutInMillis millis
    *
    * @param timeoutInMillis number of milliseconds during which the calling process will try to get the lock before it time out. -1 means no timeout
    * @return true when locked is acquired (caller is responsible for calling `release()`, false otherwise
    */
  def tryLock(timeoutInMillis: Long = -1): Boolean = {
    fileWatcher.checkPristine()

    storageHandler.mkdirs(path.getParent)
    val maxTries = if (timeoutInMillis == -1) Integer.MAX_VALUE else timeoutInMillis / checkinPeriod
    var numberOfTries = 1
    logger.info(s"Trying to acquire lock for file ${path.toString} during $timeoutInMillis ms")
    var ok = false
    while (numberOfTries <= maxTries && !ok) {
      ok = storageHandler.touchz(path) match {
        case Success(_) =>
          logger.info(
            s"Succeeded to acquire lock for file ${path.toString} after $numberOfTries tries"
          )
          watch()
          true
        case Failure(_) =>
          val lastModified = storageHandler.lastModified(path)
          val currentTimeMillis = System.currentTimeMillis()

          logger.info(s"""
              |lastModified=$lastModified
              |System.currentTimeMillis()=${currentTimeMillis}
              |checkinPeriod*4=${checkinPeriod * 4}
              |refreshPeriod*4=${refreshPeriod * 4}
              |
          """)
          if ((currentTimeMillis - lastModified) > (refreshPeriod * 4)) {
            storageHandler.delete(path)
          }
          numberOfTries = numberOfTries + 1
          Thread.sleep(checkinPeriod)
          false
      }
    }
    ok
  }

  /** Release the lock and delete the lock file.
    */
  def release(): Unit = fileWatcher.release()

  private def watch(): Unit = {
    val th = new Thread(fileWatcher, s"LockWatcher-${System.currentTimeMillis()}-${path.toString}")
    th.start()
  }
}

object FileLock {

  private class LockWatcher(path: Path, storageHandler: StorageHandler, reportingPeriod: Long)
      extends Runnable
      with StrictLogging {

    private val pristine = new AtomicBoolean(true)

    def checkPristine(): Unit = {
      val wasPristine = pristine.getAndSet(false)
      if (!wasPristine)
        throw new IllegalStateException(
          s"FileLock instance on ${path} had already been used, cannot re-use"
        )
    }
    private val spent = new AtomicBoolean(false)

    private val sem = new Semaphore(0)

    def release(): Unit = {
      val wasAlreadySpent = spent.getAndSet(true)
      if (wasAlreadySpent)
        throw new IllegalStateException(
          s"LockWatcher thread on ${path} already spent, cannot release again"
        )
      sem.release()
    }

    override def run(): Unit = {
      /* if this thread starts, this means that our parent thread owns the lock.
       *
       * Our purpose is to regularly remind the user that something on *this* JVM owns the lock, and then destroy the
       * lockfile once we've been notified we no longer need to.
       *
       * We also regularly touch the lockfile in order to demonstrate to external users (on other JVMs or on other nodes)
       * that indeed, something is still alive and interested in this lock. An operator might use this as a clue to
       * decide that a lock file is stale and deserves to be removed.
       * */
      try {
        while (!sem.tryAcquire(reportingPeriod, TimeUnit.MILLISECONDS)) {
          // we've slept checkinPeriod and failed to acquire the semaphore; let's remind the operator that we hold the lock, and carry on
          storageHandler.touch(path)
          logger.info(s"watcher $path modified=${storageHandler.lastModified(path)}")
        }
        storageHandler.delete(path)
      } catch {
        case e: InterruptedException =>
          e.printStackTrace();
      }
    }

  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy