I am trying to support cancelling a long running goal in a thread. Moreover, I want to have the goal automatically cancelled by a timeout, but support manual cancellation earlier as well.
I started by running sleep(20000)
in a thread started by thread_create/3 with the thread alias goal1
.
I then try to cancel it by calling:
% Note that I don't throw abort/0 even though that is a more reliable
% "mostly uncatchable" exception because I want the thread to stick
% around to run more goals
thread_signal(goal1, throw(cancel_goal))
This works as expected and the exception bubbles up the thread properly.
However, if I wrap the goal first in call_with_time_limit/2 like this:
call_with_time_limit(10, sleep(20000))
and then cancel it the same way:
thread_signal(goal1, throw(cancel_goal))
I get this error spit out to the top level:
Thread 4 (goal1): foreign predicate system:>/2 did not clear exception:
cancel_goal
and the error that bubbles up is actually time_limit_exceeded
, which is the exception that would be thrown normally by the call_with_time_limit/2
predicate on timeout (even though I am well before the timeout).
Am I doing something incorrectly?
[Edit: Here’s the actual code I’m using in case that helps. The issue seems timing related since it doesn’t always happen…]
% A successful test session, I don't seem to be able to repro now...
?- startGoalThread.
true.
?- sendGoalToGoalThread(20000, sleep(10000), []).
true.
?- cancelGoal.
error(cancel_goal)
true.
?- sendGoalToGoalThread(20000, sleep(1), []).
true.
?- cancelGoal.
true([[]])
true.
startGoalThread :-
thread_self(CommunicationThreadID),
thread_create(
goal_thread(CommunicationThreadID),
_,
[alias(goal1)]
).
sendGoalToGoalThread(Timeout, Goal, BindingList) :-
thread_send_message(goal1, goal(Goal, BindingList, Timeout)).
cancelGoal :-
% Insert a known exception into the thread
thread_signal(goal1, throw(cancel_goal)),
% Wait for whatever response occurs
thread_self(SelfID),
thread_get_message(SelfID, result(Result)),
writeln(Result).
% The worker predicate for the Goal thread. Looks for a message, processes it, then recurses.
% Goals always run in the same thread in case the user is setting thread
% local information.
goal_thread(RespondToThreadID) :-
thread_self(SelfID),
thread_get_message(SelfID, goal(Goal, BindingList, QueryTimeout)),
(
% Exceptions that occur while executing the goal should be returned
% run all commands in the context of the 'user' module so it acts like
% the Prolog top level
QueryTimeout == -1
->
(
ThreadGoal = catch(findall(BindingList, @(Goal, user), Answers), E, true)
)
;
(
ThreadGoal = catch(call_with_time_limit(QueryTimeout, findall(BindingList, @(Goal, user), Answers)), E, true)
)
),
debug(prologServer(command), "Executing Goal: ~w", [Goal]),
ThreadGoal,
(
var(E)
->
(
debug(prologServer(command), "Completed Goal: ~w", [Goal]),
Answers == []
->
(
thread_send_message(RespondToThreadID, result(false))
)
;
(
thread_send_message(RespondToThreadID, result(true(Answers)))
)
)
;
(
debug(prologServer(command), "Exception in Goal: ~w, ~w", [Goal, E]),
thread_send_message(RespondToThreadID, result(error(E)))
)
),
% Use tail recursion to avoid growing the stack
goal_thread(RespondToThreadID).