Skip to content

feat(dbms): close idle sessions that keep a FORCE-dropped database pinned - #4843

Merged
andrejtonev merged 5 commits into
masterfrom
evict/4583-final
Oct 2, 2026
Merged

andrejtonev merged 5 commits into
masterfrom
evict/4583-final

Conversation

@andrejtonev

@andrejtonev andrejtonev commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

What & why

DROP DATABASE … FORCE (#4573) leaves the dropped database draining until nothing holds it. A Bolt connection whose current database it was keeps it pinned until the client sends another query — which an idle pooled connection (e.g. a Neo4j driver pool) may never do — so the database never drains.

This closes such connections. Nothing in the interpreter or the Bolt session changes: it reuses the #4577 termination path (SessionRegistry → RequestTermination).

Mechanism

  • Handler<T> drain hook (SetDrainHook / ClearDrainHook): the deferred-drop worker runs it once per tick while any drop is pending, before the husks' try_delete. It is held under its own drain_hook_mutex_ while it runs, so ClearDrainHook returns only once the hook is not running and never will again. That mutex is taken only by the single defer worker and by set/clear at startup/shutdown — never by a query or a DROP.
  • InterpreterContext owns the hook for its lifetime: InterpreterContextLifetimeControl registers it after construction and unregisters it before destruction, so capturing the context is safe by construction (no dependency on declaration order in main()).
  • The hook (CloseSessionsOnDroppingDatabases, next to TerminateSessions): under interpreters.WithLock, collects the session uuid of every interpreter whose foreign_db_view().marked_for_deletion is set; after the lock, RequestTermination() on each. The session closes itself on its own strand between Bolt messages — a busy one after its current message, never mid-query — and its teardown releases the pin, so the next try_delete succeeds.
  • Why every tick, not once at DROP time: it also covers drops applied on a replica (Delete(uuid) never goes through the DROP handler) and a session that attaches just as the database is dropped.
  • Only seal() (the FORCE path) marks a database, so plain DROP, same-name recreates and sessions on other databases are untouched. Enterprise builds only (#ifdef MG_ENTERPRISE).

Behaviour notes

  • A session that is busy when selected finishes its current message and is then closed, even if that message switched it to another database (it was holding the dropped database when selected). Clients see a disconnect and reconnect.
  • A session that FORCE-drops its own current database and then stays idle for a tick is closed too (it is an idle connection holding the dropped database).

@andrejtonev andrejtonev added CI -build=coverage -test=core Run coverage build and core tests on push CI -build=coverage -test=clang_tidy CI -build=debug -test=core Run debug build and core tests on push CI -build=debug -test=integration Run debug build and integration tests on push CI -build=release -test=e2e Run release build and e2e tests on push CI -build=release -test=stress Run release build and stress tests on push Docs needed Docs needed feature feature labels Sep 15, 2026
@andrejtonev

andrejtonev commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Tracking

  • [Link to Epic/Issue]

Standard development

  • Update unit/E2E tests
  • Compare the benchmarking results between the master branch this branch
    • NO DATA yet: CI -build=release -test=benchmark was added after the last push; needs a new push to run.

CI Testing Labels

  • Select the appropriate CI test labels (CI -build=build-name -test=test-suite)

Documentation checklist

  • Add the documentation label
  • Add the bug / feature label
  • Add the milestone for which this feature is intended
    • If not known, set for a later milestone
  • Write a release note, including added/changed clauses
    • After DROP DATABASE ... FORCE, Memgraph closes the Bolt sessions still using that database within about 10 seconds, so the drop can finish. Clients see a disconnect and can reconnect. #4843
  • [ Documentation PR link feat(dbms): close idle sessions that keep a FORCE-dropped database pinned documentation#1831 ]
    • Is back linked to this development PR

@andrejtonev andrejtonev changed the title feat(query,dbms): idle-session reaper via the deferred-drop worker (always-on) feat(query,dbms): idle-session reaper via the deferred-drop worker Sep 21, 2026
@andrejtonev andrejtonev self-assigned this Sep 22, 2026
@andrejtonev
andrejtonev force-pushed the feat/detached-tenant-registry branch 2 times, most recently from 0e1c798 to 1e7412b Compare September 23, 2026 11:38
@andrejtonev andrejtonev added this to the mg-v3.14.0 milestone Sep 24, 2026
@andrejtonev
andrejtonev force-pushed the feat/detached-tenant-registry branch from 688d257 to e3d1814 Compare September 24, 2026 13:41
Base automatically changed from feat/detached-tenant-registry to master September 24, 2026 17:31

@colinbarry colinbarry 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.

Mostly great, but a few tiny concerns...

Comment thread src/query/interpreter.hpp Outdated
Comment thread src/query/interpreter.cpp Outdated
Comment thread src/communication/bolt/v1/session.hpp
Comment thread src/dbms/handler.hpp
@andrejtonev
andrejtonev force-pushed the evict/4583-final branch 2 times, most recently from bd02da7 to 3f5006e Compare September 28, 2026 09:19
@andrejtonev andrejtonev changed the title feat(query,dbms): idle-session reaper via the deferred-drop worker feat(dbms): close idle sessions that keep a FORCE-dropped database pinned Sep 28, 2026

@colinbarry colinbarry 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.

All good, other than an unlikely use-after-free that could be easily fixed.

Comment thread src/query/interpreter_context.hpp
andrejtonev added a commit that referenced this pull request Sep 30, 2026
…self (#4969)

Stacked on #4843 (base `evict/4583-final`).

#4843 closes sessions that still hold a FORCE-dropped database when the
deferred-drop worker ticks (every 10 s). A session that sends a query
before that tick releases the database itself (`ResetInterpreter` →
`ReleaseDbIfMarked`), so the hook never sees it. A client that names the
database in its Bolt metadata (`session(database="x")`) then stays on
"Database required", even after `x` is recreated: identical metadata
makes `RuntimeConfig::Configure` skip re-resolving it, until the client
reconnects.

`ReleaseDbIfMarked` now reports whether it released a database, and
`ResetInterpreter` then requests termination of its own session (the
same `SessionRegistry` → `RequestTermination` path as the drain hook).
`RequestTermination` only posts to the session's strand, so the current
message still gets its reply; the connection closes after it and the
driver reconnects. Enterprise builds only, like the hook.

Client-visible rule after this PR: a connection whose current database
is FORCE-dropped is closed — an idle one on the next drain tick, an
active one after its next message.

This replaces the `Configure` re-resolve guard that was trimmed out of
#4932.
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

This PR has potential conflicts with the following other open pull requests which modify the same files:

…nned

DROP DATABASE ... FORCE leaves the database draining until nothing holds it.
A connection whose current database it was keeps it pinned until the client
sends another query, which an idle pooled connection may never do, so the
database never drains.

The deferred-drop worker now runs a drain hook once per tick while a drop is
pending. The InterpreterContext registers one for its lifetime: it asks every
session still holding a database that is being dropped to terminate, through
the same SessionRegistry path TERMINATE SESSIONS uses. The session closes
itself between Bolt messages, never mid-query, and releasing it lets the next
try_delete succeed. Running every tick also covers drops applied on a replica
and a session that attaches just as the database is dropped.
andrejtonev and others added 3 commits September 30, 2026 11:13
…self (#4969)

Stacked on #4843 (base `evict/4583-final`).

#4843 closes sessions that still hold a FORCE-dropped database when the
deferred-drop worker ticks (every 10 s). A session that sends a query
before that tick releases the database itself (`ResetInterpreter` →
`ReleaseDbIfMarked`), so the hook never sees it. A client that names the
database in its Bolt metadata (`session(database="x")`) then stays on
"Database required", even after `x` is recreated: identical metadata
makes `RuntimeConfig::Configure` skip re-resolving it, until the client
reconnects.

`ReleaseDbIfMarked` now reports whether it released a database, and
`ResetInterpreter` then requests termination of its own session (the
same `SessionRegistry` → `RequestTermination` path as the drain hook).
`RequestTermination` only posts to the session's strand, so the current
message still gets its reply; the connection closes after it and the
driver reconnects. Enterprise builds only, like the hook.

Client-visible rule after this PR: a connection whose current database
is FORCE-dropped is closed — an idle one on the next drain tick, an
active one after its next message.

This replaces the `Configure` re-resolve guard that was trimmed out of
#4932.
…estroyed

~Server destroys the Bolt io_context while the defer worker keeps ticking
until InterpreterContextLifetimeControl is destroyed, so a tick could leave
the worker holding the last reference to a session whose reactor is gone.
Clear the hook right after the server has been awaited.
Extract the drain-hook call out of Handler::Tick_() into RunDrainHook_()
to bring its cognitive complexity back under the limit (cpp:S3776), and
capture only to_close in CloseSessionsOnDroppingDatabases (cpp:S3608).
@sonarqubecloud

Copy link
Copy Markdown

…tion

A replica applying a FORCE drop only seals the database; it never aborted a
transaction or closed a connection. Closing sessions on drop changed that:
the drain hook also closed sessions idle inside an explicit transaction,
aborting it, and every session was closed after its first query on the
dropped database, even one that could USE another database.

- The drain hook skips sessions with a live transaction (not IDLE and not
  already TERMINATED by MAIN's FORCE drop); they release the database once
  the transaction ends, or are closed on a later tick once idle.
- A session that releases the dropped database is closed only when the
  database was named in the Bolt connection metadata; a USE DATABASE
  session keeps its connection, as before.
- Commit() with nothing to commit (e.g. USE DATABASE) now returns the
  transaction status to IDLE. It stayed ACTIVE, so an idle session looked
  mid-transaction to foreign readers and the drain hook never closed it.

The replica force-drop e2e now waits past a drain tick and asserts that the
open transaction survives and the idle session is closed.
@andrejtonev andrejtonev added CI -build=community -test=core Run community build and core tests on push CI -build=release -test=benchmark Run release build and benchmark on push labels Sep 30, 2026

@colinbarry colinbarry 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.

Approved 🚀

@andrejtonev
andrejtonev added this pull request to the merge queue Oct 2, 2026
Merged via the queue into master with commit 01cc6b6 Oct 2, 2026
22 checks passed
@andrejtonev
andrejtonev deleted the evict/4583-final branch October 2, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI -build=community -test=core Run community build and core tests on push CI -build=coverage -test=clang_tidy CI -build=coverage -test=core Run coverage build and core tests on push CI -build=debug -test=core Run debug build and core tests on push CI -build=debug -test=integration Run debug build and integration tests on push CI -build=release -test=benchmark Run release build and benchmark on push CI -build=release -test=e2e Run release build and e2e tests on push CI -build=release -test=stress Run release build and stress tests on push Docs needed Docs needed feature feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants