অধ্যায় 8 · ডিপ্লয়মেন্ট, স্ট্রিমিং API ও স্কেলিং
কন্টেইনারাইজেশন ও স্ট্রিমিং ডিপ্লয়মেন্ট
- পৃষ্ঠা 15 / 22
- 24 মিনিট পড়া
বোগোতার মাতেও নিজের ল্যাপটপে EvidenceDesk অ্যাসিস্ট্যান্টের ডেমো দেখাল, সবকিছু নিখুঁত। শেয়ার্ড এনভায়রনমেন্টে প্রথম ব্যবহারকারী উত্তরের মাঝখানে ট্যাব বন্ধ করে দিল, আর মডেল আরও এক মিনিট উত্তর বানিয়েই চলল; ইমেজে প্রোভাইডারের একটা কি রয়ে গেছে, কারণ কেউ সেটা বিল্ড আর্গুমেন্ট হিসেবে দিয়েছিল; লোড ব্যালান্সার প্রতিটা স্ট্রিম একটা নির্দিষ্ট সময়ে কেটে দিচ্ছে, শেষের কোনো ইভেন্ট ছাড়াই; আর "সুস্থ" মানে শুধু প্রসেসটা আছে। (এটা একটা বোঝানোর গল্প।) উত্তরের কোনো ইউনিট টেস্টে এসব ধরা পড়ে না: ফাঁকটা "লোকালি ঠিক" আর "অন্যদের জন্য চালানো নিরাপদ"-এর মাঝখানে।
এই পাতা স্ট্রিমিং সার্ভিসের জন্য সেই ফাঁক বন্ধ করে: আগে গ্রহণযোগ্যতার মানদণ্ড, তারপর কন্টেইনার, ইমেজের বাইরে সিক্রেট, আইডি ও এরর ইভেন্টসহ সার্ভার-সেন্ট ইভেন্টে উত্তর স্ট্রিম করা, আর এমন টেস্ট যা দেখায় যে ক্লায়েন্ট সংযোগ ছাড়লে মডেলের কাজ থামে এবং ওভারলোডে অনুরোধ সীমাহীন সারিতে জমে না, প্রত্যাখ্যান হয়।
সৎ পরিধি: এই পাতা যেখানে লেখা হয়েছে সেখানে কোনো Docker ডেমন নেই। Dockerfile ও Compose ফাইল
# not run hereচিহ্নসহ, কোনো আউটপুট ছাড়া দেখানো হয়েছে। আমি সেগুলো স্ট্যাটিকভাবে যাচাই করেছি, এমন এক চেকার দিয়ে যে ফাইলকে শুধু টেক্সট হিসেবে পড়ে; আর কন্টেইনার যেসব আচরণের উপর নির্ভর করে (সেটিংস, রেডিনেস, স্ট্রিমিং, সংযোগ বিচ্ছিন্ন হওয়া, শাটডাউন) তা Python-এ টেস্ট করেছি, একবার 127.0.0.1-এ সত্যিকারের uvicorn দিয়েও। স্ট্যাটিক চেক প্রমাণ করে ফাইলে কী লেখা আছে। ইমেজ বিল্ড হয় কি না, বা Docker সেটাকে আপনার প্রত্যাশামতো থামায় কি না, তা প্রমাণ করে না।
যা শিখবেন
- কীভাবে ডিপ্লয়মেন্টের গ্রহণযোগ্যতার মানদণ্ড লিখবেন, যার প্রতিটা একটা টেস্ট, একটা স্ট্যাটিক রুল বা একটা ম্যানুয়াল কমান্ডের দিকে নির্দেশ করে।
- কীভাবে সার্ভিস প্যাকেজ করবেন (মাল্টি-স্টেজ, নন-রুট, হেলথ চেক, সুনির্দিষ্ট পিন) এবং ক্রেডেনশিয়াল ইমেজ ও সোর্স কন্ট্রোলের বাইরে রাখবেন।
- কীভাবে
EventSourceResponseদিয়ে স্ট্রিম করবেন: আইডি, কিপ-অ্যালাইভ, এরর ইভেন্ট, সাইজ সীমা ও সিক্রেট স্ক্রিন। - কীভাবে সংযোগ বিচ্ছিন্ন হওয়া, স্টল ও শাটডাউন টেস্ট করবেন, এবং চলমান ও অপেক্ষমাণ কাজ সীমিত করবেন।
কী নতুন, আর কী পুনর্ব্যবহার
কোনো বিদ্যমান ফাইল বদলায় না। নতুন: সাবপ্যাকেজ evidencedesk/serve/, ফোল্ডার kit/deploy/ (Dockerfile, Compose ফাইল, ইগনোর ফাইল, পিন করা রানটাইম রিকোয়ারমেন্টস, env.example), data/serve/acceptance.json এবং আটটা test_deploy_*.py ফাইল। ব্যবহার হচ্ছে: প্যাকেজিং, কনফিগারেশন ও পুনরুৎপাদনযোগ্যতা পাতার create_app, /health, /ready ও অফলাইন পাইপলাইন; স্ট্রিমিং ও কাঠামোবদ্ধ ফলাফল পাতার chunk_text ও "শুধু done হলেই সম্পূর্ণ" নিয়ম; বাজেট, ক্যাশিং ও ব্যবহারের সীমা পাতার UsageLimiter; নিরাপদ ব্যর্থতা ও নিরাপত্তা রিগ্রেশন টেস্ট পাতার SecretGuard ও safe_answer; স্ট্রাকচার্ড লগিং ও ট্রেসিং পাতার EventLog।
ডিপ্লয়মেন্টের গ্রহণযোগ্যতার মানদণ্ড ঠিক করা
"শেষ" মানে এমন একটা তালিকা, যার প্রতিটা দাবি যাচাই করা যায়, পাঁচটা দলে: কার্যকারিতা, নিরাপত্তা, পর্যবেক্ষণযোগ্যতা, লেটেন্সি (কোনো কিছু কত সময় অপেক্ষা করতে পারে) ও পুনরুদ্ধার (ক্লায়েন্ট চলে যায়, মডেল আটকে যায়, প্রসেস থামে)। data/serve/acceptance.json-এর প্রতিটা মানদণ্ড নিজের যাচাইয়ের নাম বলে: একটা test (ফাইল ও ফাংশন), কন্টেইনার চেকারের একটা static রুল, অথবা কন্টেইনার রানটাইম লাগে এমন একটা manual কমান্ড। যে তালিকায় কোনো দল নেই, আইডি দুবার আছে, টেস্ট নেই বা রুল অজানা, acceptance.problems তা প্রত্যাখ্যান করে, ফলে কোনো মানদণ্ড শূন্যের দিকে নির্দেশ করতে পারে না।
from collections import Counter
from evidencedesk.serve import acceptance
criteria = acceptance.load()
print(dict(Counter(c.category for c in criteria)))
print(acceptance.summary(criteria), "| problems with the list itself:", acceptance.problems(criteria))
r1 = next(c for c in criteria if c.id == "R1")
print(r1.id, r1.kind, r1.ref)
for c in criteria:
if c.kind == "manual":
print(c.id, "needs a container runtime:", c.ref.split(" && ")[0]){'functionality': 5, 'security': 6, 'observability': 2, 'latency': 2, 'recovery': 5}
{'manual': 2, 'static': 5, 'test': 13} | problems with the list itself: []
R1 test test_deploy_disconnect.py::test_hanging_up_stops_the_model_and_the_bill
M1 needs a container runtime: docker build -f deploy/Dockerfile -t evidencedesk-serve:local .
M2 needs a container runtime: docker compose -f deploy/compose.yaml stopদুটো ম্যানুয়াল মানদণ্ডই ঠিক সেগুলো যেগুলোর জন্য Docker লাগে। এই পাতা সেগুলো দাবি করে না; সেগুলো খোলা থাকে। লেটেন্সির মানদণ্ড মাপা সংখ্যা নয়, সেটিংস ব্যবহার করে (১৫ সেকেন্ডের স্ট্রিম সীমা, ২০ সেকেন্ডের শাটডাউন জানালা): এগুলো সীমা যা আপনি ঠিক করেন, তারপর টেস্ট করেন।
অ্যাপ্লিকেশন Docker দিয়ে প্যাকেজ করা
ইমেজে থাকে কোড আর শুরুতে পড়া ফিক্সচার অ্যাসেট (corpus, fixtures, data), আর কিছু না। ডিপেন্ডেন্সি এক স্টেজে বিল্ড হয় এবং শুধু তৈরি ভার্চুয়াল এনভায়রনমেন্টটা রানটাইম স্টেজে যায়; সেই স্টেজ একটা নির্দিষ্ট সংখ্যার আইডিওয়ালা ইউজার বানায়, তাতে বদলে যায়, অতিরিক্ত টুল ছাড়াই চলে এমন হেলথ চেক ঠিক করে, আর স্পষ্ট শাটডাউন জানালাসহ exec ফর্মে uvicorn চালু করে।
# not run here: needs a Docker daemon. Build context is the kit/ folder: docker build -f deploy/Dockerfile -t evidencedesk-serve:local .
# Base image: a tag is mutable. For a release build pin the digest of the tag you tested (FROM python:3.12-slim@sha256:<digest>).
FROM python:3.12-slim AS build
ENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /build
COPY deploy/requirements-serve.txt .
RUN python -m venv /opt/venv && /opt/venv/bin/pip install -r requirements-serve.txt
FROM python:3.12-slim AS runtime
RUN groupadd --gid 10001 app && useradd --uid 10001 --gid app --no-log-init --no-create-home --shell /usr/sbin/nologin app
COPY --from=build /opt/venv /opt/venv
WORKDIR /app
COPY evidencedesk ./evidencedesk
COPY corpus ./corpus
COPY fixtures ./fixtures
COPY data ./data
ENV PATH=/opt/venv/bin:$PATH PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 EVIDENCEDESK_OFFLINE=1 EVIDENCEDESK_PROVIDER=fake
USER 10001:10001
EXPOSE 8000
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 CMD ["python", "-m", "evidencedesk.serve.probe"]
CMD ["uvicorn", "evidencedesk.serve.asgi:app", "--host", "0.0.0.0", "--port", "8000", "--timeout-graceful-shutdown", "20"]টুল নোট (২০২৬-১০-১২ অনুযায়ী): Docker-এর সেরা-চর্চার পাতায় আছে মাল্টি-স্টেজ বিল্ড (একাধিক
FROM, শুধু দরকারিটুকু কপি), নন-রুট ইউজারের জন্যUSER(স্পষ্ট UID ও GID সহ), gitignore-সদৃশ প্যাটার্নের.dockerignore, এবং ট্যাগ পরিবর্তনযোগ্য (ডাইজেস্ট পিন প্রতি বিল্ডে একই ইমেজ দেয়; ট্যাগ পিন দুর্বল)।HEALTHCHECK-এর রূপHEALTHCHECK [OPTIONS] CMD commandওHEALTHCHECK NONE; ডিফল্ট interval 30s, timeout 30s, start period 0s, retries 3, আর স্বাস্থ্যের ফল ঠিক হয় এক্সিট কোড দিয়ে। বর্তমান ডকুমেন্টেশন দেখে নিন।
লেয়ার ও সাইজের যুক্তি, সংখ্যা ছাড়া। আমি কোনো ইমেজ বিল্ড করিনি, তাই কোনো সাইজ জানাই না। কাঠামোগতভাবে: শুধু /opt/venv স্টেজ পার হয়, তাই বিল্ডের অবশেষ ভেতরে যায় না; requirements-serve.txt (শুধু রানটাইম প্যাকেজ) সোর্সের আগে কপি হয়, তাই কোড বদলালে ডিপেন্ডেন্সি আবার ইনস্টল হয় না; আর Dockerfile.dockerignore tests, docs, ইতিহাস, ক্যাশ, .env* ও secrets কনটেক্সটের বাইরে রাখে। সার্ভিস যা পড়ে, ইগনোর ফাইলকে তা রাখতেও হবে: data বাদ দেওয়া রুল এমন ইমেজ দেয় যা বিল্ড হয়, তারপর চিরকাল "প্রস্তুত নয়" বলে; তাই চেকার দুই দিকই টেস্ট করে। সাইজ উদ্ধৃত করার আগে নিজে মেপে নিন।
বিল্ড-টাইম ও রানটাইম কনফিগারেশন আলাদা করা
বিল্ড-টাইম তথ্য (বেস ইমেজ, পিন করা রিকোয়ারমেন্টস) ইমেজে থাকে। রান থেকে রানে যা বদলায় (পোর্ট, সীমা, কলার, প্রোভাইডার কি) তা শুরুর সময়ের এনভায়রনমেন্টে থাকে। ServeSettings শুধু EVIDENCEDESK_* নাম পড়ে; সিক্রেট হয় ফাইলের পাথ (NAME_FILE) নয়তো মান, দুটো একসঙ্গে নয়। দুটো অভ্যাসই বেশির ভাগ কাজ করে। যে নাম কেউ পড়ে না তা এরর, কারণ MAX_IN_FLIGHT-এর জায়গায় MAX_INFLIGHT লিখলে সার্ভিস নীরবে ডিফল্টে চলত। স্ট্রিম সীমা শাটডাউন জানালার চেয়ে ছোট না হলে তা প্রত্যাখ্যাত, কারণ তাহলে শাটডাউন এমন স্ট্রিম কেটে দিত যা শেষ হতে পারত। uvicorn_argv একই সেটিংস থেকে চালু করার কমান্ড বানায়, আর একটা টেস্ট ইনস্টল করা uvicorn দিয়ে সেটা পার্স করে, তাই ভুল বানানের ফ্ল্যাগ টেস্টেই ধরা পড়ে।
import tempfile
from pathlib import Path
from evidencedesk.serve.settings import load_serve_settings, uvicorn_argv
with tempfile.TemporaryDirectory() as folder:
secret = Path(folder) / "api_tokens" # in a container this is /run/secrets/api_tokens
secret.write_text("nadia:demo-token-0001\n# comment\nomar:demo-token-0002\n", encoding="utf-8")
env = {"EVIDENCEDESK_API_TOKENS_FILE": str(secret), "EVIDENCEDESK_MAX_IN_FLIGHT": "4", "EVIDENCEDESK_GRACEFUL_S": "25"}
s = load_serve_settings(env)
print(s.tokens)
print(s.app.offline, s.app.provider, s.app.api_key, s.max_in_flight)
print(" ".join(uvicorn_argv(s)))
bad = [{"EVIDENCEDESK_MAX_INFLIGHT": "4"}, {"EVIDENCEDESK_STREAM_TIMEOUT_S": "30"},
{"EVIDENCEDESK_API_TOKENS": "a:demo-token-0001", "EVIDENCEDESK_API_TOKENS_FILE": "/x"}]
for env in bad:
try:
load_serve_settings(env)
except ValueError as exc:
print(list(env)[0][12:], "->", (exc.errors()[0]["msg"] if hasattr(exc, "errors") else str(exc))[:100])(('nadia', SecretStr('**********')), ('omar', SecretStr('**********')))
True fake None 4
uvicorn evidencedesk.serve.asgi:app --host 0.0.0.0 --port 8000 --timeout-graceful-shutdown 25
_MAX_INFLIGHT -> unknown settings: EVIDENCEDESK_MAX_INFLIGHT
_STREAM_TIMEOUT_S -> Value error, stream_timeout_s must be shorter than graceful_s: shutdown would cut streams that could
_API_TOKENS -> set EVIDENCEDESK_API_TOKENS or EVIDENCEDESK_API_TOKENS_FILE, not bothটোকেন SecretStr হিসেবে ছাপা হয়। ইমেজে কোনো কি-ই নেই: অফলাইন ও ফেক প্রোভাইডার ডিফল্ট, আর আসল প্রোভাইডারের জন্য ইচ্ছা করে EVIDENCEDESK_OFFLINE=0 দিতে হয়। চেকারের D05 রুল সেই ENV বা ARG-কে ব্যর্থ করে যার নাম কি, টোকেন, সিক্রেট, পাসওয়ার্ড বা ক্রেডেনশিয়ালের মতো এবং যাতে মান আছে, যদি না নাম _FILE দিয়ে শেষ হয়।
নিরাপত্তা নোট (২০২৬-১০-১২ অনুযায়ী): Docker-এর ডকুমেন্টেশন বলে, বিল্ড আর্গুমেন্ট ও এনভায়রনমেন্ট ভ্যারিয়েবল বিল্ডে সিক্রেট পাঠানোর জন্য অনুপযুক্ত, "কারণ সেগুলো চূড়ান্ত ইমেজে থেকে যায়"।
ARGচূড়ান্ত কন্টেইনারে থাকে না, কিন্তুdocker history-তে দেখা যায়;ENV-এর মান ইমেজ কনফিগারেশনে ও চলমান কন্টেইনারে থেকে যায়। বিল্ড-টাইম সিক্রেটেdocker build --secret id=...ওRUN --mount=type=secret,id=...ব্যবহার হয়, যা/run/secrets/<id>-এ মাউন্ট হয়। বর্তমান ডকুমেন্টেশন দেখে নিন।
উপযুক্ত জায়গায় Docker Compose সিক্রেট ব্যবহার করা
লোকাল ও একক-হোস্ট ব্যবহারে Compose কন্টেইনারকে একটা ফাইল দেয়। টপ-লেভেল secrets: এন্ট্রি সোর্সের নাম বলে, সার্ভিসকে সিক্রেটটা তালিকাভুক্ত করতে হয়, আর সেটা /run/secrets/api_tokens-এ আসে, যে পাথের দিকে EVIDENCEDESK_API_TOKENS_FILE নির্দেশ করে। ফাইলটা আমাদের হার্ডনিংও ঠিক করে: পোর্ট শুধু 127.0.0.1-এ, রিড-অনলি রুট, কোনো ক্যাপাবিলিটি নেই, রিস্টার্ট নীতি, আর ২০ সেকেন্ডের শাটডাউন জানালার চেয়ে লম্বা স্টপ জানালা। deploy/secrets/api_tokens.txt আপনি নিজে বানাবেন (প্রতি কলারে একটা name:token লাইন), ভার্সন কন্ট্রোলের বাইরে।
# not run here: needs Docker Compose. docker compose -f deploy/compose.yaml up --build
services:
app:
build:
context: ..
dockerfile: deploy/Dockerfile
image: evidencedesk-serve:local
ports:
- "127.0.0.1:8000:8000"
environment:
EVIDENCEDESK_OFFLINE: "1"
EVIDENCEDESK_PROVIDER: fake
EVIDENCEDESK_API_TOKENS_FILE: /run/secrets/api_tokens
secrets:
- api_tokens
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
stop_grace_period: 30s
restart: unless-stopped
secrets:
api_tokens:
file: ./secrets/api_tokens.txtটুল নোট (২০২৬-১০-১২ অনুযায়ী): Compose সিক্রেট "/run/secrets/<secret_name>-এ ফাইল হিসেবে মাউন্ট হয়"; সোর্স
fileবাenvironment, আরenvironmentসোর্সdocker stack deploy-এর সঙ্গে সমর্থিত নয়; হাউ-টু বলে সিক্রেট শুধু Linux কন্টেইনারে সমর্থিত, প্রতিটা একটা বাইন্ড-মাউন্ট করা ফাইল হিসেবে আসে।MYSQL_PASSWORD_FILE-এর মতো_FILEভ্যারিয়েবল কিছু ইমেজের রীতি, Docker-এর ফিচার নয়; আমাদের সার্ভিস নিজেই সেটা বাস্তবায়ন করে।stop_grace_period,read_only,cap_dropএবং<Dockerfile name>.dockerignoreপড়ার BuildKit আচরণ আমার গবেষণা-নোটে নেই: সেগুলো VERIFY করুন। বর্তমান ডকুমেন্টেশন দেখে নিন।
সীমাগুলো বাস্তব: মাউন্ট করা ফাইল মানটাকে ইমেজ লেয়ারের বাইরে রাখে, কিন্তু হোস্ট ফাইল পড়তে পারে বা কন্টেইনারে exec করতে পারে এমন যে কেউ সেটা পড়তে পারে, আর এটা ঘোরানো (রোটেট) হয় না। টোকেন কলারকেও শনাক্ত করে, তাই সার্ভিস আর নকলযোগ্য x-user-id হেডারকে বিশ্বাস করে না, যেমন বিগিনারের স্ট্যান্ড-ইন করত।
কন্টেইনার ফাইল স্ট্যাটিকভাবে যাচাই করা
container.py Dockerfile পার্স করে, PyYAML দিয়ে Compose ফাইল লোড করে, .dockerignore মেলানোর একটা ছোট উপসেট প্রয়োগ করে, এবং রুল আইডি ও সিভিয়ারিটিসহ ফলাফল ফেরত দেয়। ব্যর্থতা: রুট, হেলথ চেক নেই, ENV/ARG-এ সিক্রেট, সিক্রেট ফাইল হিসেবে নয়, কনটেক্সটে সিক্রেট, আর অসঙ্গত শাটডাউন জানালা। সতর্কতা আমাদের নীতি বা পরিচ্ছন্নতা: পরিবর্তনযোগ্য ট্যাগ, লেখা যায় এমন রুট, ক্যাপাবিলিটি, ক্যাশের ক্রম।
python -m evidencedesk.serve.containerCONTAINER CHECK (static)
[warn] D01 line 3: tag '3.12-slim' is mutable; pin a digest for a release build
[warn] D01 line 9: tag '3.12-slim' is mutable; pin a digest for a release build
RESULT: PASS (0 fail, 2 warn)"দুই সতর্কতাসহ পাস" সৎ ফল: ট্যাগ ডাইজেস্ট নয়, আর আমি ডাইজেস্ট বানিয়ে লিখব না। এবার ইচ্ছা করে ফাইল ভাঙুন; প্রতিটা ভুল নিজের রুলে ব্যর্থ হবে। (ইগনোর মেলানোর অংশ Docker-এর নিয়মের একটা ছোট উপসেট: নিরাপত্তা জাল, কপি নয়।)
import yaml
from evidencedesk.serve.container import check_compose, check_dockerfile, check_ignore
text = Path("kit/deploy/Dockerfile").read_text(encoding="utf-8")
cases = {
"runs as root": text.replace("USER 10001:10001\n", ""),
"key in ENV": text.replace("EXPOSE 8000", "ENV EVIDENCEDESK_API_KEY=placeholder\nEXPOSE 8000"),
"shutdown 5 s, not 20": text.replace('"--timeout-graceful-shutdown", "20"', '"--timeout-graceful-shutdown", "5"'),
}
for name, changed in cases.items():
print(f"{name:<22}", [f"{f.rule} {f.severity}" for f in check_dockerfile(changed, 20) if f.rule != "D01"])
doc = yaml.safe_load(Path("kit/deploy/compose.yaml").read_text(encoding="utf-8"))
doc["services"]["app"].update(ports=["8000:8000"], stop_grace_period="10s")
print(f"{'compose: open port':<22}", [f"{f.rule} {f.severity}" for f in check_compose(yaml.safe_dump(doc), 20)])
ignore = Path("kit/deploy/Dockerfile.dockerignore").read_text(encoding="utf-8")
print(f"{'ignore without .env*':<22}", sorted({f.rule for f in check_ignore(ignore.replace(".env*", ""))}))runs as root ['D03 fail']
key in ENV ['D05 fail']
shutdown 5 s, not 20 ['D08 fail']
compose: open port ['C01 fail', 'C06 fail']
ignore without .env* ['I01']হেলথ ও রেডিনেস এন্ডপয়েন্ট বাস্তবায়ন
/health বলে প্রসেস চলছে, আর কখনো ডেটার উপর নির্ভর করে না। /ready বলে সার্ভিস নিজের কাজ করতে পারে: এখানে, লাইফস্প্যানে কর্পাস লোড হয়েছে। create_stream_app বিগিনারের create_app-কে মুড়ে নেয়, তাই দুটোই অপরিবর্তিত ব্যবহার হয়। কন্টেইনারের হেলথ চেক একটা স্ট্যান্ডার্ড-লাইব্রেরি প্রোব (slim ইমেজে curl নেই), যা শুধু /ready যখন ready: true বলে তখনই ০ দিয়ে বের হয়; ৫০৩ মানে অসুস্থ, এক্সসেপশন নয়।
from fastapi.testclient import TestClient
from evidencedesk.serve import probe
from evidencedesk.serve.service import create_stream_app
from evidencedesk.serve.testing import rig_settings
app = create_stream_app(rig_settings())
cold = TestClient(app) # no `with`: the lifespan has not run, nothing is loaded yet
print("before start:", cold.get("/health").json(), cold.get("/ready").status_code, cold.get("/ready").json()["reason"])
with TestClient(app) as http:
body = http.get("/ready")
print("after start: ", body.status_code, body.json())
print("probe says ready:", probe.ready("http://127.0.0.1:8000/ready", lambda url: (body.status_code, body.text)))before start: {'status': 'alive'} 503 not_loaded
after start: 200 {'ready': True, 'reason': '', 'chunks': 36, 'offline': True}
probe says ready: Trueএই চেক যা জানে না তা হলো প্রোভাইডার নাগালে আছে কি না। সেটা পরে আলাদা সংকেত হিসেবে যোগ করুন: অন্য কারও সার্ভিস বন্ধ বলে নিজের কন্টেইনার রিস্টার্ট করলে নিজেরই বিভ্রাট হয়।
প্রোভাইডার ক্রেডেনশিয়াল ছাড়া অ্যাপ্লিকেশন চালানো
ডিফল্ট ইমেজ মক মোডে চলে: অফলাইন, ফেক প্রোভাইডার, কোনো কি নেই, টোকেন ফাইল থেকে। স্মোক টেস্ট একটা ট্রান্সপোর্টের উপর ফাংশন, তাই একই সাতটা ধাপ ইন-প্রসেসে (নিচে) বা HTTP-তে কন্টেইনারের বিপক্ষে চলে। এর সিক্রেট চেক পুনর্গঠিত উত্তর পড়ে, কারণ একটা পিস কি-কে ভাগ করে ফেলতে পারে, যা একটু পরে দেখানো হবে।
python -m evidencedesk.serve.smokeSMOKE
[ok ] liveness GET /health -> 200
[ok ] readiness GET /ready -> 200
[ok ] offline mode offline=True
[ok ] no token is refused POST /v1/stream without a token -> 401
[ok ] stream completes -> 200, stream complete
[ok ] answer is cited a source id is in the text
[ok ] no secret in output the made-up key is not in the reassembled answer
RESULT: PASSস্ট্রিমিং এন্ডপয়েন্ট ডিপ্লয় করা
POST /v1/stream (SSE যেকোনো মেথডের সঙ্গে চলে) চার ধরনের ইভেন্ট দেয়: start, token, error, done। প্রতিটার আইডি request_id:seq এবং ডেটায় seq আবার থাকে, ফলে ক্লায়েন্ট ফাঁক, পুনরাবৃত্তি বা জোড়া লাগানো স্ট্রিম ধরতে পারে। শুধু যে স্ট্রিম start দিয়ে শুরু হয়, আইডি অবিচ্ছিন্ন থাকে এবং done দিয়ে শেষ হয়, সেটাই সম্পূর্ণ: স্ট্রিমিং ও কাঠামোবদ্ধ ফলাফল পাতার নিয়ম, এখন আইডিসহ। error-এ থাকে স্থির কোড, সাধারণ বার্তা ও retryable, কখনো এক্সসেপশনের টেক্সট নয়।
টুল নোট (২০২৬-১০-১২ অনুযায়ী): FastAPI-এর ডকুমেন্টেশন বলে SSE সমর্থন 0.135.0-এ যোগ হয়েছে:
fastapi.sseথেকেresponse_class=EventSourceResponse, আইটেমdata:-তে JSON হয়ে যায়,event,id,retry-এর জন্যServerSentEventক্লাস, আর বিল্ট-ইন: নিষ্ক্রিয় থাকলে প্রতি ১৫ সেকেন্ডে একটা কিপ-অ্যালাইভ কমেন্ট,Cache-Control: no-cacheওX-Accel-Buffering: no। আমি ইনস্টল করা 0.143.0 ব্যবহার করেছি এবং নিচে সেই হেডারগুলো দেখেছি। Starlette-এর TestClient পাতা বলেhttpx2, FastAPI-এরটা এখনো বলেhttpx; কিটhttpx2পিন করে। বর্তমান ডকুমেন্টেশন দেখে নিন।
যা প্রত্যাখ্যান করা যায় সবই স্ট্রিম শুরুর আগে প্রত্যাখ্যাত হয়, কারণ ২০০-এর ভেতরে শুধু ইভেন্টই পাঠানো যায়। ক্রম: অথেন্টিকেট (যাতে অচেনা কেউ বডি ভ্যালিডেশন সম্পর্কে কিছু না জানে), রিজিউম হেডার (রিপ্লে বাফার নেই, তাই Last-Event-ID পেলে ৪০৯), প্রশ্নের সাইজ (৪১৩), রেডিনেস (৫০৩), UsageLimiter থেকে দৈনিক কোটা (৪২৯, পরের UTC মধ্যরাত পর্যন্ত Retry-After), তারপর অ্যাডমিশন গেট (৫০৩)। yield-সহ একটা ডিপেন্ডেন্সি স্লটটা ধরে রাখে, তাই রেসপন্স যেভাবেই শেষ হোক, শেষে স্লট ছাড়া হয়।
from evidencedesk.serve.events import judge, parse_stream
from evidencedesk.serve.testing import AUTH, QUESTION
with TestClient(app) as http:
for headers in ({}, {"authorization": "Bearer wrong-token-0001"}):
r = http.post("/v1/stream", json=QUESTION, headers=headers)
print(r.status_code, r.json()["code"], r.headers["www-authenticate"])
r = http.post("/v1/stream", json=QUESTION, headers=AUTH)
print(r.status_code, r.headers["content-type"], {k: v for k, v in r.headers.items() if k in ("cache-control", "x-accel-buffering")})
print(r.text[:118])
events = parse_stream(r.text)
print(len(events), "events:", events[0].kind, "then", {e.kind for e in events[1:-1]}, "then", events[-1].kind, events[-1].data)
print(judge(events))401 unauthenticated Bearer
401 unauthenticated Bearer
200 text/event-stream; charset=utf-8 {'cache-control': 'no-cache', 'x-accel-buffering': 'no'}
event: start
data: {"seq": 0, "schema_version": 1}
id: s-1:0
event: token
data: {"seq": 1, "text": "Ref"}
id: s-1:1
11 events: start then {'token'} then done {'seq': 10, 'status': 'answered', 'chunks': 9, 'chars': 127}
Outcome(status='complete', text='Refunds go back to the original payment method within 5 business days after the warehouse receives the item. [kc-returns-v3:c2]', code='')# excerpt of kit/evidencedesk/serve/service.py (the body of the try in the stream route)
call = await serve.upstream.start(ticket.question, ticket.user, rid)
async with aclosing(screened(tallied(call.pieces, tally), guard)) as it:
while (item := await _next(it, deadline - anyio.current_time())) is not END:
if item is None:
raise UpstreamError("stream_timeout", True)
if serve.poll_disconnect and await request.is_disconnected():
return # the client left: leaving the block closes the call, so the work and the bill stop
if chars + len(item) > settings.max_stream_chars or seq > settings.max_events:
raise UpstreamError("output_limit", False)
yield frame(rid, seq, "token", text=item)
seq, pieces, chars = seq + 1, pieces + 1, chars + len(item)
yield frame(rid, seq, "done", status=call.status, chunks=pieces, chars=chars)
status = "done"
except UpstreamError as exc:
status, code = "error", exc.code
yield frame(rid, seq, "error", code=exc.code, message="The answer could not be completed.", retryable=exc.retryable)উদ্ধৃতাংশটা রুটের try-এর ভেতরের অংশ (এর আগের কোড কাউন্টার ও ডেডলাইন ঠিক করে)। স্বাভাবিক শেষে done আসে। মডেল ব্যর্থ হলে, টাইমআউট বা সীমা ছুঁলে খোলা ২০০-এর ভেতরে একটা error আসে। সংযোগ ছেড়ে দিলে জেনারেটর একটা await-এ বাতিল হয়; aclosing মডেল কল বন্ধ করে, আর একটা finally ব্লক (দেখানো হয়নি) ফল লিখে রাখে এবং কোটায় উৎপাদিত পরিমাণের চার্জ দেয়, সীমার নয়। টাইমআউট একবারে একটা __anext__ মোড়ে, কখনো yield পেরিয়ে নয়, কারণ async জেনারেটরে yield পেরিয়ে ক্যানসেল স্কোপ ঠিকমতো কাজ করে না।
অফলাইন মডেল ক্রমান্বয়ে উত্তর বানায় না: ট্রান্সপোর্ট আসল, কিন্তু পিসগুলো তৈরি, যাচাই করা উত্তর থেকে কাটা। আসল প্রোভাইডার স্ট্রিম আলাদা। প্রথম পিসের আগে পুরো উত্তর যাচাই করা যায় না, তাই সিক্রেট পিসগুলোর মধ্যে ভাগ হয়ে আসতে পারে, আর প্রতি-পিস স্ক্যান কিছুই দেখে না। screened একটা লেজ ধরে রাখে (জানা সবচেয়ে লম্বা সিক্রেট আর একটা মার্জিন) এবং পুরোনো টেক্সট ছাড়ার আগে জোড়া বাফার স্ক্যান করে।
# excerpt of kit/evidencedesk/serve/screen.py
async def screened(pieces: AsyncIterator[str], guard: SecretGuard, hold: int | None = None) -> AsyncIterator[str]:
"""Yield text that the guard has seen in full. The last `hold` characters are kept back until more text (or the end) arrives.
The price is delay: the first `hold` characters of every answer wait. A match raises UpstreamError and nothing of it is sent."""
hold = hold_for(guard) if hold is None else hold
buffer = ""
async with aclosing(pieces) as source: # closing this generator must close the model call too
async for piece in source:
buffer += piece
if guard.find(buffer):
raise UpstreamError("secret_screened", False)
if len(buffer) > hold:
out, buffer = buffer[:-hold], buffer[-hold:]
yield out
if buffer:
yield bufferfrom evidencedesk.security import SIMULATED_SECRET
from evidencedesk.serve.upstream import ScriptedUpstream
def run(upstream):
app = create_stream_app(rig_settings(), upstream)
with TestClient(app) as http:
r = http.post("/v1/stream", json=QUESTION, headers=AUTH)
return r, judge(parse_stream(r.text)), app.state.serve.records[-1]
cases = [("fails after 6 pieces", ScriptedUpstream(fail_after=6)), ("key split across pieces", ScriptedUpstream(text="See [kc-x] " + SIMULATED_SECRET)),
("answer without an end", ScriptedUpstream(text="x" * 5000))]
for name, upstream in cases:
r, out, rec = run(upstream)
print(f"{name:<24} {out.status} {out.code:<16} sent={rec.chars:<4} produced={rec.produced:<4} key sent: {SIMULATED_SECRET in out.text + r.text}")fails after 6 pieces failed upstream_failed sent=27 produced=72 key sent: False
key split across pieces failed secret_screened sent=0 produced=32 key sent: False
answer without an end failed output_limit sent=1995 produced=2052 key sent: Falseদাম প্রথম সারিতে দেখা যায়: ৭২ অক্ষর উৎপন্ন, ২৭ পাঠানো, কারণ মডেল ব্যর্থ হওয়ার সময় স্ক্রিন ৪৫ আটকে রেখেছিল। এটা ইচ্ছাকৃত বিনিময়: প্রতিটা উত্তরের শুরুতে দেরি আর ব্যর্থতায় লেজ হারানো, বিনিময়ে কখনো ভাগ হওয়া কি না পাঠানো। এটা শুধু তাই পাহারা দেয় যা সে জানে বা প্যাটার্নে চেনে; এটা কনটেন্ট ফিল্টার নয়। তৃতীয় সারি সীমা: ৫,০০০-এর বদলে প্রায় ২,০০০ অক্ষরে একটা error ইভেন্ট।
ক্লায়েন্টের সংযোগ বিচ্ছিন্ন হওয়া ও টাইমআউট টেস্ট করা
পরিত্যক্ত রিকোয়েস্টের পেছনে খরচ চলতেই থাকা উচিত নয়। Starlette request.is_disconnected() ডকুমেন্ট করে; FastAPI-এর পাতা বলে yield-ধাঁচের স্ট্রিমিং বাতিল নিজেই সামলায়, আর টাস্ক শুধু await-এ বাতিল হয়। আমি কোনোটাই ধরে নিইনি। testing.call_asgi N-টা বডি চাঙ্কের পর http.disconnect পাঠায়, spec_version ২.৩ দিয়ে, যেমন uvicorn জানায়; নকল মডেল পিসের মাঝে এক মিলিসেকেন্ড থামে, যাতে বাতিলের নামার জন্য await থাকে।
import anyio
from evidencedesk.serve.testing import call_asgi
LONG = "word " * 200 # 1000 characters, 84 pieces
async def tick(_i):
await anyio.sleep(0.001)
def exchange(hang_up_after=None, **settings):
up = ScriptedUpstream(text=LONG, pause=tick)
app = create_stream_app(rig_settings(**settings), up)
with TestClient(app) as http:
seen = http.portal.call(lambda: call_asgi(app, "/v1/stream", QUESTION, AUTH, hang_up_after=hang_up_after))
return seen, up, app.state.serve
for label, k in (("read to the end", None), ("hung up after 8 events", 8)):
seen, up, serve = exchange(k, max_stream_chars=2000)
print(f"{label:<24} status={serve.records[-1].status:<9} events seen={len(seen.chunks):<3} produced under a quarter: {up.meter.chars < 250} "
f"slots held: {serve.gate.in_flight}")
async def stall(i):
if i == 1:
await anyio.sleep(3600) # the model goes silent after the first piece
up = ScriptedUpstream(text=LONG, pause=stall)
app = create_stream_app(rig_settings(stream_timeout_s=0.2), up)
with TestClient(app) as http:
r = http.post("/v1/stream", json=QUESTION, headers=AUTH)
out = judge(parse_stream(r.text))
print("silent model:", out.status, out.code, "| pieces made:", up.meter.pieces, "| slots held:", app.state.serve.gate.in_flight)read to the end status=done events seen=84 produced under a quarter: False slots held: 0
hung up after 8 events status=cancelled events seen=8 produced under a quarter: True slots held: 0
silent model: failed stream_timeout | pieces made: 1 | slots held: 0সংযোগ বিচ্ছিন্ন হওয়ার পর মডেল উত্তরের প্রায় দশ ভাগের এক ভাগে থেমেছে। টেস্ট সীমাটা বেঁধে দেয়: ৮টা ইভেন্ট দেখার পর সর্বোচ্চ ১৪টা পিস উৎপন্ন (৭টা টোকেন, স্ক্রিনে আটকে থাকা ৪টা পিস, আর FastAPI-এর এক-আইটেম বাফার থেকে প্রায় দুটো এগিয়ে থাকা), এবং কোটার চার্জ উৎপাদিতের সমান। is_disconnected পোল সরিয়েও কিছু বদলায়নি: uvicorn-এর আচরণে বাতিল এমনিতেই আসে। আমি পোলটা ফ্ল্যাগ-নিয়ন্ত্রিত দ্বিতীয় প্রতিরক্ষা হিসেবে রাখি, এটা আবশ্যিক বলে দাবি করি না। যে জেনারেটর কখনো await করে না তাকে বাতিল করা যায় না, তাই আপস্ট্রিম async হতে হবে। শেষ সারি টাইমআউট: একটা পিসের পর stream_timeout, কোনো স্লট আটকে নেই। কিপ-অ্যালাইভ FastAPI-এর নিজের: তার অভ্যন্তরীণ ব্যবধান ১৫ সেকেন্ড থেকে ২০ মিলিসেকেন্ডে বদলে একটা নিষ্ক্রিয় স্ট্রিম : ping কমেন্ট পেয়েছে এবং তবু সম্পূর্ণ বিবেচিত হয়েছে (আপগ্রেডের পর সেই টেস্ট হালনাগাদ লাগতে পারে)।
TestClient যা দেখাতে পারে না। Starlette বলে তার ক্লায়েন্ট একই সঙ্গে রিকোয়েস্ট ও রেসপন্স স্ট্রিমিং নকল করে না, সত্যিকারের সার্ভার ব্যবহার করতে বলে। তাই একটা টেস্ট থ্রেডে 127.0.0.1-এ uvicorn চালু করে, সকেট থেকে কয়েক লাইন পড়ে সকেট বন্ধ করে দেয়; আরেকটা লম্বা স্ট্রিম শুরু করে ১ সেকেন্ডের গ্রেসফুল জানালাসহ এক্সিট ফ্ল্যাগ বসায়।
python -m pytest kit/tests/test_deploy_realserver.py | tail -n 1 | sed 's/ in [0-9.]*s//'3 passedশাটডাউনের রানে uvicorn 0.54.0 লগ করেছে যে গ্রেসফুল টাইমআউট ছাড়ানোয় একটা চলমান টাস্ক বাতিল করছে, আর স্ট্রিম রেকর্ড বলেছে cancelled, কোনো স্লট আটকে নেই। এই লগ লাইন আমি যা দেখেছি, ডকুমেন্টেড বার্তা নয়।
টুল নোট (২০২৬-১০-১২ অনুযায়ী): uvicorn-এর
--timeout-graceful-shutdownহলো "গ্রেসফুল শাটডাউনের জন্য অপেক্ষার সর্বোচ্চ সেকেন্ড। এই সময়ের পর সার্ভার রিকোয়েস্ট বন্ধ করতে শুরু করবে"; uvicorn কনফিগার করা টাইমআউটের মধ্যে চলমান রেসপন্স ও ব্যাকগ্রাউন্ড টাস্ক শেষ করে। যে পাতাগুলো পড়েছি সেগুলো SIGTERM কী করে তা বলে না, তাই এ নিয়ে আমি কোনো দাবি করি না;docker stopকীভাবে সংকেত দিয়ে পরে মেরে ফেলে তাও নিশ্চিত হয়নি: সেটা VERIFY করুন এবং নিজের স্ট্রিম দিয়ে টেস্ট করুন (মানদণ্ড M2)।uvicorn.workersমডিউল ডকুমেন্টে deprecated, তাই নতুন কাজেgunicorn -k uvicorn.workers.UvicornWorkerএড়িয়ে চলুন। হোস্টিং প্ল্যাটফর্মগুলোPORTভ্যারিয়েবল ঢোকায় কি না তাতে ভিন্ন; তাদের পাতা পড়ুন। বর্তমান ডকুমেন্টেশন দেখে নিন।
কনকারেন্সি ও কিউ সীমা ঠিক করা
প্রতিটা খোলা স্ট্রিম একটা কানেকশন, মেমরি ও একটা মডেল কল ধরে রাখে, তাই সীমাহীন কনকারেন্সি মানে অপেক্ষমাণ বিল আর বিভ্রাট। AdmissionGate(max_in_flight, max_waiting, max_wait_s) দুটোই সীমিত করে: max_in_flight পেরোনো স্ট্রিম অপেক্ষা করতে পারে, কিন্তু শুধু max_waiting-টা এবং শুধু max_wait_s পর্যন্ত; বাকিরা সঙ্গে সঙ্গে ৫০৩ ও Retry-After পায়। ডিফল্ট অপেক্ষাকক্ষ নেই, কারণ অপেক্ষমাণ ক্লায়েন্ট কোনো ইভেন্ট দেখে না, আর দ্রুত প্রত্যাখ্যান সাধারণত ভদ্রতর। সংখ্যা মাপা সক্ষমতা থেকে বেছে নিন, এই পাতা থেকে নয়। নকশাটা আমাদের; HTTP শুধু বলে ৫০৩ মানে সাময়িক ওভারলোড এবং তাতে Retry-After থাকতে পারে, আর ৪২৯ (RFC 6585) মানে নির্দিষ্ট সময়ে অনেক বেশি রিকোয়েস্ট।
release = anyio.Event()
async def hold(i):
if i == 1:
await release.wait() # the first stream stays open until we let it finish
app = create_stream_app(rig_settings(max_in_flight=1, max_waiting=0), ScriptedUpstream(pause=hold))
async def scenario():
got = {}
async def first():
got["first"] = await call_asgi(app, "/v1/stream", QUESTION, AUTH)
async with anyio.create_task_group() as tg:
tg.start_soon(first)
while app.state.serve.gate.in_flight < 1:
await anyio.sleep(0.001)
got["busy"] = await call_asgi(app, "/v1/stream", QUESTION, AUTH)
release.set()
return got
with TestClient(app) as http:
got = http.portal.call(scenario)
gate = app.state.serve.gate
print("busy:", got["busy"].status, "retry-after", got["busy"].headers["retry-after"], got["busy"].text)
print("first:", judge(parse_stream(got["first"].text)).status, "| rejected:", gate.rejected, "| in flight:", gate.in_flight)busy: 503 retry-after 1 {"code":"overloaded","message":"The service is busy: try again shortly."}
first: complete | rejected: 1 | in flight: 0প্রত্যাখ্যাত রিকোয়েস্টে কোনো মডেল কল খরচ হয়নি, প্রথম স্ট্রিম অক্ষত শেষ হয়েছে, আর স্লট সংখ্যা শূন্যে ফিরেছে (একটা টেস্ট এরপরের পুনরুদ্ধার ও সীমিত অপেক্ষাকক্ষও ঢাকে)। পরের পাতা এই গেট থেকেই এগোয়।
অনুশীলন: কন্টেইনারে চলা অফলাইন সার্ভিস
Docker থাকলে ইমেজ বিল্ড করুন, টোকেন ফাইলসহ চালু করুন, হোস্ট থেকে স্মোক টেস্ট চালান, আর একটা স্ট্রিম খোলা রেখে থামান। নিচের কিছুই এই পাতার জন্য চালানো হয়নি।
# not run here: needs a Docker daemon and Compose (criteria M1 and M2)
mkdir -p kit/deploy/secrets
echo "nadia:local-demo-token-1" > kit/deploy/secrets/api_tokens.txt
docker compose -f kit/deploy/compose.yaml up --build -d
python -m evidencedesk.serve.smoke http://127.0.0.1:8000 kit/deploy/secrets/api_tokens.txt
docker compose -f kit/deploy/compose.yaml ps
docker compose -f kit/deploy/compose.yaml stopফাইলগুলো যা পূর্বাভাস দেয়, কোনো রান যা দেখিয়েছে তা নয়: /ready উত্তর দিলে ps সুস্থ বলে, স্মোক টেস্ট একই সাতটা ধাপ ছাপে, আর stop স্টপ জানালার মধ্যে ফেরে, খোলা স্ট্রিম বাধাপ্রাপ্ত স্ট্রিম হিসেবে শেষ হয়। আপনার টার্মিনাল ভিন্ন বললে সেটাকেই বিশ্বাস করুন এবং ফাইল বা মানদণ্ড ঠিক করুন। Docker ছাড়া একই যাচাই ইন-প্রসেসে চলে:
python -m pytest kit/tests/test_deploy_*.py | tail -n 1 | sed 's/ in [0-9.]*s//'103 passedএই টেস্টগুলো কোডের পথ ও ফাইলের বিষয়বস্তু প্রমাণ করে। ইমেজ বিল্ড হয় কি না, আপনার সামনের লোড ব্যালান্সার স্ট্রিম বাফার বা কেটে দেয় কি না, বা আপনার সক্ষমতার সংখ্যা ঠিক কি না, তা প্রমাণ করে না।
কাজের জায়গায় এটা কোথায় দেখা যায়
কায়রোর ওমর এমন একটা অ্যাসিস্ট্যান্ট পায় যা "স্টেজিংয়ে চলত"। এক সপ্তাহে সে যোগ করে একটা রেডিনেস প্রোব, মাউন্ট করা কি ফাইল, স্ট্রিম ও ইন-ফ্লাইট সীমা এবং একটা গ্রহণযোগ্যতার তালিকা। লোড ব্যালান্সারের প্রথম বিভ্রাট, শেষ ইভেন্ট ছাড়া কেটে যাওয়া স্ট্রিম, কয়েক মিনিটে ধরা পড়ে, কারণ ক্লায়েন্ট বাধাপ্রাপ্ত আর শেষ হওয়া স্ট্রিমের পার্থক্য বোঝে। (এটা একটা বোঝানোর গল্প।)
সাধারণ ভুল
- কি-কে
ARGবাENVহিসেবে পাঠানো। এটা ইমেজের ইতিহাস বা কনফিগারেশনে থেকে যায়। ফাইল মাউন্ট করুন, নাম দিন*_FILE, আর চেকারকে বিল্ড ব্যর্থ করতে দিন। - লেখা যায় এমন ফাইলসিস্টেমে রুট হিসেবে চালানো। একটা বাগ থেকে পুরো দখল হয়ে যায়। সংখ্যায় আইডি দেওয়া নন-রুট ইউজার, রিড-অনলি রুট ও tmpfs ব্যবহার করুন।
- সবকিছুর জন্য একটাই "সুস্থ"। লাইভনেস যদি কর্পাস দেখে, যে কন্টেইনারের শুধু সময় লাগছে তাকে রিস্টার্ট করে; রেডিনেস কিছু না দেখলে ফাঁকা সার্ভিসে ট্রাফিক যায়।
- স্ট্রিম নীরবে শেষ হতে দেওয়া। কেটে যাওয়া স্ট্রিম ছোট উত্তরের মতো দেখায়। আইডি ও শেষে
doneপাঠান; বাকি সবকিছুকে বাধাপ্রাপ্ত ধরুন। - প্রতি পিসে সিক্রেট স্ক্যান করা। ভাগ হওয়া কি পাস করে যায়। জোড়া জানালা স্ক্যান করুন এবং তার দেরি মেনে নিন।
- সীমাহীন কিউ। মেমরি শেষ না হওয়া পর্যন্ত তারা ওভারলোড লুকিয়ে রাখে। চলমান ও অপেক্ষমাণ কাজ সীমিত করুন; বাকিদের
Retry-After-সহ প্রত্যাখ্যান করুন।
নিজে চেষ্টা করুন
- সহজ। স্ট্রিমিং ও কাঠামোবদ্ধ ফলাফল পাতা আবার দেখুন। একটা আসল স্ট্রিম পাঁচ ইভেন্টের পর কেটে
judge-কে জিজ্ঞেস করুন সেটা কী। আগে অনুমান করুন, তারপর পুরো স্ট্রিমের সঙ্গে মেলান। - মাঝারি। প্যাকেজিং, কনফিগারেশন ও পুনরুৎপাদনযোগ্যতা পাতা আবার দেখুন, যার
REQUIRED_DATAসার্ভিসের শুরুতে লাগে। ইগনোর ফাইলেdataযোগ করুন এবং অনুমান করুন কোন রুল ব্যর্থ হবে আর কেন সেটা বিল্ড ব্যর্থ হওয়ার চেয়ে খারাপ। - কঠিন। স্ট্রাকচার্ড লগিং ও ট্রেসিং পাতা আবার দেখুন। একটা নতুন অ্যাপে একটা শেষ হওয়া স্ট্রিম, একটা সংযোগ-বিচ্ছিন্ন স্ট্রিম ও একটা প্রত্যাখ্যাত রিকোয়েস্ট চালান, শুধু লগ থেকে ফলাফল গুনুন, আর দেখান যে প্রশ্নের টেক্সট কোনো লাইনে নেই।
উত্তর
সহজ। পাঁচ ইভেন্ট মানে একটা start আর চারটা টোকেন, done নেই: interrupted; পুরো স্ট্রিম complete।
with TestClient(create_stream_app(rig_settings())) as http:
wire = http.post("/v1/stream", json=QUESTION, headers=AUTH).text
cut = "\n\n".join(wire.split("\n\n")[:5]) + "\n\n" # the connection drops after five events
print(len(parse_stream(cut)), "events arrived ->", judge(parse_stream(cut)).status, "| whole stream ->", judge(parse_stream(wire)).status)5 events arrived -> interrupted | whole stream -> completeমাঝারি। রুল I02। বিল্ড সফল হতো এবং কন্টেইনার চালু হতো, তারপর চিরকাল প্রস্তুত নয় বলত (লোডার কর্পাস পায় না): অর্কেস্ট্রেটর এটাকে খারাপ বিল্ড নয়, ক্র্যাশ লুপ হিসেবে দেখে।
from evidencedesk.serve.container import check_ignore
print([(f.rule, f.message[:62]) for f in check_ignore(ignore + "\ndata\n")])[('I02', "the ignore file drops 'data', which the service reads at start")]কঠিন। প্রতি স্ট্রিমে একটা stream.start, ফলাফল done ও cancelled, আর কোডসহ একটা প্রত্যাখ্যান। লগে থাকে question_chars, টেক্সট কখনো নয়, আর কলাররা ছদ্মনামে আসে।
from collections import Counter
app = create_stream_app(rig_settings())
with TestClient(app) as http:
http.portal.call(lambda: call_asgi(app, "/v1/stream", {"question": "Can I pay in instalments?"}, AUTH))
http.portal.call(lambda: call_asgi(app, "/v1/stream", QUESTION, AUTH, hang_up_after=3))
http.post("/v1/stream", json=QUESTION, headers={"authorization": "Bearer wrong-token-0001"})
records = app.state.serve.log.records()
print(sorted(Counter((r["event"], r["outcome"] or r["fields"].get("code", "")) for r in records).items()))
print("question text in any line:", any("instalments" in line for line in app.state.serve.log.lines))[(('request.rejected', 'unauthenticated'), 1), (('stream.end', 'cancelled'), 1), (('stream.end', 'done'), 1), (('stream.start', ''), 2)]
question text in any line: Falseপাস চেকলিস্ট:
- সহজ: ৫ ইভেন্টে
interrupted, পুরো স্ট্রিমেcomplete, এবং কারণ (doneনেই)। - মাঝারি: I02, যা বিল্ডের নয়, শুরুর সময়ের ব্যর্থতা হিসেবে ব্যাখ্যা করা।
- কঠিন: দুটো start, একটা
done, একটাcancelled, একটাunauthenticated, আর কোনো লাইনে প্রশ্নের টেক্সট নেই।
সারসংক্ষেপ
- আগে গ্রহণযোগ্যতার মানদণ্ড লিখুন, প্রতিটা একটা টেস্ট, স্ট্যাটিক রুল বা দৃশ্যমান ম্যানুয়াল ধাপে বাঁধা; শুধু-কন্টেইনারে-হয় এমনগুলো খোলা রাখুন।
- ইমেজ ছোট, পিন করা, নন-রুট ও সিক্রেটমুক্ত রাখুন; সিক্রেট ফাইল হিসেবে দিন; অজানা সেটিংসে ব্যর্থ হোন।
- লাইভনেস ও রেডিনেস আলাদা রাখুন; ইমেজে আগে থেকে থাকা টুল দিয়ে হেলথ চেক করুন।
- আইডি, start ও done সহ ইভেন্ট, কোডসহ error ইভেন্ট, সীমা ও কিপ-অ্যালাইভ দিয়ে স্ট্রিম করুন; স্ট্রিম শুরুর আগে প্রত্যাখ্যান করুন; সিক্রেটের জন্য জোড়া জানালা স্ক্রিন করুন।
- সংযোগ বিচ্ছিন্ন হওয়া, স্টল ও শাটডাউন কাঁচা ASGI ও একটা আসল সার্ভার দিয়ে টেস্ট করুন; চলমান ও অপেক্ষমাণ কাজ সীমিত করুন এবং ওভারলোডে ৫০৩ ও
Retry-Afterদিন। - এখানে কিছুই কন্টেইনারে চলেনি: চেকার ও টেস্ট ফাইল ও আচরণ প্রমাণ করে, আর ম্যানুয়াল মানদণ্ড আপনি চালানো পর্যন্ত খোলা থাকে।
এরপর: স্কেলিং, রেজিলিয়েন্স ও রোলব্যাক পাতায় স্টেটলেস ওয়ার্কার ও টেকসই স্টেট আলাদা করা হয়, আর যোগ হয় ব্যাকপ্রেশার, আইডেমপোটেন্সি কি-সহ রিট্রাই, রেট লিমিট, রিকোয়েস্ট শৃঙ্খলজুড়ে টাইমআউট, প্রোভাইডার বিভ্রাটের ফলব্যাক, এনভায়রনমেন্ট আলাদা করা, রোলব্যাক পরিকল্পনা ও নকল-ক্লায়েন্টের লোড টেস্ট।