Gejala: Timestamp Statis Sesuai Waktu Boot Worker
Insiden frozen timestamp sering terdeteksi di lingkungan produksi ketika audit data menemukan ratusan hingga ribuan entri baru memiliki nilai created_at yang persis sama hingga presisi mikrodetik. Nilai tersebut tidak bertambah seiring masuknya transaksi baru, melainkan melompat ke nilai statis lain hanya saat instance aplikasi di-deploy ulang atau worker di-restart.
Pemeriksaan log server web (seperti Gunicorn, uWSGI, atau Daphne) akan memperlihatkan korelasi langsung: nilai timestamp yang tersimpan di database identik dengan waktu proses worker selesai dimuat (boot time). Jika satu worker berjalan selama 4 hari tanpa restart, seluruh record yang dibuat oleh worker tersebut selama 4 hari akan memiliki nilai created_at yang sama persis.
Root Cause: Evaluasi Import Time vs. Runtime Callable
Penyebab utama dari masalah ini adalah perbedaan mendasar antara meneruskan callable reference dan hasil eksekusi fungsi pada saat inisialisasi class di Python.
# CONTOH SALAH: timezone.now() dieksekusi saat modul di-import
class Order(models.Model):
created_at = models.DateTimeField(default=timezone.now())
# CONTOH BENAR: timezone.now diteruskan sebagai callable reference
class Order(models.Model):
created_at = models.DateTimeField(default=timezone.now)Python membaca dan mengeksekusi definisi class (class definition scope) tepat satu kali ketika modul pertama kali di-import ke dalam memori oleh proses Python interpreter. Ketika baris default=timezone.now() dieksekusi, Python langsung memanggil fungsi tersebut, memperoleh sebuah objek datetime.datetime instan, dan menempelkan objek statis tersebut ke argumen default dari instance DateTimeField.
Ketika Django ORM menginisialisasi instance model baru melalui Order.objects.create(), Django memeriksa atribut default. Jika nilainya bukan callable (seperti objek datetime statis tadi), Django akan menggunakan objek yang sama berulang kali. Sebaliknya, jika kita meneruskan callable reference (default=timezone.now tanpa tanda kurung), Django mendeteksi tipe callable dan mengeksekusinya setiap kali baris baru diinstansiasi di runtime.
Catatan: Penggunaanauto_now_add=Truesecara internal menggunakan mekanisme runtime timestamp otomatis. Namun, menggunakandefault=timezone.nowtetap menjadi standar jika field tersebut perlu dimanipulasi manual pada skenario tertentu seperti fixture testing atau data seeding.
Solusi 1: Perbaikan Skema Model
Langkah pertama adalah menghapus tanda kurung pada definisi field di model Django terkait:
# models.py
from django.db import models
from django.utils import timezone
class Invoice(models.Model):
reference_number = models.CharField(max_length=64, unique=True)
created_at = models.DateTimeField(default=timezone.now, db_index=True)
updated_at = models.DateTimeField(auto_now=True)Setelah kode diubah, jalankan python manage.py makemigrations. Django akan mendeteksi perubahan dari nilai default literal menjadi fungsi callable dan menghasilkan file migrasi skema.
Solusi 2: Remediasi Data Korup via Data Migration
Memperbaiki model hanya menghentikan pembentukan data baru yang korup. Data yang sudah terlanjur tersimpan di database membutuhkan koreksi. Jika tabel Anda memiliki relasi audit atau record log eksternal (misalnya payment gateway log atau access log), gunakan referensi tersebut. Namun, jika tidak ada, alternatif yang lazim diterapkan adalah menginterpolasi nilai timestamp berdasarkan ID urut atau foreign key dari tabel induk yang valid.
Berikut adalah skrip migrasi data menggunakan RunPython untuk memperbaiki record yang terdampak berdasarkan relasi log:
# migrations/0002_fix_frozen_timestamps.py
from django.db import migrations
from django.db.models import Subquery, OuterRef
def repair_frozen_timestamps(apps, schema_editor):
Invoice = apps.get_model('billing', 'Invoice')
InvoiceLog = apps.get_model('billing', 'InvoiceLog')
# Ambil timestamp dari log pertama jika created_at identik dengan worker boot anomalies
subquery = InvoiceLog.objects.filter(
invoice_id=OuterRef('id')
).order_by('created_at').values('created_at')[:1]
Invoice.objects.filter(
created_at__in=['2024-03-01 04:00:00.123456+00:00'] # Timestamp anomali
).update(created_at=Subquery(subquery))
def rollback(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('billing', '0001_initial'),
]
operations = [
migrations.RunPython(repair_frozen_timestamps, reverse_code=rollback),
]Pencegahan Otomatis di Pipeline CI dengan flake8-bugbear
Review kode secara manual rawan melewatkan tanda kurung tambahan seperti timezone.now(). Cara paling andal untuk mencegah regresi ke repositori adalah dengan memanfaatkan Abstract Syntax Tree (AST) linting statis.
Plugin flake8-bugbear menyediakan aturan B008 (Do not perform function calls in argument defaults). Aturan ini mendeteksi pemanggilan fungsi di dalam deklarasi argumen, termasuk pada model fields.
Instalasi dan Konfigurasi
pip install flake8 flake8-bugbearTambahkan file konfigurasi .flake8 pada root proyek:
[flake8]
max-line-length = 88
extend-select = B008
exclude =
.git,
__pycache__,
*/migrations/*,
venvIntegrasi ke GitHub Actions
name: Code Quality
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install flake8 flake8-bugbear
- name: Run Linter
run: |
flake8 apps/Dengan pipeline ini, setiap PR yang memuat pemanggilan default=timezone.now() akan langsung ditolak oleh runner CI sebelum sempat mencapai branch utama atau staging.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!