Coba lagi logika dan ketahanan koneksi dengan mssql-django

Koneksi ke SQL Server dan Azure SQL dapat gagal sementara karena alasan yang tidak ada hubungannya dengan kode Anda:

  • Grup ketersediaan AlwaysOn gagal.
  • Jaringan kehilangan paket saat penyiapan koneksi.
  • Resource Governor membatasi laju database.
  • Replika Azure SQL didaur ulang selama skala atau peningkatan.

Sebagian besar kegagalan ini jelas dalam hitungan detik. Artikel ini menjelaskan cara melakukan percobaan ulang saat terjadi kesalahan sementara dalam aplikasi Django yang menggunakan backend mssql-django, serta cara mengonfigurasi Django dan driver ODBC agar dapat pulih secara otomatis dari terputusnya koneksi idle.

Kesalahan sementara

Kesalahan sementara adalah kegagalan sementara yang diselesaikan sendiri. Mencoba kembali operasi setelah penundaan singkat biasanya berhasil.

Kesalahan berikut bersifat sementara ketika terjadi selama pembentukan koneksi atau saat mengirim permintaan ke server. Coba lagi setelah penundaan singkat yang dibatasi. Kesalahan yang bertahan melewati beberapa percobaan ulang biasanya menunjukkan masalah konfigurasi (server yang salah, izin yang hilang, kuota habis) yang coba lagi tidak akan diperbaiki.

Kesalahan Message Troubleshooting
64 A connection was successfully established with the server, but then an error occurred during the login process. (provider: TCP Provider, error: 0 - The specified network name is no longer available.) Koneksi TCP terputus di tengah proses handshake. Bukan kegagalan kredensial. Jika berlanjut, periksa ketidakstabilan jaringan sisi klien atau perangkat perantara yang menghilangkan koneksi yang setengah dibuat.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Transportasi pra-login atau kegagalan TLS. Server biasanya mengembalikannya ketika tidak dapat menerima koneksi (kelelahan sumber daya, koneksi maks tercapai, atau klien yang tidak didukung). Bukan kegagalan kredensial. Verifikasi kesehatan server, lalu periksa batas waktu masuk klien, pengaturan TLS, dan kompatibilitas versi TLS klien/server.
4060 Cannot open database "%.*ls" requested by the login. The login failed. Login mengautentikasi tetapi tidak dapat membuka database yang diminta. Penyebab sementara antara lain database sedang dalam transisi (failover, pemulihan, penskalaan) atau dijeda secara otomatis. Penyebab yang bersifat permanen (database tidak ada, login tidak memiliki akses) tidak akan teratasi dengan mencoba lagi; periksa nama database, pemetaan login, dan status database.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. Replika tidak tersedia untuk masuk karena versi baris hilang untuk transaksi yang sedang dalam penerbangan saat replika didaur ulang. Batalkan atau komitkan transaksi yang aktif di server primer untuk menyelesaikan masalah. Kurangi risikonya dengan menghindari transaksi penulisan yang berlangsung lama pada node utama.
10053 A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An established connection was aborted by the software in your host machine.) Pihak lokal memutuskan koneksi. Periksa kesehatan jaringan sisi klien dan firewall lokal atau klien VPN apa pun.
10054 A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) Sisi jarak jauh mengirimkan reset TCP. Penyebab umum: proses peer mengalami kegagalan, firewall mengirim reset, atau gateway Azure SQL menutup koneksi yang tidak aktif. Untuk pola reset saat idle, aktifkan TCP keepalive pada klien atau perpendek waktu tunggu idle pada pool koneksi.
10928 Resource ID: %d. The %s limit for the database is %d and has been reached. See 'http://go.microsoft.com/fwlink/?LinkId=267637' for assistance. Database melebihi batas tata kelola sumber daya Azure SQL. ID Sumber Daya 1 menunjukkan batas pekerja; ID Sumber Daya 2 menunjukkan batas sesi. Identifikasi jenis batas berdasarkan pesan, lalu kurangi tingkat konkurensi, tingkatkan kapasitas database, atau persingkat operasi berjalan lama yang menggunakan sumber daya tersebut terlalu lama.
10929 Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d, and the current usage for the database is %d. However, the server is currently too busy to support requests greater than %d for this database. Database telah melampaui batas jaminan minimumnya, dan server yang mendasarinya sedang mengalami pembatasan. Percobaan ulang biasanya berhasil saat beban pada node tetangga menurun. Kejadian berkelanjutan menunjukkan Bahwa Anda memerlukan tingkat layanan yang lebih tinggi atau lingkungan yang kurang bising.
40020,40143,40166,40540 Dilaporkan pada slot Error code %d pada kesalahan 40197 selama failover. Sub-kode yang disematkan dalam pesan failover 40197 bahwa beberapa jalur muncul sebagai nomor kesalahan tingkat atas. Perlakukan mereka sama seperti 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Peningkatan perangkat lunak, kegagalan perangkat keras, atau peristiwa failover lainnya di Azure SQL. Penyambungan ulang akan mengarahkan Anda ke replika yang sehat. Kode kesalahan yang disematkan mengidentifikasi jenis failover. Jika kesalahan berlanjut, ambil ID pelacakan sesi dan hubungi dukungan.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Pelambatan mesin Azure SQL. Batas minimum yang direkomendasikan adalah backoff 10 detik. Throttling yang berkelanjutan menunjukkan bahwa beban kerja telah melebihi alokasi sumber daya database; tingkatkan tingkat layanan atau kurangi tingkat konkurensi.
40613 Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. If the problem persists, contact customer support, and provide them with the session tracing ID of '%.*ls'. Database tidak tersedia, biasanya saat failover sedang berlangsung atau sebentar selama operasi penskalaan. Coba lagi pada backoff; jika bertahan beberapa menit, ambil ID pelacakan sesi dan buka kasus dukungan.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. Kumpulan SQL terdedikasi (Synapse) berada dalam status dijeda. Percobaan ulang berhasil hanya setelah pool dilanjutkan kembali. Aktifkan kembali pool secara eksplisit, atau jadwalkan beban kerja agar berjalan setelah pool diaktifkan kembali.
42109 The SQL pool is warming up. Please try again. Pool SQL khusus sedang dilanjutkan. Coba kembali dengan jeda bertahap hingga pool aktif; inisialisasi awal biasanya memerlukan beberapa menit.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. Server saat ini tidak dapat mengalokasikan sumber daya yang cukup untuk memenuhi permintaan. Coba lagi pada backoff. Jika kesalahan berlanjut, tingkatkan database atau kumpulan elastis.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Batas konkurensi pada tingkat langganan untuk operasi manajemen. Kurangi panggilan pembuatan/pembaruan secara paralel atau beri jeda di antaranya.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Batas konkurensi tingkat langganan pada operasi dalam penerbangan. Kurangi paralelisme atau tunggu hingga operasi dalam penerbangan terkuras.

Kesalahan pada tingkat pernyataan tidak ada dalam daftar ini karena kesalahan tersebut terjadi setelah koneksi terjalin dan kegagalan tersebut membuat sesi tetap dapat digunakan. Kesalahan pernyataan yang dapat dicoba ulang yang paling umum adalah 1205 (korban deadlock) dan 1222 (batas waktu permintaan penguncian). Ulangi seluruh transaksi alih-alih satu pernyataan yang gagal.

Teks pesan kesalahan berasal dari kesalahan koneksi sementara Azure SQL. Driver individual mempertahankan daftar coba lagi bawaan mereka sendiri; katalog ini menjelaskan kesalahan mana yang memenuhi syarat untuk mencoba kembali di SQL Server, Azure SQL Database, Azure SQL Managed Instance, database SQL di Microsoft Fabric, dan kumpulan SQL khusus di Azure Synapse Analytics.

Ketahanan koneksi tidak aktif pada driver ODBC

Driver ODBC Microsoft untuk SQL Server menyediakan ketahanan bawaan terhadap koneksi idle melalui kata kunci string koneksi ConnectRetryCount dan ConnectRetryInterval. Pengaturan ini mengatasi koneksi idle yang terputus di tingkat driver, sebelum kode aplikasi Anda terlibat.

Aktifkan ketahanan koneksi menganggur di extra_params:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "extra_params": "ConnectRetryCount=3;ConnectRetryInterval=10",
        },
    },
}
Keyword Default Deskripsi
ConnectRetryCount 1 Jumlah upaya koneksi ulang otomatis untuk koneksi yang menganggur.
ConnectRetryInterval 10 Detik antara upaya koneksi ulang.

Note

Ketahanan koneksi saat tidak aktif menyambung ulang koneksi yang terputus saat tidak aktif. Ini tidak mencoba ulang kueri yang gagal atau melakukan pemulihan dari kesalahan yang terjadi selama transaksi aktif. Untuk skenario tersebut, gunakan logika coba lagi tingkat aplikasi.

Middleware Django untuk percobaan ulang basis data

Buat middleware Django yang menangkap kesalahan sementara dan mencoba kembali operasi database. Pendekatan ini berfungsi untuk penanganan permintaan tingkat tampilan:

# myproject/middleware.py
import random
import re
import time
import logging
from django.db import OperationalError, connection

logger = logging.getLogger(__name__)

TRANSIENT_ERROR_CODES = {
    "64", "233", "4221",
    "10053", "10054", "10928", "10929",
    "40197", "40501", "40613",
    "49918", "49919", "49920",
    # Include "4060" only if targeting Azure SQL with geo-replication failover.
    # It is usually a permanent error (wrong database name or missing permissions).
}

# Microsoft ODBC driver formats native error codes as "(<number>)" in the
# message. Extracting parenthesized codes avoids false positives that a plain
# substring match would produce for short codes like "64".
_CODE_RE = re.compile(r"\((\d+)\)")


def is_transient(error):
    codes_in_message = set(_CODE_RE.findall(str(error)))
    return bool(codes_in_message & TRANSIENT_ERROR_CODES)


class DatabaseRetryMiddleware:
    """Retry database operations on transient errors."""

    def __init__(self, get_response):
        self.get_response = get_response
        self.max_retries = 3
        self.base_delay = 1   # seconds; doubled each attempt
        self.max_delay = 30   # cap on a single sleep, regardless of attempt

    def __call__(self, request):
        for attempt in range(self.max_retries + 1):
            try:
                return self.get_response(request)
            except OperationalError as e:
                if attempt < self.max_retries and is_transient(e):
                    # Exponential backoff with full jitter, capped at max_delay.
                    # Jitter spreads simultaneous retries so many clients
                    # don't hammer the server in lock-step during an outage.
                    capped = min(self.max_delay, self.base_delay * (2 ** attempt))
                    delay = random.uniform(0, capped)
                    logger.warning(
                        "Transient DB error (attempt %d/%d), retrying in %.2fs: %s",
                        attempt + 1, self.max_retries, delay, e
                    )
                    connection.close()
                    time.sleep(delay)
                    continue
                raise

Daftarkan middleware di settings.py:

MIDDLEWARE = [
    "myproject.middleware.DatabaseRetryMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # ... other middleware
]

Important

Tempatkan DatabaseRetryMiddleware sebelum middleware lain yang mengakses database sehingga dapat menangkap dan mencoba kembali kesalahan sementara dari seluruh alur permintaan.

Coba lagi dekorator untuk operasi tertentu

Untuk kontrol yang lebih halus, gunakan dekorator pada fungsi individual:

import random
import re
import time
import functools
import logging
from django.db import OperationalError, connection

logger = logging.getLogger(__name__)

TRANSIENT_ERROR_CODES = {
    "64", "233", "4221",
    "10053", "10054", "10928", "10929",
    "40197", "40501", "40613",
    "49918", "49919", "49920",
    # Include "4060" only if targeting Azure SQL with geo-replication failover.
}

_CODE_RE = re.compile(r"\((\d+)\)")


def is_transient(error):
    codes_in_message = set(_CODE_RE.findall(str(error)))
    return bool(codes_in_message & TRANSIENT_ERROR_CODES)


def retry_on_transient(max_retries=3, base_delay=1, max_delay=30):
    """Retry on transient database errors with exponential backoff and full jitter."""

    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except OperationalError as e:
                    if attempt < max_retries and is_transient(e):
                        # Exponential cap doubled per attempt, then jittered
                        # within [0, cap] and limited by max_delay.
                        capped = min(max_delay, base_delay * (2 ** attempt))
                        delay = random.uniform(0, capped)
                        logger.warning(
                            "Transient error in %s (attempt %d/%d), retrying in %.2fs: %s",
                            func.__name__, attempt + 1, max_retries, delay, e
                        )
                        connection.close()
                        time.sleep(delay)
                        continue
                    raise
        return wrapper
    return decorator

Terapkan dekorator pada fungsi yang banyak mengakses database:

from myproject.retry import retry_on_transient

@retry_on_transient(max_retries=3, base_delay=2)
def process_order(order_id):
    """Process an order with automatic retry on transient failures."""
    order = Order.objects.select_for_update().get(id=order_id)
    order.status = "processing"
    order.save()
    return order

Coba lagi transaksi

Ketika kesalahan sementara terjadi di dalam transaksi, seluruh transaksi digulung balik oleh server. Coba lagi transaksi lengkap, bukan hanya pernyataan yang gagal:

from django.db import transaction

@retry_on_transient(max_retries=3)
def transfer_funds(from_account_id, to_account_id, amount):
    """Transfer funds between accounts with retry."""
    with transaction.atomic():
        from_account = Account.objects.select_for_update().get(id=from_account_id)
        to_account = Account.objects.select_for_update().get(id=to_account_id)

        from_account.balance -= amount
        to_account.balance += amount

        from_account.save()
        to_account.save()

Perhatian

Jangan coba lagi di dalam transaction.atomic(). Dekorator retry harus membungkus seluruh blok atomic() sehingga setiap percobaan ulang dimulai dengan transaksi baru.

Kesalahan tingkat pernyataan

Daftar kesalahan di bagian sebelumnya mencakup kegagalan tingkat koneksi. Dua kesalahan lainnya biasanya dicoba kembali pada tingkat pernyataan:

  • 1205: Sesi dipilih sebagai korban kebuntuan. Jalankan kembali transaksi.
  • 1222: Batas waktu permintaan penguncian terlampaui. Jalankan ulang transaksi, atau tingkatkan LOCK_TIMEOUT untuk sesi jika nilai default terlalu agresif.

ConnectRetryCount mencoba kembali koneksi yang rusak, sehingga tidak berlaku untuk kesalahan tingkat pernyataan ini. Tangani semuanya menggunakan pola dekorator yang sama dengan menambahkan "1205" dan "1222" ke TRANSIENT_ERROR_CODES untuk transaksi yang aman dijalankan kembali.

CONN_MAX_AGE dan koneksi kedaluwarsa

Django menggunakan kembali koneksi basis data antarpermintaan saat CONN_MAX_AGE diatur. Koneksi yang berlangsung lama mungkin menjadi kedaluwarsa jika server menutup koneksi tersebut (misalnya, selama operasi penskalaan Azure SQL atau batas waktu firewall).

Atur CONN_MAX_AGE untuk menyeimbangkan penggunaan kembali terhadap keusangan:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
        "CONN_MAX_AGE": 600,  # Close and reopen connections after 10 minutes
    },
}
  • CONN_MAX_AGE=0 (default): Tutup koneksi di akhir setiap permintaan. Paling aman tapi paling lambat.
  • CONN_MAX_AGE=600: Gunakan kembali koneksi selama 10 menit. Keseimbangan yang baik untuk sebagian besar aplikasi web.
  • CONN_MAX_AGE=None: Menjaga koneksi tetap terbuka tanpa batas waktu. Gunakan hanya dengan mekanisme percobaan ulang untuk koneksi usang.

CONN_HEALTH_CHECKS (Django 4.1 dan yang lebih baru)

Django 4.1 memperkenalkan CONN_HEALTH_CHECKS, yang memvalidasi koneksi yang digunakan kembali sebelum setiap permintaan. Aktifkan bersama CONN_MAX_AGE untuk mendeteksi koneksi kedaluarsa secara otomatis:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
        "CONN_MAX_AGE": 600,
        "CONN_HEALTH_CHECKS": True,
    },
}

Dengan pemeriksaan kesehatan diaktifkan, Django menjalankan kueri validasi ringan sebelum menggunakan kembali koneksi. Jika koneksi terputus, Django secara transparan membuka koneksi baru alih-alih memunculkan error.

Praktik terbaik

  • Gunakan backoff eksponensial dengan jitter penuh. Gandakan batas pada setiap upaya, lalu tunggu selama durasi acak dalam [0, cap]. Jitter mencegah banyak klien mencoba lagi secara serempak selama gangguan regional, yang jika tidak dapat membuat gangguan singkat berkembang menjadi beban berlebih yang berkepanjangan. Batasi waktu tunggu per percobaan (misalnya, 30 detik) agar total waktu pemulihan tetap terbatas.
  • Atur batas maksimum percobaan ulang. Tiga percobaan ulang dengan backoff eksponensial adalah default yang wajar. Lebih dari lima kali percobaan ulang biasanya menunjukkan adanya masalah yang tidak bersifat sementara.
  • Tutup koneksi sebelum mencoba kembali. Panggil connection.close() agar Django membuka koneksi baru pada upaya berikutnya.
  • Catat setiap coba lagi. Percobaan ulang yang berhasil tanpa terlihat dapat menyembunyikan masalah kinerja. Catat log pada tingkat WARNING agar Anda dapat melacak frekuensi.
  • Jangan coba lagi jika terjadi kesalahan yang tidak bersifat sementara. Kegagalan autentikasi, kesalahan izin, dan kesalahan sintaksis tidak mendapat manfaat dari percobaan ulang.
  • Coba lagi seluruh transaksi. Bungkus transaction.atomic() di dalam logika coba lagi, bukan sebaliknya.
  • Mengaktifkan CONN_HEALTH_CHECKS (Django 4.1 dan yang lebih baru) untuk aplikasi web yang menggunakan CONN_MAX_AGE.