After digging around in various forums and trying various approaches, I’m very happy to share what appears to be a relatively general-purpose solution. The solution was provided by hhaensel here:
They define a macro as follows,
macro timeout(seconds, expr, fail)
quote
tsk = @task $expr
schedule(tsk)
Timer($seconds) do timer
istaskdone(tsk) || Base.throwto(tsk, InterruptException())
end
try
fetch(tsk)
catch _
$fail
end
end
end
They give the trivial application, which assigns x = 1 if the expression within begin/end takes less than 1 second, else it assigns x = “failed”.
x = @timeout 1 begin
sleep(1.1)
println("done")
1
end "failed"
Playing around with this macro, it appears to work quite well when calling more complex libraries. For example, here are applications to NLsolve.jl and DifferentialEquations.jl:
using NLsolve
function f!(F, x)
sleep(0.1)
F[1] = (x[1]+3)*(x[2]^3-7)+18
F[2] = sin(x[2]*exp(x[1])-1)
end
function j!(J, x)
J[1, 1] = x[2]^3-7
J[1, 2] = 3*x[2]^2*(x[1]+3)
u = exp(x[1])*cos(x[2]*exp(x[1])-1)
J[2, 1] = x[2]*u
J[2, 2] = u
end
MaxTime = 10
res1 = @timeout MaxTime begin
nlsolve(f!, j!, [ 0.1; 1.2]).zero
end NaN
println("Given MaxTime = 10 seconds, we get res1 = ", res1)
MaxTime = 0.1
res2 = @timeout MaxTime begin
nlsolve(f!, j!, [ 0.1; 1.2]).zero
end NaN
println("Given MaxTime = 0.1 seconds, we get res2 = ", res2)
On my computer, this returns the correct solution when MaxTime = 10s, but returns NaN when MaxTime = 0.1s. Similarly for DifferentialEquations.jl we have,
using DifferentialEquations
function f(u, p, t)
sleep(0.001)
return 1.01 * u
end
u0 = 1 / 2
tspan = (0.0, 1.0)
prob = ODEProblem(f, u0, tspan)
@time sol = solve(prob, Tsit5(), reltol = 1e-8, abstol = 1e-8)
MaxTime = 100
sol = @timeout MaxTime begin
sol = solve(prob, Tsit5(), reltol = 1e-8, abstol = 1e-8)
end NaN
println("Given 100 seconds, sol converges, so sol(0.1) = ", sol(0.1))
MaxTime = 0.01
sol = @timeout MaxTime begin
sol = solve(prob, Tsit5(), reltol = 1e-8, abstol = 1e-8)
end NaN
println("Given 0.01 seconds, sol doesn't converge, and sol = ", sol)
There are a few challenges. For example, BlackBoxOptim automatically recovers and returns a partial solution in case of an InterruptException, which is what the macro throws. You need to turn this off explicitly to make the macro work as intended:
using BlackBoxOptim
function rosenbrock2d(x)
sleep(0.0001)
return (1.0 - x[1])^2 + 100.0 * (x[2] - x[1]^2)^2
end
MaxTime = 1
x = @timeout MaxTime begin
sleep(0.9)
res = bboptimize(rosenbrock2d; SearchRange = (-5.0, 5.0), NumDimensions = 2,
TraceMode = :silent, RecoverResults=false)
res
end NaN
println("Given MaxTime = 1 second, we get x = ", x)
MaxTime = 30
x = @timeout MaxTime begin
sleep(0.9)
res = bboptimize(rosenbrock2d; SearchRange = (-5.0, 5.0), NumDimensions = 2,
TraceMode = :silent, RecoverResults=false)
res
end NaN
println("Given MaxTime = 30 seconds, we converge, with get best_fitness(x) = ", best_fitness(x))
I don’t know enough about the @task macro to determine if/when this might fail to work as intended, but for now it certainly seems a useful macro.