Implementing new logic of choosing the daemon, startup and shutdown, some related refactorings and fixes
This commit is contained in:
+24
-128
@@ -20,9 +20,9 @@ import net.rubygrapefruit.platform.ProcessLauncher
|
|||||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
||||||
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
|
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
|
||||||
import org.jetbrains.kotlin.rmi.*
|
import org.jetbrains.kotlin.rmi.*
|
||||||
import java.io.*
|
import java.io.File
|
||||||
import java.rmi.ConnectException
|
import java.io.OutputStream
|
||||||
import java.rmi.registry.LocateRegistry
|
import java.io.PrintStream
|
||||||
import java.util.concurrent.Semaphore
|
import java.util.concurrent.Semaphore
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
@@ -61,7 +61,7 @@ public object KotlinCompilerClient {
|
|||||||
?.let { File(it) }
|
?.let { File(it) }
|
||||||
?.ifOrNull { exists() }
|
?.ifOrNull { exists() }
|
||||||
?: newFlagFile()
|
?: newFlagFile()
|
||||||
return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart, checkId)
|
return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun connectToCompileService(compilerId: CompilerId,
|
public fun connectToCompileService(compilerId: CompilerId,
|
||||||
@@ -69,55 +69,31 @@ public object KotlinCompilerClient {
|
|||||||
daemonJVMOptions: DaemonJVMOptions,
|
daemonJVMOptions: DaemonJVMOptions,
|
||||||
daemonOptions: DaemonOptions,
|
daemonOptions: DaemonOptions,
|
||||||
reportingTargets: DaemonReportingTargets,
|
reportingTargets: DaemonReportingTargets,
|
||||||
autostart: Boolean = true,
|
autostart: Boolean = true
|
||||||
checkId: Boolean = true
|
|
||||||
): CompileService? {
|
): CompileService? {
|
||||||
|
|
||||||
var attempts = 0
|
var attempts = 0
|
||||||
var fileLock: FileBasedLock? = null
|
|
||||||
var shutdonwnPerformed = false
|
|
||||||
try {
|
try {
|
||||||
while (attempts++ < DAEMON_CONNECT_CYCLE_ATTEMPTS) {
|
while (attempts++ < DAEMON_CONNECT_CYCLE_ATTEMPTS) {
|
||||||
val service = tryFindDaemon(File(daemonOptions.runFilesPath), compilerId, reportingTargets)
|
val (service, newJVMOptions) = tryFindSuitableDaemonOrNewOpts(File(daemonOptions.runFilesPath), compilerId, daemonJVMOptions, { cat, msg -> reportingTargets.report(cat, msg) })
|
||||||
if (service != null) {
|
if (service != null) {
|
||||||
if (!checkId || service.checkCompilerId(compilerId)) {
|
// the newJVMOptions could be checked here for additional parameters, if needed
|
||||||
service.registerClient(clientAliveFlagFile.absolutePath)
|
service.registerClient(clientAliveFlagFile.absolutePath)
|
||||||
reportingTargets.report(DaemonReportCategory.DEBUG, "connected to the daemon")
|
reportingTargets.report(DaemonReportCategory.DEBUG, "connected to the daemon")
|
||||||
return service
|
return service
|
||||||
}
|
|
||||||
reportingTargets.report(DaemonReportCategory.DEBUG, "compiler identity don't match: " + compilerId.mappers.flatMap { it.toArgs("") }.joinToString(" "))
|
|
||||||
if (!autostart) return null
|
|
||||||
reportingTargets.report(DaemonReportCategory.DEBUG, "shutdown the daemon")
|
|
||||||
service.shutdown()
|
|
||||||
// TODO: find more reliable way
|
|
||||||
Thread.sleep(1000)
|
|
||||||
reportingTargets.report(DaemonReportCategory.DEBUG, "daemon shut down correctly")
|
|
||||||
shutdonwnPerformed = true
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!autostart) return null
|
if (!autostart) return null
|
||||||
reportingTargets.report(DaemonReportCategory.DEBUG, if (shutdonwnPerformed) "starting a new daemon" else "no suitable daemon found, starting a new one")
|
reportingTargets.report(DaemonReportCategory.DEBUG, "no suitable daemon found, starting a new one")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileLock == null || !fileLock.isLocked()) {
|
startDaemon(compilerId, newJVMOptions, daemonOptions, reportingTargets)
|
||||||
File(daemonOptions.runFilesPath).mkdirs()
|
reportingTargets.report(DaemonReportCategory.DEBUG, "daemon started, trying to find it")
|
||||||
fileLock = FileBasedLock(compilerId, daemonOptions)
|
|
||||||
// need to check the daemons again here, because of possible racing conditions
|
|
||||||
// note: the algorithm could be simpler if we'll acquire lock right from the beginning, but it may be costly
|
|
||||||
attempts--
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
startDaemon(compilerId, daemonJVMOptions, daemonOptions, reportingTargets)
|
|
||||||
reportingTargets.report(DaemonReportCategory.DEBUG, "daemon started, trying to find it")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e: Exception) {
|
catch (e: Exception) {
|
||||||
reportingTargets.report(DaemonReportCategory.EXCEPTION, e.toString())
|
reportingTargets.report(DaemonReportCategory.EXCEPTION, e.toString())
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
fileLock?.release()
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,60 +246,22 @@ public object KotlinCompilerClient {
|
|||||||
|
|
||||||
// --- Implementation ---------------------------------------
|
// --- Implementation ---------------------------------------
|
||||||
|
|
||||||
fun DaemonReportingTargets.report(category: DaemonReportCategory, message: String, source: String = "daemon client") {
|
private fun DaemonReportingTargets.report(category: DaemonReportCategory, message: String, source: String = "daemon client") {
|
||||||
if (category == DaemonReportCategory.DEBUG && !verboseReporting) return
|
if (category == DaemonReportCategory.DEBUG && !verboseReporting) return
|
||||||
out?.println("[$source] ${category.name()}: $message")
|
out?.println("[$source] ${category.name}: $message")
|
||||||
messages?.add(DaemonReportMessage(category, "[$source] $message"))
|
messages?.add(DaemonReportMessage(category, "[$source] $message"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun String.extractPortFromRunFilename(digest: String): Int =
|
private fun tryFindSuitableDaemonOrNewOpts(registryDir: File, compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, report: (DaemonReportCategory, String) -> Unit): Pair<CompileService?, DaemonJVMOptions> {
|
||||||
makeRunFilenameString(timestamp = "[0-9TZ:\\.\\+-]+", digest = digest, port = "(\\d+)", escapeSequence = "\\").toRegex()
|
val aliveWithOpts = walkDaemons(registryDir, compilerId, report)
|
||||||
.find(this)
|
.map { Pair(it, it.getDaemonJVMOptions()) }
|
||||||
?.groups?.get(1)
|
.filter { it.second.isGood }
|
||||||
?.value?.toInt()
|
// TODO: consider to sort the found daemons by memory settings and take the largest (rather that the first found), but carefully analyze possible situations first
|
||||||
?: 0
|
val opts = daemonJVMOptions.copy()
|
||||||
|
return aliveWithOpts.firstOrNull { daemonJVMOptions memorywiseFitsInto it.second.get() }
|
||||||
|
?.let { Pair(it.first, opts.updateMemoryUpperBounds(it.second.get())) }
|
||||||
private fun tryFindDaemon(registryDir: File, compilerId: CompilerId, reportingTargets: DaemonReportingTargets): CompileService? {
|
?: Pair(null, aliveWithOpts.fold(daemonJVMOptions, { opts, d -> opts.updateMemoryUpperBounds(d.second.get()) }))
|
||||||
val classPathDigest = compilerId.compilerClasspath.map { File(it).absolutePath }.distinctStringsDigest().toHexString()
|
|
||||||
val daemons = registryDir.walk()
|
|
||||||
.map { Pair(it, it.name.extractPortFromRunFilename(classPathDigest)) }
|
|
||||||
.filter { it.second != 0 }
|
|
||||||
.mapNotNull {
|
|
||||||
assert(it.second > 0 && it.second < 0xffff)
|
|
||||||
reportingTargets.report(DaemonReportCategory.DEBUG, "found suitable daemon on port ${it.second}, trying to connect")
|
|
||||||
val daemon = tryConnectToDaemon(it.second, reportingTargets)
|
|
||||||
// cleaning orphaned file; note: daemon should shut itself down if it detects that the run file is deleted
|
|
||||||
if (daemon == null && !it.first.delete()) {
|
|
||||||
reportingTargets.report(DaemonReportCategory.INFO, "WARNING: unable to delete seemingly orphaned file '${it.first.absolutePath}', cleanup recommended")
|
|
||||||
}
|
|
||||||
daemon
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
return when (daemons.size) {
|
|
||||||
0 -> null
|
|
||||||
1 -> daemons.first()
|
|
||||||
else -> throw IllegalStateException("Multiple daemons serving the same compiler, reset with the cleanup required")
|
|
||||||
// TODO: consider implementing automatic recovery instead, e.g. getting the youngest or least used daemon and shut down others
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun tryConnectToDaemon(port: Int, reportingTargets: DaemonReportingTargets): CompileService? {
|
|
||||||
try {
|
|
||||||
val daemon = LocateRegistry.getRegistry(LoopbackNetworkInterface.loopbackInetAddressName, port)
|
|
||||||
?.lookup(COMPILER_SERVICE_RMI_NAME)
|
|
||||||
if (daemon != null)
|
|
||||||
return daemon as? CompileService ?:
|
|
||||||
throw ClassCastException("Unable to cast compiler service, actual class received: ${daemon.javaClass}")
|
|
||||||
reportingTargets.report(DaemonReportCategory.EXCEPTION, "daemon not found")
|
|
||||||
}
|
|
||||||
catch (e: ConnectException) {
|
|
||||||
reportingTargets.report(DaemonReportCategory.EXCEPTION, "cannot connect to registry: " + (e.getCause()?.getMessage() ?: e.getMessage() ?: "unknown exception"))
|
|
||||||
// ignoring it - processing below
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -396,51 +334,9 @@ public object KotlinCompilerClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FileBasedLock(compilerId: CompilerId, daemonOptions: DaemonOptions) {
|
|
||||||
|
|
||||||
private val lockFile: File =
|
|
||||||
File(daemonOptions.runFilesPath,
|
|
||||||
makeRunFilenameString(timestamp = "lock",
|
|
||||||
digest = compilerId.compilerClasspath.map { File(it).absolutePath }.distinctStringsDigest().toHexString(),
|
|
||||||
port = "0"))
|
|
||||||
@Volatile private var locked = acquireLockFile(lockFile)
|
|
||||||
private val channel = if (locked) RandomAccessFile(lockFile, "rw").channel else null
|
|
||||||
private val lock = channel?.lock()
|
|
||||||
|
|
||||||
public fun isLocked(): Boolean = locked
|
|
||||||
|
|
||||||
@Synchronized public fun release(): Unit {
|
|
||||||
if (locked) {
|
|
||||||
lock?.release()
|
|
||||||
channel?.close()
|
|
||||||
lockFile.delete()
|
|
||||||
locked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized private fun acquireLockFile(lockFile: File): Boolean {
|
|
||||||
val maxAttempts = COMPILE_DAEMON_STARTUP_LOCK_TIMEOUT_MS / COMPILE_DAEMON_STARTUP_LOCK_TIMEOUT_CHECK_MS + 1
|
|
||||||
var attempt = 0L
|
|
||||||
while (true) {
|
|
||||||
if (lockFile.createNewFile()) break
|
|
||||||
// attempt to delete if file is orphaned
|
|
||||||
if (lockFile.delete() && lockFile.createNewFile()) break
|
|
||||||
if (lockFile.exists() && ++attempt >= maxAttempts)
|
|
||||||
throw IOException("Timeout waiting the release of the lock file '${lockFile.absolutePath}")
|
|
||||||
Thread.sleep(COMPILE_DAEMON_STARTUP_LOCK_TIMEOUT_CHECK_MS)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public enum class DaemonReportCategory {
|
|
||||||
DEBUG, INFO, EXCEPTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
public data class DaemonReportMessage(public val category: DaemonReportCategory, public val message: String)
|
public data class DaemonReportMessage(public val category: DaemonReportCategory, public val message: String)
|
||||||
|
|
||||||
public class DaemonReportingTargets(public val out: PrintStream? = null, public val messages: MutableCollection<DaemonReportMessage>? = null)
|
public class DaemonReportingTargets(public val out: PrintStream? = null, public val messages: MutableCollection<DaemonReportMessage>? = null)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2015 JetBrains s.r.o.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.rmi
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.rmi.ConnectException
|
||||||
|
import java.rmi.registry.LocateRegistry
|
||||||
|
|
||||||
|
|
||||||
|
internal val MAX_PORT_NUMBER = 0xffff
|
||||||
|
|
||||||
|
|
||||||
|
enum class DaemonReportCategory {
|
||||||
|
DEBUG, INFO, EXCEPTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun makeRunFilenameString(timestamp: String, digest: String, port: String, escapeSequence: String = ""): String = "$COMPILE_DAEMON_DEFAULT_FILES_PREFIX$escapeSequence.$timestamp$escapeSequence.$digest$escapeSequence.$port$escapeSequence.run"
|
||||||
|
|
||||||
|
|
||||||
|
fun String.extractPortFromRunFilename(digest: String): Int =
|
||||||
|
makeRunFilenameString(timestamp = "[0-9TZ:\\.\\+-]+", digest = digest, port = "(\\d+)", escapeSequence = "\\").toRegex()
|
||||||
|
.find(this)
|
||||||
|
?.groups?.get(1)
|
||||||
|
?.value?.toInt()
|
||||||
|
?: 0
|
||||||
|
|
||||||
|
|
||||||
|
fun walkDaemons(registryDir: File, compilerId: CompilerId, report: (DaemonReportCategory, String) -> Unit): Sequence<CompileService> {
|
||||||
|
val classPathDigest = compilerId.compilerClasspath.map { File(it).absolutePath }.distinctStringsDigest().toHexString()
|
||||||
|
return registryDir.walk()
|
||||||
|
.map { Pair(it, it.name.extractPortFromRunFilename(classPathDigest)) }
|
||||||
|
.filter { it.second != 0 }
|
||||||
|
.map {
|
||||||
|
assert(it.second > 0 && it.second < MAX_PORT_NUMBER)
|
||||||
|
report(DaemonReportCategory.DEBUG, "found daemon on port ${it.second}, trying to connect")
|
||||||
|
val daemon = tryConnectToDaemon(it.second, report)
|
||||||
|
// cleaning orphaned file; note: daemon should shut itself down if it detects that the run file is deleted
|
||||||
|
if (daemon == null && !it.first.delete()) {
|
||||||
|
report(DaemonReportCategory.INFO, "WARNING: unable to delete seemingly orphaned file '${it.first.absolutePath}', cleanup recommended")
|
||||||
|
}
|
||||||
|
daemon
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inline fun tryConnectToDaemon(port: Int, report: (DaemonReportCategory, String) -> Unit): CompileService? {
|
||||||
|
try {
|
||||||
|
val daemon = LocateRegistry.getRegistry(LoopbackNetworkInterface.loopbackInetAddressName, port)
|
||||||
|
?.lookup(COMPILER_SERVICE_RMI_NAME)
|
||||||
|
if (daemon != null)
|
||||||
|
return daemon as? CompileService ?:
|
||||||
|
throw ClassCastException("Unable to cast compiler service, actual class received: ${daemon.javaClass}")
|
||||||
|
report(DaemonReportCategory.EXCEPTION, "daemon not found")
|
||||||
|
}
|
||||||
|
catch (e: ConnectException) {
|
||||||
|
report(DaemonReportCategory.EXCEPTION, "cannot connect to registry: " + (e.cause?.message ?: e.message ?: "unknown exception"))
|
||||||
|
// ignoring it - processing below
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -36,11 +36,14 @@ public interface CompileService : Remote {
|
|||||||
public val NO_SESSION: Int = 0
|
public val NO_SESSION: Int = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class CallResult<R> : Serializable {
|
public sealed class CallResult<out R> : Serializable {
|
||||||
class Good<R>(val result: R) : CallResult<R>()
|
class Good<R>(val result: R) : CallResult<R>()
|
||||||
class Ok : CallResult<Nothing>()
|
class Ok : CallResult<Nothing>()
|
||||||
class Dying : CallResult<Nothing>()
|
class Dying : CallResult<Nothing>()
|
||||||
class Error(val message: String) : CallResult<Nothing>()
|
class Error(val message: String) : CallResult<Nothing>()
|
||||||
|
|
||||||
|
val isGood: Boolean get() = this is Good<*>
|
||||||
|
|
||||||
fun get(): R = when (this) {
|
fun get(): R = when (this) {
|
||||||
is Good<R> -> this.result
|
is Good<R> -> this.result
|
||||||
is Dying -> throw IllegalStateException("Service is dying")
|
is Dying -> throw IllegalStateException("Service is dying")
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import java.io.Serializable
|
|||||||
import java.lang.management.ManagementFactory
|
import java.lang.management.ManagementFactory
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import kotlin.reflect.KMutableProperty1
|
import kotlin.reflect.KMutableProperty1
|
||||||
|
import kotlin.text.RegexOption
|
||||||
|
|
||||||
|
|
||||||
public val COMPILER_JAR_NAME: String = "kotlin-compiler.jar"
|
public val COMPILER_JAR_NAME: String = "kotlin-compiler.jar"
|
||||||
@@ -55,8 +56,6 @@ public val COMPILE_DAEMON_DEFAULT_RUN_DIR_PATH: String get() =
|
|||||||
|
|
||||||
val CLASSPATH_ID_DIGEST = "MD5"
|
val CLASSPATH_ID_DIGEST = "MD5"
|
||||||
|
|
||||||
public fun makeRunFilenameString(timestamp: String, digest: String, port: String, escapeSequence: String = ""): String = "$COMPILE_DAEMON_DEFAULT_FILES_PREFIX$escapeSequence.$timestamp$escapeSequence.$digest$escapeSequence.$port$escapeSequence.run"
|
|
||||||
|
|
||||||
|
|
||||||
open class PropMapper<C, V, P : KMutableProperty1<C, V>>(val dest: C,
|
open class PropMapper<C, V, P : KMutableProperty1<C, V>>(val dest: C,
|
||||||
val prop: P,
|
val prop: P,
|
||||||
@@ -307,3 +306,35 @@ public fun configureDaemonOptions(opts: DaemonOptions): DaemonOptions {
|
|||||||
|
|
||||||
|
|
||||||
public fun configureDaemonOptions(): DaemonOptions = configureDaemonOptions(DaemonOptions())
|
public fun configureDaemonOptions(): DaemonOptions = configureDaemonOptions(DaemonOptions())
|
||||||
|
|
||||||
|
|
||||||
|
fun String.memToBytes(): Long? =
|
||||||
|
"(\\d+)([kmg]?)".toRegex()
|
||||||
|
.matchEntire(this.trim().toLowerCase())
|
||||||
|
?.groups?.let { match ->
|
||||||
|
match.get(1)?.value?.let {
|
||||||
|
it.toLong() *
|
||||||
|
when (match.get(2)?.value) {
|
||||||
|
"k" -> 1 shl 10
|
||||||
|
"m" -> 1 shl 20
|
||||||
|
"g" -> 1 shl 30
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun daemonJVMOptionsMemoryProps() =
|
||||||
|
listOf(DaemonJVMOptions::maxMemory, DaemonJVMOptions::maxPermSize, DaemonJVMOptions::reservedCodeCacheSize)
|
||||||
|
|
||||||
|
|
||||||
|
infix fun DaemonJVMOptions.memorywiseFitsInto(other: DaemonJVMOptions): Boolean =
|
||||||
|
daemonJVMOptionsMemoryProps()
|
||||||
|
.all { (it.get(this).memToBytes() ?: 0) <= (it.get(other).memToBytes() ?: 0) }
|
||||||
|
|
||||||
|
|
||||||
|
fun DaemonJVMOptions.updateMemoryUpperBounds(other: DaemonJVMOptions): DaemonJVMOptions {
|
||||||
|
daemonJVMOptionsMemoryProps()
|
||||||
|
.forEach { if ((it.get(this).memToBytes() ?: 0) < (it.get(other).memToBytes() ?: 0)) it.set(this, it.get(other)) }
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|||||||
+45
-16
@@ -30,6 +30,7 @@ import java.rmi.registry.Registry
|
|||||||
import java.rmi.server.UnicastRemoteObject
|
import java.rmi.server.UnicastRemoteObject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
@@ -81,12 +82,14 @@ class CompileServiceImpl(
|
|||||||
private val clientProxies: MutableSet<ClientOrSessionProxy> = hashSetOf()
|
private val clientProxies: MutableSet<ClientOrSessionProxy> = hashSetOf()
|
||||||
private val sessions: MutableMap<Int, ClientOrSessionProxy> = hashMapOf()
|
private val sessions: MutableMap<Int, ClientOrSessionProxy> = hashMapOf()
|
||||||
|
|
||||||
private val sessionsIdCounter: AtomicInteger = AtomicInteger(0)
|
private val sessionsIdCounter = AtomicInteger(0)
|
||||||
private val internalRng = Random()
|
private val internalRng = Random()
|
||||||
|
|
||||||
private val classpathWatcher = LazyClasspathWatcher(compilerId.compilerClasspath)
|
private val classpathWatcher = LazyClasspathWatcher(compilerId.compilerClasspath)
|
||||||
|
|
||||||
private val compilationsCounter: AtomicInteger = AtomicInteger(0)
|
private val compilationsCounter = AtomicInteger(0)
|
||||||
|
private val shutdownQueued = AtomicBoolean(false)
|
||||||
|
|
||||||
@Volatile private var _lastUsedSeconds = nowSeconds()
|
@Volatile private var _lastUsedSeconds = nowSeconds()
|
||||||
public val lastUsedSeconds: Long get() = if (rwlock.isWriteLocked || rwlock.readLockCount - rwlock.readHoldCount > 0) nowSeconds() else _lastUsedSeconds
|
public val lastUsedSeconds: Long get() = if (rwlock.isWriteLocked || rwlock.readLockCount - rwlock.readHoldCount > 0) nowSeconds() else _lastUsedSeconds
|
||||||
|
|
||||||
@@ -138,7 +141,7 @@ class CompileServiceImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// assuming wrap, jumping to random number to reduce probability of further ckashes
|
// assuming wrap, jumping to random number to reduce probability of further clashes
|
||||||
newId = sessionsIdCounter.addAndGet(internalRng.nextInt())
|
newId = sessionsIdCounter.addAndGet(internalRng.nextInt())
|
||||||
}
|
}
|
||||||
throw Exception("Invalid state or algorithm error")
|
throw Exception("Invalid state or algorithm error")
|
||||||
@@ -163,7 +166,7 @@ class CompileServiceImpl(
|
|||||||
override fun getUsedMemory(): CompileService.CallResult<Long> = ifAlive { usedMemory(withGC = true) }
|
override fun getUsedMemory(): CompileService.CallResult<Long> = ifAlive { usedMemory(withGC = true) }
|
||||||
|
|
||||||
override fun shutdown() {
|
override fun shutdown() {
|
||||||
ifAliveExclusive {
|
ifAliveExclusive(ignoreCompilerChanged = true) {
|
||||||
log.info("Shutdown started")
|
log.info("Shutdown started")
|
||||||
alive = false
|
alive = false
|
||||||
UnicastRemoteObject.unexportObject(this, true)
|
UnicastRemoteObject.unexportObject(this, true)
|
||||||
@@ -261,7 +264,8 @@ class CompileServiceImpl(
|
|||||||
|
|
||||||
// 3. clean dead clients, then check if any left - conditional shutdown (with small delay)
|
// 3. clean dead clients, then check if any left - conditional shutdown (with small delay)
|
||||||
synchronized(clientProxies) { clientProxies.removeAll( clientProxies.filter{ !it.isAlive }) }
|
synchronized(clientProxies) { clientProxies.removeAll( clientProxies.filter{ !it.isAlive }) }
|
||||||
if (clientProxies.none()) {
|
if (clientProxies.none() && compilationsCounter.get() > 0 && !shutdownQueued.get()) {
|
||||||
|
log.info("No more clients left, delayed shutdown in ${daemonOptions.shutdownDelayMilliseconds}ms")
|
||||||
shutdownWithDelay()
|
shutdownWithDelay()
|
||||||
}
|
}
|
||||||
// 4. check idle timeout - shutdown
|
// 4. check idle timeout - shutdown
|
||||||
@@ -279,11 +283,17 @@ class CompileServiceImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun shutdownWithDelay() {
|
private fun shutdownWithDelay() {
|
||||||
|
shutdownQueued.set(true)
|
||||||
val currentCompilationsCount = compilationsCounter.get()
|
val currentCompilationsCount = compilationsCounter.get()
|
||||||
timer.schedule(daemonOptions.shutdownDelayMilliseconds) {
|
timer.schedule(daemonOptions.shutdownDelayMilliseconds) {
|
||||||
|
shutdownQueued.set(false)
|
||||||
if (currentCompilationsCount == compilationsCounter.get()) {
|
if (currentCompilationsCount == compilationsCounter.get()) {
|
||||||
|
log.fine("Execute delayed shutdown")
|
||||||
shutdown()
|
shutdown()
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
log.info("Cancel delayed shutdown due to new client")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +346,7 @@ class CompileServiceImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun<R> checkedCompile(args: Array<out String>, serviceOut: PrintStream, rpcProfiler: Profiler, body: () -> R): R {
|
private fun<R> checkedCompile(args: Array<out String>, serviceOut: PrintStream, rpcProfiler: Profiler, body: () -> R): R {
|
||||||
try {
|
try {
|
||||||
if (args.none())
|
if (args.none())
|
||||||
throw IllegalArgumentException("Error: empty arguments list.")
|
throw IllegalArgumentException("Error: empty arguments list.")
|
||||||
@@ -378,20 +388,39 @@ class CompileServiceImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun<R> ifAlive(body: () -> R): CompileService.CallResult<R> = rwlock.read {
|
private fun<R> ifAlive(ignoreCompilerChanged: Boolean = false, body: () -> R): CompileService.CallResult<R> = rwlock.read {
|
||||||
if (!alive) CompileService.CallResult.Dying()
|
ifAliveChecksImpl(ignoreCompilerChanged) { CompileService.CallResult.Good(body()) }
|
||||||
CompileService.CallResult.Good( body() )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: find how to implement it without using unique name for this variant; making name deliberately ugly meanwhile
|
// TODO: find how to implement it without using unique name for this variant; making name deliberately ugly meanwhile
|
||||||
fun ifAlive_Nothing(body: () -> Unit): CompileService.CallResult<Nothing> = rwlock.read {
|
private fun ifAlive_Nothing(ignoreCompilerChanged: Boolean = false, body: () -> Unit): CompileService.CallResult<Nothing> = rwlock.read {
|
||||||
if (!alive) CompileService.CallResult.Dying()
|
ifAliveChecksImpl(ignoreCompilerChanged) {
|
||||||
body()
|
body()
|
||||||
CompileService.CallResult.Ok()
|
CompileService.CallResult.Ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun<R> ifAliveExclusive(body: () -> R): CompileService.CallResult<R> = rwlock.write {
|
private fun<R> ifAliveExclusive(ignoreCompilerChanged: Boolean = false, body: () -> R): CompileService.CallResult<R> = rwlock.write {
|
||||||
if (!alive) CompileService.CallResult.Dying()
|
ifAliveChecksImpl(ignoreCompilerChanged) { CompileService.CallResult.Good(body()) }
|
||||||
CompileService.CallResult.Good( body() )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline private fun<R> ifAliveChecksImpl(ignoreCompilerChanged: Boolean = false, body: () -> CompileService.CallResult<R>): CompileService.CallResult<R> =
|
||||||
|
when {
|
||||||
|
!alive -> CompileService.CallResult.Dying()
|
||||||
|
!ignoreCompilerChanged && classpathWatcher.isChanged -> {
|
||||||
|
log.info("Compiler changed, scheduling shutdown")
|
||||||
|
timer.schedule(1) { shutdown() }
|
||||||
|
CompileService.CallResult.Dying()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
try {
|
||||||
|
body()
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
log.log(Level.SEVERE, "Exception", e)
|
||||||
|
CompileService.CallResult.Error(e.message ?: "unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() {
|
|||||||
val compilerId by lazy(LazyThreadSafetyMode.NONE) { CompilerId.makeCompilerId(compilerClassPath) }
|
val compilerId by lazy(LazyThreadSafetyMode.NONE) { CompilerId.makeCompilerId(compilerClassPath) }
|
||||||
|
|
||||||
private fun compileOnDaemon(clientAliveFile: File, compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): CompilerResults {
|
private fun compileOnDaemon(clientAliveFile: File, compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, daemonOptions: DaemonOptions, vararg args: String): CompilerResults {
|
||||||
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, clientAliveFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true, checkId = true)
|
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, clientAliveFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true)
|
||||||
TestCase.assertNotNull("failed to connect daemon", daemon)
|
TestCase.assertNotNull("failed to connect daemon", daemon)
|
||||||
daemon?.registerClient(clientAliveFile.absolutePath)
|
daemon?.registerClient(clientAliveFile.absolutePath)
|
||||||
val strm = ByteArrayOutputStream()
|
val strm = ByteArrayOutputStream()
|
||||||
@@ -264,7 +264,7 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() {
|
|||||||
try {
|
try {
|
||||||
val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath)
|
val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath)
|
||||||
val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = false, inheritAdditionalProperties = false)
|
val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = false, inheritAdditionalProperties = false)
|
||||||
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true, checkId = true)
|
val daemon = KotlinCompilerClient.connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, DaemonReportingTargets(out = System.err), autostart = true)
|
||||||
TestCase.assertNotNull("failed to connect daemon", daemon)
|
TestCase.assertNotNull("failed to connect daemon", daemon)
|
||||||
|
|
||||||
val (registry, port) = findPortAndCreateRegistry(10, 16384, 65535)
|
val (registry, port) = findPortAndCreateRegistry(10, 16384, 65535)
|
||||||
|
|||||||
Reference in New Issue
Block a user