Tashqi dunyo: HTTP, timeout va qayta urinish
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
"""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
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))
server: http://127.0.0.1:39435
| yo'l | timeout, s | kod | vaqt, ms | xato |
|---|---|---|---|---|
| /ok | 5 | 200 | 1 | - |
| /sekin | 1 | NaN | 1 001 | TimeoutError |
| /sekin | 5 | NaN | 5 003 | TimeoutError |
/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
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'l | ma'lumot | xato |
|---|---|---|
| /ok | {'holat': 'yaxshi', 'talabalar': 3} | - |
| /xato500 | None | HTTP 500 |
| /topilmadi | None | HTTP 404 |
| /nomalum | None | HTTP 404 |
| (o'chiq port) | None | ulanib 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
"""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})")
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")
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.
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")
6 urinishda jami kutish — doimiy: 3.0 s | eksponensial: 6.3 s
| urinish | doimiy kutish (0.5 s) | eksponensial (0.1 s dan) |
|---|---|---|
| 1 | 0.50 s | 0.10 s |
| 2 | 0.50 s | 0.20 s |
| 3 | 0.50 s | 0.40 s |
| 4 | 0.50 s | 0.80 s |
| 5 | 0.50 s | 1.60 s |
| 6 | 0.50 s | 3.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:
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")
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
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
.venv/bin/python3 -m pytest tests/test_tarmoq.py -v
============================= 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
olishga jitter qo'shing:kutish * random.uniform(0.5, 1.5). Testlarni qanday o'zgartirish kerak bo'ldi (tasodifiylik testni buzadimi)?Retry-Aftersarlavhasini hisobga oling: server 429 bilan birga necha soniya kutishni aytsa, o'shani ishlating./sekinyo'liga timeout bilan qayta urinish qo'shing. Umumiy vaqt qancha bo'ladi va bu foydalanuvchi uchun maqbulmi?test_500_uch_marta_urinadidaboshlangich_kutish=0.001berilgan. 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.