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: Penggunaan auto_now_add=True secara internal menggunakan mekanisme runtime timestamp otomatis. Namun, menggunakan default=timezone.now tetap 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-bugbear

Tambahkan file konfigurasi .flake8 pada root proyek:

[flake8]
max-line-length = 88
extend-select = B008
exclude = 
    .git,
    __pycache__,
    */migrations/*,
    venv

Integrasi 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.