09 - Celery ile periyodik işler

June 10, 2018 6 minute read

En son, ekranlarımızı güncelleyen bir komut hazırlamıştık. Şimdi bu komutu periyodik olarak çalıştırmamız gerekiyor. Önceki yazımızda cron ile bunu nasıl yapabileceğimize dair değinmiştik; ama biz bunu Celery1 ile yapmayı deneyeceğiz.

Özet

  • Docker ile, Celery’i kolayca kurup kullanabileceğimiz geliştirme ortamını hazırlayacağız.
  • Django komutumuzu Celery görev fonksiyonunda kullanabilir hale getireceğiz.
  • Admin panelinden görevlerin çalışma zamanlarını ayarlayacağız.
  • Bu yazıyla ilgili kodlara buradan erişebilirsiniz.

Neden Celery ve neden Docker?

Önceki yazımızda hazırladığımız checkurls komutunu periyodik olarak çalıştırabilmek için bir görev yöneticisine ihtiyacımız var. Celery benim bu tip işler için en sık kullandığım araç. Celery ile görevleri yönetmek kolay, deployment süreçlerinde sunucuya daha az müdahale ediliyor, görev çalıştırma zamanlarını admin panelinden programlamak mümkün.

Bir web projesi geliştirirken DEVELOPMENT, STAGING, PRODUCTION gibi farklı sunucu ortamları hazırlanır2 ve bu ortamların birbirine olabildiğince benzemesi arzu edilir, böylece çevre farklılıklarından doğacak sorunları önlemiş oluruz. Bunu yapabilmek için sanallaştırmaya ihtiyacımız var ve bunun çeşitli yöntemleri var:

  1. Daha önce ben Vagrant üzerine bir yazı yazmıştım. Alışıldığında oldukça pratik yöntemdir, hiçbir ayar yapmasanız bile elinizin altında PRODUCTION sunucunuza benzer geliştirme ortamınız olur.
  2. Docker kullanabilirsiniz, alışması yeni başlayanlar için öğrenme süreci zaman alıyor. Ama uzun vadede hız ve esneklik kazanırsınız.
  3. WSL, VMware veya Virtualbox ile sanal işletim sistemi yükleme ve onun üzerinden geliştirme yapabilirsiniz. Olabilecek en kötü sanallaştırma yöntemidir, kısa vadede belki hız kazandırır; ama uzun vadede deployment süreçlerinde canınızı sıkabilir.

Ben Docker’i seçtim ve merak etmeyin, bu eğitimi devam ettirebilmek için Docker konusuna olabildiğince az değineceğim. Şimdilik Docker ve Docker Compose‘u yükleyin3 ve Docker servisinin başladığına emin olun. Sonra repomuzdan Article-09/hello_django dizinine girin ve aşağıdaki komutu çalıştırın:

# terminal
$ docker-compose up --build -d
Creating network "hello_django_default" with the default driver
Building web
Step 1/7 : FROM python:3.6
 ---> d69bc9d9b016
...
Creating hello_django_web_1  ... done
Creating hello_django_amqp_1 ... done
Creating hello_django_celery_1 ... done
Creating hello_django_celery-beat_1 ... done

Uzun bir çıktı ve nihayetinde dört konteyner ile geliştirme ortamımız hazır. Her konteynerin içinde bir komut çalışıyor, örneğin web’in içinde python manage.py runserver 0:8000 komutu çalışıyor, Python yorumlayıcımız artık bu konteynerin içinde ve konteyner ayağa kalktığında http://localhost:8000 bağlantısından projemize de erişebiliyor olacağız. Diğer üç konteynerin birinde Celery için gerekli AMQP hizmeti çalışıyor, diğer ikisinde de görevlerimizi çalıştıran bir Celery worker ve periyodik olarak görev emirlerini oluşturan Celery beat var.

Celery ile ilk görevin tanımlanması

Docker konteynerlerimiz hazırlanırken requirements.txt dosyamızdan bağımlılıklarımız da okunuyor. Birkaç yeni bağımlılık ekledik:

# requirements.txt

# Görev yönetimi için Celery
celery==4.1.1
# Görev zamanlarını Django Admin panelinden yönetmek
# için ek bir bağımlılık daha
django_celery_beat==1.1.1
# Bu da ekranlarımızı filtrelerken kullanacağımız yardımcı bir bağımlılık
python-dateutil==2.7.3

Şimdi Celery konteynerimizin nasıl çalıştığını ve hangi komutu çalıştırdığını anlayalım. Önce settings.py dosyamızda Celery ile ilgili kurulum ve ayarlarımızı ekledik:

# settings.py
...
INSTALLED_APPS = [
    ...
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_celery_beat',  # Admin panelinde görev zamanlarını yönetmek için
    'hello_palette',
    ...
]
...
CELERY_BROKER_URL = 'amqp'  # Docker konteynerimizin adını yazdık.
CELERY_IMPORTS = [
    'hello_uptime.tasks'  # Görevlerimizi bu modülde saklayacağız.
]
# Test mesajlarımız için basit bir email backend yapılandırması.
# Emailleri konsolda göreceğiz.
DEFAULT_FROM_EMAIL = '[email protected]'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Sonra, Celery uygulamamızı Django projemiz ile entegre çalışabilmesi için bir script hazırladık:

# hello_django/celery.py
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery

# Ayarları settings.py dosyamızdan alacak.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hello_django.settings')

app = Celery('hello_django')

# Ayar değişkenlerimiz hep CELERY diye başlayacak.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Tüm görev fonksiyonlarımızı Celery kendisi tespit edecek.
app.autodiscover_tasks()
# hello_django/__init__.py
from __future__ import absolute_import, unicode_literals

# Bu aslında zorunlu değil; ama görevlerimizi tanımlarken `shared_task` yardımcı
# fonksiyonunu kullanmak, uygulamayı bağımsız hale getiriyor. Örneğini sonra göreceğiz.
from .celery import app as celery_app

__all__ = ['celery_app']

Bunu her projede bir seferlik yapıyoruz. Bundan sonraki aşamadak artık görev fonksiyonları yazmak. Şimdi checkurls komutumuzu bir görev fonksiyonu haline getirelim:

# hello_uptime/tasks.py
from celery import shared_task
from django.core.management import call_command


@shared_task
def check_monitors():
    call_command('checkurls', mail_clients=True)

Django komutumuzu görev fonksiyonu içinde --mail_clients parametresiyle çalıştırmış olduk. Fakat önceki yazımızda parametre olarak sadece urls vardı ve onu da zorunlu yapmıştık. Şimdi --mail_clients parametresine ihtiyacımız var; çünkü herhangi bir ekran erişilemez olduğunda, kullanıcıya bir bildirim epostası göndermek istiyoruz. Komut satırında böyle bir ihtiyacımız yoktu, çünkü her şeyi zaten komut satırında görüyorduk:

# hello_uptime/management/commands/checkurls.py
...
class Command(BaseCommand):
    ...
    def add_arguments(self, parser):
        # urls parametresi artık zorunlu değil
        parser.add_argument('urls', nargs=argparse.ZERO_OR_MORE, type=str)
        # mail_clients parametresi de tercihe bağlı
        parser.add_argument('--mail_clients', action='store_true', dest='mail_clients')

    def handle(self, *args, **options):
        now = timezone.now()
        offline_urls = []

        # Monitörler kaç dakikada bir kontrol edilmesi istendiyse, en az bir o kadar dakika kadar öncesine göre
        # monitörleri filtreliyoruz. Bir de sadece aktif olanları tekrar kontrol edeceğiz.
        available_monitors = Monitor.objects.filter(is_active=True).filter(
            models.Q(interval=MonitoringInterval.MIN_5,
                     checked_at__lt=now - relativedelta(seconds=MonitoringInterval.MIN_5)) |
            ...
            models.Q(interval=MonitoringInterval.HOUR_6,
                     checked_at__lt=now - relativedelta(seconds=MonitoringInterval.HOUR_6)))
        ...
        # Burada da tüm erişilemeyen ekranlar için her kullanıcıya eposta göndereceğiz.
        if options['mail_clients'] and offline_urls:
            for url in offline_urls:
                self.mail_clients(url, available_monitors)

    def mail_clients(self, url, available_monitors):
        subject = "[Hello Uptime] Monitor is DOWN: {}".format(url)
        for monitor in available_monitors.filter(url=url, user__isnull=False):
            message_list = [
                "Hi {},".format(monitor.user.get_full_name()),
                "The monitor ({}) is currently DOWN.".format(url),
            ]
            send_mail(
                subject, '\n'.join(message_list), from_email=settings.DEFAULT_FROM_EMAIL,
                recipient_list=[monitor.user.email])

Görevin periyodik olarak çalıştırılması

Artık sona yaklaşmak üzereyiz. Konteynerimizde checkurls komutumuzu şöyle çalıştırabiliriz:

# terminal
$ docker-compose exec web python manage.py checkurls
https://gokmengorgen.net - 2 monitor(s): online

Şimdi Celery ile görev fonksiyonumuzu çalıştıralım:

# terminal
$ docker-compose exec web python manage.py shell
Python 3.6.5 (default, May  5 2018, 03:09:35)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from hello_uptime.tasks import check_monitors

In [2]: check_monitors.delay()
Out[2]: <AsyncResult: c2f340dc-...f>

In [3]:

Görev asenkron çalıştığı için bir sonuç göremedik, onun için Celery konteynerindeki logları okumamız gerekiyor:

# terminal
$ docker-compose logs -f --tail 100 celery
celery_1  | [...] Received task: hello_uptime.tasks.check_monitors[31399a19-...e]
celery_1  | [...] https://gokmengorgen.net - 2 monitor(s):
celery_1  | [...] online
celery_1  | [...] Task hello_uptime.tasks.check_monitors[31399a19-...e] succeeded in 0.8740212999982759s: None
celery_1  | [...] Received task: hello_uptime.tasks.check_monitors[c2f340dc-...f]
celery_1  | [...] Task hello_uptime.tasks.check_monitors[c2f340dc-...f] succeeded in 0.013204600007156841s: None

Logları sadeleştirdim yine de çirkin, bu konuya daha sonra değiniriz, ama başarılı bir şekilde çalıştığını gördük. Peki ama nasıl periyodik çalıştırılacak bu görev? Hemen Admin panelimizi açıp bir Interval girdisi oluşturalım:

Celery admin interval

Ve bir de Periodic Task girdisi:

Celery admin periodic task

Şimdi tekrar Celery loglarımıza bakıp süreci takip edelim, gerçekten de dakikada bir çalışıyor mu:

# terminal
$ docker-compose logs -f --tail 100 celery
celery_1  | [2018-06-12 16:34:08,298: INFO] Received task: hello_uptime.tasks.check_monitors[92ef036c-...3]
celery_1  | [2018-06-12 16:34:09,926: INFO] Task hello_uptime.tasks.check_monitors[92ef036c-5341-...3] succeeded in 1.6s: None
celery_1  | [2018-06-12 16:35:08,298: INFO] Received task: hello_uptime.tasks.check_monitors[e9a53576-...c]
celery_1  | [2018-06-12 16:35:08,305: INFO] Task hello_uptime.tasks.check_monitors[e9a53576-...c] succeeded in 0.01s: None
celery_1  | [2018-06-12 16:36:08,298: INFO] Received task: hello_uptime.tasks.check_monitors[5574fbc6-...0]
celery_1  | [2018-06-12 16:36:08,306: INFO] Task hello_uptime.tasks.check_monitors[5574fbc6-...0] succeeded in 0.01s: None

Gayet beklediğimiz gibi ilerliyor. Yazıyı 5 dakikaya sığdırmak epey zor oldu; ama seriyi devam ettirmeye enerjim var, merak etmeyin. Sonraki yazımızda görüşmek üzere.


  1. Celery websitesinde detaylı bilgi bulabilirsiniz. [return]
  2. Önce development konusunu uptime ile bitirelim, sonraki yazılarda deployment konusuna değineceğiz. [return]
  3. Buradan indirebilirsiniz. MacOS, Windows, Ubuntu, hatta yerele kurmayıp AWS gibi cloud çözümler üzerinden bile Docker kullanmak mümkün. [return]