Skip to content

Commit

Permalink
Replaced actor pulling by pushing
Browse files Browse the repository at this point in the history
The states are stored in the class, the actor uses methods to request state changes.

Added regression tests.

Fix #1001308
  • Loading branch information
Luc Bourlier committed Nov 6, 2012
1 parent 1410361 commit 1b4ead3
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.sun.jdi.event.EventQueue
import scala.tools.eclipse.debug.BaseDebuggerActor
import org.junit.After
import scala.tools.eclipse.debug.PoisonPill
import com.sun.jdi.event.VMDeathEvent

class ScalaDebugTargetTest {

Expand Down Expand Up @@ -62,6 +63,20 @@ class ScalaDebugTargetTest {
assertEquals("Wrong thread name", ThreadName, threads2(0).getName)
}

/**
* Check that calling #getThreads doesn't create a freeze. It used to be making a sync call to the actor, even if it was shutdown.
* #1001308
*/
@Test(timeout = 2000)
def getThreadsFreeze() {

val debugTarget= createDebugTarget

debugTarget.eventActor ! mock(classOf[VMDeathEvent])
debugTarget.getThreads

}

/**
* Create a debug target with most of the JDI implementation mocked
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.junit.Before
import org.eclipse.debug.core.DebugPlugin
import com.sun.jdi.ObjectCollectedException
import com.sun.jdi.ThreadGroupReference
import org.junit.Ignore
import scala.tools.eclipse.debug.BaseDebuggerActor
import org.junit.After
import scala.tools.eclipse.debug.PoisonPill
Expand All @@ -19,6 +20,17 @@ object ScalaThreadTest {
when(jdiThreadGroup.name).thenReturn("some")
jdiThreadGroup;
}

final private val WaitingStep = 50

private def waitUntil(condition: => Boolean, timeout: Int) {
val timeoutEnd = System.currentTimeMillis() + timeout
while (!condition) {
assertTrue("Timed out before condition was satisfied", System.currentTimeMillis() < timeoutEnd)
Thread.sleep(WaitingStep)
}
}

}

/**
Expand Down Expand Up @@ -100,7 +112,10 @@ class ScalaThreadTest {
/**
* Check that the underlying thread is resume only once when the resume() method is called.
* See #1001199
* FIXME: With the changes for #1001308, this test is not working correctly any more. It was relying on the fact that #getStackFrames was generating a sync call.
* See #1001321
*/
@Ignore
@Test
def threadResumedOnlyOnce_1001199() {
val jdiThread = mock(classOf[ThreadReference])
Expand All @@ -115,4 +130,19 @@ class ScalaThreadTest {
verify(jdiThread, times(1)).resume()
}

}
/**
* Check that calling #getStackFrame doesn't create a freeze. It used to be making a sync call to the actor, even if it was shutdown.
* #1001308
*/
@Test(timeout = 2000)
def getStackFramesFreeze() {

val jdiThread = mock(classOf[ThreadReference])

val thread = createThread(jdiThread)

thread.eventActor ! ScalaThreadActor.TerminatedFromScala
thread.getStackFrames
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import com.sun.jdi.request.ThreadStartRequest
import scala.actors.Actor
import scala.tools.eclipse.debug.PoisonPill


object ScalaDebugTarget extends HasLogger {

def apply(virtualMachine: VirtualMachine, launch: ILaunch, process: IProcess): ScalaDebugTarget = {
Expand All @@ -50,7 +49,7 @@ object ScalaDebugTarget extends HasLogger {
}

debugTarget.startJdiEventDispatcher()

debugTarget.fireCreationEvent()

debugTarget
Expand Down Expand Up @@ -78,8 +77,8 @@ abstract class ScalaDebugTarget private (val virtualMachine: VirtualMachine, lau

override def getName: String = "Scala Debug Target" // TODO: need better name
override def getProcess: org.eclipse.debug.core.model.IProcess = process
override def getThreads: Array[org.eclipse.debug.core.model.IThread] = internalGetThreads.toArray
override def hasThreads: Boolean = !internalGetThreads.isEmpty
override def getThreads: Array[org.eclipse.debug.core.model.IThread] = threads.toArray
override def hasThreads: Boolean = !threads.isEmpty
override def supportsBreakpoint(breakpoint: IBreakpoint): Boolean = ???

// Members declared in org.eclipse.debug.core.model.IDisconnect
Expand Down Expand Up @@ -112,7 +111,7 @@ abstract class ScalaDebugTarget private (val virtualMachine: VirtualMachine, lau
vmDisconnected()
eventActor ! PoisonPill
}

// Members declared in scala.tools.eclipse.debug.model.ScalaDebugElement

override def getDebugTarget: ScalaDebugTarget = this
Expand All @@ -121,6 +120,8 @@ abstract class ScalaDebugTarget private (val virtualMachine: VirtualMachine, lau

@volatile
private var running: Boolean = true
@volatile
private var threads = List[ScalaThread]()

protected[debug] val eventDispatcher: ScalaJdiEventDispatcher

Expand All @@ -141,52 +142,79 @@ abstract class ScalaDebugTarget private (val virtualMachine: VirtualMachine, lau
})
}

/**
* Callback from the breakpoint manager when a platform breakpoint is hit
*/
private[debug] def threadSuspended(thread: ThreadReference, eventDetail: Int) {
eventActor !? ScalaDebugTargetActor.ThreadSuspended(thread, eventDetail)
}

/*
* Methods used by the companion actor to update this object internal states
* FOR THE COMPANION ACTOR ONLY.
*/

/**
* Callback form the actor when the connection with the vm is enabled
*/
private[model] def vmStarted() {
breakpointManager.init()
fireChangeEvent(DebugEvent.CONTENT)
}

/**
* Callback from the actor when the connection with the vm as been lost
*/
def vmDisconnected() {
private[model] def vmDisconnected() {
running = false
eventDispatcher.dispose()
breakpointManager.dispose()
disposeThreads()
fireTerminateEvent()
}

private def disposeThreads() {
threads.foreach { _.dispose() }
threads= Nil
}

/**
* Callback from the breakpoint manager when a platform breakpoint is hit
* Add a thread to the list of threads.
* FOR THE COMPANION ACTOR ONLY.
*/
private[debug] def threadSuspended(thread: ThreadReference, eventDetail: Int) {
eventActor !? ScalaDebugTargetActor.ThreadSuspended(thread, eventDetail)
private[model] def addThread(thread: ThreadReference) {
if (!threads.exists(_.thread == thread))
threads = threads :+ ScalaThread(this, thread)
}

/**
* Remove a thread from the list of threads
* FOR THE COMPANION ACTOR ONLY.
*/
private[model] def removeThread(thread: ThreadReference) {
val (removed, remainder) = threads.partition(_.thread eq thread)
threads = remainder
removed.foreach(_.terminatedFromScala())
}

/**
* Return the current list of threads, using the actor system
* Set the initial list of threads.
* FOR THE COMPANION ACTOR ONLY.
*/
private def internalGetThreads(): List[ScalaThread] = {
if (running) {
//FIXME: An unlucky timing can block the current thread forever. Example: If the `eventActor` has terminated,
// no answer will be sent back. However, using a timeout doesn't look like the right thing to do here,
// because it's hard to tell what should be the timeout's value. A better idea is to simply fire a
// CONTENT change event every time the `ScalaThread`s change. A correct synchronization policy has
// to be put in place, but there is a lot to gain from preventing blocking calls as the one below.
// (ticket #1001308)
(eventActor !! ScalaDebugTargetActor.GetThreads).asInstanceOf[Future[List[ScalaThread]]]()
}
else Nil
private[model] def initializeThreads(t: List[ThreadReference]) {
threads= t.map(ScalaThread(this, _))
}

/**
* Return the current list of threads
*/
private[model] def getScalaThreads= threads

}

private[model] object ScalaDebugTargetActor {
case class ThreadSuspended(thread: ThreadReference, eventDetail: Int)
case object GetThreads


def apply(threadStartRequest: ThreadStartRequest, threadDeathRequest: ThreadDeathRequest, debugTarget: ScalaDebugTarget): ScalaDebugTargetActor = {
val actor = new ScalaDebugTargetActor(threadStartRequest, threadDeathRequest, debugTarget)
actor.start()
Expand All @@ -197,43 +225,34 @@ private[model] object ScalaDebugTargetActor {
/**
* Actor used to manage a Scala debug target. It keeps track of the existing threads.
* This class is thread safe. Instances are not to be created outside of the ScalaDebugTarget object.
*
* The `ScalaDebugTargetActor` is linked to both the `ScalaJdiEventDispatcherActor and the
* `ScalaDebugBreakpointManagerActor`, this implies that if any of the three actors terminates (independently
* of the reason), all other actors will also be terminated (an `Exit` message will be sent to each of the
* linked actors).
*
* The `ScalaDebugTargetActor` is linked to both the `ScalaJdiEventDispatcherActor and the
* `ScalaDebugBreakpointManagerActor`, this implies that if any of the three actors terminates (independently
* of the reason), all other actors will also be terminated (an `Exit` message will be sent to each of the
* linked actors).
*/
private class ScalaDebugTargetActor private (threadStartRequest: ThreadStartRequest, threadDeathRequest: ThreadDeathRequest, debugTarget: ScalaDebugTarget) extends BaseDebuggerActor {
import ScalaDebugTargetActor._

private var threads = List[ScalaThread]()

override protected def behavior = {
case _: VMStartEvent =>
vmStarted()
reply(false)
case threadStartEvent: ThreadStartEvent =>
if (!threads.exists(_.thread == threadStartEvent.thread))
threads :+= createScalaThread(threadStartEvent.thread)
debugTarget.addThread(threadStartEvent.thread)
reply(false)
case threadDeathEvent: ThreadDeathEvent =>
val (removed, remainder) = threads.partition(_.thread eq threadDeathEvent.thread)
threads = remainder
removed.foreach(_.terminatedFromScala())
debugTarget.removeThread(threadDeathEvent.thread)
reply(false)
case _: VMDeathEvent | _: VMDisconnectEvent =>
vmDisconnected()
reply(false)
case ThreadSuspended(thread, eventDetail) =>
// forward the event to the right thread
threads.find(_.thread == thread).get.suspendedFromScala(eventDetail)
debugTarget.getScalaThreads.find(_.thread == thread).get.suspendedFromScala(eventDetail)
reply(None)
case GetThreads =>
reply(threads)
}

private def createScalaThread(threadRef: ThreadReference): ScalaThread = ScalaThread(debugTarget, threadRef)

private def vmStarted() {
val eventDispatcher = debugTarget.eventDispatcher
// enable the thread management requests
Expand All @@ -243,19 +262,13 @@ private class ScalaDebugTargetActor private (threadStartRequest: ThreadStartRequ
threadDeathRequest.enable()
// get the current requests
import scala.collection.JavaConverters._
threads = debugTarget.virtualMachine.allThreads.asScala.toList.map(createScalaThread)
debugTarget.initializeThreads(debugTarget.virtualMachine.allThreads.asScala.toList)
debugTarget.vmStarted()
}

override protected def preExit(): Unit = {
debugTarget.vmDisconnected()
disposeThreads()
}

private def disposeThreads() {
threads.foreach { _.dispose() }
threads = Nil
}

private def vmDisconnected(): Unit = poison()
}
}

0 comments on commit 1b4ead3

Please sign in to comment.