This module implements a prioritized, heterogenous job queue that sends completed job results of arbitrary type to the initiator/caller.
This is intended for running heavy multi-threaded jobs that should be run one at a time to avoid resource contention. By using this queue, multiple (async) tasks can initiate these tasks and wait for results without need of any other synchronization.
note: Other rust job queues investigated cerca 2024 either did not support waiting for job results or else were overly complicated, requiring backend database, etc.
Both blocking and non-blocking (async) jobs are supported. Non-blocking jobs are called inside spawn_blocking() in order to execute on tokio's blocking thread-pool. Async jobs are simply awaited.
It supports prioritizing Jobs. The order of job execution is not a simple FIFO or LIFO but rather depends on the assigned priority of each job. Job priority level can be specified via any type that implements [Ord] such as a custom enum.
There is no upper limit on the number of jobs. (except RAM).
Jobs may be of mixed (heterogenous) types in a single [JobQueue] instance. Any type that implements the Job trait may be a job.
Jobs may be async or blocking. Both types can be run in the same JobQueue instance concurrently.
Job results also may be of any type. Typically each type of Job will return a single concrete result type. A [JobResultWrapper] is provided to facilitate this usage pattern.
Each Job has an associated [JobHandle] that is used to await or cancel the
job. If the JobHandle is dropped, the job will be cancelled.
hello job-queue world.
Here we demonstrate the most basic usage by creating a HelloJobAsync job
and running it once in the JobQueue.
We choose an async job for this example because it's a little bit simpler. We don't have to check for job-cancellation in the job itself.
use JobResultWrapper;
use JobQueue;
use *;
// define our custom job type that just returns "hello <name>"
;
// implement Job trait.
async
async vs blocking Job.
To demonstrate the difference, we will implement an example job first as an async job and then as a blocking job.
The example job finds all the prime numbers in a provided range.
We create 100 jobs, each searching a range of 100 numbers. So the first 10000 integers are searched by all jobs.
We use a JobQueue<QueueJobPriority> where QueueJobPriority is an enum we
define that simply has Low and High variants.
The jobs are added to the queue in ascending order but each is assigned a random job priority (either High or Low). High priority jobs will process first, thus queue-processing order will not match the order of adding jobs.
In this example, job results are obtained by awaiting each JobHandle in the order it was added. Thus results are obtained in FIFO order despite the out-of-order processing.
Alternatively a JoinSet or join_all() could be used to await all job-handles simultaneously and obtain results in queue-processing order as they complete.
Likewise in an application with many concurrent tasks, each task might be submitting a job and immediately awaiting the JobHandle. In that scenario whichever task has submitted the highest priority job will obtain results first.
Async Job considerations
- the Job::is_async() impl returns true.
- Job::run_async() or Job::run_async_cancellable() must be implemented.
It is important the job yield regularly to the async runtime. Our processing is inherently blocking, so we accomplish this simply by making the is_prime() fn async, which is called in every loop iteration.
Blocking job considerations
- the Job::is_async() impl returns false.
- Job::run() must be implemented.
- it is necessary to regularly poll for a job-cancellation message in the job's main processing loop.
Example
use JobCompletion;
use JobResultWrapper;
use JobQueue;
use JobCancelReceiver;
use *;
use Rng;
// ### First lets define some common types ###
// -------------------------------------------
// define job priority levels for this job-queue.
// define type alias for a wrapper around the data returned by our
// job type. The wrapper is not required, but simplifies
// conversions.
type FindPrimesJobResult = ;
// ### Now we define an async Job ###
// ----------------------------------
// define our custom job type that finds prime numbers within a range
// The prime-number finding algorithm can be described as:
// Trial Division with Square Root Limit and 6k ± 1 Optimization
//
// we make the functions async because our "impl Job"
// defines this as an async job and thus the runtime needs
// some await points for cancellation and cooperating with
// other async tasks.
// implement Job trait.
// ### Define an equivalent blocking job ###
// -----------------------------------------
// define our custom job type that finds prime numbers within a range
// The prime-number finding algorithm can be described as:
// Trial Division with Square Root Limit and 6k ± 1 Optimization
//
// None of the functions are async because our "impl Job"
// defines this as blocking job. It will be run in tokio's blocking
// threadpool via a spawn_blocking() call in the job-queue.
// implement Job trait.
// Now let's run these jobs.
// -------------------------
// Note that we will be mixing jobs of two different types.
// Result processing is simplified because they both return the same
// result type but that's not necessary.
async