PY-18029 Fix thread leakage in PythonConsoleTest

This commit is contained in:
Alexander Koshevoy
2018-08-22 23:16:40 +03:00
parent f557409430
commit efd4a11b12
3 changed files with 71 additions and 56 deletions
@@ -56,7 +56,7 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
/**
* Thrift RPC client for sending messages to the server.
*/
private PythonConsoleBackendService.Iface myClient;
private PythonConsoleBackendServiceDisposable myClient;
/**
* This is the server responsible for giving input to a raw_input() requested.
@@ -208,17 +208,18 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
}
/**
* Returns thread safe {@link PythonConsoleBackendService.Iface}: requests to
* the returned {@link PythonConsoleBackendService.Iface} will be processed
* sequentially. Also if Python Console process is detected to be finished
* the current request will be interrupted with
* {@link PyConsoleProcessFinishedException}.
* Returns thread safe, Python Console process-aware and disposable
* {@link PythonConsoleBackendService.Iface}. Requests to the returned
* {@link PythonConsoleBackendService.Iface} will be processed sequentially.
* If Python Console process is detected to be finished the current request
* will be interrupted and {@link PyConsoleProcessFinishedException} is
* thrown.
*
* @return thread safe and related Python Console process-aware
* {@link PythonConsoleBackendService.Iface}
*/
@NotNull
private PythonConsoleBackendService.Iface getPythonConsoleBackendClient() {
private PythonConsoleBackendServiceDisposable getPythonConsoleBackendClient() {
if (myClient == null) {
myClient = PythonConsoleClientUtil.synchronizedPythonConsoleClient(PydevConsoleCommunication.class.getClassLoader(),
getInitialPythonConsoleBackendClient(), getPythonConsoleProcess());
@@ -250,6 +251,9 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
catch (Exception e) {
//Ok, we can ignore this one on close.
}
finally {
getPythonConsoleBackendClient().dispose();
}
}
}.queue();
}
@@ -278,6 +282,9 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
catch (Exception e) {
//Ok, we can ignore this one on close.
}
finally {
getPythonConsoleBackendClient().dispose();
}
indicator.setText2("Waiting for Python Console process to finish...");
try {
do {
@@ -319,8 +326,9 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
}
finally {
if (myServer != null) {
myServer.stop();
Future<Void> stopFuture = myServer.stop();
myServer = null;
stopFuture.get();
}
}
return null;
@@ -0,0 +1,10 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.console
/**
* Not really [com.intellij.openapi.Disposable] as [dispose] method is expected
* to be called directly.
*/
interface PythonConsoleBackendServiceDisposable : PythonConsoleBackendService.Iface {
fun dispose()
}
@@ -8,60 +8,57 @@ import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.util.concurrent.*
import java.util.concurrent.Callable
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
private const val PYTHON_CONSOLE_COMMAND_THREAD_FACTORY_NAME: String = "Python Console Command Executor"
@JvmOverloads
fun synchronizedPythonConsoleClient(loader: ClassLoader,
delegate: PythonConsoleBackendService.Iface,
pythonConsoleProcess: Process? = null): PythonConsoleBackendService.Iface {
val executorService = newSingleThreadPythonConsoleCommandExecutor()
return Proxy.newProxyInstance(loader, arrayOf<Class<*>>(PythonConsoleBackendService.Iface::class.java),
InvocationHandler { _, method, args ->
// we evaluate the original method in the other thread in order to control it
val future = executorService.submit(Callable {
return@Callable invokeOriginalMethod(args, method, delegate)
})
pythonConsoleProcess: Process): PythonConsoleBackendServiceDisposable {
val executorService = ConcurrencyUtil.newSingleThreadExecutor(PYTHON_CONSOLE_COMMAND_THREAD_FACTORY_NAME)
// make the `PythonConsoleBackendService.Iface` process-aware and thread-safe
val proxy = Proxy.newProxyInstance(loader, arrayOf<Class<*>>(PythonConsoleBackendService.Iface::class.java),
InvocationHandler { _, method, args ->
// we evaluate the original method in the other thread in order to control it
val future = executorService.submit(Callable {
return@Callable invokeOriginalMethod(args, method, delegate)
})
if (pythonConsoleProcess == null) {
try {
return@InvocationHandler future.get()
}
catch (e: ExecutionException) {
throw e.cause ?: e
}
}
while (true) {
try {
return@InvocationHandler future.get(10L, TimeUnit.MILLISECONDS)
}
catch (e: TimeoutException) {
if (!pythonConsoleProcess.isAlive) {
val exitValue = pythonConsoleProcess.exitValue()
throw PyConsoleProcessFinishedException(exitValue)
}
// continue waiting for the end of the operation execution
}
catch (e: ExecutionException) {
if (!pythonConsoleProcess.isAlive) {
val exitValue = pythonConsoleProcess.exitValue()
throw PyConsoleProcessFinishedException(exitValue)
}
throw e.cause ?: e
}
}
}) as PythonConsoleBackendService.Iface
}
/**
* Creates new single thread executor with [ThreadPoolExecutor.keepAliveTime]
* equals to 60 sec.
*/
private fun newSingleThreadPythonConsoleCommandExecutor(): ExecutorService {
val threadFactory = ConcurrencyUtil.newNamedThreadFactory(PYTHON_CONSOLE_COMMAND_THREAD_FACTORY_NAME)
return ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, LinkedBlockingQueue<Runnable>(), threadFactory)
while (true) {
try {
return@InvocationHandler future.get(10L, TimeUnit.MILLISECONDS)
}
catch (e: TimeoutException) {
if (!pythonConsoleProcess.isAlive) {
val exitValue = pythonConsoleProcess.exitValue()
throw PyConsoleProcessFinishedException(exitValue)
}
// continue waiting for the end of the operation execution
}
catch (e: ExecutionException) {
if (!pythonConsoleProcess.isAlive) {
val exitValue = pythonConsoleProcess.exitValue()
throw PyConsoleProcessFinishedException(exitValue)
}
throw e.cause ?: e
}
}
}) as PythonConsoleBackendService.Iface
// make the `proxy` disposable
return object : PythonConsoleBackendServiceDisposable, PythonConsoleBackendService.Iface by proxy {
override fun dispose() {
executorService.shutdownNow()
try {
while (!executorService.awaitTermination(1L, TimeUnit.SECONDS)) Unit
}
catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
}
}
}
private fun invokeOriginalMethod(args: Array<out Any>?, method: Method, delegate: Any): Any? {