Recently I noticed that I seem to write a similar pattern way too often: Some form of scheduling or task management part. Usually this is due to the need of executing some task in a specific thread, or simply wanting to schedule something for processing in the background.
Load it through ASDF or Quicklisp:
Before we can do anything, we need to have a manager to run tasks with. These are called
runners. Usually you will want a
queued-runner which will use threading capabilities if the system provides them, and thus can process tasks in the background.
(defvar *runner* (make-instance 'simple-tasks:queued-runner))
Next we'll want to start the runner, if possible in a separate thread. The standard
start-runner function does not do this, as you might want to set up the thread scenario yourself, or perhaps even inject the runner into a different thread. However, for the common scenario, there's
(defvar *thread* (simple-tasks:make-runner-thread *runner*))
Now we can finally create
tasks and send them off for processing! Out of the box, simple-tasks provides three classes to do remote evaluation with:
callback-task. However, adding your own is very easy. Just subclass
task and implement
(defvar *task* (make-instance 'simple-tasks:call-task :func (lambda () (print "hi"))))
Finally we need to send off the task for processing.
(simple-tasks:schedule-task *task* *runner*)
call-tasks also save the
return-values of the function they're calling, so you can read that out later. Usually if you want to do so, you'll want to either block until the task is done, or delegate to a callback function. The former is made convenient through
(simple-tasks:with-body-as-task (*runner*) (sleep 1) (expt 2 12))
In the case that a task encounters a failure during a run, it sets its status to
:errored and saves the current environment. You can inspect this environment at any later point by fetching it with
error-environment and looking at its contents with the various functions Dissect provides. This is mostly useful in a scenario where you cannot use a debugger and thus just automatically invoke the
skip restart in the runner thread. With the environment saved, the error can still be inspected elsewhere or at a later date.
(dissect:present (simple-tasks:error-environment (simple-tasks:schedule-task (make-instance 'simple-tasks:blocking-call-task :func (lambda () (error "Hi"))) *runner*)))
And that's pretty much it.
If you want to add flexibility by creating your own specialised task classes, you should look at
schedule-task. Usually you can get away by just subclassing
task, and adding a
run-task method to do your calculations in. If you also need to modify the behaviour of the scheduling, adding
:after methods to
schedule-task can also be useful.
In case that the existing runners aren't suited to your needs, adding one should also not be much of a problem. Simply subclass
runner, and implement appropriate methods for
schedule-task. The existing methods on
runner will take care of keeping the
status in sync and making sure no invalid calls can be made.
- Dissect for error environment capture and inspection.