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 dapat bersifat sementara ketika terjadi saat proses koneksi terbentuk atau saat mengirim permintaan ke server. Coba lagi setelah penundaan singkat yang dibatasi. Kesalahan yang bertahan setelah beberapa kali percobaan ulang biasanya menunjukkan masalah konfigurasi, seperti server yang salah, izin yang hilang, pengaturan enkripsi yang tidak kompatibel, atau kuota yang habis, yang tidak dapat diperbaiki oleh upaya ulang.

Kesalahan Jenis kegagalan Message Troubleshooting
64 Transportasi saat login 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 Transport sebelum login atau TLS The client was unable to establish a connection because of an error during connection initialization process before login. Server biasanya mengembalikan kesalahan ini ketika tidak dapat menerima koneksi karena kehabisan sumber daya, batas koneksi, 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 Ketersediaan atau akses basis data 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 Transisi sekunder yang dapat dibaca 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 Penghentian transportasi lokal 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 Setel ulang transport jarak jauh 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.
10060 Waktu koneksi habis A connection attempt failed because the connected party did not properly respond after a period of time. Server atau perangkat jaringan perantara tidak menjawab sebelum timeout koneksi TCP. Periksa kesehatan server, routing, aturan firewall, dan apakah host serta port yang dikonfigurasi dapat dijangkau.
10928 Batas sumber daya basis data Resource ID: %d. The %s limit for the database is %d and has been reached. 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 Pembatasan basis data 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 subkode failover Azure SQL Dilaporkan pada slot Error code %d pada kesalahan 40197 selama failover. Subkode yang tertanam dalam pesan failover 40197 bahwa beberapa jalur muncul sebagai nomor kesalahan tingkat atas. Perlakukan mereka sama seperti 40197.
40197 Azure SQL failover 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 Pembatasan Azure SQL 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 berkelanjutan menunjukkan beban kerja melebihi alokasi sumber daya database; Tingkatkan tingkat layanan atau kurangi konkurensi.
40613 Database tidak tersedia 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 Pool SQL dijeda 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 Melanjutkan SQL pool 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 Kekurangan sumber daya layanan 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 Pembatasan operasi manajemen 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 Pembatasan operasi langganan 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. Kesalahan ini dapat dicoba ulang di SQL Server, Azure SQL Database, Azure SQL Managed Instance, database SQL di Microsoft Fabric, dan pool 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.