MRQ Documentation
repository·master·Indexed 19 days ago
https://github.com/pricingassistant/mrqMRQ is an opinionated, distributed task queue for Python that uses MongoDB for job and log storage and Redis for queue management. It provides tools for asynchronous task execution via mrq-worker, synchronous execution via mrq-run, and a monitoring interface via mrq-dashboard. The library supports scheduled tasks, timed set queues, and high-throughput IO-bound processing using Gevent greenlets.
What's inside MRQ
- MRQ is a distributed task queue for Python designed to be simple and easy to extend, similar to RQ, while maintaining performance levels close to Celery. It is built on top of MongoDB, Redis, and Gevent. It is specifically optimized for heterogeneous workloads containing both IO-bound and CPU-bound tasks.
Key features of MRQ
masterMRQ provides several features for managing distributed tasks:
- Gevent worker: Enables parallel execution of IO-bound tasks within a single UNIX process for high throughput.
- Dashboard: Provides visibility and control over queued jobs, current jobs, and worker status.
- Per-job logs: Allows viewing the log output of individual tasks via the dashboard.
- Job management: Supports retrying, requeueing, and cancelling jobs via code or the dashboard.
- Job routing: Supports default queues, timeouts, and TTL (Time To Live) values per job.
- Builtin scheduler: Allows scheduling tasks by interval or specific time of day.
- Strategies: Supports sequential or parallel dequeue orders, and a 'burst mode' for batch jobs.
- Subqueues: Supports dequeuing multiple sub-queues using auto-discovery from the worker side.
- Debugging tools: Includes Greenlet tracing for CPU-intensive jobs and an integrated memory leak debugger using
objgraph.
What is a Worker in MRQ
masterA Worker is the unit of processing in MRQ responsible for dequeuing jobs and executing them.
Key characteristics:
- Queue Subscription: A worker is started with a specific list of queues to listen to, processed in the order they are provided.
- Concurrency: A single worker can be configured with concurrency options to handle multiple jobs in parallel using multiple processes and/or multiple greenlets. Even when using multiple processes/greenlets, the entire group is referred to as a single 'worker'.
- Dispatch Mechanism: When concurrency is enabled, the worker polls for waiting jobs and dispatches them to its internal pool of processes or greenlets (e.g., a single Python process managing a pool of greenlets).
Understand Timed Set Execution Behavior
masterWhen using timed set queues, the execution of tasks is tied to their scheduled timestamp.
Important Timing Note: If you enqueue tasks for the future but delay starting your worker, the worker will immediately execute any tasks whose scheduled execution time has already passed.
For example, if you enqueue tasks every 10 seconds but wait 20 seconds before starting the worker, the first two tasks will be executed immediately because their scheduled time is in the past. Subsequent tasks will then follow the intended interval relative to their scheduled times.
Use raw queues for high performance
masterRaw queues prioritize performance by storing only serialized task parameters in Redis. Tasks are only inserted into MongoDB after being dequeued by a worker. This reduces visibility for individual queued jobs but increases throughput.
There are four types of raw queues determined by their name suffix:
_raw: Simplest type, stored in a Redis LIST._set: Stored in a Redis SET. Supports "unique" tasks (only one instance of a specific task/parameter pair can be queued at a time)._sorted_set: Stored in a Redis ZSET. Allows ordering/re-ordering tasks. Like_set, task parameters are unique._timed_set: A special_sorted_setwhere tasks are sorted by a UNIX timestamp, allowing you to schedule tasks for a specific time in the future.
Understand the difference between Tasks and Jobs
masterMRQ distinguishes between the definition of work and its execution:
- Task: A Python class (subclassing
mrq.task.Task) that wraps a unit of processing. It defines what to do. - Job: An instance of a Task being executed. A Job is linked to a specific Task via its path, has specific parameters, and is queued in a Queue to be processed by a Worker. It tracks execution metadata like status and tracebacks.
A Task can invoke other tasks either synchronously or asynchronously by queuing them as new Jobs.
- Task: A Python class (subclassing
Design tasks to be reentrant
masterBecause MRQ workers can be interrupted at any time (e.g., due to server restarts or process termination), all tasks should be designed to be reentrant. A reentrant task is one that can be interrupted mid-execution and safely called again before the previous invocation completes. MRQ handles interruptions by automatically requeueing jobs, but your task logic must be able to handle being restarted without causing side effects or data corruption.Use the MRQ scheduler for recurring tasks
masterMRQ includes a built-in scheduler that allows you to run tasks at regular intervals (e.g., every X units of time), similar to how
crontabworks.If you need to schedule a job to run at a specific, precise time in the future rather than on a recurring interval, use
timed_setavailable via raw queues.Use regular queues in MRQ
masterRegular queues store tasks directly in MongoDB. By default, they follow a FIFO (First-In-First-Out) pattern. You can transform a regular queue into a LIFO (Last-In-First-Out) 'pile' by appending
_reverseto the queue name when starting a worker.# To dequeue the last jobs added to the queue "default" (LIFO) $ mrq-worker default_reverseJob statuses and lifecycle
masterJobs transition through various statuses. Understanding these is critical for monitoring and error handling:
Standard Lifecycle:
queued: Created and waiting for a Worker.started: A Worker has begun execution.success: Execution completed successfully.
Error and Interruption States:
failed: A Python Exception was raised during execution.cancel: The job was cancelled (usually via the Dashboard). Note: Cancelling astartedjob does not interrupt the currently running code.abort: The job was stopped viaabort_current_job(). Used for unrecoverable errors you want to log without retrying.interrupt: The worker process received aSIGTERMor twoSIGINTs. (Note:SIGKILLor power loss will leave the job instartedstate).timeout: The job exceeded its configured timeout.retry:task.retry()was called to schedule a retry.maxretries: The task reached its maximum allowed retry attempts (default is 3).
Compare MRQ Queue Types: Regular, Raw, and Redis-only
masterChoosing a queue type involves balancing performance, dashboard visibility, and job safety.
Queue type Regular Raw Raw with no_storage config Storage for queued jobs MongoDB Redis Redis Storage for started & success jobs MongoDB MongoDB None Performance + ++ +++ Visibility in the dashboard Full After start Job counts & failed jobs Safety +++ ++ + Safety Considerations
- Regular Queue: Highest safety; jobs are guaranteed not to be lost once inserted in MongoDB.
- Raw Queue: Risk of job loss if the worker exits abruptly between dequeuing from Redis and inserting into MongoDB.
- Redis-only Raw Queue: Lowest safety; cannot guarantee a job is finished if the worker exits abruptly after dequeuing.
Send metrics to Graphite using METRIC_HOOK
masterMRQ does not support Graphite out of the box, but you can integrate it by implementing a
METRIC_HOOKin yourmrq-configfile. This hook is called by MRQ whenever a metric is recorded.To use Graphite, you should install the
graphiteudppackage via pip, initialize aGraphiteUDPClient, and then define theMETRIC_HOOKfunction to send the metric name and increment value to your Graphite instance. You can use logic within the hook (such as a whitelist) to filter which metrics are sent to avoid overwhelming your monitoring system.import graphiteudp # Install this via pip # Initialize the Graphite UDP Client _graphite_client = graphiteudp.GraphiteUDPClient(host, port, prefix, debug=False) _graphite_client.init() def METRIC_HOOK(name, incr=1, **kwargs): # You can use this to avoid sending too many different metrics whitelisted_metrics = ["queues.all.", "queues.default.", "jobs."] if any([name.startswith(m) for m in whitelisted_metrics]): _graphite_client.send(name, incr)