Summary
With postgresql+asyncpg:// and DatabaseSessionService, every SSE/stream
client disconnect mid-run produces:
sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;
can't call await_only() here.
Reproduces 100% of the time when the client closes the stream while
append_event is committing.
Version
- google-adk: 1.32.0 (verified identical code on main / 1.33 / 1.34 / 2.0)
- SQLAlchemy: 2.x
- asyncpg: 0.31.0
- Python: 3.12
- Driver recommended by ADK docs: asyncpg
Root cause
database_session_service.py:208 forces pool_pre_ping=True for any
non-SQLite URL (added in da73e71 on 2026-02-03 — this is a regression
for asyncpg users).
append_event at L740 does await sql_session.commit(), then at L743
accesses storage_session.update_time via
storage_session.get_update_timestamp(is_sqlite).
- With async engine + commit, that attribute access lazy-loads → pool
checkout → _do_ping_w_event → await_only() outside the greenlet
bridge → MissingGreenlet.
- Normally the request completes before this triggers. A client disconnect
raises GeneratorExit through parallel_agent._merge_agent_run, which
surfaces the bad code path.
Repro
- Run any agent under an SSE endpoint with
DatabaseSessionService(db_url="postgresql+asyncpg://...") (no extra kwargs — defaults only).
- Disconnect the client mid-stream (close tab / abort fetch).
- Server log shows the MissingGreenlet stack from
append_event.
Stack trace (abridged)
File ".../google/adk/sessions/database_session_service.py", line 743, in append_event
session.last_update_time = storage_session.get_update_timestamp(is_sqlite)
File ".../google/adk/sessions/schemas/v1.py", line 131, in get_update_timestamp
return self.update_time.timestamp()
File ".../sqlalchemy/orm/attributes.py", line 569, in __get__
return self.impl.get(state, dict_)
...
File ".../sqlalchemy/dialects/postgresql/asyncpg.py", line 816, in ping
_ = self.await_(self._async_ping())
File ".../sqlalchemy/util/_concurrency_py3k.py", line 123, in await_only
raise exc.MissingGreenlet(...)
sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;
can't call await_only() here.
Workaround (confirmed)
Pass pool_pre_ping=False — kwargs are forwarded to create_async_engine:
DatabaseSessionService(db_url=url, pool_pre_ping=False)
But pool_pre_ping is the recommended setting for production, so
disabling it is not a real fix.
Prior reports (closed stale, not fixed)
Suggested fixes
-
Refresh update_time explicitly before commit, so the post-commit
access doesn't lazy-load:
await sql_session.refresh(storage_session, ["update_time"])
await sql_session.commit()
-
Read update_time into a local before commit (cheapest fix):
updated_at = storage_session.update_time
await sql_session.commit()
session.last_update_time = updated_at.timestamp()
-
Use expire_on_commit=False consistently and guard the lazy-load path.
Happy to send a PR if a maintainer can confirm preferred approach.
Summary
With
postgresql+asyncpg://andDatabaseSessionService, every SSE/streamclient disconnect mid-run produces:
sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;
can't call await_only() here.
Reproduces 100% of the time when the client closes the stream while
append_eventis committing.Version
Root cause
database_session_service.py:208forcespool_pre_ping=Truefor anynon-SQLite URL (added in da73e71 on 2026-02-03 — this is a regression
for asyncpg users).
append_eventat L740 doesawait sql_session.commit(), then at L743accesses
storage_session.update_timeviastorage_session.get_update_timestamp(is_sqlite).checkout →
_do_ping_w_event→await_only()outside the greenletbridge → MissingGreenlet.
raises
GeneratorExitthroughparallel_agent._merge_agent_run, whichsurfaces the bad code path.
Repro
DatabaseSessionService(db_url="postgresql+asyncpg://...")(no extra kwargs — defaults only).append_event.Stack trace (abridged)
Workaround (confirmed)
Pass
pool_pre_ping=False— kwargs are forwarded tocreate_async_engine:But
pool_pre_pingis the recommended setting for production, sodisabling it is not a real fix.
Prior reports (closed stale, not fixed)
Suggested fixes
Refresh
update_timeexplicitly before commit, so the post-commitaccess doesn't lazy-load:
Read
update_timeinto a local before commit (cheapest fix):Use
expire_on_commit=Falseconsistently and guard the lazy-load path.Happy to send a PR if a maintainer can confirm preferred approach.