Pemanggilan instance.save() di dalam receiver sinyal post_save merupakan penyebab umum terjadinya RecursionError pada Django. Kesalahan konfigurasi ini memicu infinite loop yang menghabiskan limit call stack Python, berujung pada crash HTTP 500 dan kegagalan proses transaksi pada level database.
Gejala Runtime dan Traceback
Ketika endpoint dieksekusi, web server (Gunicorn, uWSGI, atau runserver) mendadak berhenti memproses request dan melempar exception berikut:
Internal Server Error: /api/orders/1/pay/
Traceback (most recent call last):
File "django/core/handlers/exception.py", line 55, in inner
...
File "myapp/signals.py", line 14, in update_order_code
instance.save()
File "django/db/models/base.py", line 814, in save
...
RecursionError: maximum recursion depth exceeded while calling a Python objectGejala ini ditandai dengan lonjakan tajam CPU, pemakaian memori sementara akibat penumpukan frame fungsi di call stack, dan kegagalan eksekusi tanpa ada log database commit.
Root Cause: Siklus Dispatcher Signal post_save
Akar masalah RecursionError Django ini berasal dari lifecycle internal ORM Django. Alur kerja metode save() melibatkan pemanggilan dispatcher sinyal:
- Aplikasi memanggil
order.save(). - Django mengeksekusi query
INSERTatauUPDATEke database. - Django mengeksekusi
post_save.send(sender=Order, instance=order, created=...). - Receiver menangkap sinyal lalu memanggil
instance.save()kembali. - Tahap 1 berulang kembali tanpa terminasi hingga kedalaman rekursi Python (umumnya 1000 frame) terlewati.
Kode Bermasalah (Sebelum Perbaikan)
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Order
@receiver(post_save, sender=Order)
def generate_order_code(sender, instance, created, **kwargs):
if created and not instance.order_code:
instance.order_code = f"ORD-{instance.id:06d}"
# BUG: Memanggil save() di sini akan memicu kembali sinyal post_save
instance.save()3 Strategi Perbaikan Teknis
1. Bypass ORM Signal Menggunakan QuerySet.update()
Pendekatan paling bersih adalah melakukan eksekusi SQL UPDATE langsung tanpa melalui lifecycle Model.save().
@receiver(post_save, sender=Order)
def generate_order_code(sender, instance, created, **kwargs):
if created and not instance.order_code:
new_code = f"ORD-{instance.id:06d}"
Order.objects.filter(pk=instance.pk).update(order_code=new_code)
instance.order_code = new_codeAnalisis: Metode QuerySet.update() dieksekusi langsung pada level database SQL. Metode ini tidak mengeksekusi instance.save(), sehingga post_save tidak ditembakkan ulang.
2. Guard Clause Menggunakan update_fields
Jika logika bisnis mewajibkan eksekusi override save() pada model, gunakan parameter update_fields sebagai filter pembatas.
@receiver(post_save, sender=Order)
def generate_order_code(sender, instance, created, **kwargs):
update_fields = kwargs.get("update_fields")
# Hentikan rekursi jika pemanggilan save() hanya menargetkan order_code
if update_fields and "order_code" in update_fields:
return
if created and not instance.order_code:
instance.order_code = f"ORD-{instance.id:06d}"
instance.save(update_fields=["order_code"])3. Custom State Flag pada Instance
Untuk kasus kompleks di mana update_fields tidak memungkinkan, gunakan atribut in-memory sementara pada instance Python.
@receiver(post_save, sender=Order)
def generate_order_code(sender, instance, created, **kwargs):
if getattr(instance, "_skip_signal", False):
return
if created and not instance.order_code:
instance.order_code = f"ORD-{instance.id:06d}"
instance._skip_signal = True
try:
instance.save()
finally:
instance._skip_signal = FalsePerbandingan Dampak Query Database
| Strategi | Jumlah Query SQL | Overhead Sinyal | Efek Samping |
|---|---|---|---|
QuerySet.update() | 1 direct UPDATE | Nol (Sinyal ter-bypass) | Field auto_now tidak terupdate otomatis |
update_fields | 1 targeted UPDATE | 1 extra loop (terhenti di guard clause) | Aman, memicu sinyal sekali lagi dengan filter |
_skip_signal flag | 1 full UPDATE | 1 extra loop (terhenti di guard clause) | State hanya bertahan di memory instance aktif |
Rekomendasi: Gunakan
QuerySet.update()jika tidak ada sinyal lanjutan yang dibutuhkan. Gunakanupdate_fieldsjika sistem Anda memiliki listener downstream lain yang memantau perubahan atribut tersebut.
Unit Test Pencegah Regresi
Tulis unit test untuk memastikan alur penyimpanan data tidak memicu rekursi tak terbatas saat instance dibuat.
from django.test import TestCase
from .models import Order
class OrderSignalTest(TestCase):
def test_post_save_does_not_raise_recursion_error(self):
"""Memastikan create() selesai dan order_code terisi tanpa RecursionError."""
try:
order = Order.objects.create(total_amount=150000)
except RecursionError:
self.fail("post_save signal memicu RecursionError tak terbatas.")
order.refresh_from_db()
self.assertEqual(order.order_code, f"ORD-{order.id:06d}")
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!