Implementing new logic of choosing the daemon, startup and shutdown, some related refactorings and fixes

This commit is contained in:
Ilya Chernikov
2015-11-11 18:30:04 +01:00
parent 375679677c
commit 2612ce56b4
6 changed files with 184 additions and 149 deletions
@@ -20,9 +20,9 @@ import net.rubygrapefruit.platform.ProcessLauncher
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
import org.jetbrains.kotlin.progress.CompilationCanceledStatus
import org.jetbrains.kotlin.rmi.*
import java.io.*
import java.rmi.ConnectException
import java.rmi.registry.LocateRegistry
import java.io.File
import java.io.OutputStream
import java.io.PrintStream
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
@@ -61,7 +61,7 @@ public object KotlinCompilerClient {
?.let { File(it) }
?.ifOrNull { exists() }
?: newFlagFile()
return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart, checkId)
return connectToCompileService(compilerId, flagFile, daemonJVMOptions, daemonOptions, reportingTargets, autostart)
}
public fun connectToCompileService(compilerId: CompilerId,
@@ -69,55 +69,31 @@ public object KotlinCompilerClient {
daemonJVMOptions: DaemonJVMOptions,
daemonOptions: DaemonOptions,
reportingTargets: DaemonReportingTargets,
autostart: Boolean = true,
checkId: Boolean = true
autostart: Boolean = true
): CompileService? {
var attempts = 0
var fileLock: FileBasedLock? = null
var shutdonwnPerformed = false
try {
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 (!checkId || service.checkCompilerId(compilerId)) {
service.registerClient(clientAliveFlagFile.absolutePath)
reportingTargets.report(DaemonReportCategory.DEBUG, "connected to the daemon")
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
// the newJVMOptions could be checked here for additional parameters, if needed
service.registerClient(clientAliveFlagFile.absolutePath)
reportingTargets.report(DaemonReportCategory.DEBUG, "connected to the daemon")
return service
}
else {
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()) {
File(daemonOptions.runFilesPath).mkdirs()
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")
}
startDaemon(compilerId, newJVMOptions, daemonOptions, reportingTargets)
reportingTargets.report(DaemonReportCategory.DEBUG, "daemon started, trying to find it")
}
}
catch (e: Exception) {
reportingTargets.report(DaemonReportCategory.EXCEPTION, e.toString())
}
finally {
fileLock?.release()
}
return null
}
@@ -270,60 +246,22 @@ public object KotlinCompilerClient {
// --- 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
out?.println("[$source] ${category.name()}: $message")
out?.println("[$source] ${category.name}: $message")
messages?.add(DaemonReportMessage(category, "[$source] $message"))
}
private fun String.extractPortFromRunFilename(digest: String): Int =
makeRunFilenameString(timestamp = "[0-9TZ:\\.\\+-]+", digest = digest, port = "(\\d+)", escapeSequence = "\\").toRegex()
.find(this)
?.groups?.get(1)
?.value?.toInt()
?: 0
private fun tryFindDaemon(registryDir: File, compilerId: CompilerId, reportingTargets: DaemonReportingTargets): CompileService? {
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
private fun tryFindSuitableDaemonOrNewOpts(registryDir: File, compilerId: CompilerId, daemonJVMOptions: DaemonJVMOptions, report: (DaemonReportCategory, String) -> Unit): Pair<CompileService?, DaemonJVMOptions> {
val aliveWithOpts = walkDaemons(registryDir, compilerId, report)
.map { Pair(it, it.getDaemonJVMOptions()) }
.filter { it.second.isGood }
// 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
val opts = daemonJVMOptions.copy()
return aliveWithOpts.firstOrNull { daemonJVMOptions memorywiseFitsInto it.second.get() }
?.let { Pair(it.first, opts.updateMemoryUpperBounds(it.second.get())) }
?: Pair(null, aliveWithOpts.fold(daemonJVMOptions, { opts, d -> opts.updateMemoryUpperBounds(d.second.get()) }))
}
@@ -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 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 sealed class CallResult<R> : Serializable {
public sealed class CallResult<out R> : Serializable {
class Good<R>(val result: R) : CallResult<R>()
class Ok : CallResult<Nothing>()
class Dying : CallResult<Nothing>()
class Error(val message: String) : CallResult<Nothing>()
val isGood: Boolean get() = this is Good<*>
fun get(): R = when (this) {
is Good<R> -> this.result
is Dying -> throw IllegalStateException("Service is dying")
@@ -22,6 +22,7 @@ import java.io.Serializable
import java.lang.management.ManagementFactory
import java.security.MessageDigest
import kotlin.reflect.KMutableProperty1
import kotlin.text.RegexOption
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"
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,
val prop: P,
@@ -307,3 +306,35 @@ public fun configureDaemonOptions(opts: DaemonOptions): 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
}
@@ -30,6 +30,7 @@ import java.rmi.registry.Registry
import java.rmi.server.UnicastRemoteObject
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.logging.Level
@@ -81,12 +82,14 @@ class CompileServiceImpl(
private val clientProxies: MutableSet<ClientOrSessionProxy> = hashSetOf()
private val sessions: MutableMap<Int, ClientOrSessionProxy> = hashMapOf()
private val sessionsIdCounter: AtomicInteger = AtomicInteger(0)
private val sessionsIdCounter = AtomicInteger(0)
private val internalRng = Random()
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()
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())
}
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 shutdown() {
ifAliveExclusive {
ifAliveExclusive(ignoreCompilerChanged = true) {
log.info("Shutdown started")
alive = false
UnicastRemoteObject.unexportObject(this, true)
@@ -261,7 +264,8 @@ class CompileServiceImpl(
// 3. clean dead clients, then check if any left - conditional shutdown (with small delay)
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()
}
// 4. check idle timeout - shutdown
@@ -279,11 +283,17 @@ class CompileServiceImpl(
}
private fun shutdownWithDelay() {
shutdownQueued.set(true)
val currentCompilationsCount = compilationsCounter.get()
timer.schedule(daemonOptions.shutdownDelayMilliseconds) {
shutdownQueued.set(false)
if (currentCompilationsCount == compilationsCounter.get()) {
log.fine("Execute delayed 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 {
if (args.none())
throw IllegalArgumentException("Error: empty arguments list.")
@@ -378,20 +388,39 @@ class CompileServiceImpl(
}
}
fun<R> ifAlive(body: () -> R): CompileService.CallResult<R> = rwlock.read {
if (!alive) CompileService.CallResult.Dying()
CompileService.CallResult.Good( body() )
private fun<R> ifAlive(ignoreCompilerChanged: Boolean = false, body: () -> R): CompileService.CallResult<R> = rwlock.read {
ifAliveChecksImpl(ignoreCompilerChanged) { CompileService.CallResult.Good(body()) }
}
// 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 {
if (!alive) CompileService.CallResult.Dying()
body()
CompileService.CallResult.Ok()
private fun ifAlive_Nothing(ignoreCompilerChanged: Boolean = false, body: () -> Unit): CompileService.CallResult<Nothing> = rwlock.read {
ifAliveChecksImpl(ignoreCompilerChanged) {
body()
CompileService.CallResult.Ok()
}
}
fun<R> ifAliveExclusive(body: () -> R): CompileService.CallResult<R> = rwlock.write {
if (!alive) CompileService.CallResult.Dying()
CompileService.CallResult.Good( body() )
private fun<R> ifAliveExclusive(ignoreCompilerChanged: Boolean = false, body: () -> R): CompileService.CallResult<R> = rwlock.write {
ifAliveChecksImpl(ignoreCompilerChanged) { 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) }
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)
daemon?.registerClient(clientAliveFile.absolutePath)
val strm = ByteArrayOutputStream()
@@ -264,7 +264,7 @@ public class CompilerDaemonTest : KotlinIntegrationTestBase() {
try {
val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath)
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)
val (registry, port) = findPortAndCreateRegistry(10, 16384, 65535)