Skip to content

Commit 93699b1

Browse files
committed
Preparatory work for timing out individual bots
1 parent eb34c63 commit 93699b1

File tree

7 files changed

+95
-25
lines changed

7 files changed

+95
-25
lines changed

Scalatron/src/scalatron/botwar/BotWar.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import java.awt.event.{WindowEvent, WindowAdapter, KeyEvent, KeyListener}
99
import scalatron.scalatron.api.Scalatron
1010
import scalatron.scalatron.impl.{TournamentRoundResult, TournamentState, Plugin, PluginCollection, Game}
1111
import akka.dispatch.ExecutionContext
12+
import akka.actor.ActorSystem
1213

1314

1415
/** BotWar: an implementation of the Scalatron Game trait.
@@ -32,7 +33,7 @@ case object BotWar extends Game
3233
secureMode: Boolean,
3334
verbose: Boolean
3435
)(
35-
executionContextForTrustedCode: ExecutionContext,
36+
actorSystem: ActorSystem,
3637
executionContextForUntrustedCode: ExecutionContext
3738
)
3839
{
@@ -74,6 +75,7 @@ case object BotWar extends Game
7475
val stepCallback = (state: SimState) => {
7576
tournamentState.updateMostRecentState(state)
7677

78+
val executionContextForTrustedCode = actorSystem.dispatcher
7779
renderer.draw(display.renderTarget, state.gameState)(executionContextForTrustedCode)
7880

7981
// enforce maxFPS (max frames per second) by putting this thread to sleep if appropriate
@@ -113,7 +115,7 @@ case object BotWar extends Game
113115

114116
// run game
115117
val runner = Simulation.Runner(factory, stepCallback, resultCallback)
116-
runner(plugins, randomSeed)(executionContextForTrustedCode, executionContextForUntrustedCode)
118+
runner(plugins, randomSeed)(actorSystem, executionContextForUntrustedCode)
117119

118120
roundIndex += 1
119121

@@ -132,7 +134,7 @@ case object BotWar extends Game
132134
secureMode: Boolean,
133135
verbose: Boolean
134136
)(
135-
executionContextForTrustedCode: ExecutionContext,
137+
actorSystem: ActorSystem,
136138
executionContextForUntrustedCode: ExecutionContext
137139
)
138140
{
@@ -186,7 +188,7 @@ case object BotWar extends Game
186188

187189
// run game
188190
val runner = Simulation.Runner(factory, stepCallback, resultCallback)
189-
runner(plugins, randomSeed)(executionContextForTrustedCode, executionContextForUntrustedCode)
191+
runner(plugins, randomSeed)(actorSystem, executionContextForUntrustedCode)
190192

191193
roundIndex += 1
192194

Scalatron/src/scalatron/botwar/BotWarSimulation.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ package scalatron.botwar
66
import scala.util.Random
77
import scalatron.scalatron.impl.{Plugin, TournamentRoundResult}
88
import akka.dispatch.ExecutionContext
9+
import akka.actor.ActorSystem
910

1011

1112
/** Implementations of generic Simulation traits for the BotWar game. */
1213
object BotWarSimulation
1314
{
1415
case class SimState(gameState: State) extends Simulation.State[SimState,TournamentRoundResult] {
15-
def step(executionContextForUntrustedCode: ExecutionContext) = {
16+
def step(actorSystem: ActorSystem, executionContextForUntrustedCode: ExecutionContext) = {
1617
// to make results reproducible, generate a freshly seeded randomizer for every cycle
1718
val rnd = new Random(gameState.time)
1819

1920
// apply the game dynamics to the game state
20-
Dynamics(gameState, rnd, executionContextForUntrustedCode) match {
21+
Dynamics(gameState, rnd, actorSystem, executionContextForUntrustedCode) match {
2122
case Left(updatedGameState) => Left(SimState(updatedGameState))
2223
case Right(gameResult) => Right(gameResult)
2324
}

Scalatron/src/scalatron/botwar/Dynamics.scala

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@ package scalatron.botwar
55

66
import scala.util.Random
77
import scalatron.scalatron.impl.TournamentRoundResult
8-
import akka.util.Duration
98
import akka.util.duration._
109
import akka.dispatch._
1110
import java.util.concurrent.TimeoutException
11+
import akka.actor.ActorSystem
1212

1313

1414
/** Game dynamics. Function that, when applied to a game state, returns either a successor
1515
* game state or a game result.
1616
*/
17-
case object Dynamics extends ((State, Random, ExecutionContext) => Either[State,TournamentRoundResult])
17+
case object Dynamics extends ((State, Random, ActorSystem, ExecutionContext) => Either[State,TournamentRoundResult])
1818
{
19-
def apply(state: State, rnd: Random, executionContextForUntrustedCode: ExecutionContext) = {
19+
def apply(state: State, rnd: Random, actorSystem: ActorSystem, executionContextForUntrustedCode: ExecutionContext) = {
2020
// determine which bots are eligible to move in the nest step
2121
val eligibleBots = computeEligibleBots(state)
2222

2323
// have each bot compute its command
24-
val botCommandsAndTimes = computeBotCommands(state, eligibleBots)(executionContextForUntrustedCode) // was: actorSystem
24+
val botCommandsAndTimes = computeBotCommands(state, eligibleBots, actorSystem)(executionContextForUntrustedCode) // was: actorSystem
2525

2626
// store time taken with each bot
2727
var updatedBoard = state.board
@@ -100,11 +100,12 @@ case object Dynamics extends ((State, Random, ExecutionContext) => Either[State,
100100
}
101101
})
102102

103+
type BotResponse = (Entity.Id,(Long,String,Iterable[Command]))
103104

104105
// returns a collection of tuples: (id, (nanoSeconds, commandList))
105-
def computeBotCommands(state: State, eligibleBots: Iterable[Bot])(implicit executionContextForUntrustedCode: ExecutionContext): Iterable[(Entity.Id,(Long,String,Iterable[Command]))] = {
106-
// use Akka to work this out
107-
val future = Future.traverse(eligibleBots)(bot => Future {
106+
def computeBotCommands(state: State, eligibleBots: Iterable[Bot], actorSystem: ActorSystem)(implicit executionContextForUntrustedCode: ExecutionContext): Iterable[(Entity.Id,(Long,String,Iterable[Command]))] = {
107+
// use Akka Futures to work this out
108+
val outerFuture = Future.traverse(eligibleBots)(bot => Future {
108109
try {
109110
val timeBefore = System.nanoTime
110111
val (inputString,commands) = bot.respondTo(state)
@@ -121,9 +122,71 @@ case object Dynamics extends ((State, Random, ExecutionContext) => Either[State,
121122
}
122123
})
123124

125+
126+
/*
127+
// This is based on Viktor Klang's proposal, see https://groups.google.com/forum/?fromgroups#!topic/akka-user/5MY8lsYGbYM
128+
val TimeoutSentinel : Option[BotResponse] = None
129+
val timeout = Promise[Option[BotResponse]]()
130+
val c = actorSystem.scheduler.scheduleOnce(1000 millis){ timeout.success(TimeoutSentinel) }
131+
timeout onComplete { case _ => c.cancel() }
132+
133+
val outerFuture = Future.traverse(eligibleBots)(bot =>
134+
Promise[Option[BotResponse]]()
135+
.completeWith(Future {
136+
try {
137+
val timeBefore = System.nanoTime
138+
val (inputString,commands) = bot.respondTo(state)
139+
val timeSpent = System.nanoTime - timeBefore
140+
if(commands.isEmpty) None else Some((bot.id,(timeSpent,inputString,commands)))
141+
} catch {
142+
case t: NoClassDefFoundError =>
143+
// we fake a Log() command issued by the bot to report the error into the browser UI:
144+
Some((bot.id,(0L,"",Iterable[Command](Command.Disable("error: class not found: " + t.getMessage)))))
145+
146+
case t: Throwable =>
147+
// we inject a Disable() command as-if-issued-by-the-bot to report the error into the browser UI:
148+
Some((bot.id,(0L,"",Iterable[Command](Command.Disable(t.getMessage)))))
149+
}
150+
})
151+
.completeWith(timeout)
152+
)
153+
*/
154+
/*
155+
// this is an attempt based on nested Futures. Does not seems to work at all: every call times out. ?!?
156+
val TimeoutSentinel : Option[BotResponse] = None
157+
val outerFuture = Future.traverse(eligibleBots)(bot =>
158+
Future {
159+
val innerFuture = Future {
160+
try {
161+
val timeBefore = System.nanoTime
162+
val (inputString,commands) = bot.respondTo(state)
163+
val timeSpent = System.nanoTime - timeBefore
164+
if(commands.isEmpty) None else Some((bot.id,(timeSpent,inputString,commands)))
165+
} catch {
166+
case t: NoClassDefFoundError =>
167+
// we fake a Log() command issued by the bot to report the error into the browser UI:
168+
Some((bot.id,(0L,"",Iterable[Command](Command.Disable("error: class not found: " + t.getMessage)))))
169+
170+
case t: Throwable =>
171+
// we inject a Disable() command as-if-issued-by-the-bot to report the error into the browser UI:
172+
Some((bot.id,(0L,"",Iterable[Command](Command.Disable(t.getMessage)))))
173+
}
174+
}
175+
try {
176+
Await.result(innerFuture, 5000 millis) // generous timeout - for now, we only want to capture infinite loops
177+
} catch {
178+
case t: TimeoutException =>
179+
System.err.println("warning: timeout while invoking the control function of bot: " + bot.name)
180+
TimeoutSentinel
181+
}
182+
}
183+
)
184+
*/
185+
124186
// Note: an overall timeout across all bots is a temporary solution - we want timeouts PER BOT
125187
try {
126-
val result = Await.result(future, 2000 millis) // generous timeout - note that this is over ALL plug-ins
188+
val result = Await.result(outerFuture, 10000 millis) // generous timeout - note that this is over ALL plug-ins
189+
// result.filter(TimeoutSentinel ==).foreach(r => println("bot timed out"))
127190
result.flatten
128191
} catch {
129192
case t: TimeoutException =>

Scalatron/src/scalatron/botwar/Simulation.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package scalatron.botwar
66
import scalatron.scalatron.impl.Plugin
77
import akka.util.Duration
88
import akka.dispatch.{ExecutionContext, Future, Await}
9+
import akka.actor.ActorSystem
910

1011

1112
/** Traits for generic simulations, of which a game like BotWar is an example.
@@ -22,7 +23,7 @@ object Simulation
2223
* @tparam R type of the result returned by the simulator (arbitrary)
2324
*/
2425
trait State[S <: State[S, R], R] extends UntypedState {
25-
def step(executionContextForUntrustedCode: ExecutionContext) : Either[S, R]
26+
def step(actorSystem: ActorSystem, executionContextForUntrustedCode: ExecutionContext) : Either[S, R]
2627
}
2728

2829

@@ -46,15 +47,15 @@ object Simulation
4647
{
4748
/** @param plugins the collection of external plug-ins to bring into the simulation
4849
* @param randomSeed the random seed to use for initializing the simulation
49-
* @param executionContextForTrustedCode execution context whose threads are trusted (e.g. actor system)
50+
* @param actorSystem execution context whose threads are trusted (e.g. actor system)
5051
* @param executionContextForUntrustedCode execution context whose threads are untrusted (sandboxed by the security manager)
5152
* @return an optional simulation result (if the simulation was not prematurely aborted)
5253
*/
5354
def apply(
5455
plugins: Iterable[Plugin.External],
5556
randomSeed: Int
5657
)(
57-
executionContextForTrustedCode: ExecutionContext,
58+
actorSystem: ActorSystem,
5859
executionContextForUntrustedCode: ExecutionContext
5960
): Option[R] =
6061
{
@@ -68,7 +69,8 @@ object Simulation
6869
// the callback will generally render the prior state to the screen.
6970

7071
// process state update, returns either next state or result
71-
val stepFuture = Future( { currentState.step(executionContextForUntrustedCode) } )(executionContextForTrustedCode) // compute next state
72+
val executionContextForTrustedCode = actorSystem.dispatcher
73+
val stepFuture = Future( { currentState.step(actorSystem, executionContextForUntrustedCode) } )(executionContextForTrustedCode) // compute next state
7274

7375
// process callback (usually rendering) on prior state, returns true if to continue simulating, false if not
7476
val callbackFuture = Future( {

Scalatron/src/scalatron/scalatron/impl/Game.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scalatron.scalatron.impl
22

33
import akka.dispatch.ExecutionContext
4+
import akka.actor.ActorSystem
45

56
/** This material is intended as a community resource and is licensed under the
67
* Creative Commons Attribution 3.0 Unported License. Feel free to use, modify and share it.
@@ -23,7 +24,7 @@ trait Game {
2324
* @param tournamentState the tournament state object to update whenever a round ends
2425
* @param secureMode if true, certain bot processing restrictions apply
2526
* @param verbose if true, log to the console verbosely
26-
* @param executionContextForTrustedCode execution context for trusted code (e.g. from Akka ActorSystem)
27+
* @param actorSystem execution context for trusted code (e.g. from Akka ActorSystem)
2728
* @param executionContextForUntrustedCode execution context for untrusted code (e.g. for bot control functions)
2829
*/
2930
def runVisually(
@@ -34,7 +35,7 @@ trait Game {
3435
secureMode: Boolean,
3536
verbose: Boolean
3637
)(
37-
executionContextForTrustedCode: ExecutionContext,
38+
actorSystem: ActorSystem,
3839
executionContextForUntrustedCode: ExecutionContext
3940
)
4041

@@ -47,7 +48,7 @@ trait Game {
4748
* @param tournamentState the tournament state object to update whenever a round ends
4849
* @param secureMode if true, certain bot processing restrictions apply
4950
* @param verbose if true, log to the console verbosely
50-
* @param executionContextForTrustedCode execution context for trusted code (e.g. from Akka ActorSystem)
51+
* @param actorSystem execution context for trusted code (e.g. from Akka ActorSystem)
5152
* @param executionContextForUntrustedCode execution context for untrusted code (e.g. for bot control functions)
5253
*/
5354
def runHeadless(
@@ -58,7 +59,7 @@ trait Game {
5859
secureMode: Boolean,
5960
verbose: Boolean
6061
)(
61-
executionContextForTrustedCode: ExecutionContext,
62+
actorSystem: ActorSystem,
6263
executionContextForUntrustedCode: ExecutionContext
6364
)
6465

Scalatron/src/scalatron/scalatron/impl/ScalatronImpl.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,9 @@ case class ScalatronImpl(
239239
val headless = (argMap.get("-headless").getOrElse("no") == "yes")
240240
val executionContextForTrustedCode = actorSystem.dispatcher
241241
if(headless) {
242-
game.runHeadless(pluginBaseDirectoryPath, argMap, rounds, tournamentState, secureMode, verbose)(executionContextForTrustedCode, executionContextForUntrustedCode)
242+
game.runHeadless(pluginBaseDirectoryPath, argMap, rounds, tournamentState, secureMode, verbose)(actorSystem, executionContextForUntrustedCode)
243243
} else {
244-
game.runVisually(pluginBaseDirectoryPath, argMap, rounds, tournamentState, secureMode, verbose)(executionContextForTrustedCode, executionContextForUntrustedCode)
244+
game.runVisually(pluginBaseDirectoryPath, argMap, rounds, tournamentState, secureMode, verbose)(actorSystem, executionContextForUntrustedCode)
245245
}
246246
}
247247

Scalatron/src/scalatron/scalatron/impl/ScalatronSandboxState.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ case class ScalatronSandboxState(sandbox: ScalatronSandbox, simState: BotWarSimu
1414
def time = simState.gameState.time.toInt // CBB: warn on truncation from Long to Int
1515

1616
def step(count: Int): SandboxState = {
17+
val actorSystem = sandbox.user.scalatron.actorSystem
1718
val executionContextForUntrustedCode = sandbox.user.scalatron.executionContextForUntrustedCode
1819
var updatedState = simState
1920
for( i <- 0 until count ) {
20-
updatedState.step(executionContextForUntrustedCode) match {
21+
updatedState.step(actorSystem, executionContextForUntrustedCode) match {
2122
case Left(successorState) =>
2223
updatedState = successorState
2324

0 commit comments

Comments
 (0)