feat(21.8): an index carries its own schema-cache TTL (Part D) - #314
Merged
Conversation
Story 21.8 Part D. Since #306 every executed statement reads the cached schema, so a stale entry no longer means a stale column list — it means Painless emitted for the previous mapping. Retype a column keyword -> long and this client keeps emitting Long.parseLong(doc['x'].value) against a numeric field for the rest of the TTL. The TTL stopped being a performance knob, and it was a hardcoded five minutes in three files. D.1 — the default is now `elastic.schema-cache.ttl` (ELASTIC_SCHEMA_CACHE_TTL, 5m), a HOCON duration. D.2 — an index may override it for itself. The volatility of a mapping is a property of the index, so the value lives with the index, at `_meta.schema_cache_ttl`, and each cache entry expires on its own clock (CachedSchema + one `isExpired` rule on a shared CacheEntry). Two spellings, ONE thing written: `ALTER TABLE t SET SCHEMA CACHE TTL [=] '10m'` and `DROP SCHEMA CACHE TTL` are sugar that desugar to the existing AlterTableMapping / DropTableMapping on that path — no new AST, no new merge, diff or render arm. The duration is validated at parse time by the same parse the client applies (`err`, never `throw`, #250), so a misspelled TTL is refused before it reaches the cluster instead of being ignored for the life of the index. Lead ruling (D.4/OQ-2): the shard-count cache follows the SAME value — the shortest TTL among the indices its key names — and `shardCountCacheTtlMs` is deleted. A schema cached for an hour beside a shard count re-probed every five minutes was incoherent, and two constants in two files drift silently. The 404 negative cache follows the default only, and downwards only: it records a MISS, so there is no metadata to read a per-index TTL from, and nothing invalidates it — a lengthened miss would leave a table created after a failed probe running with no schema attached, no conversions and no temporal resolution, for that whole period. The schema cache is also bounded now (it never was): past 256 entries a miss drops the expired ones, past 1024 live entries it drops the cache. Its values are whole schemas, and expiry alone bounds nothing once an index can ask for a long TTL over ever-changing index names. Verified on real Elasticsearch on all five clients (6.8 rest + jest, 7.17, 8.18, 9.0): the TTL round-trips through `_meta` and survives an unrelated later ALTER, which is what `Table.update()` rebuilding its own `_meta` keys puts at risk. 931 sql + 944 core unit tests; 15 mutations run, each RED as predicted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fupelaqu
marked this pull request as ready for review
September 8, 2026 16:37
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.
Story 21.8 — Part D: per-index schema-cache TTL
Part D only. Parts A/B/C/E/F/G of story 21.8 are untouched and live on
feature/21.8.Why this is not a performance knob
Since #306 every executed statement reads the cached schema. A stale entry no longer means a stale
column list — it means Painless emitted for the previous mapping. Retype a column
keyword→longand, for the rest of the TTL, the client keeps emittingLong.parseLong(doc['x'].value)against a field that is already numeric. It was a hardcoded five minutes, in three files.
What it does
D.1 — the default.
elastic.schema-cache.ttl(ELASTIC_SCHEMA_CACHE_TTL, default5m), aHOCON duration.
D.2 — the per-index TTL. Stored in the index's own mapping metadata (
_meta.schema_cache_ttl),so every client against that cluster picks it up. Each cache entry is stamped with the TTL that
governed it and expires on its own clock (
CachedSchema, with a singleisExpiredrule on a sharedCacheEntry).Precedence: index metadata >
elastic.schema-cache.ttl> the built-in 5 minutes.Two spellings, one thing written.
ALTER TABLE t SET SCHEMA CACHE TTL [=] '10m'andDROP SCHEMA CACHE TTLare sugar: they desugar to the existingAlterTableMapping/DropTableMappingon that_metapath — no new AST, no new merge / diff / render arm, one keyderivation. These three are identical:
The duration is validated at parse time, by the same parse the client applies (
err, neverthrow— #250), so a misspelled TTL is refused before it reaches the cluster rather than silentlyignored for the life of the index.
The lead rulings this implements
ScrollApi.shardCountCacheTtlMsisdeleted; the cache takes the shortest resolved TTL among the indices its key names. A schema
cached for an hour next to a shard count re-probed every five minutes is incoherent, and two
constants in two files drift silently — each half reading as correct in isolation.
_metawrite and dedicated keywords), sharing one derivation of the key.Deliberate asymmetries
metadata to read a per-index TTL from (D.3.2). It is also capped at the built-in five minutes:
nothing invalidates a miss and no lookup is attempted while one stands, so a lengthened one would
leave a table created after a failed probe running with no schema attached — no conversions,
no temporal-literal resolution — for that whole period.
the old period governs. Bounded, and documented.
1024 live entries it drops the cache. Values here are whole schemas, and expiry alone bounds
nothing once an index can ask for a long TTL over ever-changing index names.
Verification
TTL round-trips through
_meta, survives an unrelated later ALTER (whatTable.update()rebuilding its own
_metakeys puts at risk), is replaceable, drops back to the default, and abad duration never reaches the cluster.
sql+ 944coreunit tests; 2.12 cross-compile (main + test);scalafmtCheckAllandheaderCheckclean.min→maxover an index set,>=→>on theexpiry boundary, and the metadata key/path identity.
are fixed in this branch (see below).
Review findings fixed here
compute, before the entry is installed), resurrecting the #276/R4-21 stale-alias-mapping defectscalafmtCheckwas vacuous —project.git = true, and every new file was untrackedshardCountTtlMs(indices)feeds bothPlus LOW fixes: the alias assertion (the case D.3.3 says must be decided) now asserts an observed
fetch, not just the resolver's return value; two integration tests no longer depend on the previous
test's state; a dead scaladoc link.
🔴 One fix is ungated and I would rather say so. The alias-row fix cannot be covered by a test:
the divergent state exists only between the alias
putandcomputeinstalling the entry, and everyseam a test can override runs before that
put. The structural cure is to carry the target on thecache entry and delete the second map — recorded at the code site, deliberately not done here
because it rewrites #276's invalidation path.
Release notes
elastic.schema-cache.ttl(ELASTIC_SCHEMA_CACHE_TTL, default5m). It alsogoverns the primary shard-count cache and (downwards only) the 404 negative cache, each of which
had its own hardcoded five minutes. A deployment that sets nothing sees no behaviour change.
ALTER TABLE … SET SCHEMA CACHE TTL [=] '<duration>'/DROP SCHEMA CACHE TTL.ScrollApi.shardCountCacheTtlMs(protected) is REMOVED. A subclass overriding it no longercompiles — override
schemaCacheTtlMsinstead. No occurrence in extensions / jdbc / arrow.ElasticConfigarity9→10 (new defaulted
schemaCache, source-compatible), andIndicesApi/ScrollApi/SearchApieach gain
SchemaCacheTtlApias a parent (schemaCacheTtlMsmoved there).SCHEMA,CACHE,TTL— are highlighted and completed in the REPL. Theyare not parser-reserved:
SELECT schema FROM t,ADD COLUMN cache INTandSET MAPPING schema = …all still parse (pinned).Documentation
documentation/sql/ddl_statements.md,documentation/client/search.md(the Tuning section),documentation/client/scroll.md,documentation/client/common_principles.md(HOCON reference), andthe REPL help corpus (
help/commands/ddl/alter_table.json, guarded byHelpCorpusSpec).Two stale claims were corrected while there: the schema lookup is no longer "every 5 minutes", and
dql_statements.mdstill said the resolution happens "only for statements whoseWHEREcompares astring literal to a column" — false since #306 made the attach unconditional.
Every published SQL example was run through the real parser, and the distinctive ones are pinned so
they cannot rot. The docs-site twin is SOFTNETWORK-APP/softclient4es-web#54.
🤖 Generated with Claude Code