Mercurial > hg > dml-open-cliopatria
view cpack/dml/lib/async.pl @ 0:718306e29690 tip
commiting public release
author | Daniel Wolff |
---|---|
date | Tue, 09 Feb 2016 21:05:06 +0100 |
parents | |
children |
line wrap: on
line source
/* Part of DML (Digital Music Laboratory) Copyright 2014-2015 Samer Abdallah, University of London This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ :- module(async, [ async_memo/4 , async_current_job/5 , async_thread_progress/4 , async_get_progress_callback/1 , async_set_progress_callback/1 , async_unset_progress_callback/0 , async_cancel/3 , async_cancel/2 , with_progress_stack/1 , with_cont/4 , simple_task/2 , stepwise_task/4 , updatable_status_task/3 , map_with_progress/3 , map_fold_with_progress/5 , with_global/3 , with_new_ref/3 ]). /** <module> Asynchronous memoised computations with progress and partial results This module provides services for managing memoised computations in a thread pool with the ability to check on the progress of running jobs and receive partial results if they are available. Completed computations are memoised using library(memo). ---++ Job control The first half of the module is concerned with managing memoised computations. Computations are requested using async_memo/4. If the result is already available in the memoised database, the result is returned. If the computation is already running, status information about the job is returned. Otherwise, a new job is started. Status information is returned as the type =|async_status(A)|=. == async_status(A) ---> done(metadata) ; initiated(job_id,natural) ; waiting(natural) ; running(job_id,A)) ; recompute(job_id,natural,metadata). job_id == atom. == The type =|metadata|= is defined in library(memo). ---++ Helper meta-predicates The second half of the module provides a collection of predicates for computations to use to handle requests for progress information, in the form of a stack of tasks. It recognises the following levels of progress information: == summary :: progress_level(list(any)). partial_result :: progress_level(any). == */ :- meta_predicate async_set_progress_callback(2) , async_memo(+,0,-,+) , with_global(+,+,0) , with_new_ref(-,+,0) , with_progress_stack(0) , with_progress_handler(2,0) , with_cont(+,1,2,-) , simple_task(+,0) , stepwise_task(+,+,-,0) % , stepwise_partial_task(+,+,-,-,0) % , fold_with_progress(3,+,+,-) , map_with_progress(2,+,-) , map_fold_with_progress(2,3,+,+,-). :- use_module(library(thread_pool)). :- use_module(library(insist)). :- use_module(library(memo)). :- use_module(library(dcg_core),[seqmap/4]). %% async_memo(+P:thread_pool, +G:memo_goal, -S:async_status(_), Opts:options) is det. % % Start running a goal asynchronously in a thread pool if it is not % already running. % In order for the goal to provide progress information, it must use nb_setval/2 % to set the global variable =|progress|=. % % Options: % * status_var(Var:status_var(A)) % If computation is already running, information of type =|A|= % associated with job is returned in Stats. Default is =|progress(summary)|=. % * recompute(R:oneof([none,failed,test(Pred)])) % If =|none|=, then a previously failed computation is not retried, and the % status =|done(Comp-Result)|= is returned, where Comp includes % the computation time and host and Result is =|fail|= or =|ex(Exception)|=. % If =|failed|=, unsuccessful computations are retried and the status % =|recomputing(ID,Pos,Meta)|= is returned, where =|ID|= is the ID of the % new job, =|Pos|= its initial position in the queue, and =|Meta=Comp-Result|= % the status of the previous computation. % If =|test(Pred)|=, then the computation is retried if =|call(Pred,Meta)|= % succeeds, where Meta is the memoisied computation metadata and the variables % in Goal will be unified with their values resulting from that computation. % The status returned is =|initiated(ID,Pos)|=. Pred must include its module % specifier as a I am too lazy to worry about meta-predicate options right now. % * globals(list(pair(atom,any))) % Set global variables to given values in new thread. % % The value of the =|status_var(_)|= option determines the information returned % if the status of the computation is =|running(ID,StatusVals)|=. % == % progress(progress_level(A)) :: status_var(A). % stats(list(stats_key)) :: status_var(list(stats_vals)). % time :: status_var(pair(time,float)). % start and elapsed times % status_var(A)-status_var(B) :: status_var(pair(A,B)). % % time==float. % as returned by get_time/1. % == % The type =|stats_key|= denotes any atom accepted by thread_statistics/3, and % =|stats_vals|= the type of values returned. async_memo(_,Goal,done(Meta),Opts) :- option(recompute(none),Opts,none), browse(Goal,Meta), !. async_memo(Pool,Goal,Status,Opts) :- with_mutex(async,unsafe_async_memo(Pool,Goal,Status,Opts)). unsafe_async_memo(Pool,Goal,Status,Opts) :- option(globals(Globals),Opts,[]), ( browse(Goal,Meta) -> option(recompute(Recompute),Opts,none), ( recompute(Recompute,Meta) -> clear_all(Goal,Meta), spawn(Pool,Goal,Globals,ID,Waiting), Status=recomputing(ID,Waiting,Meta) ; Status=done(Meta) ) ; % !!! it's possible that another thread might be computing % this goal and complete at this point. If this happens, % that job will not be detected and a new a job will be started % here. If that happens, the newly created job will find the result % and complete quickly. ( async_current_job(Pool,Goal,ID,T0,S) -> option(progress_levels(Levels),Opts,[summary]), % !!! it's possible that the job will complete here % and so the progress request will create an error goal_status(ID,T0,Levels,S,Status) ; spawn(Pool,Goal,Globals,ID,Waiting), Status=spawned(ID,Waiting) ) ). recompute(failed,_-fail). recompute(failed,_-ex(_)). recompute(test(P),M) :- call(P,M). goal_status(ID, TSubmit, _, waiting(Pos), waiting(ID,TSubmit,Pos)). goal_status(ID, _, Levels, running(Thread,TStart), running(ID,TStart,Thread,Progress)) :- catch( ( async_thread_progress(Thread,Levels,T,Vals), Progress=just(T-Vals)), Ex, ( print_message(warning,error_getting_job_progress(Thread,Ex)), Progress=nothing)). spawn(Pool,Goal,Globals,ID,Waiting) :- uuid(ID), (thread_pool_property(Pool,backlog(Waiting)) -> true; Waiting=0), get_time(Time), setup_call_catcher_cleanup( recordz(Pool,job(ID,Time,Goal),Ref), thread_create_in_pool(Pool,spawnee(ID,Goal,Globals),_, [ detached(true), at_exit(erase(Ref))]), exception(_), erase(Ref)). spawnee(ID,Goal,Globals) :- setup_call_cleanup( with_mutex(async, ( recorded(ID,async:cancelled,Ref) -> debug(async,'Cancelled before starting: ~q',[Goal]), erase(Ref), fail ; pairs_keys_values(Globals,GNames,GVals), maplist(nb_setval,GNames,GVals), get_time(T0), thread_self(Thread), recordz(ID,running(Thread,T0),Ref) )), ( freeze(Res,check_result(Res)), % prevent memoisation if abort(_) is caught debug(async,"Running asyc goal ~q",[Goal]), memo(Goal,_-Res)), erase(Ref)). check_result(Ex) :- ( Ex=ex(abort(Reason)) -> debug(async,'Computation aborted (~w)',[Reason]), throw(abort(Reason)) ; true ). %% async_current_job(+Pool:thread_pool, -Goal:callable, -ID:job_id, -T:time, -S:job_status) is nondet. % Retrieves information about current pending jobs. A job is pending if it is running % or waiting to run in the specified thread pool. T is the time the job was submitted. % Job status is: % == % job_status ---> waiting(natural) % queued at given postion % ; running(thread_id,time). % with start time % == async_current_job(Pool,Goal,ID,Time,Status) :- recorded(Pool,job(ID,Time,Goal)), % this finds ALL jobs ( recorded(ID,running(Thread,StartTime)) -> Status=running(Thread,StartTime) ; thread_pool_property(Pool,waiting(Waiting)), nth1(Pos,Waiting,create(Pool,async:spawnee(ID,_,_),_,_,_)) -> Status=waiting(Pos) ; print_message(warning, async_job_status_indeterminate(Pool,ID,Goal)), Status=indeterminate % could be finished, or transferring from waiting to running ). %% async_thread_progress(+Thread:thread_id,+Levels:list(progress_level), -T:time, -Vals:list) is det. % Called in main thread to get information about job running in thread Thread. % Will throw an exception if: % - the thread has terminated % - the thread does not respond with progress info in less than 3 seconds % - the thread does not have a progress handler call back registered % - the progress handler fails on any of the progress levels requested. async_thread_progress(Thread,Levels,Time,Vals) :- thread_self(Me), gensym(prog,RID), % progress request ID flush_all_messages(Me,progress(_,_,_,_)), thread_signal(Thread,report_progress(RID,Levels,Me)), ( thread_get_message(Me,progress(RID,Time,Vals,St),[timeout(3)]) -> true ; throw(get_thread_progress_timeout(Thread,Levels)) ), debug(async,'Got progress ~w from thread ~w: ~w',[Levels,Thread,Vals]), memo:reflect(St). flush_all_messages(Queue,Pattern) :- repeat, \+thread_get_message(Queue,Pattern,[timeout(0)]), !. %% report_progress(+RID:atomic, +Var:status_var(A), +Thread:thread_id) is det. % Called from job thread in response to a signal, to send progress information to thread Thread. report_progress(RID,Levels,Thread) :- get_time(Time), memo:reify(async:my_progress(Levels,Vals),St), !, debug(async,'reporting progress(~w): ~q',[Levels,Vals]), thread_send_message(Thread,progress(RID,Time,Vals,St)). my_progress(Levels,Vals) :- nb_getval(async_progress,G), insist(maplist(G,Levels,Vals)). %% async_cancel(+Pool:thread_pool, +ID:job_id, +Ex:term) is det. %% async_cancel(+Pool:thread_pool, +ID:job_id) is det. % Cancel job with given ID, if it is currently pending. If it is running, the % exception term Ex is thrown (=|abort(cancel)|= in async_cancel/2). % If not, the recorded database is used to mark the job for immediate failure % as soon as it is started. async_cancel(Pool,ID) :- async_cancel(Pool,ID,abort(cancel)). async_cancel(Pool,ID,Ex) :- debug(async,'Cancelling ~w:~w with ~w',[Pool,ID,Ex]), with_mutex(async, (async_current_job(Pool,_,ID,_,S) -> cancel(ID,S,Ex); true)). cancel(ID,waiting(_),_) :- recordz(ID,async:cancelled). cancel(_, running(Thread,_),Ex) :- catch( thread_signal(Thread,throw(Ex)), E, print_message(warning,error_signalling_job_thread(Thread,E))). cancel(ID,indeterminate,_) :- print_message(warning,indeterminate_job_state_on_cancel(ID)). %% async_set_progress_callback(+P:pred(+Level:any,-Info:any)) is det. % Sets the progress handler for the current thread. async_set_progress_callback(Pred) :- nb_setval(async_progress,Pred). %% async_get_progress_callback(-P:pred(+Level:any,-Info:any)) is semidet. % Gets the progress handler for the current thread, if one has been set. async_get_progress_callback(Pred) :- nb_current(async_progress,Pred). %% async_unset_progress_callback is det. % Unsets the progress handler for the current thread. Subsequent calls to % async_get_progress_callback/1 will fail. async_unset_progress_callback :- nb_delete(async_progress). % ----------------------------------------------------------------------- %% with_progress_stack(+Goal:callable) is nondet. % % Runs Goal with the current thread's progress handler (which must be initially % unset) set to an empty task stack. Tasks (goals which run in cooperation with a % a progress handler) can be pushed on to the task stack using with_progress_handler/2. % Fails if progress callback has already been set to something else. % % The progress handler recognises progress level terms: % * summary :: progress_level(list(any)) % Yields a list of progress terms, one for each entry in the task stack. % * partial_result :: progress_level(any) % Yields partial results of the computation if they are available. with_progress_stack(Goal) :- async_get_progress_callback(CB), !, CB=async:get_stack_progress, call(Goal). with_progress_stack(Goal) :- with_global(async_progress_stack, nil, setup_call_cleanup( async_set_progress_callback(get_stack_progress), Goal, async_unset_progress_callback)). %% get_stack_progress(+Level:progress_level(A),-Info:A) is semidet. % Gets the request level of progress information from the currently installed % progress stack. get_stack_progress(summary,P) :- nb_getval(async_progress_stack,H), call(H,progress(summary),P). get_stack_progress(partial_result,R) :- nb_getval(async_progress_stack,H), call(H,partial_result,R). %% with_progress_handler(+H:progress_handler, +Goal:callable) is nondet. % Pushes the progress handler H to the top of the task stack associated with % the current thread and calls Goal. The handler is removed on exit. with_progress_handler(H,Goal) :- nb_getval(async_progress_stack,H0), call(H0,push(H),H1), setup_call_cleanup( nb_setval(async_progress_stack,H1), Goal, nb_setval(async_progress_stack,H0)). nil(progress(_),[]). nil(push(H),chain(H,nil)). nil(partial_result,nothing). nil(partial_result(Cont),R) :- call(Cont,nothing,R). chain(Head,Tail1,push(H),chain(Head,Tail2)) :- call(Tail1,push(H),Tail2). chain(Head,Tail,progress(L),[PH|PT]) :- call(Head,progress(L),PH), call(Tail,progress(L),PT). chain(Head,Tail,partial_result,MR) :- call(Tail,partial_result,R1), (call(Head,partial_result(R1),R) -> MR=just(R); MR=nothing). %% simple_task(+Desc,+Goal:callable) is det. % % Calls goal with a progress handler that reports Desc when progress % information is requested. No partial results are available. simple_task(Desc,Goal) :- with_progress_handler(simple_progress(Desc),Goal). simple_progress(Desc,progress(_),Desc). %% with_cont(+Desc,+Pred:pred(-A),+Cont:pred(+A,-B),-Result:B) is det. % % Calls Pred and then passes the result to Cont yielding Result. % A progress handler is installed such that any partial results yielded by % subtasks in Pred are passed to Cont to yield a partial result. Progress requests % yield Desc. with_cont(Desc,Pred,Cont,Result) :- with_progress_handler(cont(Desc,Cont), (call(Pred,R),call(Cont,R,Result))). cont(_,Cont,Tail1,push(H),cont(Cont,Tail2)) :- call(Tail1,push(H),Tail2). cont(Desc,_,progress(_),Desc). cont(_,Cont,partial_result(just(R1)),R) :- call(Cont,R1,R). %% stepwise_task(+Desc,+Total:nonneg,-Step:callable,+Goal:callable) is det. % % Calls Goal with a progress handler that is able to report progress as a term % =|stepwise(Desc,Done/Total)|=. Goal is permitted to call Step in order to increment % the =|Done|= counter. Partial results are not available. stepwise_task(Desc,Total,async:increment(Counter),Goal) :- with_new_ref(Counter,0, with_progress_handler(stepwise_progress(Desc,Total,Counter), Goal)). stepwise_progress(Desc,Total,Counter,progress(_),stepwise(Desc,Done/Total)) :- b_getval(Counter,Done). %% updatable_status_task(+Initial,-Update:pred(any),+Goal:callable) is det. % % Calls goal with Update set to a predicate which can be used to set the % current status of the task. This will be reported if progress is requested. % No partial results are available. Initial is the status before Update is called. :- meta_predicate updatable_status_task(+,-,0). updatable_status_task(Initial,async:set_ref(Status),Goal) :- with_new_ref(Status,Initial, with_progress_handler(status_progress(Status),Goal)). status_progress(Status,progress(_),S) :- b_getval(Status,S). map_with_progress(P,Xs,Ys) :- length(Xs,Total), stepwise_task(map(P),Total,Step, maplist(call_and_step(Step,P),Xs,Ys)). call_and_step(Step,P,X,Y) :- call(P,X,Y), call(Step). /* fold_with_progress(Folder,Items,S0,S1) :- length(Items,N), stepwise_partial_task(folding(Folder),N,Step,Update, seqmap(step_update(Step,Update,Folder),Items,S0,S1)). step_update(Step,Update,P,X,S0,S1) :- call(P,X,S0,S1), call(Update,S1), call(Step). */ :- setting(map_fold_batch,nonneg,512,"Batch size for flushing fold queue."). %% map_fold_with_progress(+Mapper:pred(A,B), +Folder:pred(list(B),S,S), +Items:list(A), +S1:S, -S2:S) is det. % % Map Mapper over Items and fold over the results with Folder, with progress information % partial results available. Note that Folder operates on a _list_ of items, not just one % item at a time. Mapper is used to accumulate up to N items before folding them batchwise % into the accumulator, where N is the value of the setting =|map_fold_batch|=. % % Progress information is returned as a term =|stepwise(Desc,Done/Total)|=, where % =|Desc=map_fold(Mapper,Folder)|=. If partial results are requested, the current batch % of mapped items is first folded in before returning the value of the accumulator. map_fold_with_progress(Mapper,Folder,Items,S1,S2) :- length(Items,Total), setting(map_fold_batch,M1), with_new_ref(StateRef,s(Items,0,M1,Head,Head,S1), ( Handler=map_fold_progress(map_fold(Mapper,Folder),Total,StateRef), with_progress_handler(Handler, ( map_fold_loop(Mapper,Folder,StateRef), call(Handler,partial_result(nothing),S2))))). map_fold_loop(Mapper,Folder,StateRef) :- b_getval(StateRef,s(Xs1,N1,M1,Head,Ys1,S)), ( Xs1=[] -> Ys1=[] ; Xs1=[X|Xs2] -> call(Mapper,X,Y), !, Ys1=[Y|Ys2], succ(N1,N2), State1=s(Xs2,N2,M2,Head,Ys2,S), ( succ(M2,M1) -> State2=State1 ; simple_task(folding,flush_fold(Folder,State1,State2)) ), b_setval(StateRef,State2), !, map_fold_loop(Mapper,Folder,StateRef) ). map_fold_progress(Desc,Total,StateRef,progress(_),stepwise(Desc,Done/Total)) :- b_getval(StateRef,s(_,Done,_,_,_,_)). map_fold_progress(map_fold(_,Folder),_,StateRef,partial_result(_),S2) :- b_getval(StateRef,State1), flush_fold(Folder,State1,State2), b_setval(StateRef,State2), State2=s(_,_,_,_,_,S2). flush_fold(Folder,s(Xs,Done,_,Head,[],S1),s(Xs,Done,M1,H2,H2,S2)) :- setting(map_fold_batch,M1), call(Folder,Head,S1,S2), !. % for managing global variable new_ref(Ref) :- gensym('$ref',Ref). new_ref(Ref,Val) :- new_ref(Ref), b_setval(Ref,Val). with_global(Name,Val,Goal) :- setup_call_cleanup(b_setval(Name,Val), Goal, nb_delete(Name)). with_new_ref(Name,Val,Goal) :- new_ref(Name), with_global(Name,Val,Goal). set_ref(Ref,Val) :- nb_setval(Ref,Val). increment(Counter) :- b_getval(Counter,N), succ(N,M), b_setval(Counter,M).