Repository navigation
Conversation
The query cache is an ETS set with no eviction policy. Its key is the query shape, and several constructs embed runtime values in the shape: string field names in field/2, interpolated JSON paths in json_extract_path/2, identifier/1, constant/1 and the :prefix option. Each distinct value therefore adds a new entry holding the planned query and the adapter prepared statement that is never released, so applications that let request data reach those constructs grow their memory without bound. Add a :query_cache_max_size repository option (default 10_000, :infinity to disable). The limit is only checked on cache misses and, once reached, the cache is cleared before inserting the new query. Document the per-value caching behaviour in field/2 and json_extract_path/2 and point to query_cache: false for user-controlled values.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The repository query cache is an ETS
:setwith no size limit and no eviction. Its key is the query shape, and several constructs put runtime values into the shape rather than into the parameter list:field(x, ^"name")with a string,json_extract_path(f, ^path)/f[^key],identifier/1,constant/1, the:prefixoption, and literal leaves of an interpolated select map. Each distinct value adds one entry holding the planned query and the adapter's prepared statement, and the entry is never released. Queries that fail at execution (an unknown column) are cached too, since planning succeeded. Thejson_extract_path/2docs encourage dynamic paths and thefield/2docs recommend string names "to avoid creating a large number of atoms", neither mentions that every value is kept forever.Measured through
Ecto.CachingTestAdapter:With a real adapter each entry also creates a named server-side prepared statement on every pooled connection.
This adds a
:query_cache_max_sizerepo option (default10_000,:infinityto disable). The limit is read from the cache table itself and checked only on inserts, so hits cost nothing extra; once the table is past the limit it is cleared before the new query is inserted, and queries are re-prepared as they are executed.field/2,json_extract_path/2and theEcto.Repooption docs now say that each distinct value is a separately cached query and point atquery_cache: falsefor user-controlled values.One limitation worth knowing: clearing the ETS table does not close the server-side prepared statements ecto_sql created for the evicted entries. Those live for the connection lifetime regardless. Bounding them needs a hook in ecto_sql (
DEALLOCATEon eviction, or treating those query shapes as:nocache), which I left for a follow-up there.Checks run:
mix test test/ecto/query/planner_test.exs test/ecto/repo_test.exs: 5 new tests (default, invalid values, clear at limit,:infinity, repo option wiring); all pass (331 in the two files); the clear-at-limit test fails on master.mix test: 1617 passed (97 doctests, 1520 tests)mix compile --force --warnings-as-errors: cleanmix format --check-formatted: clean. Note:mix formatalso reformatted a few unrelated lines inplanner_test.exsandrepo_test.exsthat were not formatted on master; happy to drop those hunks if you prefer the diff minimal.Not run: the integration suites in ecto_sql (no database). Elixir 1.20.2 / OTP 29 only.
Prepared with AI assistance (Claude); I reproduced the growth against
Ecto.Query.Plannerdirectly and verified the red-green tests.