Skip to content

fix(tracing): disable autoflush on async_sessionmaker to prevent span FK violations - #14229

Closed
ashutoshdharibm wants to merge 1 commit into
langflow-ai:mainfrom
ashutoshdharibm:fix/tracing-autoflush-span-fk-violation
Closed

fix(tracing): disable autoflush on async_sessionmaker to prevent span FK violations#14229
ashutoshdharibm wants to merge 1 commit into
langflow-ai:mainfrom
ashutoshdharibm:fix/tracing-autoflush-span-fk-violation

Conversation

@ashutoshdharibm

@ashutoshdharibm ashutoshdharibm commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

SQLAlchemy's default autoflush=True causes implicit flushes between individual session.merge() calls inside _flush_to_database(). When this fires, a child span is written to the DB before its parent span, producing a FK violation on span.parent_span_id → span.id — even though topological_sort_spans() (added in #12242) has already sorted spans into the correct order.

The topological sort was correct all along. The problem is SQLAlchemy's autoflush defeating it mid-loop.

Root cause

In DatabaseService.__init__(), both async_sessionmaker() call-sites use the default autoflush=True. Inside _flush_to_database() in native.py:

async with session_scope() as session:
    for span in topological_sort_spans(spans):
        session.merge(span)        # ← autoflush can fire here mid-loop
        await session.flush()

When autoflush fires between two merge() calls, a child span whose parent hasn't been merged yet lands in the DB first → IntegrityError.

Fix

Pass autoflush=False to both async_sessionmaker() call-sites in DatabaseService.__init__(). No writes are lost because session_scope() calls await session.commit() explicitly on exit.

# before
self.async_session = async_sessionmaker(engine, expire_on_commit=False)

# after
self.async_session = async_sessionmaker(engine, expire_on_commit=False, autoflush=False)

Tests

4 regression tests added in test_autoflush_disabled.py:

Test Purpose
test_async_session_maker_has_autoflush_false Guards factory config
test_default_autoflush_is_true_without_fix Documents the unsafe baseline
test_topo_sorted_merge_with_autoflush_false_does_not_raise E2E: topological merge + FK constraint with autoflush disabled
test_session_maker_factory_produces_autoflush_false_sessions Confirms produced sessions inherit the flag

All 4 pass locally.

Relationship to prior work

Fixes: DSLF-524

Summary by CodeRabbit

  • Bug Fixes

    • Improved database session handling to prevent foreign-key errors when saving related records.
    • Ensured parent and child span records can be merged and committed successfully.
  • Tests

    • Added regression coverage for session configuration and span persistence.
    • Verified behavior with foreign-key enforcement enabled.

… FK violations

SQLAlchemy's default autoflush=True can fire an implicit flush between
individual session.merge() calls inside _flush_to_database().  When that
happens, a child span may reach the database before its parent, producing
an IntegrityError on span.parent_span_id -> span.id even though
topological_sort_spans() has already sorted the spans in the correct order.

Fix: pass autoflush=False to both async_sessionmaker() call-sites in
DatabaseService.__init__().  Writes are never lost because session_scope()
calls await session.commit() explicitly on exit.

Adds 4 regression tests in test_autoflush_disabled.py:
- test_async_session_maker_has_autoflush_false  (guards factory config)
- test_default_autoflush_is_true_without_fix    (documents unsafe baseline)
- test_topo_sorted_merge_with_autoflush_false_does_not_raise  (E2E w/ FK)
- test_session_maker_factory_produces_autoflush_false_sessions

Fixes: DSLF-524
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Async database sessionmakers now use autoflush=False during initialization and engine reload. Regression tests verify the setting and confirm parent-child span merges commit successfully with foreign-key enforcement.

Changes

Async session autoflush fix

Layer / File(s) Summary
Sessionmaker configuration
src/backend/base/langflow/services/database/service.py
DatabaseService sets autoflush=False for sessionmakers created during initialization and engine reload.
Foreign-key regression coverage
src/backend/tests/unit/services/database/test_autoflush_disabled.py
Tests verify explicit and default autoflush behavior, configure foreign-key-enforced SQLite, and validate successful parent-child span persistence.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested labels: bug


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Test Coverage For New Implementations ❌ Error The new tests are real, but they recreate sessionmakers directly and never instantiate DatabaseService or call reload_engine, so the changed code isn’t covered. Update tests to use a real DatabaseService fixture, assert its initial async_session_maker, then call reload_engine() and verify the rebuilt factory still has autoflush=False.
Test Quality And Coverage ⚠️ Warning Tests mostly recreate async_sessionmaker manually; none instantiate DatabaseService or exercise reload_engine, so they don't guard the changed code paths. Use a real DatabaseService fixture, assert its initial async_session_maker/autoflush, call reload_engine(), and assert the rebuilt factory still produces autoflush=False sessions.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: disabling autoflush on async_sessionmaker to avoid span foreign-key violations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test File Naming And Structure ✅ Passed The backend test file is correctly named under unit tests, uses pytest classes/methods with clear names, has yield-based fixture cleanup, and covers both positive and negative cases.
Excessive Mock Usage Warning ✅ Passed No mocks are used in the added database tests; they exercise real async SQLite engines and SQLModel sessions instead of mocked core logic.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/backend/tests/unit/services/database/test_autoflush_disabled.py (1)

134-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the redundant asyncio marker.

pytest-asyncio auto mode already runs these async test methods. Based on learnings, pytest-asyncio is configured with asyncio_mode = 'auto'; tests should avoid unnecessary pytest.mark.asyncio decorators.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/services/database/test_autoflush_disabled.py` around
lines 134 - 135, Remove the redundant `@pytest.mark.asyncio` decorator from the
TestAutoflushFKViolationRegression class, relying on the configured
pytest-asyncio auto mode while leaving the async test methods unchanged.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/tests/unit/services/database/test_autoflush_disabled.py`:
- Around line 58-64: Update
src/backend/tests/unit/services/database/test_autoflush_disabled.py at lines
58-64, 182-186, and 214-219 to test the real DatabaseService rather than
independently recreated async_sessionmaker instances: use
DatabaseService.async_session_maker for the initial-session assertions, run the
merge regression through that service-created factory, and call
DatabaseService.reload_engine() before asserting the replacement factory still
has autoflush=False.

---

Nitpick comments:
In `@src/backend/tests/unit/services/database/test_autoflush_disabled.py`:
- Around line 134-135: Remove the redundant `@pytest.mark.asyncio` decorator from
the TestAutoflushFKViolationRegression class, relying on the configured
pytest-asyncio auto mode while leaving the async test methods unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ffced404-848d-40e1-9ba0-e5ccfb0025bb

📥 Commits

Reviewing files that changed from the base of the PR and between c1030a3 and bccee43.

📒 Files selected for processing (2)
  • src/backend/base/langflow/services/database/service.py
  • src/backend/tests/unit/services/database/test_autoflush_disabled.py

Comment on lines +58 to +64
# Replicate the exact factory construction used in DatabaseService.__init__
factory = async_sessionmaker(
engine,
class_=SQLModelAsyncSession,
expire_on_commit=False,
autoflush=False,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Test DatabaseService rather than recreated session factories.

These tests can all pass if DatabaseService.__init__() or reload_engine() stops passing autoflush=False. Use a real DatabaseService fixture, assert a session from its initial factory, call reload_engine(), then assert a session from the rebuilt factory.

  • src/backend/tests/unit/services/database/test_autoflush_disabled.py#L58-L64: replace the standalone factory with the initial DatabaseService.async_session_maker.
  • src/backend/tests/unit/services/database/test_autoflush_disabled.py#L182-L186: run the merge regression through the service-created session factory.
  • src/backend/tests/unit/services/database/test_autoflush_disabled.py#L214-L219: exercise DatabaseService.reload_engine() and assert its replacement factory retains autoflush=False.

As per coding guidelines, new backend bug fixes must include tests that cover the changed behavior rather than placeholders.

📍 Affects 1 file
  • src/backend/tests/unit/services/database/test_autoflush_disabled.py#L58-L64 (this comment)
  • src/backend/tests/unit/services/database/test_autoflush_disabled.py#L182-L186
  • src/backend/tests/unit/services/database/test_autoflush_disabled.py#L214-L219
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/services/database/test_autoflush_disabled.py` around
lines 58 - 64, Update
src/backend/tests/unit/services/database/test_autoflush_disabled.py at lines
58-64, 182-186, and 214-219 to test the real DatabaseService rather than
independently recreated async_sessionmaker instances: use
DatabaseService.async_session_maker for the initial-session assertions, run the
merge regression through that service-created factory, and call
DatabaseService.reload_engine() before asserting the replacement factory still
has autoflush=False.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants