Repository navigation
v0.26.0
[0.26.0] - 2026-10-25
Fixed
- The Pod Identity sidecar's startup probe no longer passes on an earlier
sidecar process's file once its credential has expired or when it records
no expiry. The credentials volume outlives a sidecar
restart, so the probe found the old file and passed before the new process
had fetched anything. The sidecar now removes its written marker
(.credentials.json.written) when it starts and creates it after each
successful write. Without the marker the probe passes only while the file's
credential, whose expiry the sidecar now records in the file as
expiration, has not expired, so a sidecar restarted during a short Pod
Identity agent outage is not killed by its probe. The previous credentials
file stays in place, so a running server keeps its credentials while the
new process fetches. - A slow Pod Identity agent no longer holds back operator reconciles that its
cached credential can serve. The operator's credential cache held its lock
across the agent fetch, so every reconcile that needed credentials waited on
that fetch. A cached credential that has not expired is now returned at
once while a refresh is in flight, and callers with nothing cached share
the one fetch instead of each starting their own. A caller that waited
takes the shared result only if it has not expired at its own time, and
otherwise fetches itself. - A logs scan reserves a dictionary's values before building them. The
SQL memory pool now counts a shared dictionary's Arrow values while a block
is built, not only after, and a pool that cannot hold them refuses the
build with a resources-exhausted error instead of after the allocation. - The SQL memory pool keeps a grouped aggregate's final output charged
until the query releases it. Previously, on a statement running without
spill, the last output batch of aGROUP BYwas released from the query,
tenant and process budgets while it was still in memory. - A logs scan with no filter, or only numeric-range filters, no longer
fetches BLOOM and POSTINGS. A ranged open places BLOOM and POSTINGS only when the query carries a
content arm, a stream-attribute arm, or a prune arm other than a numeric
range, and the reader reads BLOOM only for a content-channelHasWordarm or
string equality of at most 64 bytes, so a prune-channelEqualshas the
fetcher place BLOOM that the reader does not read. Previously every ranged
open fetched both sections, one GET each, so a predicate-free or
numeric-range scan now saves one GET and the section's stored bytes per
section per object.
Added
RavelClustercan pin the query pods' memory budget (ADR-1170, issue
#3200 amendment).spec.query.memoryBudgetBytesrenders
--memory-budget-byteson the query pods only; unset, the query Deployment
renders exactly as before and the budget still derives from the memory
limit, so a larger limit alone raises the budget with it and adds no
headroom beyond the reserve. With the field set, a query memory limit that
is missing, not a valid quantity, or below the budget plus the 256 MiB the
server holds outside it in--mode queryleaves the query Deployment
unchanged (none is created on a new cluster) and reportsDegradedwith
reasonQueryMemoryBudgetExceedsLimitorQueryMemoryLimitUnparseable. A
limit above that floor but below the floor plus the headroom rolls the
query pods and reportsDegradedwith reasonQueryMemoryHeadroomLow.
spec.query.memoryHeadroomBytessets the headroom; the provisional default
isceil(0.53 * budget) + ceil(min(budget, 8 GiB) * 6.6 / 8), from the
uncharged SQL state measured in #3181 and the allocator retention measured
in ADR-2633. It was sized before this release's #3181 fixes, which leave it
unchanged; measure the query pods' peak resident memory and set the field.
The headroom is checked only against a pinned budget, so a CEL rule refuses
spec.query.memoryHeadroomByteswithoutspec.query.memoryBudgetBytesat
admission (issue #3203); a cluster stored that way before the rule keeps
reconciling, with the headroom ignored.ravel-cli export --parquetwrites bloom filters on the id columns
(ADR-1751 decision 4, issue #3229). Every row group carries a Parquet bloom
filter, at a 1% false-positive target, on the mapping's trace id, span id
and (when set) parent span id output columns for spans, and on its trace id
and span id output columns for logs when the mapping sets them; metrics
exports carry none. Each filter is sized for the row group's maximum row
count and folded down by the parquet writer to the values it actually
holds, so a row group holding one trace's spans carries a filter of a few
dozen bytes.--parquet-bloom-columns <a,b,...>replaces the default list,
--parquet-bloom-columns nonewrites no filter, and a name that is not an
output column of the export is refused before the export reads anything.stats.accounting.lateServedBytesreports the bytes another in-flight
read of the same object handed the statement. Those bytes are counted
neither incacheBytesnor in the GET bytes. The field appears in every
response that carriesstats.accounting.
Changed
- The logs scan hands its partitions work from one shared queue (ADR-3072
decision 2, ADR-0102 decision 1 as amended by #3080). Once the plan
resolves, units of work are published to one queue and each partition
takes the next unit when it finishes its current one, instead of being
dealt everynth block or segment up front, so a partition that finishes
early takes more of the remaining work. Under an ascendingtsTopK with a
read cache the units and their order are unchanged at every partition
count, one included: the global deal's row groups in ascendingts, with
the consecutive row groups of one segment that the deal gave one partition
merged into one unit, so a segment is opened exactly as often as before.
Outside that TopK, with a read cache and more than one SQL partition, a
segment ofGsurviving row groups is cut into runs ofceil(G / n)
consecutive row groups fornpartitions, so it is opened at most
min(G, n)times; without a read cache, or with one partition, each
segment is one unit. A
row group is still read by exactly one partition, the declared partition
count is unchanged, and a statement returns the same rows. The
predicate-free whole-segment fast path keeps its round-robin assignment.
Two costs move: a run of adjacent row groups is fetched as one coalesced
range, so it issues fewer requests and also moves the bytes between its
groups, and a re-executed scan fetches its carried whole objects again
instead of reusing the first execution's. --cache-bulk-scan-min-bytes <BYTES>reads filteredlogsstatements
larger than the cache as bulk scans (ADR-3072's size trigger amendments).
It applies tologsstatements sent as SQL text, through
POST /api/v1/sqlor as an alert rule's SQL, not through Flight SQL; default
unset, which keeps the predicate-free rule alone. Set, such a statement is
also a bulk scan when it names onelogstable and no Parquet table, its
optimized plan has no equality on a field the tenant indexes in KEY_IDX, and
the last successful execution of the same SQL text for the same tenant, over
a window of the same length with the same row-window setting and row cap,
read at least the threshold (bytes served from the cache, bytes a concurrent
read of the same object handed it, and bytes read from the store, every
phase;stats.accountingreports the second aslateServedBytes). Its first
execution in a process therefore reads as interactive, and a statement whose
text changes every run never reads as bulk, though each of its runs still
writes a record. The process keeps one record, for every tenant together,
of the 4,096 most recent statements, so such a stream evicts the records of
statements that repeat. The threshold in force is the flag raised to
--cache-max-bytes, the RAM tier's size, when that is larger, and
startup logs it as aperformance default resolvedline with
setting="cache_bulk_scan_min_bytes"and asourceofflagor
flag-floored-to-cache-max-bytes. A bulk scan's reads go to the fetcher
cache's bulk region (--cache-bulk-share) instead of the interactive
queues, andstats.cache.intentreportsbulkScan.samplesstatements and
lowered ES|QL statements are unaffected. Rows returned are unchanged.
0is refused at startup; omit the flag to leave the trigger off.
sql_latency_bench --cache-bulk-scan-min-bytesapplies the same floor at
--cache-bytes.- The SQL memory pool charges a grouped aggregate ahead of what it asks
for. On a SQLGROUP BYor an ES|QLSTATS ... BYthat runs on the
migrated aggregate streams, the
query, tenant and process budgets are charged 1.25 times the bytes the
aggregate reserves. The pool also reserves, on the process budget, an
envelope for the next step of the largest building aggregates, so an
aggregate whose next step the process budget cannot hold is refused
before it makes it. The envelope is capped at 2.43 times the aggregate's
asked size and sized from the plan's aggregate layout: the group table's
entry size and the per-group bytes of the columns and accumulator states
that grow with the group count. A layout whose group table and columns
report their capacity is also bounded by the share of the reservation each
can hold, and a layout the planner does not classify uses five times its
largest step, under the same cap (#3181). Under concurrent load, large
GROUP BYstatements are refused more often than in 0.25, and more of the
refusals come from the process budget than from the tenant budget, so one
tenant's load can now refuse another tenant's large aggregate.ravel-server
turns this on and has no setting to turn it off. Library embedders can set
SqlConfig::aggregate_reserve_ahead, whereAggregateReserveAhead::OFF
restores one-to-one charging and no envelope. - A repeated logs scan alternates the direction it reads its segments in
(ADR-0102's #3192 amendment). With a read cache wired, no confirmed
ORDER BY ts LIMITTopK and no pushed fetch limit, every second execution
of the same scan (same tenant, projection and pushed filters, whichever
objects the window selects) publishes its segments in reverse order, so it
reads first what the previous execution read last, while the cache still
holds it. Previously a dashboard re-running a statement over more data than
the cache held was served nothing from it on warm runs. A tenant still
ingesting keeps the alternation as new objects arrive. The executor keeps the count per scan
in a record of at most 4,096 scans, charged to no cache. Rows without an
ORDER BYcan come back in a different order between executions, and
some statements can return different rows, each set a valid answer: a
LIMITthe scan does not take as a pushed fetch (one above a filter the
scan does not evaluate exactly, such asbody LIKE '%x%', or above a
SELECT DISTINCT), anORDER BYon a key other thants, or on
ts DESC, whoseLIMITcuts through tied rows, andfirst_value,
last_valueorARRAY_AGGwithout anORDER BY. These now differ on
alternate executions even at one partition, where they used to repeat;
with more than one partition they already varied with timing. Of the
scans that read through a read cache and leave the predicate-free
whole-segment fast path, only one under a pushed fetch or a confirmed
TopK keeps its order. The fast path, the TopK order and uncached scans
are unchanged.
Security
- Maintain-worker registry records are authenticated, and
--mode maintain
requires--maintain-key-file. A principal holding the maintain role's
S3 write could previously put a fresh heartbeat under
sys/maintain/workers/and join the live set, owning maintenance units
(retention, compaction, erasure, the catalog fold, scrub) that nobody then
worked.WorkerHeartbeatmoves toformat_version2 with a MAC keyed by
the new key file, and both live-set readers, the heartbeat task and scrub,
admit a sibling only when its MAC verifies. Rejected records are counted in
ravel_maintain_worker_record_rejects_total{reason}. Every other mode,
--mode allincluded, refuses the flag. A maintain process on this release
and one on an earlier release do not admit each other's records, so a
rolling upgrade does some maintenance work twice until it completes. The
operator renders the flag from the newRavelClusterfield
spec.maintain.keySecretRef, which is required while maintain is enabled.
Without it, or while the Secret it names does not exist, the operator
leaves an existing maintain Deployment unchanged, creates none, and reports
Degradedwith reasonMaintainKeySecretRefMissingorSecretNotFound.
An existing cluster keeps reconciling its gateway and query Deployments; a
fresh cluster on per-role storage credentials creates noravel-server
Deployment until the Secret exists, because gateway and query wait for
maintain to create
sys/gc. Ifspec.imageis a mutable tag, create the Secret before
upgrading: an old maintain pod that restarts and pulls this release cannot
start without the flag. See the three-step key rotation in the deployment
guide. - Maintain memo snapshots are authenticated. A principal holding the
maintain role's S3 write could previously put a snapshot under
sys/maintain/memo/that seeded terminal verdicts into every maintain
process, so the owners of those buckets skipped them until the verdicts went
stale. Each snapshot now carries a MAC under the--maintain-key-filekeys
over the writer's process id, which a reader takes from the object key, so a
snapshot that no key holder wrote, or one copied under another process's
key, seeds nothing and is never decoded. A principal with read and write
access can still put a process's own earlier snapshot back at its key; the
1-hour staleness bound limits that replay to snapshots under an hour old, so
it defers a bucket's re-evaluation by at most an hour. Rejected snapshots are
logged with a count per reason, and snapshot objects last modified more than
2 hours ago are skipped unread and logged below warning level. During a
rolling upgrade, processes on this release and on an earlier release cannot
read each other's snapshots, so each maintenance unit starts cold once and
rescans its buckets. - The allocation a maintain memo snapshot can cause is now bounded. The
counts a snapshot carries still size what decoding it allocates, but only up
to two caps: a process skips a snapshot object over 32 MiB and refuses one
whose runs expand to more than 4,194,304 per-hour entries, checked before
each run is expanded, leaving the units it covers to a cold rescan. A
snapshot of its own over either cap is skipped and never written. - tokio is updated to 1.53.3 for RUSTSEC-2026-0386. The advisory marks
tokio 1.53.1's safeAsyncFdconstructors unsound. Ravel never constructs
anAsyncFd, so no shipped path reached the unsound API.
What's Changed
- fix(server): bound TLS handshake and add HTTP/2 keepalive on dials by @pmoust in #3180
- test(server): time the SQL keepalive bound from the blackhole by @pmoust in #3183
- refactor(sql): hand logs scan partitions units from one shared queue by @pmoust in #3164
- feat(sql): classify large logs scans as bulk behind a byte threshold by @pmoust in #3170
- feat(fleet): authenticate maintain-worker registry records by @pmoust in #3184
- docs: size the gate disk band by feature lane by @pmoust in #3185
- docs(config): leave headroom under an explicit memory budget by @pmoust in #3190
- docs(distrib): name the HTTP/2 handshake among the unbounded dial stalls by @pmoust in #3191
- Trigger bulk scans on a statement's own read bytes by @pmoust in #3193
- docs(config): raise the explicit-budget headroom to the profiled gap by @pmoust in #3194
- List lateServedBytes among the exemplars accounting exclusions by @pmoust in #3196
- test(commit): cite IAM call sites by function, not by line by @pmoust in #3198
- fix(otap): bound the decoded bytes one OTAP batch may hold by @pmoust in #3186
- Keep the kernel log and memory gauges in kit evidence by @pmoust in #3195
- fix(gates): run the documentation claims check in gates.sh by @pmoust in #3197
- feat(operator): let RavelCluster pin the query memory budget by @pmoust in #3201
- fix(maintain): bound the memo snapshot read and decode by @pmoust in #3202
- chore(release): record 0.25.1 on main by @pmoust in #3206
- docs(ingest): state the OTAP decode bound where it said unquantified by @pmoust in #3207
- fix(maintain): authenticate memo snapshots with a MAC by @pmoust in #3210
- Alternate a repeated logs scan's publication direction by @pmoust in #3214
- Key a repeated scan's count without its segment set by @pmoust in #3216
- fix(sql): reserve dictionary values before building them by @pmoust in #3218
- chore(deps): bump tokio to 1.53.3 for RUSTSEC-2026-0386 by @pmoust in #3222
- fix(sql): hold a grouped aggregate's last release until it unregisters by @pmoust in #3220
- fix(query): skip BLOOM and POSTINGS for a scan that reads neither by @pmoust in #3221
- feat(operator): refuse a query headroom without a budget by @pmoust in #3205
- fix(operator): harden the Pod Identity sidecar and credential cache by @pmoust in #3208
- docs: add ADR-3217 on the ClickBench cold floor of the stock entry by @pmoust in #3224
- test(bench): judge the ADR-1170 memory gap in the ClickBench kit by @pmoust in #3223
- fix(sql): charge migrated aggregates an undercount factor before they grow by @pmoust in #3225
- feat(cli): write bloom filters on the id columns in export --parquet by @pmoust in #3233
- fix(sql): size the aggregate envelope from the plan's aggregate layout by @pmoust in #3226
- chore(release): prepare 0.26.0 by @pmoust in #3238
Full Changelog: v0.25.0...v0.26.0
Downloads
The attached binaries were extracted unmodified from the signed container
images this release published, so each one is byte-identical to the file inside
the image you can pull. They are glibc-dynamic, built against Debian 12
(the runtime image is gcr.io/distroless/cc-debian12), so they need glibc 2.36
or newer. On an older distribution, run the container image instead:
docker pull ghcr.io/nofireai/ravel-server:0.26.0One binary per platform, named <binary>-<os>-<arch>:
ravel-server-linux-amd64,ravel-server-linux-arm64ravel-cli-linux-amd64,ravel-cli-linux-arm64ravel-operator-linux-amd64,ravel-operator-linux-arm64ravel-ingest-router-linux-amd64,ravel-ingest-router-linux-arm64
Verifying the downloads
SHA256SUMS covers every attached binary and symbol archive. Check the files
you downloaded, then verify SHA256SUMS itself against the signature and
certificate beside it. The signature is keyless, bound to this release
workflow's identity, so there is no key to obtain:
sha256sum --check --ignore-missing SHA256SUMS
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/NOFireAI/ravel/.github/workflows/publish-images.yml@refs/tags/v0.26.0' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
SHA256SUMSDebug symbols
The shipped binaries are stripped of debug info but keep their symbol table,
so a backtrace resolves function names with nothing extra installed. Line
numbers come from the matching symbols archive. Each binary records the
basename of its symbol file, so unpacking the archive next to the binary is
enough and no symbol-file step is needed:
tar -xzf ravel-debug-symbols-linux-amd64.tar.gz
gdb ./ravel-server-linux-amd64gdb looks for ravel-server.debug in the same directory (and in
.debug/), which is the name inside the archive. Do not rename the extracted
.debug files.