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

com.nawforce.apexlink.indexer.Indexer.scala Maven / Gradle / Ivy

/*
 * Copyright (c) 2023 Certinia Inc. All rights reserved.
 */
package com.nawforce.apexlink.indexer

import com.nawforce.apexlink.api.ServerOps
import com.nawforce.pkgforce.diagnostics.LoggerOps
import com.nawforce.pkgforce.documents.DirectoryTree
import com.nawforce.pkgforce.path.PathLike
import com.nawforce.runtime.platform.Path

import java.util.concurrent._
import java.util.concurrent.locks.ReentrantLock
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer

/* File system watcher that detects rapid mass file change events and performs a delayed recursive re-scan to ensure
 * that all changes are being detected. The detection is very simple, we assume a mass change is happening if we see
 * two changes occur within a short period. The assumption here is that change events won't be lost if the rate
 * of change is always less than the fixed trigger interval. A re-scan is only started after a quite period with no
 * file change events to ensure the mass change event has completed. */
abstract class Indexer(path: PathLike, launcher: Monitor) extends Callable[Unit] {

  // Root in Path form, to allow access to 'native' on JVM
  private val rootPath: Path = path.asInstanceOf[Path]

  // Re-scan will fire if two events are less then this apart
  private final val config = ServerOps.getIndexerConfiguration

  // Last file change event time tick
  private var lastEventTick: Long = 0

  // Next re-scan callback, cancel future to remove callback
  private var rescanFuture: Option[ScheduledFuture[Unit]] = None

  // Thread pool for scheduled callbacks, we only ever need 1
  private lazy val scheduler: ScheduledExecutorService = {
    val s = Executors.newScheduledThreadPool(1).asInstanceOf[ScheduledThreadPoolExecutor]
    s.setRemoveOnCancelPolicy(true)
    s
  }

  // Acquire within callbacks to protect state
  private val callbackLock = new ReentrantLock(true)

  // Current in-memory state of the tree, used to calculate changes
  private var root: Option[DirectoryTree] = {
    if (config.enabled) {
      LoggerOps.debugTime(s"Indexer scanned $rootPath") {
        val index = DirectoryTree(rootPath, new ArrayBuffer[String]())
        launcher.monitor(
          rootPath,
          (file: String) => {
            onFileChange(file)
          }
        )
        Some(index)
      }
    } else {
      LoggerOps.debug(s"Indexer for $rootPath not started due to config")
      None
    }
  }

  /* Override to detect file changes */
  def onFilesChanged(path: Array[String], rescan: Boolean): Unit

  // Inject a change, mostly useful for testing
  def injectFileChange(file: String): Unit = {
    onFileChange(file)
  }

  // Stop the indexer, mostly useful for testing, see also Monitor.stop()
  def stop(): Unit = {
    rescanFuture.foreach(f => f.cancel(false))
    scheduler.shutdown()
  }

  // Detect action to be taken on each reported change
  private def onFileChange(file: String): Unit = {
    callbackLock.synchronized {
      val now = System.currentTimeMillis()
      if (rescanFuture.nonEmpty || now - lastEventTick < config.rescanTriggerTimeMs) {
        rescanFuture.foreach(_.cancel(false))
        rescanFuture = Some(
          scheduler
            .schedule[Unit](this, config.quietPeriodForRescanMs, TimeUnit.MILLISECONDS)
        )
      } else {
        onFilesChanged(Array(file), rescan = false)
      }
      lastEventTick = now
    }
  }

  // Timer callback for detect end of quiet period after mass change
  def call(): Unit = {
    rescanFuture = None
    refresh()
  }

  // Perform a full scan of the directory to capture all changes
  private def refresh(): Unit = {
    LoggerOps.debugTime(s"Indexer re-scanned $rootPath") {
      val changed = new mutable.ArrayBuffer[String]()
      root = root.map(_.refresh(changed))
      onFilesChanged(changed.toArray, rescan = true)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy