Asosiy qismga o'tish

Tashqi dunyo: HTTP, timeout va qayta urinish

12-bob 9 kod bloki 2613 so'z

Tarmoq — ishonchsiz

Fayl o'qish deyarli har doim ishlaydi. Tarmoq so'rovi esa yo'q: server sekin javob berishi, xato qaytarishi, yoki umuman javob bermasligi mumkin.

Shuning uchun tarmoq kodi uch narsani hisobga olishi shart: timeout, qayta urinish va xato holatlari.

Sinov uchun o'z serverimizni ko'taramiz — tashqi internetga bog'lanmaymiz, natija takrorlanadigan bo'lsin.

shu faylni yaratingsinov_server.py

python49 qator
"""Sinov uchun kichik HTTP server: turli xatolarni ataylab qaytaradi."""
import json
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

HOLAT = {"urinishlar": 0}


class Ishlovchi(BaseHTTPRequestHandler):
    def log_message(self, *args):          # jim turadi
        pass

    def _javob(self, kod: int, tana: dict) -> None:
        xom = json.dumps(tana).encode()
        self.send_response(kod)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(xom)))
        self.end_headers()
        self.wfile.write(xom)

    def do_GET(self):
        yol = self.path
        if yol == "/ok":
            self._javob(200, {"holat": "yaxshi", "talabalar": 3})
        elif yol == "/sekin":
            time.sleep(3)
            self._javob(200, {"holat": "kech javob"})
        elif yol == "/xato500":
            self._javob(500, {"xato": "server ichki xatosi"})
        elif yol == "/topilmadi":
            self._javob(404, {"xato": "topilmadi"})
        elif yol == "/uchinchida":
            HOLAT["urinishlar"] += 1
            if HOLAT["urinishlar"] < 3:
                self._javob(503, {"xato": "band", "urinish": HOLAT["urinishlar"]})
            else:
                self._javob(200, {"holat": "nihoyat", "urinish": HOLAT["urinishlar"]})
        elif yol == "/nolla":
            HOLAT["urinishlar"] = 0
            self._javob(200, {"holat": "nollandi"})
        else:
            self._javob(404, {"xato": "noma'lum yo'l"})


def ishga_tushir(port: int = 0) -> tuple[HTTPServer, str]:
    server = HTTPServer(("127.0.0.1", port), Ishlovchi)
    threading.Thread(target=server.serve_forever, daemon=True).start()
    return server, f"http://127.0.0.1:{server.server_port}"

Timeout — majburiy

python24 qator
import sys
sys.path.insert(0, str(ISH))
import time
import urllib.error
import urllib.request
from sinov_server import ishga_tushir

server, manzil = ishga_tushir()
print("server:", manzil)

def sorov(yol, timeout):
    t0 = time.perf_counter()
    try:
        with urllib.request.urlopen(manzil + yol, timeout=timeout) as j:
            return j.status, round((time.perf_counter() - t0) * 1000), None
    except Exception as e:
        return None, round((time.perf_counter() - t0) * 1000), type(e).__name__

import pandas as pd
qatorlar = []
for yol, t in [("/ok", 5), ("/sekin", 1), ("/sekin", 5)]:
    kod, ms, xato = sorov(yol, t)
    qatorlar.append({"yo'l": yol, "timeout, s": t, "kod": kod, "vaqt, ms": ms, "xato": xato or "-"})
show(pd.DataFrame(qatorlar))
Chiqish
server: http://127.0.0.1:39435
yo'ltimeout, skodvaqt, msxato
/ok52001-
/sekin1NaN1 001TimeoutError
/sekin5NaN5 003TimeoutError

/sekin 3 soniya kutadi. 1 soniyalik timeout bilan u uzildi, 5 soniyalik bilan o'tdi.

:::gotcha Timeout sukut bo'yicha YO'Q urllib.request.urlopen(url) va requests.get(url) — timeout sizsiz cheksiz kutadi. Server javob bermasa, dasturingiz abadiy osilib qoladi. Bu ishlab chiqarishdagi eng keng tarqalgan osilish sababi. Har so'rovda timeout yozing. :::

Xato holatlarini ajratish

python22 qator
import urllib.error, urllib.request, json

def olish(manzil, yol, timeout=5):
    """(ma'lumot, xato_matni) qaytaradi."""
    try:
        with urllib.request.urlopen(manzil + yol, timeout=timeout) as j:
            return json.loads(j.read()), None
    except urllib.error.HTTPError as e:
        return None, f"HTTP {e.code}"
    except urllib.error.URLError as e:
        return None, f"ulanib bo'lmadi: {e.reason}"
    except json.JSONDecodeError:
        return None, "javob JSON emas"

import pandas as pd
qatorlar = []
for yol in ["/ok", "/xato500", "/topilmadi", "/nomalum"]:
    malumot, xato = olish(manzil, yol)
    qatorlar.append({"yo'l": yol, "ma'lumot": str(malumot), "xato": xato or "-"})
qatorlar.append({"yo'l": "(o'chiq port)", "ma'lumot": str(olish("http://127.0.0.1:1", "/x", 2)[0]),
                 "xato": olish("http://127.0.0.1:1", "/x", 2)[1]})
show(pd.DataFrame(qatorlar))
yo'lma'lumotxato
/ok{'holat': 'yaxshi', 'talabalar': 3}-
/xato500NoneHTTP 500
/topilmadiNoneHTTP 404
/nomalumNoneHTTP 404
(o'chiq port)Noneulanib bo'lmadi: [Errno 111] Connection refused

To'rt xil muammo, to'rt xil xabar. except Exception bilan hammasi "xato" bo'lib qolardi.

Qayta urinish va backoff

Ba'zi xatolar vaqtinchalik: server band (503), tarmoq uzildi. Ularda qayta urinish mantiqiy. Boshqalari doimiy: 404, 401 — qayta urinish ma'nosiz.

shu faylni yaratingsrc/hisobot/tarmoq.py

python50 qator
"""HTTP so'rovlari: timeout, qayta urinish, backoff."""
import json
import logging
import time
import urllib.error
import urllib.request
from typing import Any

log = logging.getLogger(__name__)

QAYTA_URINSA_BOLADI = {408, 429, 500, 502, 503, 504}


class TarmoqXato(Exception):
    """So'rov bajarilmadi."""


def olish(
    manzil: str,
    *,
    timeout: float = 5.0,
    urinishlar: int = 3,
    boshlangich_kutish: float = 0.1,
) -> dict[str, Any]:
    """JSON oladi. Vaqtinchalik xatolarda qayta urinadi (eksponensial backoff).

    Doimiy xatolarda (404, 401) darhol to'xtaydi — qayta urinish ma'nosiz.
    """
    oxirgi = ""
    for urinish in range(1, urinishlar + 1):
        try:
            with urllib.request.urlopen(manzil, timeout=timeout) as j:
                malumot: dict[str, Any] = json.loads(j.read())
                return malumot
        except urllib.error.HTTPError as e:
            oxirgi = f"HTTP {e.code}"
            if e.code not in QAYTA_URINSA_BOLADI:
                log.debug("doimiy xato %s — qayta urinilmaydi", oxirgi)
                raise TarmoqXato(f"{manzil}: {oxirgi}") from None
        except urllib.error.URLError as e:
            oxirgi = f"ulanib bo'lmadi: {e.reason}"
        except TimeoutError:
            oxirgi = f"timeout ({timeout} s)"

        if urinish < urinishlar:
            kutish = boshlangich_kutish * (2 ** (urinish - 1))
            log.warning("%s (%d-urinish), %.2f s dan keyin qayta urinaman", oxirgi, urinish, kutish)
            time.sleep(kutish)

    raise TarmoqXato(f"{manzil}: {urinishlar} urinishdan keyin ham bajarilmadi ({oxirgi})")
python27 qator
import logging, sys, importlib, time
sys.path.insert(0, str(ISH / "src"))
import hisobot.tarmoq as tarmoq
importlib.reload(tarmoq)

logging.basicConfig(level=logging.WARNING, format="  [%(levelname)s] %(message)s",
                    stream=sys.stdout, force=True)

print("1) Uchinchi urinishda muvaffaqiyat:")
tarmoq.olish(manzil + "/nolla")
t0 = time.perf_counter()
natija = tarmoq.olish(manzil + "/uchinchida")
print("   natija:", natija, f"| jami {(time.perf_counter() - t0) * 1000:.0f} ms")

print("\n2) Doimiy xato (404) — qayta urinish YO'Q:")
t0 = time.perf_counter()
try:
    tarmoq.olish(manzil + "/topilmadi")
except tarmoq.TarmoqXato as e:
    print("   xato:", e, f"| {(time.perf_counter() - t0) * 1000:.0f} ms")

print("\n3) Vaqtinchalik xato (500) — uch urinish ham yetmadi:")
t0 = time.perf_counter()
try:
    tarmoq.olish(manzil + "/xato500")
except tarmoq.TarmoqXato as e:
    print("   xato:", e, f"| {(time.perf_counter() - t0) * 1000:.0f} ms")
Chiqish
1) Uchinchi urinishda muvaffaqiyat:
  [WARNING] HTTP 503 (1-urinish), 0.10 s dan keyin qayta urinaman
  [WARNING] HTTP 503 (2-urinish), 0.20 s dan keyin qayta urinaman
   natija: {'holat': 'nihoyat', 'urinish': 3} | jami 302 ms

2) Doimiy xato (404) — qayta urinish YO'Q:
   xato: http://127.0.0.1:39435/topilmadi: HTTP 404 | 0 ms

3) Vaqtinchalik xato (500) — uch urinish ham yetmadi:
  [WARNING] HTTP 500 (1-urinish), 0.10 s dan keyin qayta urinaman
  [WARNING] HTTP 500 (2-urinish), 0.20 s dan keyin qayta urinaman
   xato: http://127.0.0.1:39435/xato500: 3 urinishdan keyin ham bajarilmadi (HTTP 500) | 302 ms

Uch holat, uch xil xulq. Ikkinchisiga e'tibor bering: 404 da darhol to'xtadi. Agar u ham uch marta uringanda, foydalanuvchi bekorga kutgan bo'lardi.

Eksponensial backoff — har urinishdan keyin kutish vaqti ikki barobar oshadi (0.1, 0.2, 0.4...). Sababi: server band bo'lsa, hamma mijoz bir vaqtda qayta urinsa, u yanada ko'proq bosim ko'radi.

python11 qator
import pandas as pd
qatorlar = []
for urinish in range(1, 7):
    qatorlar.append({
        "urinish": urinish,
        "doimiy kutish (0.5 s)": f"{0.5:.2f} s",
        "eksponensial (0.1 s dan)": f"{0.1 * 2 ** (urinish - 1):.2f} s",
    })
show(pd.DataFrame(qatorlar))
print("6 urinishda jami kutish — doimiy:", 6 * 0.5, "s | eksponensial:",
      round(sum(0.1 * 2 ** i for i in range(6)), 2), "s")
Chiqish
6 urinishda jami kutish — doimiy: 3.0 s | eksponensial: 6.3 s
urinishdoimiy kutish (0.5 s)eksponensial (0.1 s dan)
10.50 s0.10 s
20.50 s0.20 s
30.50 s0.40 s
40.50 s0.80 s
50.50 s1.60 s
60.50 s3.20 s

Amalda backoff ga jitter (tasodifiy qo'shimcha) ham qo'shiladi, aks holda hamma mijoz aynan bir vaqtda qayta uradi.

requests va httpx

Standart urllib ishlaydi, lekin noqulay. Amalda requests (sinxron) yoki httpx (sinxron + asinxron) ishlatiladi:

python21 qator
import sys
sys.path.insert(0, str(ISH))
try:
    import requests
    j = requests.get(manzil + "/ok", timeout=5)
    print("requests:", j.status_code, j.json())

    s = requests.Session()                      # ulanishni qayta ishlatadi — tezroq
    import time
    t0 = time.perf_counter()
    for _ in range(20):
        requests.get(manzil + "/ok", timeout=5)
    sessiyasiz = (time.perf_counter() - t0) * 1000
    t0 = time.perf_counter()
    for _ in range(20):
        s.get(manzil + "/ok", timeout=5)
    sessiya_bilan = (time.perf_counter() - t0) * 1000
    print(f"20 so'rov — sessiyasiz: {sessiyasiz:.0f} ms | sessiya bilan: {sessiya_bilan:.0f} ms")
    s.close()
except ImportError:
    print("requests o'rnatilmagan")
Chiqish
requests: 200 {'holat': 'yaxshi', 'talabalar': 3}
20 so'rov — sessiyasiz: 13 ms | sessiya bilan: 11 ms

Session TCP ulanishni qayta ishlatadi. Ko'p so'rov yuborayotgan bo'lsangiz, bu sezilarli farq beradi.

Kutubxona Qachon
urllib.request Bog'liqlik qo'shishni istamasangiz
requests Odatiy tanlov; sodda va keng tarqalgan
httpx async kerak bo'lsa yoki HTTP/2

Testda tarmoqqa chiqmang

Test tarmoqqa bog'liq bo'lsa, u sekin va ishonchsiz bo'ladi. Ikki yechim bor.

1-usul: soxta (mock) qo'yish.

shu faylni yaratingtests/test_tarmoq.py

bash47 qator
import json
import urllib.error
from unittest.mock import MagicMock, patch

import pytest

from hisobot.tarmoq import TarmoqXato, olish


def soxta_javob(malumot):
    j = MagicMock()
    j.read.return_value = json.dumps(malumot).encode()
    j.__enter__.return_value = j
    j.__exit__.return_value = False
    return j


def test_muvaffaqiyat():
    with patch("urllib.request.urlopen", return_value=soxta_javob({"a": 1})):
        assert olish("http://x/y") == {"a": 1}


def test_404_qayta_urinmaydi():
    xato = urllib.error.HTTPError("http://x/y", 404, "yo'q", {}, None)
    with (
        patch("urllib.request.urlopen", side_effect=xato) as m,
        pytest.raises(TarmoqXato, match="404"),
    ):
        olish("http://x/y", urinishlar=3)
    assert m.call_count == 1                     # faqat BIR marta chaqirilgan


def test_500_uch_marta_urinadi():
    xato = urllib.error.HTTPError("http://x/y", 500, "xato", {}, None)
    with (
        patch("urllib.request.urlopen", side_effect=xato) as m,
        pytest.raises(TarmoqXato),
    ):
        olish("http://x/y", urinishlar=3, boshlangich_kutish=0.001)
    assert m.call_count == 3


def test_ikkinchida_tuzaladi():
    xato = urllib.error.HTTPError("http://x/y", 503, "band", {}, None)
    with patch("urllib.request.urlopen", side_effect=[xato, soxta_javob({"ok": True})]) as m:
        assert olish("http://x/y", urinishlar=3, boshlangich_kutish=0.001) == {"ok": True}
    assert m.call_count == 2
matn1 qator
.venv/bin/python3 -m pytest tests/test_tarmoq.py -v
Chiqish (qaytish kodi 0, 1812 ms)
============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-9.1.1, pluggy-1.6.0
rootdir: /mnt/c/Users/Admin/Desktop/Course Material/Python_Muhandisligi/ish
configfile: pyproject.toml
plugins: cov-7.1.0
collected 4 items

tests/test_tarmoq.py ....                                                [100%]

============================== 4 passed in 0.11s ===============================

call_count tekshiruvi muhim: u qayta urinish mantiqini sinaydi, natijani emas. 404 da bir marta, 500 da uch marta — aynan biz xohlagandek.

2-usul: haqiqiy lokal server. Yuqorida shuni ishlatdik. U sekinroq, lekin haqiqiyroq — HTTP qatlamining o'zi ham sinaladi.

:::tip Nimani mock qilish kerak Mock ni tizim chegarasida qo'ying: tarmoq, soat, tasodifiy son, fayl tizimi. O'z kodingizni mock qilsangiz, testingiz kodning tuzilishini sinaydi, xulqini emas — va har refaktoringda buziladi. :::

Xulosa

Tushuncha Asosiy fikr
Timeout Sukut bo'yicha yo'q; har so'rovda yozing
Xato turlari HTTPError, URLError, TimeoutError, JSON xatosi — ajrating
Qayta urinish Faqat vaqtinchalik xatolarda (408, 429, 5xx)
404 / 401 Doimiy — qayta urinish ma'nosiz, darhol to'xtang
Backoff Eksponensial + jitter; serverga bosimni kamaytiradi
Session Ulanishni qayta ishlatadi — ko'p so'rovda tezroq
Testda mock call_count bilan qayta urinish mantiqini sinang
Mock chegarasi Tizim chegarasida mock qiling, o'z kodingizni emas

Talaba ishi

  1. olish ga jitter qo'shing: kutish * random.uniform(0.5, 1.5). Testlarni qanday o'zgartirish kerak bo'ldi (tasodifiylik testni buzadimi)?
  2. Retry-After sarlavhasini hisobga oling: server 429 bilan birga necha soniya kutishni aytsa, o'shani ishlating.
  3. /sekin yo'liga timeout bilan qayta urinish qo'shing. Umumiy vaqt qancha bo'ladi va bu foydalanuvchi uchun maqbulmi?
  4. test_500_uch_marta_urinadi da boshlangich_kutish=0.001 berilgan. Nega? Uni olib tashlasangiz test qancha vaqt oladi?

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar