rq icon indicating copy to clipboard operation
rq copied to clipboard

Enable automatic reload upon source code changes

Open nvie opened this issue 14 years ago • 18 comments

This shouldn't be too fancy, but it is extremely useful to avoid making mistakes where you change some of your job function's code and forget to reload your worker.

nvie avatar Nov 14 '11 11:11 nvie

This would be a nice feature indeed +1

honi avatar Apr 13 '15 14:04 honi

i support this

mschettler avatar Oct 02 '15 23:10 mschettler

Can you manually send a signal to reload a worker?

honi avatar Jul 03 '17 14:07 honi

One solution for this would be to write a script that runs workers in --burst mode infinitely.

Example:

#!/usr/bin/env bash
while true; do
  rqworker --burst
  sleep 1
done

Of course it won't work reliably if you have long monolithic jobs, but in that case you have a bigger problem than reloading your workers ;)

ffigiel avatar Nov 24 '17 09:11 ffigiel

Any update on this?

theomessin avatar Jun 15 '19 02:06 theomessin

No one is working on this as far as I know. It would be nice to have this feature though. Django uses watchman to trigger reloads. I'd accept a PR that implements live reload functionality :)

selwin avatar Jun 15 '19 02:06 selwin

Thanks for the update. I'll see if I find the time to submit a PR. For now just went with using entr with ack (doesn't work if you add new files though):

ack -f | entr -r -s "rq worker -u redis://redis:6379"

theomessin avatar Jun 15 '19 03:06 theomessin

Was going through the code and I couldn't really see why you'd have to reload. Aren't functions "imported" using getattr during execution: https://github.com/rq/rq/blob/549648bd1bd3e2d8e27c2a2d3ac1628f9252e5fe/rq/job.py#L191-L200

I tested this real-quick by starting a worker where the job initially made a file "old" and issued a job (the file "old" was made). Then I deleted that file, changed the filename to "new", and issued another job (without restarting the worker). A "new" file was made. Am I missing something?

theomessin avatar Jun 15 '19 05:06 theomessin

This is how I'm currently autoreloading the worker process. This is a management command I called devrqworker, so instead of running manage.py rqworker I simply run manage.py devrqworker during dev.

import os
import shlex
import subprocess

from django.core.management.base import BaseCommand
from django.utils.autoreload import run_with_reloader


class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument('queues', nargs='+', type=str)
        parser.add_argument('--worker-pid-file', action='store', dest='worker_pid_file', default='/tmp/rqworker.pid')

    def handle(self, *args, **options):
        run_with_reloader(run_worker, options['queues'], options['worker_pid_file'])


def run_worker(queues, worker_pid_file):
    if os.path.exists(worker_pid_file):
        worker_pid = subprocess.run(['cat', worker_pid_file], stdout=subprocess.PIPE).stdout.decode('utf-8')
        kill_worker_cmd = f'kill {worker_pid}'
        subprocess.run(shlex.split(kill_worker_cmd), stderr=subprocess.PIPE)

    queues = ' '.join(queues)
    start_worker_cmd = f'{get_managepy_path()} rqworker {queues} --pid={worker_pid_file}'
    subprocess.run(shlex.split(start_worker_cmd))


def get_managepy_path():
    managepy_path = None
    search_path = os.path.dirname(__file__)
    while not managepy_path:
        if os.path.exists(os.path.join(search_path, 'manage.py')):
            managepy_path = os.path.abspath(os.path.join(search_path, 'manage.py'))
        else:
            search_path = os.path.join(search_path, '../')
    return managepy_path

honi avatar Jun 15 '19 12:06 honi

There's a package called django-rq-wrapper that implements autoreload and start multiple workers. I didn't test it, but seems pretty simple and not evasive since it brings a new management command called "rqworkers".

https://github.com/istrategylabs/django-rq-wrapper

sidneijp avatar Sep 26 '19 14:09 sidneijp

@honi THanks. Works well.

PSA: Don't use it in production. This eats HELLA memory. Like 100 mb (regular rq worker) vs 300 mb (this custom version)

taewookim avatar Aug 13 '20 21:08 taewookim

Correct! In production you should use the default rqworker management command.

honi avatar Aug 13 '20 22:08 honi

Like in https://github.com/rq/rq/issues/2#issuecomment-502336474 I made a simple test and it worked. Can someone send me an example so I can look at it.

rpkak avatar May 12 '21 07:05 rpkak

When you enqueue() something, it creates a job using the reference for the function/callable (which can be both a string or a callable, if the latter, it will be converted to a string - basically the import path). This sets a couple of private attributes to the job (_func_name and _instance), those attributes will then be used by the job itself to getattr or import the relevant code (this happens on the func property of the job).

What the worker does is basically just call the perform method of the job, that's what actually executes the callable. So indeed, there shouldn't be a need to add a reload functionality, since the worker doesn't know the job before it actually pulls it from the queue.

If anyone has any issues with this, feel free to open a new issue so we can investigate. Closing it for now.

ccrvlh avatar Jan 27 '23 01:01 ccrvlh

Python will cache modules that have already been imported, and the default worker will fork new processes when executing tasks, so it will not be affected by module caching. If you use simple worker, there will be problems when making code changes

aksu-104128 avatar Jul 08 '23 18:07 aksu-104128