Still making little progress fixing my stray engine problem.
This commit is contained in:
parent
c6165af40e
commit
085905557d
6 changed files with 171 additions and 53 deletions
|
|
@ -132,8 +132,19 @@ class CRUDService(Generic[T]):
|
|||
self._session_factory = session_factory
|
||||
self.polymorphic = polymorphic
|
||||
self.supports_soft_delete = hasattr(model, 'is_deleted')
|
||||
# Cache backend info once. If not provided, derive from session bind.
|
||||
bind = session_factory().get_bind()
|
||||
|
||||
# Derive engine WITHOUT leaking a session/connection
|
||||
bind = getattr(session_factory, "bind", None)
|
||||
if bind is None:
|
||||
tmp_sess = session_factory()
|
||||
try:
|
||||
bind = tmp_sess.get_bind()
|
||||
finally:
|
||||
try:
|
||||
tmp_sess.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
eng: Engine = bind.engine if isinstance(bind, Connection) else cast(Engine, bind)
|
||||
self.backend = backend or make_backend_info(eng)
|
||||
|
||||
|
|
@ -147,6 +158,14 @@ class CRUDService(Generic[T]):
|
|||
return self.session.query(poly), poly
|
||||
return self.session.query(self.model), self.model
|
||||
|
||||
def _debug_bind(self, where: str):
|
||||
try:
|
||||
bind = self.session.get_bind()
|
||||
eng = getattr(bind, "engine", bind)
|
||||
print(f"SERVICE BIND [{where}]: engine_id={id(eng)} url={getattr(eng, 'url', '?')} session={type(self.session).__name__}")
|
||||
except Exception as e:
|
||||
print(f"SERVICE BIND [{where}]: failed to introspect bind: {e}")
|
||||
|
||||
def _apply_not_deleted(self, query, root_alias, params) -> Any:
|
||||
if self.supports_soft_delete and not _is_truthy((params or {}).get("include_deleted")):
|
||||
return query.filter(getattr(root_alias, "is_deleted") == False)
|
||||
|
|
@ -224,6 +243,7 @@ class CRUDService(Generic[T]):
|
|||
- forward/backward seek via `key` and `backward`
|
||||
Returns a SeekWindow with items, first/last keys, order spec, limit, and optional total.
|
||||
"""
|
||||
self._debug_bind("seek_window")
|
||||
session = self.session
|
||||
query, root_alias = self.get_query()
|
||||
|
||||
|
|
@ -455,6 +475,7 @@ class CRUDService(Generic[T]):
|
|||
|
||||
def get(self, id: int, params=None) -> T | None:
|
||||
"""Fetch a single row by id with conflict-free eager loading and clean projection."""
|
||||
self._debug_bind("get")
|
||||
query, root_alias = self.get_query()
|
||||
|
||||
# Defaults so we can build a projection even if params is None
|
||||
|
|
@ -549,6 +570,7 @@ class CRUDService(Generic[T]):
|
|||
|
||||
def list(self, params=None) -> list[T]:
|
||||
"""Offset/limit listing with smart relationship loading and clean projection."""
|
||||
self._debug_bind("list")
|
||||
query, root_alias = self.get_query()
|
||||
|
||||
# Defaults so we can reference them later even if params is None
|
||||
|
|
|
|||
|
|
@ -1,20 +1,58 @@
|
|||
# crudkit/integrations/flask.py
|
||||
from __future__ import annotations
|
||||
from flask import Flask
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from ..engines import CRUDKitRuntime
|
||||
from ..config import Config
|
||||
|
||||
def init_app(app: Flask, *, runtime: CRUDKitRuntime | None = None, config: type[Config] | None == None):
|
||||
def init_app(app: Flask, *, runtime: CRUDKitRuntime | None = None, config: type[Config] | None = None):
|
||||
"""
|
||||
Initializes CRUDKit for a Flask app. Provies `app.extensions['crudkit']`
|
||||
Initializes CRUDKit for a Flask app. Provides `app.extensions['crudkit']`
|
||||
with a runtime (engine + session_factory). Caller manages session lifecycle.
|
||||
"""
|
||||
runtime = runtime or CRUDKitRuntime(config=config)
|
||||
app.extensions.setdefault("crudkit", {})
|
||||
app.extensions["crudkit"]["runtime"] = runtime
|
||||
|
||||
Session = runtime.session_factory
|
||||
if Session is not None:
|
||||
app.extensions["crudkit"]["Session"] = scoped_session(Session)
|
||||
# Build ONE sessionmaker bound to the ONE true engine object
|
||||
# so engine id == sessionmaker.bind id, always.
|
||||
engine = runtime.engine
|
||||
SessionFactory = runtime.session_factory or sessionmaker(bind=engine, **runtime._config.session_kwargs())
|
||||
app.extensions["crudkit"]["SessionFactory"] = SessionFactory
|
||||
app.extensions["crudkit"]["Session"] = scoped_session(SessionFactory)
|
||||
|
||||
# Attach pool listeners to the *same* engine the SessionFactory is bound to.
|
||||
# Don’t guess. Don’t hope. Inspect.
|
||||
try:
|
||||
bound_engine = getattr(SessionFactory, "bind", None) or getattr(SessionFactory, "kw", {}).get("bind") or engine
|
||||
pool = bound_engine.pool
|
||||
|
||||
from sqlalchemy import event
|
||||
|
||||
@event.listens_for(pool, "checkout")
|
||||
def _on_checkout(dbapi_conn, conn_record, conn_proxy):
|
||||
sz = pool.size()
|
||||
chk = pool.checkedout()
|
||||
try:
|
||||
conns_in_pool = pool.checkedin()
|
||||
except Exception:
|
||||
conns_in_pool = "?"
|
||||
print(f"POOL CHECKOUT: Pool size: {sz} Connections in pool: {conns_in_pool} "
|
||||
f"Current Overflow: {pool.overflow()} Current Checked out connections: {chk} "
|
||||
f"engine id= {id(bound_engine)}")
|
||||
|
||||
@event.listens_for(pool, "checkin")
|
||||
def _on_checkin(dbapi_conn, conn_record):
|
||||
sz = pool.size()
|
||||
chk = pool.checkedout()
|
||||
try:
|
||||
conns_in_pool = pool.checkedin()
|
||||
except Exception:
|
||||
conns_in_pool = "?"
|
||||
print(f"POOL CHECKIN: Pool size: {sz} Connections in pool: {conns_in_pool} "
|
||||
f"Current Overflow: {pool.overflow()} Current Checked out connections: {chk} "
|
||||
f"engine id= {id(bound_engine)}")
|
||||
except Exception as e:
|
||||
print(f"[crudkit.init_app] Failed to attach pool listeners: {e}")
|
||||
|
||||
return runtime
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue