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

sbt.internal.nio.PollingWatchService.scala Maven / Gradle / Ivy

/*
 * sbt IO
 * Copyright Scala Center, Lightbend, and Mark Harrah
 *
 * Licensed under Apache License 2.0
 * SPDX-License-Identifier: Apache-2.0
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package sbt.internal.nio

import java.nio.file.StandardWatchEventKinds._
import java.nio.file.{ WatchService => _, _ }
import java.util
import java.util.concurrent._
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.{ List => JList }

import sbt.internal.nio.FileEvent.{ Creation, Deletion, Update }
import sbt.io._
import sbt.nio.file.{ AnyPath, Glob }

import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.collection.immutable
import scala.concurrent.duration.{ Deadline => _, _ }
import scala.util.Random

/** A `WatchService` that polls the filesystem every `delay`. */
private[sbt] class PollingWatchService(delay: FiniteDuration, timeSource: TimeSource)
    extends WatchService
    with Unregisterable {
  def this(delay: FiniteDuration) = this(delay, TimeSource.default)
  private[this] implicit def ts: TimeSource = timeSource
  private[this] val closed = new AtomicBoolean(false)
  private[this] val registered = new ConcurrentHashMap[Path, PollingWatchKey].asScala
  private[this] val lastModifiedConverter: Path => Long = p => IO.getModifiedTimeOrZero(p.toFile)
  private[this] val pollQueue: util.Queue[PollingWatchKey] =
    new LinkedBlockingDeque[PollingWatchKey]
  private[this] val random = new Random()
  override def close(): Unit = if (closed.compareAndSet(false, true)) {
    registered.clear()
  }

  override def init(): Unit = {
    ensureNotClosed()
  }

  override def poll(timeout: Duration): WatchKey = {
    ensureNotClosed()
    val numKeys = pollQueue.size
    val (adjustedTimeout, deadline) = timeout match {
      case t: FiniteDuration => t -> (Deadline.now + t)
      case _                 => (numKeys * 2.millis) -> Deadline.Inf
    }
    val millis = adjustedTimeout.toMillis
    val (batchSize, batchTimeout) = (1 to Int.MaxValue)
      .dropWhile(i => (millis * i) / numKeys == 0)
      .headOption
      .fold(1 -> 1.millis)(i => i -> (adjustedTimeout / i.toLong))
    pollImpl(batchSize, batchTimeout, deadline).orNull
  }

  @tailrec
  private def pollImpl(
      batchSize: Int,
      duration: FiniteDuration,
      deadline: Deadline
  ): Option[WatchKey] = {
    pollQueue.poll() match {
      case null => None
      case key =>
        pollQueue.add(key)
        key.poll() match {
          case r if r.isDefined => r
          case _ =>
            if (batchSize > 1) {
              pollImpl(batchSize - 1, duration, deadline)
            } else if (!deadline.isOverdue) {
              Thread.sleep(duration.toMillis)
              pollImpl(batchSize, duration, deadline)
            } else {
              None
            }
        }
    }
  }

  override def pollEvents(): Map[WatchKey, immutable.Seq[WatchEvent[Path]]] = {
    ensureNotClosed()
    registered.values.map(k => (k: WatchKey) -> k.pollEventsImpl.asScala.toVector).toMap
  }

  override def register(path: Path, events: WatchEvent.Kind[Path]*): WatchKey = {
    ensureNotClosed()
    registered.get(path) match {
      case Some(k) => k
      case None =>
        val newKey = new PollingWatchKey(path, events: _*)
        registered.put(path, newKey)
        pollQueue.add(newKey)
        newKey
    }
  }
  override def unregister(path: Path): Unit = {
    ensureNotClosed()
    registered.remove(path)
    pollQueue.removeIf(_.path == path)
    ()
  }

  private def ensureNotClosed(): Unit =
    if (closed.get()) throw new ClosedWatchServiceException

  private object Overflow
      extends PollingWatchEvent(null, OVERFLOW.asInstanceOf[WatchEvent.Kind[Path]])
  private class PollingWatchKey(
      private[PollingWatchService] val path: Path,
      eventKinds: WatchEvent.Kind[Path]*
  ) extends WatchKey {
    private[this] val events =
      new ArrayBlockingQueue[FileEvent[Long]](256)
    private[this] val hasOverflow = new AtomicBoolean(false)
    private[this] lazy val acceptCreate = eventKinds.contains(ENTRY_CREATE)
    private[this] lazy val acceptDelete = eventKinds.contains(ENTRY_DELETE)
    private[this] lazy val acceptModify = eventKinds.contains(ENTRY_MODIFY)
    private[this] val glob = Glob(path, AnyPath)
    private[this] val fileCache =
      new FileCache[Long](lastModifiedConverter)
    fileCache.register(glob)
    private[this] def nextPollTime: Deadline =
      Deadline.now + random.nextInt(2 * delay.toMillis.toInt).millis
    private[this] val lastPolled = new AtomicReference(nextPollTime)
    override def cancel(): Unit = {
      reset()
      registered.remove(path)
      ()
    }
    override def isValid: Boolean = true
    override def pollEvents(): JList[WatchEvent[?]] =
      pollEventsImpl.asInstanceOf[JList[WatchEvent[?]]]
    override def reset(): Boolean = events.synchronized {
      events.clear()
      true
    }
    override def watchable(): Watchable = path

    private[PollingWatchService] def poll(): Option[WatchKey] = {
      lastPolled.get match {
        case d if d < Deadline.now =>
          val res = fileCache.refresh(glob)
          lastPolled.set(Deadline.now)
          res.foreach(maybeAddEvent)
          if (events.isEmpty) None else Some(this)
        case _ => None
      }
    }
    private[PollingWatchService] def pollEventsImpl: JList[WatchEvent[Path]] = {
      events.synchronized {
        val overflow = hasOverflow.getAndSet(false)
        val size = events.size + (if (overflow) 1 else 0)
        val rawEvents = new util.ArrayList[FileEvent[Long]](size)
        events.drainTo(rawEvents)
        val res = new util.ArrayList[WatchEvent[Path]](size)
        res.addAll(rawEvents.asScala.map {
          case Creation(p, _)  => new PollingWatchEvent(p, ENTRY_CREATE)
          case Deletion(p, _)  => new PollingWatchEvent(p, ENTRY_DELETE)
          case Update(p, _, _) => new PollingWatchEvent(p, ENTRY_MODIFY)
        }.asJava)
        if (overflow) res.add(Overflow)
        res
      }
    }
    private[PollingWatchService] def maybeAddEvent(
        event: FileEvent[Long]
    ): Option[PollingWatchKey] = {
      def offer(event: FileEvent[Long]): Option[PollingWatchKey] = {
        if (!events.synchronized(events.offer(event))) hasOverflow.set(true)
        Some(this)
      }
      event match {
        case _: Creation[?] if acceptCreate => offer(event)
        case _: Deletion[?] if acceptDelete => offer(event)
        case _: Update[?] if acceptModify   => offer(event)
        case _                              => None
      }
    }
  }

}

private class PollingWatchEvent(
    override val context: Path,
    override val kind: WatchEvent.Kind[Path]
) extends WatchEvent[Path] {
  override val count: Int = 1
  override def toString: String = kind match {
    case ENTRY_CREATE => s"Creation($context)"
    case ENTRY_DELETE => s"Deletion($context)"
    case ENTRY_MODIFY => s"Modify($context)"
    case _            => "Overflow"
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy