sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called وہ error ہے جو sync SQLAlchemy سے async پر منتقل ہونے والی تقریباً ہر FastAPI ٹیم کو خوش آمدید کہتا ہے، اور شاذ ہی واضح ہوتا ہے کہ یہ دراصل شکایت کس بات کی کر رہا ہے۔ traceback عموماً ایسی لائن کی طرف اشارہ کرتا ہے جو مکمل طور پر بے ضرر لگتی ہے — جیسے Jinja template میں order.customer.name تک رسائی، یا response serializer میں، یعنی ایسی جگہ جہاں آپ نے خود کوئی database call لکھی ہی نہیں ہوتی۔ پہلی instinct یہ ہوتی ہے کہ اسے کسی مبہم async compatibility bug کے طور پر دیکھا جائے۔ ایسا نہیں ہے۔ SQLAlchemy آپ کو، جتنی درستگی سے ممکن ہو، یہ بتا رہا ہوتا ہے کہ آپ نے async context کے باہر ایک lazy-loaded query چلانے کی کوشش کی، حالانکہ lazy loading اسی context میں ممکن ہوتی ہے۔
FastAPI میں async SQLAlchemy کو درست طریقے سے استعمال کرنا صرف ایک error message یاد رکھنے کا معاملہ نہیں — اصل بات یہ سمجھنا ہے کہ session کا lifecycle، engine کا lifecycle، اور relationship کی loading strategy، تینوں کا ایک دوسرے کے ساتھ ہم آہنگ ہونا ضروری ہے، اور async ان مقامات پر جہاں یہ خاموشی سے ایک دوسرے سے غیر ہم آہنگ ہو سکتے ہیں، sync SQLAlchemy کے مقابلے میں کہیں کم forgiving ہے۔ یہ پوسٹ درست session-per-request scoping، MissingGreenlet کو حقیقت میں کیا trigger کرتا ہے، وہ relationships جنہیں آپ نے کبھی واضح طور پر query بھی نہیں کیا ہوتا response کو کیوں crash کر سکتی ہیں، اور app کے lifespan کے مقابلے میں engine خود کہاں رہنا چاہیے — ان سب کا احاطہ کرتی ہے۔
آپ یہ سیکھیں گے:
- FastAPI کے
Dependsاورyieldکی مدد سے ایک async SQLAlchemy session کو ہر request کے لیے درست طور پر کیسے scope کیا جائے MissingGreenletکا اصل مطلب کیا ہے، اور وہ تین جگہیں جہاں یہ سب سے زیادہ عام طور پر ظاہر ہوتا ہے- session context ختم ہونے کے بعد کسی unloaded relationship attribute تک رسائی کیوں lazy-load explosion کا سبب بنتی ہے
- lifespan-managed engine اور request-scoped session میں کیا فرق ہے، اور یہ دونوں ایک ہی lifecycle کیوں نہیں ہیں
- Eager loading strategies (
selectinload,joinedload) جو lazy-load errors کو ہونے سے پہلے روکتی ہیں - ایک مکمل، کام کرنے والا async session setup جسے آپ براہِ راست اپنے project میں نقل کر سکتے ہیں
فہرستِ مضامین
- بنیادی باتیں
- Session-Per-Request Scoping
- MissingGreenlet Error کی وضاحت
- Lazy-Load Explosions
- Lifespan-Managed Engine بمقابلہ Request-Scoped Session
- Eager Loading Strategies
- ایک مکمل کام کرنے والا Setup
- Async SQLAlchemy Code کی Testing
- Concurrent Load کے تحت Connection Pool Sizing
- عام غلطیاں
- Production Best Practices
بنیادی باتیں
Async SQLAlchemy، Sync سے مختلف رویہ کیوں رکھتا ہے
Sync SQLAlchemy کی lazy loading تقریباً حادثاتی طور پر forgiving ہوتی ہے: جب آپ order.customer تک رسائی حاصل کرتے ہیں اور وہ ابھی تک load نہیں ہوا ہوتا، تو SQLAlchemy وہیں فوراً ایک نئی synchronous query چلاتا ہے اور نتیجہ واپس کر دیتا ہے۔ یہ اس لیے کام کرتا ہے کیونکہ Python attribute access کے اندر blocking database call نظر نہیں آتی — Python “تیز attribute access” اور “ایسی attribute access جو خفیہ طور پر I/O کر رہی ہو” میں فرق نہیں کرتا۔
Async SQLAlchemy ایسا خاموشی سے نہیں کر سکتا، کیونکہ query جاری کرنے کے لیے await درکار ہوتا ہے، اور آپ سادہ attribute access کے اندر await نہیں کر سکتے (__getattr__ ایسے coroutine کی صورت میں نہیں ہو سکتا جسے Python آپ کے لیے خود بخود await کرے)۔ SQLAlchemy کا حقیقی حل greenlet نامی ایک bridge ہے، جو بعض sync جیسے نظر آنے والے calls کو async context کے اندر چلنے دیتا ہے، کیونکہ یہ execution کو ایسے greenlet میں منتقل کرتا ہے جو آپ کی طرف سے await کر سکتا ہے — مگر یہ bridge صرف انہی حدود کے اندر موجود ہوتا ہے جو SQLAlchemy اس کے لیے قائم کرتا ہے، خاص طور پر جب session کا async context فعال ہو۔ ان حدود سے باہر نکلتے ہی MissingGreenlet دراصل SQLAlchemy کا یہ کہنے کا طریقہ ہے کہ وہ bridge اب وہاں موجود نہیں رہا۔
Engine، Session، اور Relationship — تین مختلف Lifecycles
FastAPI app میں تقریباً ہر async SQLAlchemy bug کے پسِ منظر میں بار بار سامنے آنے والا موضوع یہ ہے کہ تین چیزوں کے lifecycles ایک دوسرے کے ساتھ compatible ہونے چاہییں، اور ان میں mismatch کرنا بہت آسان ہے: engine (طویل المدت، app startup پر ایک بار create ہوتا ہے)، session (مختصر المدت، ہر request کے لیے ایک)، اور relationship loading (اس وقت ہونی چاہیے جب وہ session جسے loading کرنی ہے ابھی بھی کھلا ہو)۔ ان میں سے کسی ایک کو بھی غلط سنبھالنے سے اسی بنیادی مسئلے کی مختلف شکل سامنے آتی ہے۔
Session-Per-Request Scoping
درست pattern ایک yield-based dependency ہے، بالکل وہی pattern جس پر عمومی طور پر FastAPI Dependency Injection: Patterns and Anti-Patterns میں گفتگو کی گئی ہے — request شروع ہوتے ہی ایک session کھولا جاتا ہے، اور request ختم ہوتے ہی اس کے بند ہونے کی ضمانت دی جاتی ہے، چاہے handler کامیاب ہو یا exception raise کرے:
# app/core/database.py
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
AsyncSessionLocal = async_sessionmaker(
bind=engine,
expire_on_commit=False,
class_=AsyncSession,
)
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
@router.get("/orders/{id}")
async def get_order(id: int, db: AsyncSession = Depends(get_db)):
order = await db.get(Order, id)
return order
یہاں دو چیزیں ایسی ہیں جنہیں غلط کرنا آسان ہے۔ پہلی، expire_on_commit=False خاص طور پر FastAPI کے لیے اہم ہے: default طور پر SQLAlchemy commit کے بعد تمام loaded attributes کو expire کر دیتا ہے، یعنی اگلی بار کسی attribute تک رسائی lazy load کو دوبارہ trigger کرتی ہے — جو، جیسا کہ آگے بیان کیا گیا ہے، بالکل وہی صورتِ حال ہے جو MissingGreenlet پیدا کرتی ہے اگر یہ رسائی response serialization شروع ہونے کے بعد ہو۔ اسے False پر set کرنے سے پہلے سے loaded attributes commit کے بعد بھی بغیر نئی query trigger کیے قابلِ استعمال رہتے ہیں۔ دوسری بات، session کو dependency function کے اندر create ہونا چاہیے، module import time پر نہیں — ایک بار create کیا گیا اور مختلف requests میں دوبارہ استعمال ہونے والا session خاموشی سے state (اور اس سے بھی بدتر، connections) کو غیر متعلقہ requests کے درمیان share کرتا ہے، جو اس پورے “one session per request” model کو توڑ دیتا ہے جس پر یہ pattern انحصار کرتا ہے۔
MissingGreenlet Error کی وضاحت
MissingGreenlet اس وقت آتا ہے جب SQLAlchemy کو I/O چلانے کی ضرورت ہو — جیسے implicit lazy load، flush، یا refresh — مگر وہ greenlet-bridged async context کے باہر ہو؛ عملی طور پر اس کا مطلب یہ ہوتا ہے کہ session (یا اس کا underlying connection) پہلے ہی بند ہو چکا ہے یا code path دراصل کسی awaited async call کے اندر چل ہی نہیں رہا۔ یہ تین جگہوں پر بہت کثرت سے سامنے آتا ہے:
1. Request کے response مکمل ہونے کے بعد کسی relationship تک رسائی۔ اگر Pydantic response model یا background task، get_db کے async with block سے نکل جانے کے بعد order.customer کو touch کرے، تو session بند ہو چکا ہوتا ہے اور implicit query چلانے کے لیے greenlet bridge باقی نہیں رہتا۔
2. Unloaded relationships والے ORM object پر کسی sync serialization library کو چلانا۔ بعض serializers (یا پرانے Pydantic configurations جو گہرے nested models پر .from_orm() استعمال کرتے ہیں) serialization کے دوران ہر attribute کو touch کرتے ہیں، ان attributes سمیت جنہیں آپ نے واضح طور پر load بھی نہیں کیا ہوتا — اگر ان میں سے کوئی بھی session کے active context سے باہر lazy load trigger کر دے، تو نتیجہ MissingGreenlet ہوتا ہے۔
3. Async session یا engine پر براہِ راست sync-style call استعمال کرنا۔ Sync SQLAlchemy codebase سے copy-paste کیا گیا code — await db.execute(select(...)) کی جگہ db.query(...)، یا execute call کو پہلے await کیے بغیر .scalars() تک رسائی — async driver کی توقعات کو مکمل طور پر bypass کر دیتا ہے اور یہی error پیدا کرتا ہے، کیونکہ sync API path کو شروع سے greenlet bridge کے ذریعے wire ہی نہیں کیا گیا تھا۔
ہر صورت میں حل کی شکل ایک جیسی ہے: اس بات کو یقینی بنائیں کہ database سے data درکار ہونے والا ہر کام session کے کھلے ہونے کے دوران اور کسی await شدہ call کے اندر ہو، اس کے بعد نہیں۔
Lazy-Load Explosions
یہاں تک کہ جب MissingGreenlet نہ بھی آئے — مثلاً اگر lazy="select" relationships ابھی بھی تکنیکی طور پر session کے کھلے ہونے کے دوران access ہو رہی ہوں — تو ایک مختلف مسئلہ سامنے آتا ہے: N+1 query explosion، جس کی قیمت اب ہر lazy load کے لیے سستی sync call کے بجائے ایک مکمل async round-trip کے اضافی overhead کے ساتھ ادا کرنی پڑتی ہے۔
@router.get("/orders")
async def list_orders(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Order))
orders = result.scalars().all()
return [
{"id": o.id, "customer": o.customer.name} # implicit lazy load, per order
for o in orders
]
100 orders کے لیے، یہ orders لانے کے لیے 1 query جاری کرتا ہے، اور پھر list comprehension کے اندر، o.customer تک ہر رسائی پر 100 تک اضافی implicit queries — یعنی ہر order کے لیے ایک — جن میں سے ہر ایک database تک مکمل async round trip ہے۔ Sync SQLAlchemy میں بھی یہ پہلے ہی performance کا مسئلہ ہے؛ async SQLAlchemy میں یہ اکثر correctness کا مسئلہ بھی بن جاتا ہے، کیونکہ lazy load کامیاب بھی ہوگا یا نہیں، یہ اس بات کے نازک وقت پر منحصر ہوتا ہے کہ access کے وقت session کا greenlet context ابھی بھی active سمجھا جا رہا ہے یا نہیں۔ یہی وہ قسم کی چیز ہے جو تیز local test میں کام کر جاتی ہے مگر حقیقی request patterns کے تحت وقفے وقفے سے fail ہوتی رہتی ہے۔
Lifespan-Managed Engine بمقابلہ Request-Scoped Session
Engine اور session ایک ہی object کے دو مختلف نام نہیں ہیں — یہ مختلف مقاصد کے لیے مختلف lifecycles ہیں، اور ان دونوں کو آپس میں گڈمڈ کرنا connection pool exhaustion کا ایک عام سبب ہے۔
Engine اصل connection pool کا مالک ہوتا ہے اور اسے بالکل ایک بار، application startup پر create ہونا چاہیے، اور بالکل ایک بار، shutdown پر dispose ہونا چاہیے — FastAPI کے lifespan کے ذریعے۔ یہ بالکل وہی قسم کا cross-cutting، domain-agnostic code بھی ہے جو مشترک core/database.py میں ہونا چاہیے، جیسا کہ FastAPI Project Structure That Survives Growth میں بیان کیا گیا ہے — ہر domain کا router وہاں سے get_db import کرتا ہے، اور ان میں سے کسی کو یہ جاننے کی ضرورت نہیں ہوتی کہ engine خود کیسے construct کیا گیا تھا:
# app/main.py
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(
"postgresql+asyncpg://user:pass@localhost/db",
pool_size=20,
max_overflow=10,
pool_pre_ping=True,
)
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
await engine.dispose()
app = FastAPI(lifespan=lifespan)
Session سستا، مختصر المدت، اور ایک ہی request تک محدود ہوتا ہے، جیسا کہ پہلے دکھائی گئی get_db dependency کے ذریعے — یہ request کے دورانیے کے لیے engine کے pool سے ایک connection ادھار لیتا ہے اور session بند ہونے پر اسے واپس کر دیتا ہے۔ ہر request پر نیا engine create کرنا (یعنی per app کے بجائے per request) وہ غلطی ہے جو load کے تحت واقعی database کی connection limit ختم کر دیتی ہے، کیونکہ ہر engine اپنا الگ pool لاتا ہے، اور وہ pools جنہیں کبھی dispose نہ کیا جائے، اپنی connections غیر معینہ مدت تک کھلی رکھتے ہیں۔ Production میں pool_pre_ping=True کو default بنانا فائدہ مند ہے — یہ request کو connection دینے سے پہلے اس بات کی تصدیق کرتا ہے کہ pooled connection stale نہیں ہے (database side سے بند نہیں ہوئی، یا load balancer کے idle timeout کی وجہ سے ختم نہیں ہوئی)، اور معمولی latency cost کے بدلے اس سے کہیں زیادہ الجھا دینے والے “connection already closed” error سے بچاتا ہے جو ورنہ request کے درمیان ظاہر ہوتا ہے۔
Eager Loading Strategies
MissingGreenlet اور N+1 explosions دونوں کا پائیدار حل ایک ہی ہے: جو data درکار ہے اسے session کے کھلے ہونے کے دوران، واضح طور پر load کریں، اس کے بجائے کہ بعد میں ہونے والی implicit lazy loading پر انحصار کریں۔
selectinload متعلقہ rows کے لیے ایک دوسری، الگ query جاری کرتا ہے، جسے primary key کے حساب سے batch کیا جاتا ہے — one-to-many relationships کے لیے یہ درست default ہے:
from sqlalchemy.orm import selectinload
result = await db.execute(
select(Order).options(selectinload(Order.items))
)
orders = result.scalars().all()
# order.items is already loaded — no further query needed
joinedload اسی query میں SQL JOIN کے ذریعے متعلقہ data لے آتا ہے — many-to-one یا one-to-one relationships کے لیے بہتر، جہاں ہر row کے اضافی columns سستے ہوں:
from sqlalchemy.orm import joinedload
result = await db.execute(
select(Order).options(joinedload(Order.customer))
)
کسی ایسی relationship کے لیے جس کی ضرورت آپ کو اکثر read paths میں پڑتی ہو، خود relationship definition پر lazy="raise" set کرنے پر غور کریں — یہ غلطی سے ہونے والی implicit lazy load کو فوراً، واضح exception میں بدل دیتا ہے، بجائے اس کے کہ اصل غلطی سے کئی stack frames دور MissingGreenlet نظر آئے:
customer = relationship("Customer", lazy="raise")
اس طرح ایک الجھا دینے والی runtime error کے بدلے آپ کو کہیں زیادہ واضح error ملتی ہے، بالکل اسی جگہ جہاں اصل میں missing eager-load option شامل کیا جانا چاہیے۔
ایک مکمل کام کرنے والا Setup
آئیے تمام حصوں کو یکجا کریں — engine، session factory، dependency، model، اور endpoint:
# app/core/database.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# app/orders/models.py
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"))
customer: Mapped["Customer"] = relationship(lazy="raise")
items: Mapped[list["OrderItem"]] = relationship()
# app/orders/router.py
@router.get("/orders/{id}", response_model=OrderOut)
async def get_order(id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Order)
.options(selectinload(Order.items), joinedload(Order.customer))
.where(Order.id == id)
)
order = result.unique().scalar_one_or_none()
if order is None:
raise HTTPException(404)
return order
Collection-producing query پر joinedload استعمال کرتے وقت result پر .unique() کو نوٹ کریں — اس کے بغیر rows کو پھیلا دینے والا join duplicate parent objects واپس کر سکتا ہے، ایک ایسی تفصیل جسے نظر انداز کرنا آسان ہے یہاں تک کہ response میں بے وجہ duplicate entries کے طور پر یہ سامنے آ جائے۔
Async SQLAlchemy Code کی Testing
Async database code کی درست testing کا مطلب یہ ہے کہ ہر test کو وہی request-scoped session guarantee دی جائے جو production کو ملتی ہے، بغیر اس کے کہ ہر unit test کے لیے مکمل HTTP client چلانا پڑے۔ سب سے صاف طریقہ یہ ہے کہ ہر test کو اس کی اپنی transaction میں لپیٹا جائے اور بعد میں rollback کر دیا جائے، تاکہ execution order کچھ بھی ہو، tests ایک دوسرے میں state leak نہ کریں:
# tests/conftest.py
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession
@pytest_asyncio.fixture
async def db_session():
async with engine.connect() as conn:
await conn.begin()
async with AsyncSession(bind=conn, expire_on_commit=False) as session:
yield session
await conn.rollback()
db_session استعمال کرنے والے ہر test کو database کا مکمل isolated view ملتا ہے — ایک test میں کی گئی inserts اگلے test کو نظر نہیں آتیں، کیونکہ ہر test کے بعد outer transaction rollback ہو جاتی ہے چاہے test نے خود commit() call کیا ہو یا نہ کیا ہو۔ یہ tests کے درمیان tables truncate کرنے کے مقابلے میں معنی خیز طور پر تیز ہے، اور test-order-dependent flakiness کی ایک پوری قسم سے بچاتا ہے جو test suite کے چند files سے آگے بڑھنے پر سامنے آتی ہے۔
Endpoint-level tests کے لیے، اسے FastAPI Dependency Injection: Patterns and Anti-Patterns کے dependency_overrides pattern کے ساتھ ملائیں — get_db کو override کریں تاکہ وہ اسی transaction-wrapped db_session کو yield کرے، یوں httpx.AsyncClient کے ذریعے HTTP-level test اور کسی service-function کا direct test ایک جیسی isolation guarantees حاصل کرتے ہیں۔
Concurrent Load کے تحت Connection Pool Sizing
pool_size اور max_overflow صرف ظاہری settings نہیں ہیں — یہ براہِ راست طے کرتی ہیں کہ ایک وقت میں کتنی concurrent requests database work کر سکتی ہیں، اس سے پہلے کہ بعد والی requests connection کے لیے queue ہونا شروع ہو جائیں۔ حقیقی concurrency کے تحت اگر pool بہت چھوٹا ہو تو یہ زور دار انداز میں fail نہیں ہوتا؛ بس ہر request تھوڑی سست ہو جاتی ہے کیونکہ requests ایک free connection کے انتظار میں اپنی باری کا انتظار کرتی ہیں، اور اسے آسانی سے خود database کی سستی سمجھ لیا جاتا ہے، حالانکہ bottleneck دراصل pool ہوتا ہے۔
engine = create_async_engine(
DATABASE_URL,
pool_size=20, # connections kept open and ready
max_overflow=10, # additional connections allowed under burst load
pool_timeout=30, # seconds to wait for a connection before raising
pool_recycle=1800, # recycle connections older than 30 minutes
)
ایک مناسب ابتدائی نقطہ یہ ہے کہ pool کو آپ کے ہر worker process کے عام concurrent request volume کو آرام سے سنبھالنے کے قابل رکھا جائے، یہ یاد رکھتے ہوئے کہ اگر آپ متعدد Uvicorn/Gunicorn workers چلاتے ہیں تو ہر worker کو اپنا الگ engine اور اسی وجہ سے اپنا الگ pool ملتا ہے — database کی حقیقی connection ceiling صرف ایک engine میں configured تعداد نہیں ہوتی، بلکہ pool_size + max_overflow کو worker processes کی تعداد سے ضرب دینے سے بنتی ہے۔ pool_recycle خاص طور پر managed databases (RDS, Cloud SQL) کے لیے اہم ہے، جو اپنی طرف سے ایک خاص idle threshold سے زیادہ دیر تک کھلی رہنے والی connections کو خاموشی سے بند کر دیتے ہیں — اس کے بغیر وہ stale connections request کے درمیان الجھا دینے والی failures کے طور پر سامنے آتی ہیں جنہیں pool_pre_ping پکڑ تو لیتا ہے، مگر recycle کرنے سے آپ ابتدا ہی میں اس قیمت سے بچ جاتے ہیں۔
عام غلطیاں
غلطی: session کو per-request کے بجائے module scope پر create کرنا۔ مختلف requests کے درمیان shared session isolation کو توڑ دیتا ہے اور غیر متعلقہ users کے درمیان state leak کرتا ہے۔ حل: ہمیشہ session کو get_db dependency کے اندر create کریں، ایک request تک محدود رکھیں۔
غلطی: بصورتِ دیگر async app میں sync engine کو ملا دینا۔ async def route کے اندر sync SQLAlchemy call event loop کو بالکل اسی طرح block کرتی ہے جیسا کہ Why Your FastAPI Endpoint Blocks the Event Loop میں بیان کیا گیا ہے، اور اس کے اوپر async/sync API mismatch اپنے الگ errors بھی پیدا کرتی ہے۔ حل: مستقل طور پر create_async_engine اور asyncpg driver استعمال کریں — ایک ہی codebase میں sync اور async SQLAlchemy کو نہ ملائیں۔
غلطی: Pydantic response model میں relationships تک eager loading کے بغیر رسائی۔ Production میں MissingGreenlet errors کا یہ سب سے عام ذریعہ ہے، کیونکہ یہ serialization کے دوران ہوتا ہے، یعنی handler کا اپنا code ختم ہونے کے بعد۔ حل: ہر وہ relationship جسے آپ کا response model touch کرے گا، query کے اندر ہی واضح طور پر eager-load کریں۔
غلطی: ہر request پر نیا engine create کرنا۔ ہر engine اپنے pool کا مالک ہوتا ہے، اس لیے حقیقی concurrency کے تحت یہ بہت جلد database کی max connections ختم کر دیتا ہے۔ حل: engine کو ایک بار، lifespan میں create کریں، اور ہر session کے لیے دوبارہ استعمال کریں۔
غلطی: pool_pre_ping set نہ کرنا۔ Connection pool ایسی connections کے references سنبھالے رکھ سکتا ہے جنہیں database پہلے ہی بند کر چکا ہو (idle timeouts، restarts)، جس سے request کے درمیان الجھا دینے والی failures سامنے آتی ہیں۔ حل: Production میں pool_pre_ping=True فعال کریں، خاص طور پر load balancer یا managed database کے پیچھے جہاں اپنا idle timeout موجود ہو۔
Production Best Practices
- ہر app کے لیے ایک engine، ہر request کے لیے ایک session۔ ان دونوں lifecycles کو کبھی ایک نہ سمجھیں۔
expire_on_commit=Falseset کریں تاکہ committed objects response serialization کے دوران نئی lazy loads trigger کیے بغیر قابلِ استعمال رہیں۔- ہر response path کے لیے واضح طور پر eager-load کریں، collections کے لیے
selectinloadاور single related objects کے لیےjoinedloadاستعمال کرتے ہوئے۔ - Read-heavy endpoints میں استعمال ہونے والی relationships پر
lazy="raise"پر غور کریں تاکہ خاموش lazy-load bugs کو واضح، فوراً قابلِ عمل errors میں بدلا جا سکے۔ - Session dependency میں ہمیشہ واضح طور پر commit یا rollback کریں، implicit commit پر کبھی انحصار نہ کریں — غیر committed write جو autoflush behavior کی وجہ سے local طور پر “کام کرتی” دکھائی دی ہو، production میں عام حیرت کا سبب بنتی ہے۔
اختتامیہ
MissingGreenlet کوئی پراسرار async incompatibility نہیں — یہ SQLAlchemy کی ایک درست رپورٹ ہے کہ کسی چیز کو database کی ضرورت اس وقت پڑی جب session کا async context پہلے ہی ختم ہو چکا تھا، اور یہ تقریباً ہمیشہ framework bug کے بجائے session lifecycle یا eager-loading میں خلا کا مسئلہ ہوتا ہے۔ Engine کے lifespan، session کے per-request scope، اور آپ کی relationships کی loading strategy کو ایک دوسرے کے ساتھ align کر دیں، تو یہ error class case by case debug کرنے کی ضرورت کے بغیر مکمل طور پر غائب ہو جاتی ہے۔
کیا آپ کے response models ایسی relationships کو touch کر رہے ہیں جنہیں آپ نے کبھی واضح طور پر eager-load نہیں کیا؟ اگر آپ کو یقین نہیں، تو staging environment میں ایک دن کے لیے اپنے مصروف ترین models پر lazy="raise" set کرنا یہ جاننے کا ایک تیز، کم خطرے والا طریقہ ہے — ہر خاموش lazy load ایک stack trace میں بدل جائے گی جو ٹھیک اسی missing selectinload یا joinedload call کی طرف اشارہ کرے گی۔
