Repository navigation
Releases: NOFireAI/ravel
Release list
v0.23.0
[0.23.0] - 2026-10-09
Changed
- The server role templates allow the key-epoch record only conditional
writes (issue #2462).deploy/iam/gateway.json,query.jsonand
maintain.jsonnow allow writing a tenant's key-epoch recordt/<hash>/enc
only as a create-if-absent or a compare-and-swap, not as an unconditional
overwrite. The code already wrote it only those ways, so nothing a server or
ravel-clidoes changes. Operators must re-apply the three templates. /metricssays why a flush was handed back (issue #2600).
ravel_ingest_rerouted_flushes_totalgains areasonlabel,
retired_indexorgeneration_mismatch, so a query that selects the family
without the label now gets two series per signal. Every hand-back is counted
under exactly one reason.ravel-cli load's stride cursors no longer each hold a whole batch of
decoded Arrow rows (issue #2613). Each stride cursor used to decode a
full--batch-rowsArrow batch of every column and keep the undealt
remainder, so K cursors held K whole batches at once: about 7 GB of a
21 GB live heap at 16 cursors and 500,000-row batches on ClickBench. A
cursor now decodesceil(--batch-rows / K)rows at a time, so the decoded
rows held across all cursors no longer scale with K; each cursor still
keeps its own Parquet reader and page-decode state. Batches are dealt
exactly as before: a differential test compares every batch's rows with a
copy of the previous dealer over uneven row groups,--batch-rowsnot
divisible by K, short and empty partitions,--skip-rowsand K = 1, plus
generated files, and the RLOG encodings match the previous dealer's at
(--batch-rows, K) = (1000, 4), (333, 5), (1000, 3) and (700, 1). In one
360 s measurement per version (ClickBenchhits.parquet,--batch-rows 500000 --read-cursors 16 --shards 4, memory store, a 16-vCPU 30 GB x86_64
host, jemalloc heap profiling on in both runs) peak RSS fell from 22.7 GB
to 15.5 GB. The after-run predates the batch-composition fix, which
changes only which rows a batch takes at a partition's end, not how many
rows a cursor holds.- The columnar log write path moves each batch into its shards instead of
copying it (issue #2624).LogIngestRouter::write_columnarused to clone
every cell, residual attribute list and stream blob of the batch into
per-shard copies while the batch itself stayed alive until the write
returned. The batch is now freed column by column as its rows are dealt,
and is gone before any shard is sent its part; the severity text, body and
trace and span ids are still copied. String and byte column dictionaries
are still dropped at the split, as before: carrying them would keep one
copy of each distinct value per shard, memory the ingest byte budget does
not count. Stored objects do not change: tests compare every per-shard
batch, dictionaries included, with the previous partition's over fixed and
generated input,
and every stored object the two partitions write, including two writes
merged into one flush and a shard's buffer handed back to new targets.
Peak RSS ofravel-cli loadon ClickBenchhits.parquetat--batch-rows 500000 --read-cursors 16 --shards 4fell from 15.48 GB to 11.37 GB. - Columnar log batches store attribute cells as typed columns (issue
#2625). AColumnarLogBatchdynamic column used to hold oneAttrValue
per cell; it now holds one dense payload per type: string and byte values
as offsets plus bytes, integers and floats as plain vectors, and bools as
a vector of flags.ravel-cli loadfills those columns straight from the
Arrow arrays, copying each string's bytes once, and no longer attaches a
string column dictionary to the batch. Stored objects do not change: the
row and columnar builders still write byte-identical RLOG objects, which
tests check over generated batches and a Parquet file holding every
type the loader maps, NaN payloads and -0.0 included. A string, byte,
body or severity text column that would passu32::MAXbytes in one
batch (the most its 32-bit offsets address, one byte short of 4 GiB) is
now a load error naming the column and suggesting a smaller
--batch-rows, where it used to panic.
Measured on ClickBenchhits.parquetat--batch-rows 500000 --read-cursors 16 --shards 4, stopped after 360 seconds, peak RSS over
the whole run was 11.02 GB, against 11.37 GB for the previous change, on
a different host (8 cores and 16.0 GB of memory, against 16 cores and
32.1 GB), so the comparison does not isolate this change. The run kept
every written object in the measured process, since the load wrote to the
in-memory object store (9.4 GB of object buffers in its last heap dump).
Of the four targets set before measuring, three were missed: 0.5 to 1 KB
of heap per batch row, under 1 GB held at batch build and at shard
partitioning, and a peak RSS of 5 to 7 GB. The fourth was met: at
--batch-rows 1000000the load ran its 360 seconds without running out
of memory, at a 13.33 GB peak. ravel-cli loadholds its log batches under one byte budget,
--load-memory-bytes(issue #2626). Each batch is charged before it
is built, at an estimate from the previous batch's bytes per row (zero
for the first), and corrected to its measured in-memory size once built,
so the first batch and any batch wider than its estimate are built partly
uncharged. The charge stays through the decode queue, the write window,
the shard buffers and the flush that writes its rows; when the budget is
full the decoder waits instead of failing. With the flag set, a load
whose first batch alone does not fit is refused before anything is
written, naming the batch's size, the budget and the floor. Unset, the
budget is host memory (or a lower cgroup limit) less an estimated floor
of 128 MiB plus 32 MiB per read cursor and 72 MiB per concurrent flush,
with a 4 GiB fallback, logged, where host memory cannot be read; a
derived budget below one batch admits one batch at a time with a
warning instead of refusing. The load summary prints the budget, its
source, the floor, the peak bytes charged, the largest batch and the
decoder's waits. Stored bytes are unchanged unless the budget binds
above--target-bytes 1, where the loader flushes the shard buffers
early when the decoder waits on the budget with nothing released:
smaller objects, never a hang.--target-bytesabove1lets shards
merge several batches into one object, up to--pipeline-depth
batches, since each Strict write waits for its own batch's flush. The
target counts estimated uncompressed content: on ClickBench
hits.parquet,--batch-rows 50000 --target-bytes 25000000stored
objects of 1.1 to 2.4 MB (10th to 90th percentile); the recipe for
larger objects from small batches is documented separately (issue #2627).
Measured with the
in-memory object store at--batch-rows 500000and a 6 GB budget, peak
RSS, which includes the store's held objects, was 12.02 GB against
11.85 GB with the host-derived budget on the same host. That run is no
evidence the budget bound: both runs read the same bytes, the sampled
heap outside the store's objects stayed under 6 GB (at most 5,430 MB),
and the peak charge was not read.ravel-cli loaddocuments a measured recipe for large log objects
(issues #2627 and #2651). The--target-bytesand--pipeline-depthhelp
and the ingest guide now give the measured settings for large stored
objects from small batches, in place of "pending": on ClickBench
--batch-rows 100000 --target-bytes 1850000000 --max-flush-delay 30s --pipeline-depth 32 --load-memory-bytes 6500000000stored 25.1 MB mean
objects in 896 s at a 7.03 GB peak loader RSS, and the guide gives the
depth and budget used on 16, 8 and 4 GB hosts with the object size each
stored (24.5, 23.9 and 17.1 MB median).
The earlier statement that
stored objects come out about 15 times below--target-byteswas wrong:
on that corpus they come out about 65 to 75 times below it, and the
ratio depends on the data. The help and guide also say that--pipeline-depth
must cover the batches one object needs on a shard and that
--max-flush-delaymust be long enough for one object to fill.- A failed logs load still prints the one-batch memory warning. When the
memory budget derived from host memory is smaller than one batch, the
loader admits one batch at a time and warns. That warning was printed only
when the load succeeded; it is now printed beside the error too. - A columnar hand-back no longer holds the buffered rows twice. When a
shard buffer holding row records receives a columnar batch, it converts
those records by moving them rather than copying them, and puts them back
unchanged if the conversion is refused.
Fixed
- Inputs a stale snapshot still names are now re-folded instead of held
until retention (issue #1763). When the maintain-role sweep holds a late
compaction's or erasure rewrite's superseded inputs because the catalog
snapshot still names them, in an hour outside the fold's reconcile window,
it now hands that hour to the scheduled fold of the same tenant and signal
in the same process. The hour stays queued through no-op, skipped and failed
folds, and the pair's next successful fold that advances the watermark
re-lists it so the inputs can be collected. Each fold takes at most
frontier_reconcile_max_hoursof a pair's oldest queued hours, the most it
reconciles, and leaves the rest queued for the next such fold. A maintain
process whose scheduled fold is disabled queues nothing. The queue holds up
to 256 tenant and signal pairs and evicts the earliest-inserted pair when
full, counted onravel_catalog_fold_refold_requests_dropped_total; an
evicted hour, or one lost when the process restarts, ...
v0.22.0
[0.22.0] - 2026-10-05
Changed
- SQL spill is on by default when
ravel-serverruns with--cache-dir
(ADR-0954 amendment, issue #2416). With no spill environment set, an
eligible query that outgrows its memory pool now spills under
<cache-dir>/sql-spill/<instance-id>, with a ceiling of half of what is
left of the free bytes measured at startup on that volume once the read
cache's disk-tier bound is subtracted, capped at four times the process
memory budget and raised to 1 GiB when that cap is below 1 GiB. When that
half is below 1 GiB, spill stays off, and both startup lines give the
sourcecache-dir-insufficient-space. The
ceiling, derived or configured, now bounds
all of the process's queries together: each qualifying query reserves its
spill limit from it, and one that cannot reserve 64 MiB (or the whole of a
smaller ceiling) runs with spill off.RAVEL_SQL_SPILL_DIRwith
RAVEL_SQL_SPILL_MAX_BYTESstill wins, andRAVEL_SQL_SPILL_MAX_BYTESset
alone replaces the derived ceiling. The new--sql-spill offflag disables
spill whatever the environment or--cache-dirsays. Startup logs two more
performance default resolvedlines,sql_spill_dirand
sql_spill_max_bytes, each with its source. The startup of a process that
serves SQL first sweeps orphaned spill directories under
<cache-dir>/sql-spill, whenever
--cache-diris set,--sql-spillis notoffand that directory exists:
it removes each directory whose owner lock it can take and leaves and logs
any whose owner still holds it. The sweep runs before the free space is
measured, so it also runs when spill then resolves to the
RAVEL_SQL_SPILL_DIRpair's directory or off for lack of space. A process
whose spill resolves under--cache-dirthen takes an exclusive lock on
its own spill directory before serving. With spill resolving under
--cache-dir, startup now refuses to start when it cannot create
<cache-dir>/sql-spill, measure its free space, or lock its own
directory, and a half-set spill environment names the missing variable.
The lock only proves ownership on one host, so--cache-dirmust be a
local volume unless--sql-spill offis set: the sweep runs wherever
spill resolves. - An
ORDER BYover a spill-eligible aggregation can now spill (ADR-0954
amendment, issue #2416). Previously anySortkept a query from spilling.
Whenever a statement's plan has aSortdirectly over an aggregation of
onlyCOUNT,SUMandAVGwith no floatGROUP BYcolumn (or over a
projection directly over it), its sort key gets the aggregation's
GROUP BYcolumns appended as trailing tiebreak terms, ascending with
nulls last, whatever the spill setting and whatever the types of the
aggregated columns. The order is then total, and a
spill-eligible statement returns the same rows in the same order with
spill forced, with spill off, and in memory, including one whoseAVG
reads a 32-bit integer column. A group column the projection renames is
matched under its new name, one it drops is carried through to the sort
and removed again, and a select-list alias that only shares a group
column's name is not taken for it. ASortover any other aggregate, over
a float group column, over grouping sets, over aHAVINGfilter, or over
nested projections is left as written and still keeps the query from
spilling. - A logs statement that stripes a segment across SQL partitions now decodes
the segment's directories once and deals whole row groups (ADR-2414 decision
A1, issue #2417). The plan phase decodes STREAM_DIR, FIELD_DIR, SKIP_IDX and
PAGE_DIR once per relevant segment and every partition's open reuses them, so
the per-phasedecompressed_bytesof such a statement moves from the scan
phase to the plan phase: the plan phase carries the four sections' decoded
length and the scan phase only the projected block pages. A partition reads
only the pages of the row groups it owns, and the chunk-run reads of
partitions sharing one object no longer overlap, so the block bytes they move
for it sum to at most its BLOCKS section. A partition sharing an object never
reads it whole: its coverage crossover weighs its runs against its own span
and collapses them to that span, split only where another partition's pages
lie inside it. Request counts change with it: on
the 8-segment selective-scan fixture the q37 and q20 shapes issue 48 GETs
where they issued 56, and a segment whose carried plan result was dropped to
bound memory is reopened with 4 chunk-run GETs where it was reopened with 1
block-data GET. The carried directories are reserved against the fetch memory
budget, for segments that have a surviving block, from the plan phase until
every partition that owns one of the segment's row groups has finished it.
The plan phase reserves all of them before the scan starts, so the peak is
the decoded directories of every segment with a surviving block, and a
statement can now fail with the fetch memory error where it did not before.
An open whose own pruning disagrees with the plan's survivor list fails with a
survivor-mismatch error instead of reading a subset of what the plan counted.
A statement that late-materializes rows through the whole-segment fast path
fails with the same error, rather than returning another block's rows, when
that path's open has pruned a block the catalog's bounds placed inside the
window ahead of a block it stamps. - The whole-segment fast path pipelines a partition's ranged opens (issue
#2418). While a partition drains a segment it opened ranged, it opens its
next owned segments ahead of their turn, up to max(2, store GET concurrency
/ SQL partition count) segments at once, and emits them in owned order, so a
partition's ranged reads overlap instead of costing its round trips one
after another. Rows and accounting are unchanged while the fetch memory
budget refuses none of those opens. When it refuses a partition's current
open, the statement's pipeline turns off, every partition's unconsumed
prefetches are dropped (their segments open again at their turns, in owned
order), and the refused open is retried once, at once, without waiting for
any other partition. Within one statement a current open is reported
refused only if the budget refused it twice, the second time after every
unconsumed prefetch of every partition had been dropped; across statements
the budget stays fail-fast, and that refusal is typed. The scan counts the
retries inprefetch_memory_reopensand the dropped prefetches in
prefetch_revocations; rows are unchanged, and the requests the dropped
opens already made are counted on top. - The default fetch policy reads narrow projections of large log objects
ranged on the intra-region profile (issue #2418). The store cost profile
gains two optional measured timings,request_latency_microsand
per_connection_throughput_bytes_per_s(set together or not at all), and
cost-basedtakes the larger of its price term and their product. The
reference profile records 70 ms and 90 MB/s, so its request cost is 6,300,000
bytes instead of saturating, and a projection is read ranged only when it
skips more than the projection break-even, the larger of the routing threshold
and three request costs (18,900,000 bytes by default, issue #2555: a ranged
read of a narrow projection was measured at about four GETs per object, so it
pays when the bytes it skips exceed three request costs); every object at or
below that is still read whole, a 3 MB flush object included, and with no tail
probe on the planned route (a statement the whole-segment fast path refuses)
and on the log-series route's stream-directory read. An explicit
--logs-block-range-thresholdis no longer overridden on that profile. The
logs fetch policy resolvedstartup line gainsrate_term,
projection_break_even_bytes(the break-even in force, the routing threshold
when the policy derives none) andbreak_even_source(profileor
routing-threshold); on the reference profile it reads
projection_break_even_bytes=18900000 break_even_source="profile". An
explicit--logs-request-cost-bytes, the other fetch policies, the coverage
crossover and the coalescing gap are unchanged. Above the break-even the
planned route still probes an object before its coverage crossover reads it
whole for a wide projection, since it does not weigh the projected fraction;
that is left to a follow-up. ravel-server's process memory budget now starts from available memory,
not raw total (ADR-1170, amended 2026-10-03 by issue #2367, issue #2483).
With no cgroup memory limit and a readableMemAvailablein
/proc/meminfo,memory_budget_bytesresolves tomin(MemTotal - RESERVE, max(FLOOR, MemAvailable + own RSS - RESERVE)), logged with
source="derived-available".FLOORis 1 GiB and floors only the
MemAvailable + own RSS - RESERVEterm; when it binds, aWARNnames the
MemAvailablereading and--memory-budget-bytesas the remedy. The outer
minkeeps the budget at or belowMemTotal - RESERVE, so a host whose
MemTotalis at or below the reserve still derives 0 and is refused at
startup, as before. A cgroup memory limit keeps the pre-amendment rule
(source="derived-cgroup",MemAvailablenot consulted), including when
/proc/meminfocannot be read, since the limit is then the memory figure.
An unreadableMemAvailableon a host whoseMemTotalparses keeps the
MemTotal-minus-reserve rule (source="derived"). With neither a readable
MemTotalnor a cgroup limit (every non-Linux build, for example) no budget
is derived:source="fallback", budget unlimited, as before. The new
--memory-budget-bytesflag overrides every derivation except gateway mode,
which clai...
v0.21.0
[0.21.0] - 2026-10-03
Changed
- An ingest shard whose flushes stay deferred up to the read-side slack now
also refuses new writes with a retryable overload error (issue #1916). The
refusal comes in addition to the deferral, not in place of it: the deferred
flush still waits for a queue slot for as long as the stall lasts. The
deferral cap is the 2 h flush slack lessmax_flush_lifetime, less the
slowest flush trigger (the largest of--max-flush-delay,
--max-flush-delay-idleand the adaptive corridor's widest ceiling) and one
flush tick: 3559.8 s at the defaults, with or without an idle flush byte
floor. Once a flush deferred at the queued-flush cap has waited that long,
the router refuses every write to the shard before enqueue, in strict and
buffered mode alike, with the newDeferralCapReachederror (HTTP 429 with
Retry-After, gRPCRESOURCE_EXHAUSTED), until the deferred flushes open.
The cap stops new writes; it does not force the deferred flush open, so
buffered rows already acknowledged in that buffer, buffered rows held under
a non-zero--idle-flush-byte-floor, and the rows of a strict write that
already timed out can still land past the slack. A strict write already
waiting on the capped flush is answered 503 (outcome unknown): its rows are
still written by the flush that opens past the cap, so retrying it duplicates
logs and spans. A multi-shard
write whose capped shard fails after a sibling committed answers the
partial-write 503. A shard whose actor died while a flush was deferred
answers the dead-shard error rather than refusing at the cap. Refusals are
counted onravel_ingest_deferral_cap_refused_totalby signal, and deferred
flushes now retry oldest deferral first. - The L1 memory split target of log compaction is derived from host memory
when unset (issue #2351). For RLOG mergesl1_part_memory_target_bytes
now defaults to the smallest of the memory budget divided by 8 and by the
number of concurrent merges, the part size the claim lease supports
(lease * 10 MiB/s / 2, 1500 MiB at the default 300 s) and 8 GiB, lifted to
a 256 MiB floor, instead of a fixed 256 MiB that closed wide-schema parts at
about 2 MB stored. The memory budget is host memory (capped by a cgroup
limit) less a 2 GiB overhead reserve and less the 20 GiB merge cursor
budget: a 32 GiB host undercompact-bucketderives 1.25 GiB. The log
merge's stored-size cap follows the derived target, so the memory target
closes every part and no exact-encode probe runs; an explicit
--max-l1-part-bytessets the log and metrics caps together. Span merges
keep the fixed 256 MiB, since span parts have no stored-size cap.
ravel-serverderives it from its memory budget and
--maintain-unit-concurrencyand takes the new
--maintain-l1-part-memory-target-bytesflag;ravel-cli maintainderives
it from the host's memory and--bucket-concurrency, andcompact-bucket
gains--l1-part-memory-target-bytesand--max-l1-part-bytes. An explicit
flag still wins for both log and span merges, an unknown budget falls back
to 256 MiB, and both binaries report the values they resolved, where they
came from and which term bound them. The CLI report prints one line per
codec:rlog_l1_part_memory_target_bytes,rlog_max_l1_part_bytes,
rspan_l1_part_memory_target_bytesandmax_l1_part_bytes, and the
server's startup log carries the same fields; the unprefixed
l1_part_memory_target_bytesline is gone. The server's startup lease check
now sizes the largest part as the larger of the two stored-size caps.
ravel-serverandravel-clishare one host memory parser; the server still
treats memory as unknown outside Linux (ADR-0088). The error for a
converged compaction whose winning part is missing now names the settings a
rebuild must reproduce and says that no shipped command performs the rebuild
today (a rerun returnsAlreadyCompacted, issue #2370), so the bucket's L0
inputs should be held withravel-cli hold setmeanwhile. ravel-serverrefuses at startup a flush cadence that leaves the flush
deferral cap at 0 (issue #2410). Flag values the previous startup check
admitted at equality, such as--max-flush-delay-idle 3600swith the default
flush lifetime, now fail startup with an error naming the flags and the terms
of the arithmetic: with the deferral cap at 0, a shard would refuse every
write from the first flush trigger its full queue deferred. Lower
--max-flush-delay-idleso that it plus the flush lifetime plus one flush
tick stays below the 2 h flush slack.- The derived per-query SQL memory pool is now 50% of
MemTotal, the same
share as the per-tenant ceiling (issue #2419). A lone statement may use the
tenant's whole SQL share: on the 30 GiB reference host
--sql-max-query-bytesderives to 16,106,127,360 bytes, up from
8,053,063,680. The per-query pool nests inside the per-tenant
pool, so statements running together still share the same tenant total, but
a second concurrent statement no longer has a guaranteed quarter of memory.
An explicit--sql-max-query-bytesstill wins: over an explicit
--sql-tenant-max-bytesit is clamped to that ceiling, and over a derived
tenant ceiling it raises the ceiling to match, as before. An explicit
--sql-tenant-max-bytesbetween 25% and 50% ofMemTotalnow clamps the
derived per-query pool, which the resolved line reports asclamped=true
with a startup warning naming--sql-max-query-bytes. To keep the earlier
split, set--sql-max-query-bytesto half the tenant ceiling, 25% of
MemTotal.
Fixed
- Retention and superseded-input sweeps no longer delete objects a pinned
query can still read (ADR-1133, issue #1133). A query that pinned a HEAD
just before the fold dropped an object could fail withSnapshotInvalidated
when the next sweep deleted it. The sweeps now write an unnamed-since marker
undert/<tenant_hash>/<signal>/maint/unn/the first time they find a
candidate unnamed, and delete it only oncemax_query_duration + head_cache_ttl + 4 * clock_skew_allowancehas passed, holding it until then
under the newpinned_windowreason. The maintain IAM template gains delete
and list ont/*/*/maint/*, an erasure request's.dreqlives longer by
the same window, andgc-config setand maintain startup refuse a
protection horizon below
max_compaction_lifetime + 4 * clock_skew_allowance. - The server's catalog read decodes on the read CPU gate (issues #1702,
#2402). The snapshot part, postings and column-statistics decodes a query
resolve runs now run on the read CPU gate the server builds, as ADR-1702
decision 3 specifies, instead of on the resolving task. Fold and scrub
decodes are unchanged. A catalog decode job that panics, or that the gate
cancels at shutdown, does not fail the query: a snapshot part falls back to
listing, postings disable pruning, and column statistics degrade to a scan,
so the query answers exactly by the slower path. The query guide and HTTP
reference no longer list a catalog decode job among the 503 and 500 causes.
Such a column-statistics job is not counted as a decode refusal and does not
silence the warning a later genuine refusal of the same object logs. A
cancelled job warns on every occurrence, since a retry can succeed. A
panicked one panics again on the same bytes, so it warns once per object key
and is counted on every occurrence in the new
Catalog::column_stats_decode_panics. - A catalog decode that the decoded caches cannot make room for no longer
empties them (issue #2108). The eviction pass a refused decode runs before
retrying is now skipped, with no retry counted, when the bytes the cached
entries' reservations hold could not admit the charge
(reserved - held + want > limit), as it already was for a decode larger
than the budget's whole limit. The cached entries survive and the first
refusal stands, instead of every tenant's decoded entries being flushed for
a retry that is refused anyway. The metadata cache's serve read stays on the
bare budget, since it has no catalog caches to evict. The server does not yet
pass a finite budget to the catalog, so no deployment sees the pass today. - A compaction is fenced against publishing over a bucket an erasure
rewrite has already rewritten, which served erased rows again (issue
#2199). The
compaction and the erasure rewrite of a bucket now take the same bucket claim
before they build and hold it through their publish, whatever the bucket's
size, and each re-lists the bucket before it publishes and publishes nothing
if the bucket's records changed. A pass refused the claim backs off and
retries on a later pass; an unreadable claim object now holds its bucket
until an operator removes it. A claimed pass that stalls past its claim's
lease between its re-list and its publish is not caught.
ravel-cli maintain migrateis fenced the same way, and takes--no-claim.
Theclaim_min_input_bytessetting (--maintain-claim-min-input-bytes) no
longer has any effect and will be removed. - The catalog fold now rebuilds a HEAD stamped below the supported format
version instead of refusing it as newer (issue #2271). A HEAD with
format_version0, or any other value below the version this build reads,
is rebuilt like a HEAD that fails to decode, with a log line saying it is
below the supported version. A HEAD above the supported version is still
refused and left untouched (ADR-0066 decision 2). - A server started through its library API refuses a zero store-probe,
admission-reconcile or scrub interval, and a zero fold-lag interval, at
startup (issue #2330). A zero store-probe or admission-reconcile interval
used to start a loop that issued store requests back to back, a zero scrub...
v0.20.0
[0.20.0] - 2026-10-02
Changed
- RLOG compaction keeps clustered inputs clustered and writes L1 segments at zstd level 9
- The RLOG writer stores a string column's values on one row-group dictionary when that is strictly smaller, and writes encoding tags 12 (the dictionary page) and 13 (per-block id pages)
--require-bucket-protectionreads the S3 bucket's protection configuration at startup and refuses on more conditionsravel-serversends a CRC64-NVME upload checksum on every S3 PUT by defaultravel-cli --store s3checksums every PUT with CRC-64/NVME by default, andstore qualifyreads the bucket and reports whether the endpoint returns the stored checksum- The RLOG writer now chooses each i64 and string page's encoding by its stored size, and writes encoding tags 10 (GCD i64) and 11 (an
observed_tsequal tots, stored as a reference to it) s3_e2e_benchcounts adaptive-age flushes- RLOG log objects are now written and read at trailer version 5
- SQL fetches now reserve against the process-wide memory budget
ravel-cli maintain migratenames every bucket whose refusal no re-run clearsravel-cli maintain migratenames a bucket held below the target only by a losing compaction record's parts- The background supervisor now takes an advisory claim before compacting a large bucket, so two processes whose ownership overlaps no longer both pay for the same merge
ravel-cli maintain compact-bucketandcompact-tenanttake the same advisory compaction claims, so an operator's run and a background supervisor no longer both merge one large bucket- A ranged RLOG read holds only the bytes it placed, not a buffer the size of the object
- This release reads provisioning record format 3 and still writes 2, and
ravel-cli maintain audit-versionsnow classifies every recorded format floor - The query fetchers and PromQL evaluation can run their CPU-bound work on the read CPU gate
- The catalog can decode snapshot parts, postings and column statistics on the read CPU gate
- A ranged log read's chunk-run GETs against one L0 RLOG object are now bounded at
MAX_PAGE_RANGE_GETS_PER_L0_SEGMENT(4) - Changelog entries are written as fragments under
changelog.d/ sys/gcformat version 2 records the HEAD cache TTL, andravel-cli maintain sweepreadssys/gc- The catalog's decoded-part and postings caches evict the least recently used entry at their per-tenant entry cap
Security
- A distributed SQL slice fetch no longer carries the client's credential
- The Flight SQL ticket keys come from
--sql-ticket-key-file, not from the fragment key - SQL slice fetches ride the dedicated TLS fragment listener, and the public gRPC listener refuses slice tickets once one is configured
Fixed
- A server process refused access to
sys/gcat startup now fails with an error naming the fix - A store error reads as throttled only for a 429 or 503 status
- A delete against a missing S3 bucket fails instead of reporting success
- A tiered-cache miss no longer issues a duplicate GET
- A zero loop interval is refused at startup
- The operator CRD refuses a zero maintain or fold interval at admission
- An S3 delete refused for one key now reads as access denied, so the superseded sweep holds back that one chain instead of failing the pass
- A gateway starts under a small memory limit
- One refused delete no longer stops the superseded-input sweep for every chain
- SQL answers corrupt stored data as an internal error, not a retryable unavailable
- The maintain role reaps dead query-worker records under
sys/query/workers/ ravel-cli load --signal logsloads a dictionary-encoded hextrace_idorspan_idcolumnravel-cli load --signal logsand--signal metricsrefuse a negative timestamp- The fold checks a previous postings object's tenant before reusing it
- A writer whose clock lags the object store's clock refuses its flush instead of publishing into a sealed hour
- Alert sink delivery is bounded per evaluation tick, and ADR-0117's stated per-tick publish bound is corrected
- A Parquet load resolves each dictionary-encoded column once per batch instead of once per cell
- The shipped IAM templates grant the control-plane keys each role uses
- An explicit multipart upload through a scheduled store could write parts past its class's permit budget
- A store error is no longer misclassified by its retry field name or a stray 416
- The ingest router refuses a zero JWKS refresh interval or a zero round-robin idle-entry TTL at startup
- A store error is no longer classified as throttled or as a timeout because those words appear in the bucket, endpoint or object key of the request URI
Added
- The per-tenant config record can store a clustering key and a bloom scope behind an operator opt-in
- The RLOG writer can sort an object by a clustering key and limit which columns BLOOM covers
- The log ingest flush writes each RLOG object with the tenant's clustering key and bloom scope
ravel-cli load --zstd-level,ravel-cli clustering-key showandravel-cli bloom-scope show, and the clustering fields inravel-cli rlog inspectravel-cli clustering-key set,ravel-cli clustering-key clearandravel-cli bloom-scope setwrite a tenant's clustering key and bloom scoperavel-cli store verify-protectionchecks a bucket's protection configuration/metricsexports the Flight SQL slice capability rejects/metricsexports the fragment capability rejects/metricsexports the superseded-input sweep's hold counters--audit-retentionsets the query-audit retention windowravel-serverexports the unverified-read and control-plane store counters at/metrics- A read-only bucket-protection control plane in
ravel-object-store ravel-cli export --signal metricswrites a tenant's stored metric samples to a Parquet fileload --signal metricsreads back onto the same series/metricsrenders the three writer clock-lag counters, and a shipped alert pages on a refused flush- Readers accept and validate a version 2 compaction record
- Resolution honours a version 2 compaction record
- The sweep reclaims what a version 2 compaction record supersedes, and the erasure rewrite pass treats one as a link
validate_rewriterefuses a non-canonicalsuperseded_record_keyravel-cli maintain inspectprints a compaction record'ssuperseded_record_key- Advisory compaction claims report five
/metricscounters and gain three server flags ravel-cli rlog footprintattributes every stored byte of RLOG objects- The per-tenant config record reader accepts format version 3, which adds a clustering key and a bloom scope
- The maintenance loop sweeps alert history older than
--alert-retention, default 90 days ravel-cli load --signal spansloads the spans signal/metricsrendersravel_health_heartbeat_age_secondsravel-serverhas an off-by-defaultheap-profilingcargo featureravel-cli loadtakes--signal {metrics,logs,spans}and loads the metrics signal- A documentation claims registry and its gate
- An alert-signal retention sweep that keeps every identity's current-state record
- Catalog and PromQL decodes reserve their decoded output against the process memory budget before they run
- The
ravel-operatorDeployment itself now has a securityContext, a metrics/health HTTP surface, and liveness/readiness probes - A request-budget refusal now names fold lag when fold lag is what the budget was spent on
- An end-to-end proof that a stalled fold pages before it refuses a query
ravel_maintain_bytes_reclaimed_total,ravel_maintain_retention_lag_secondsandravel_maintain_units_scan_failedrender on/metrics- A full-object GET now verifies the body against the checksum the store recorded at upload
ravel-ingesthas an opt-in idle flush byte floor, off by default- The S3 adapter observes the store's own clock from response
Dateheaders ravel-server --idle-flush-byte-floorexposes that floor, and/metricsreports what it holdsObjectStoreBackend::get_pinnedreads exactly the bytes a recorded pin names, andravel-object-storegains read-only external stores per credential profile- Parquet table location grants and in-place manifest format
ravel-cli tenant parquet-grant add|remove|lsandravel-cli parquet ls|sweepravel-parquet: DataFusion's Parquet scan over a table manifest- The logs SQL scan skips segments whose declared-column statistics exclude the predicate
- The logs SQL scan does less work per cell when it builds typed attribute columns
- Parquet tables answer
POST /api/v1/sqland Flight SQL (ADR-2040 D3, D4 ravel-parquet:snapshot::snapshot_location, the file listCREATE EXTERNAL TABLEwill pin- Tenant resolution carries an optional
ddlcapability - Parquet queries reserve the bytes they fetch against the process memory budget and obey the request and byte budgets every other SQL query obeys
- A Flight SQL statement over a Parquet table reads the manifest version
GetFlightInforesolved ravel-sqlgains a typed DDL core for the three ADR-2040 D2 Parquet statement forms
The full entries are in CHANGELOG.md at this tag.
What's Changed
v0.19.0
[0.19.0] - 2026-09-27
Fixed
-
The chaos lane reads labeled metrics by label and passes multi-shard
commit tokens one per parameter (issue #534). Thescripts/chaos/
parser now scans a sample's label set quote-aware, reads the value before
any timestamp, and takeslabel=valueselectors, and the mid-flush trigger
selectssignal="metrics". A comma-joinedx-ravel-commit-tokenheader is
split into its opaque tokens, and the header name is matched without the
gawk-onlyIGNORECASE. Both scenarios now exit 3 on a setup error:
scenario 1 used to exit 1 there, the same code as its strict-ack oracle
failure, and scenario 2's exit 2 stays the release-blocking verdict.
scripts/chaos/lib.test.shcovers the
helpers on every pull request, and the nightly job annotates each
scenario's exit code. -
OTLP and Remote Write ingest now take the in-flight permit and check the
tenant credential before the request body is read or decoded (issue
#1705).
Every ingest handler took the--max-inflight-ingest-requestspermit and
resolved the tenant as its first two statements, but on both transports
those statements ran too late to bound anything: on the HTTP surfaces the
handler's body argument is an extractor, which axum runs before the handler
itself, so the whole body was buffered (and a gzip body inflated) first,
and on the gRPC surfaces the handler runs only after tonic has read and
decoded the message. An unauthenticated caller could therefore make a
process read and decode a full request body no matter how far over the
ceiling it was. For OTLP metrics, logs and traces on both transports and
for Remote Write, both decisions now happen on the request head, through
middleware on the HTTP ingest routes and a tower layer on the gRPC
listener, so a refused request costs the bytes of one request head. The
OTAP stream is only partly covered: the gRPC layer checks its credential
on the stream's head, before any frame is read, but its permit is still
taken per batch after tonic has decoded that batch's protobuf frame, so an
authenticated OTAP client over the ceiling still costs one decoded frame
per shed batch. Because the permit is now taken before the body arrives,
the ceiling counts concurrent uploads as well as concurrent decodes, and
the body wait is bounded: a body that has not fully arrived 30 seconds
after admission is refused with 503 andRetry-Afteron HTTP or
DEADLINE_EXCEEDEDfor a unary gRPC export, and its permit is released, so
a trickled upload holds its slot for at most 30 seconds. The OTAP stream holds no permit while it
waits for a frame and is not bounded this way. The gRPC layer claims only
the ingest services the listener registers, so a process without OTLP
ingest (for example--mode query) still answersUNIMPLEMENTEDon those
paths. No HTTP/2 stream cap is derived from the ceiling, so the Flight SQL
and fragment listeners keep their previous stream limits. The ceiling, its
flag, and theravel_ingest_concurrency_shed_totalcounter are unchanged,
and a refusal keeps the status and message it had: 429 withRetry-After
and 401 for bad credentials on HTTP,RESOURCE_EXHAUSTEDand
UNAUTHENTICATED("invalid or missing tenant credentials") on gRPC. -
CI no longer pulls the AWS CLI image from ECR Public to create a test
bucket (issue #2036). ECR Public caps anonymous pulls per source IP, and
hitting that cap failedbench-smoke,quickstartand
object-store-contract. Those jobs and the MetricsBench nightly now create
their RustFS bucket with the runner's ownawsCLI through
scripts/ci-create-bucket.sh, which retries only the create call's own
transient failures. The CI quickstart applies a CI-only compose override,
deploy/docker-compose/ci-host-bucket.yml, that turnscreatebucketinto a
no-op on the locally built image; the documented quickstart command is
unchanged.demo/kill-and-recover.shnow restartsravel-serverwith
--no-deps, so replacing the server no longer re-runs the stack's one-shot
services. -
Retention no longer deletes logs or spans objects whose format version
this build cannot read (issue #530). The physical sweep's version hold
covered metrics only, so after a binary rollback across an RLOG or RSPAN
format bump, a tombstoned logs or spans bucket holding objects written at
the newer version was deleted once its protection horizon elapsed, even
though the newer build could still read them. The sweep now reads each
data object's trailer through the gate of the bucket's own format (one
16-byte ranged GET per object, the cost metrics already paid) and, if any
object is outside this build's reader window, deletes nothing in the
bucket, logs a warning, and counts the objects on the same in-process
held-object counter, exactly as it does for metrics. A corrupt trailer is still swept. ADR-0531 and ADR-0066
carry dated amendments recording the wider hold. -
A PromQL alert rule raises one alert per matching series (issue #117,
ADR-0117). A rule used to collapse its result vector into one alert with
the rule's labels; each series that satisfies the condition is now its own
alert, whose labels are the series labels without__name__overlaid by
the rule labels, and whose identity hashes those labels. The Alertmanager
sink'salertnamestays the rule id, or the rule's ownalertnamelabel
when it sets one; a series label namedalertnamenever replaces it and,
when the rule sets noalertname, is sent asexported_alertname(with
Prometheus'exported_exported_rule on a further conflict) so series
differing only in that label stay distinct Alertmanager alerts. A
series that stops matching resolves only its own alert, and
repeat_intervalapplies to each alert on its own. A rule matching more
than 1000 series fails the tick withTooManyAlerts, and two series
merging to one label set fail it withDuplicateAlertIdentity, whose
message names the label names and the collidingalert_idbut no label
value; neither writes a record, and both count in
ravel_alert_rules_failed_total. A scalar query and a
SQL rule keep their existing single alert and identity. Two rules in one
tenant may no longer share arule_id, even with different labels: such a
rules file now fails startup. Per-series rules over churning label sets
should wait for alert state pruning (#1438). A new
ravel_alert_undelivered_notificationsgauge reports how many
notifications wait for every sink to accept them; while a sink keeps
failing it grows by one for every alert identity that transitions. A rule
whose write fails partway through its alerts now counts the records
already written inravel_alert_records_written_total.Upgrade notes:
- Expect a notification burst on the first tick. A PromQL rule whose
matching series carry labels besides__name__that the rule labels do
not override gets a new identity for each series. If its single
rule-level alert is pending or firing at upgrade, that tick writes one
Resolved transition for it and one new transition per matching series.
The webhook sink is notified of every one of them, and the Alertmanager
sink of every one except a pending transition. - A rule that fires today can start failing every tick. It fails with
DuplicateAlertIdentitywhen two matching series merge to one label
set, for example a selector over several metric names
({__name__=~"a|b"}) whose series differ only in__name__, or a rule
label that overrides the series label that told them apart. It fails
withTooManyAlertswhen more than 1000 series match. On every tick it
fails, the rule writes no record, and its existing alerts keep their
state. - A rules file in which two rules of one tenant share a
rule_idnow
fails startup withrule id "<id>" is used by more than one rule in tenant "<tenant>". Give each rule its ownrule_idbefore upgrading. - Repeat notifications multiply by the firing series.
repeat_interval
now applies to each alert, so a rule with 500 firing series sends 500
repeat notifications per interval to every sink where it used to send
one. Raiserepeat_interval, or set it to0s, on rules that match many
series.
- Expect a notification burst on the first tick. A PromQL rule whose
Added
ravel-serverbuilds a read and a write CPU gate and reports their
queueing on/metrics(ADR-1702, issue #1702). A new crate,
ravel-cpu-gate, holds the gate: a job at or above its inline floor
(256 KiB, or 100,000 samples for a PromQL evaluation) waits for one of a
fixed number of permits, then runs on the tokio blocking pool and keeps the
permit until it returns; a smaller job runs on the calling thread.
--cpu-gate-read-permitsand--cpu-gate-write-permitssize the two gates.
Unset, they aremax(1, cores - 1)andmax(1, cores / 2), and0is
refused at startup./metricsrendersravel_cpu_gate_permits,
ravel_cpu_gate_running,ravel_cpu_gate_queued, the
ravel_cpu_gate_wait_secondsandravel_cpu_gate_run_secondssum and count
pairs andravel_cpu_gate_abandoned_totalpergate, plus
ravel_cpu_gate_jobs_totalandravel_cpu_gate_inline_totalpergateand
site, and the tokio runtime'sravel_runtime_workers,
ravel_runtime_alive_tasks,ravel_runtime_global_queue_depthand
ravel_runtime_worker_busy_seconds_total. No decode or encode path submits
work to the gates yet; the later ADR-1702 tasks move them.ravel-cli export --signal logswrites a tenant's stored logs back out to
a Parquet fileravel-cli loadreads in (ADR-1751, issue #1712). The
command takes the store and tenancy flags the other read commands take, plus
--tenant, an RFC 3339--start/--endwindow (Zor any numeric
offset, converted to UTC),--parquetfor the output path, and ...
v0.18.0
[0.18.0] - 2026-09-26
Changed
ravel-serverdefaults tobyte-minimallogs fetching against a
loopback S3 endpoint (ADR-2014). Unset,--logs-fetch-policyused to
always resolve tocost-based; now, a--store s3deployment whose
--s3-endpointis loopback (localhostor a loopback IPv4/IPv6 literal)
resolves tobyte-minimalinstead, because on such a store the cold path
is disk-bound rather than network-bound and the whole-object reads
cost-basedpicks at the reference cost profile spend local disk I/O a
ranged read would have skipped. Measured on the ClickBench reference
machine (RustFS on loopback, 42 statements): cold wall-clock 1,720.4s to
1,186.6s, hot wall-clock 272.3s to 88.5s. An explicit--logs-fetch-policy
always wins, including an explicitcost-basedon a loopback endpoint, and
a non-loopback--s3-endpoint(or--store memory) sees no change. The
resolved policy's source (flag,default, or
derived-loopback-endpoint) is now logged alongside the policy on the
logs fetch policy resolvedstartup line.
What's Changed
- feat(server): default to byte-minimal fetching on a loopback store by @pmoust in #2015
- chore(release): prepare 0.18.0 by @pmoust in #2016
Full Changelog: v0.17.0...v0.18.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.18.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.18.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.
v0.17.0
[0.17.0] - 2026-09-25
Fixed
ravel-benchpassesclippy -D warningsunder--all-features
(issue #1925). Theread_path_accountingbinary'sBackendenum carried
an unboxedS3Configin its S3 variant next to a data-less variant, so
clippy::large_enum_variantfailedcargo clippy --workspace --all-targets --all-features -- -D warnings. The build and tests were never affected;
only the lint gate failed. The field is now boxed, with no change in
behavior.
Changed
- The remaining MinIO identifiers are renamed to RustFS (issue #2008).
The object store moved from MinIO to RustFS in 0.16.0 (#2002), which kept
the old names; they now match. TheRAVEL_MINIO_*variables that gate the
S3 contract suite and the bench smoke tests are nowRAVEL_RUSTFS_*, and
the old names are no longer read, so anyone running those suites locally
must rename them. The testsminio_contractandminio_ingest_read_smoke
are nowrustfs_contractandrustfs_ingest_read_smoke, and CI's checks
that those tests really ran match the new names.sql_latency_bench
reports the backend label"rustfs"instead of"minio", and
read_path_accounting'sBackend::MinioisBackend::RustFs. Test
fixture hostnames and doc comments follow; where a comment stated how
MinIO specifically behaved, it now makes a store-neutral statement
instead.
What's Changed
- chore: rename the remaining MinIO identifiers to RustFS by @pmoust in #2009
- chore(release): prepare 0.17.0 by @pmoust in #2010
Full Changelog: v0.16.1...v0.17.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.17.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.17.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.
v0.16.1
[0.16.1] - 2026-09-25
Fixed
- A release whose changelog section is too long for a GitHub release body
now publishes with that section's entry headlines instead of failing
(ADR-0086, amendment 2026-09-25). The v0.16.0 tag published its images, then
its release job failed atgh release createwith "body is too long (maximum
is 125000 characters)": the 0.16.0 section alone is 165,435 characters, so
v0.16.0 has no GitHub Release.publish-images.ymlnow measures the composed
notes against a 120,000-byte budget. Over it, the section keeps its headings
and each entry's bold headline, followed by a link to the full section in the
taggedCHANGELOG.md; the generated pull request list and the downloads text
are unchanged. If the notes still do not fit, the job fails with both sizes
before it creates the Release.
What's Changed
- fix(release): fit the release notes within GitHub's body limit by @pmoust in #2006
- chore(release): prepare 0.16.1 by @pmoust in #2007
Full Changelog: v0.16.0...v0.16.1
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.16.1One 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.16.1' \
--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.
v0.15.0
[0.15.0] - 2026-09-08
Added
- The operator qualifies the object store before serving (issue #36). Each
reconcile pass renders a one-shot<cluster>-qualifyJob running
ravel-cli store qualifywith the server image, credentials Secret, bucket,
region, and endpoint of the tiers it gates, and creates the gateway, query,
and maintain Deployments only once that Job reports Complete. A
StoreQualifiedstatus condition carries the gate state (Pending while the
Job runs, Succeeded on completion, Failed with the Job's message once its
backoffLimit is exhausted). The qualified inputs (bucket, region, endpoint,
image, credentials Secret name and resourceVersion) are hashed into
status.storeQualifiedHash, so a later pass with unchanged inputs proceeds
without re-running qualification even after the Job's TTL garbage-collects
it; an input change deletes and recreates the Job and flips the condition
back to Pending. The Job as a whole is bounded byactiveDeadlineSeconds
(1400 s across both attempts), so a hung attempt fails rather than holding
StoreQualifiedat Pending; a Failed Job is
deleted and recreated on the next pass instead of sitting until its TTL,
and a status-only reconcile no longer re-triggers the gate. The kind
lane's hand-run qualification Job is removed since the lane now deploys
through the operator. - The object-store conformance suite probes concurrent creates, listing
order, and deletes (issue #1302). Four new probes close gaps the
qualification suite left open:ConcurrentCreateIfAbsentSingleWinnerraces
eight writers on one absent key and checks for exactly one winner,
LexicographicListingOrderandCrossPageListingcheck listing order and
start_afterresumption across page boundaries, andDeleteVisibility
checks that a delete is reflected in both a follow-up get and a follow-up
listing. The suite tolerates the behaviors the object-store contract
explicitly permits: a key repeated across a listing page boundary, and a
losing racer's write landing before its own retryable-conflict response is
retried. The shipped Admin IAM template gainss3:DeleteObjecton
sys/qualify/*so the delete probe can run under it; no other resource
gets a delete grant.CONFORMANCE_SUITE_VERSIONstays at 1 on purpose, so
a bucket that already carries asys/qualificationrecord does not
re-qualify and never runs these four new probes. - A derived per-tenant alert-state memo bounds alert evaluation cost
(issue #1294). The alert evaluator previously re-folded a tenant's entire
Signal::Alertstransition history on every tick, so cost grew with the
cumulative transition count rather than the rule count. A durable memo at
t/<tenant_hash>/a/state/latestnow seeds each tick's fold, and a tick
reads only the commit records after the memo's watermark. The watermark is
bound to the reader's own seal-bound hour: a watermark the memo carries
above that seal-bound hour is clamped down to it rather than trusted, so
an evaluator with a stale or ahead clock cannot skip a legally late
transition. A memo with a duplicatealert_idis treated as a decode
failure rather than served, and a failed memo encode leaves the previous
memo untouched instead of overwriting it with corrupt bytes. A missing or
unreadable memo falls back to a full fold. - A bounded top-k optimizer rule for
GROUP BY ... ORDER BY ... LIMIT k
(issue #1402). A grouped aggregate whose only consumer is an
ORDER BY ... LIMIT kon amin/maxaggregate over a non-nullable,
non-float column now keeps only the top k groups in a priority map instead
of materializing one accumulator per distinct group; ADR-0013's
exact-semantics contract is unaffected because the rule fires only for an
aggregate expression that is provably exact under the bound (max under
DESC, min under ASC). - The operator hardens every rendered pod and scopes its own Secrets RBAC
per namespace, keeping a cluster-wide watch onRavelCluster. Every
rendered gateway, query, maintain, and ingest-router container now carries
aSecurityContext:runAsNonRoot, no privilege escalation, every Linux
capability dropped, and a read-only root filesystem (safe because the only
local write any of these processes can opt into is the disk cache, and the
operator renders no--cache-dirflag). The operator's ServiceAccount no
longer holds a cluster-wide read on every Secret; it reads a
RavelCluster's referenced Secrets through a namespaced grant in that
object's own namespace, while its watch onRavelClusterobjects stays
cluster-wide so a cluster outsideravel-systemstill reconciles. Serving
a namespace other thanravel-systemalso needs the namespaced
RoleBinding this release ships (deploy/k8s/operator/secrets-rolebinding.yaml)
copied into that namespace, or Secret resolution 403s. The same change
adds a PodDisruptionBudget per tier (maxUnavailable: 1) and preferred
pod anti-affinity to every rendered Deployment.
Changed
ravel-serverin modeallorquerynow installs the query-audit
pipeline and records query text tokenized by default (--audit-text redacted). A process with no key refuses to start: an unkeyed deployment
(--tenant-hash-unkeyed) must setRAVEL_AUDIT_TOKEN_KEYto 64 hex
characters or pass--audit-text plaintext; a keyed deployment
(--tenant-hash-key-file) derives the key and needs no action. Gateway and
maintain processes are unaffected. The docker-compose quickstart now ships a
development key.- CAS-mutable sys records (provisioning, tenant config, metric metadata)
move to format_version 2 (ADR-0066). An earlier release added fields to
these records without a version bump, so a lagging binary reading a
current record silently dropped the fields it did not model and wrote the
stripped record back under compare-and-swap. Every reader gate for these
three records now accepts exactly the version set {1, 2} instead of a
ceiling-only check, and a CAS rewrite now refuses a record whose version
exceeds what this build's writer stamps rather than re-encoding it through
an older field set. Every writer now stamps new records at version 2. The
background metric-metadata refresh reads through the same permissive
reader the cache miss path uses, so a tenant's record upgraded to version 2
during the writer rollout is no longer rejected by refresh and stuck
serving a stale pre-upgrade snapshot for the rest of the process's life.
The auth token map and key epoch record read gates gained the same floor:
a version-0, unstamped record now fails closed instead of being decoded
and rewritten. - Every query transport (HTTP, gRPC, mTLS, Flight SQL) now runs through one
query service layer (issue #1377). Each surface previously carried its
own copy of admission, cost accounting, and audit controls, and the copies
had drifted: analytics and exemplars ran outside the fleet-wide
concurrency ceiling, exemplars recorded no cost, only the SQL surface
billed a query a client disconnected mid-flight, admission was taken
before authentication on some routes, and a malformedmatch[]selector
could consume an admission permit before being rejected. All four
transports now authenticate, then admit, then run, then audit in the same
order; a malformed selector is rejected with 400 before any permit is
taken; a cancelled, timed-out, or failed PromQL, metadata, or analytics
query now bills the spend it reached instead of zero;label_valuesaudit
events are tagged as their own audit language, distinguishable from the
other label route; an audit-sink submission failure is reported with the
audit pipeline's own failure message instead of the storage-outage string;
and the mTLS listener gets its own instance of the service layer so a
certificate-identified caller is resolved against the mTLS tenant resolver
rather than the public listener's bearer-token resolver. - The store-request ceiling (
max_s3_requests,--max-s3-requests)
is now checked immediately after catalog resolution, combining every
lane's spend so far, not only during segment fetches (issue #1376). A
query whose snapshot resolved to zero segments never reached the
incremental checks in the fetch loop, so it could spend more catalog
requests during resolve than the ceiling allowed and still succeed; a
mixed metrics-and-logs query is now checked against its combined resolve
cost across both lanes. Applies to PromQL, SQL execute, SQL explain, and
the Flight SQLresolve_snapshotpath. - Metrics compaction releases each L1 segment's encoded bytes at PUT
instead of holding them until the record publishes. Peak memory during
RSEG compaction previously carried a term that grew with the whole
bucket's L1 output; the RLOG path already had this shape (ADR-0979
decision 3), and this applies it to RSEG. - The RLOG plan phase's whole-object read is carried into the scan, bound
by plan fan-out rather than the corpus. On the whole-object fallback,
the plan phase and the scan each fetched the same object; the plan
phase's bytes are now handed to the scan and charged tobytesReused,
with retention bounded to the plan fan-out in objects times the object
size. On ClickBench q20 that was 6,785 GETs and 21.1 GB, where a
corpus-sized cache gives 4,533 GETs and 11.24 GB. - The operator now bounds qualify-Job recreations for a store that keeps
failing qualification (issue #36). A failingravel-cli store qualifyJob is
recreated on a capped exponential backoff (30 s doubling to a 480 s ceiling)
and, after 6 consecutive failures, held in a one-hour terminal cooldown that
only an input change or the cooldown's expiry clears, rather than looping
delete/recreate every retry interval. The failure count and next-retry instant
...
v0.14.0
[0.14.0]
Added
- A
latency-firstlogs fetch policy (ADR-0996 amendment, superseded by
ADR-1196). Measured on a reference cold-cache corpus, thecost-based
default resolves to whole-object reads and moves 3x the bytes of
byte-minimalat a deployment where transfer and retrieval are free,
because the derived per-request rate saturates.--logs-fetch-policy latency-firstresolves the same byte quantities asbyte-minimal. It is
an intent, not a tuning constant: it carries no concurrency default of its
own, and resolves--store-get-concurrency,--sql-partition-count, and
--promql-fetch-fanoutexactly as every other policy does.cost-based
stays the default;latency-firstis an operator opt-in for deployments
where cold wall-clock matters more than the request bill, and it pays off
only once the operator raises concurrency explicitly to the measured
configuration (ravel_query::LATENCY_FIRST_MEASURED_CONCURRENCY, 256), at
a measured cost of about 5.45x the GET requests for about 41% less cold
time. Raising that concurrency also raises in-flight fetch memory, which
is not yet bounded by a process-wide budget (issues #1170, #1007); the
flag's own documentation and a startup log line underlatency-firstmake
that precondition operator-visible. - The
alertsandauditSQL tables (ADR-1101).POST /api/v1/sqland
Flight SQL serve five tables, and FlightGetTableslists all five; naming
two in one query is still rejected before any listing.alertsis alert
history, one row per state transition, and each row carries the write
identity of its record (writer_id,writer_epoch,writer_seq), so a
ROW_NUMBER()fold ordered by time and write identity returns exactly one
current row per alert even when two evaluators overlap at a lease handover.
auditreads back a tenant's own legal-hold and reshard records, which the
maintenance process writes directly. It also serves query-audit records, but
no shipped startup path installs the pipeline those go through, so
attrs['kind'] = 'query'selects nothing until a deployment attaches one. - A read-side shard floor for fixed-shard signals (ADR-1101). Alert and
audit writers pin their shards by constant and neither signal is provisioned,
so the catalog's scan-set derivations now take the maximum of the
provisioning history and the signal's fixed shard count. Anauditquery on
a--shards 1deployment reads the query-audit shard instead of silently
omitting it, and a wider deployment scans exactly as before. ravel-memory, one process-wide memory budget (ADR-1170). The server
derived four memory ceilings from the host and enforced each in a component
that knew nothing of the others, so N tenants could each reserve half the
box.ravel-memoryis a leaf crate holding one counter every ledger draws
from: a compare-and-swap reserve for the SQL adapter and an RAII reservation
for the fetch layer.- Bloom pruning for PromQL
__body__matchers (ADR-1103 follow-up). A
__body__equality, or an anchored regex with a token-bounded mandatory
literal run, now pushes that literal onto the scan as ahas_word
predicate, so the RLOG block bloom skips blocks before decode. The
per-record check still runs on every decoded record: the pushed word only
prunes, it never decides. Negated matchers, unsupported metacharacters and
+-quantified patterns are rejected by the extractor rather than pushed,
because token matching is not a superset of substring matching. - A gate against wall-clock waits in injected-clock tests.
scripts/check-injected-clock-helpers.sh, run bygates.shand CI, fails on
thread::sleep,tokio::time::sleep,tokio::time::timeout,Instant::,
SystemTime, a baresleep(...)or.elapsed()inside a helper that takes
aTestClockorFixedClock, unless the line carries
// allow-wall-clock: <reason>. Its default scope is the loader's test
module inravel-cli; the one wait it found there was made clock-driven
rather than exempted. scripts/verify-dispatch-gates.sh --with-gatesrunsgates.shitself
inside the cold worktree instead of a hand-listed command set, so a
dispatched branch is checked against the same feature lanes CI runs and the
run leaves a gate receipt the merge script can reuse.
Changed
-
Cache warm-up keys off each tenant's latest ingest hour, not the current
hour. On a tenant whose data is older than the warm-up window the previous
pass issued about 1,900 small object reads from the first query's own path
before warming nothing; those reads are gone, and the first query on a cold
process is about 3 s faster on the reference corpus. The replacement probe
costs about 1 s at startup, so the end-to-end saving on a cold start is
about 2.5 s, not 3. The probe fans out on the configured resolve concurrency,
asserts tenant isolation on every listed key, and is bounded per shard. -
Catalog resolve GET concurrency is configurable, default 128. Every
record GET inCatalog::resolve_implpassed through a fixed bound of 16.
Measured against S3 on a 10,000-record unsealed tail, one cold resolve each:
23.2 s at 16, 4.4 s at 64, 2.3 s at 128, with the same 10,001 GETs at every
level. Cold resolve is concurrency-bound; this was the lever. -
SQL reservations are charged to the process budget (ADR-1170). Every SQL
reservation now flows query, then tenant, then process on the way up, with a
process refusal rolling the tenant and query charges back before surfacing
asResourcesExhaustednaming the process figures. The infallible grow path
trips a ceiling breach when the process limit is exceeded, so a DataFusion
overshoot still ends in a typed error on the stream's next poll rather than
an unaccounted allocation. -
--fetch-concurrencyunbundled into three flags (ADR-1195): the SQL
scan partition count, the PromQL/analytics per-query fetch fan-out, and the
object-store GET concurrency were one knob with three coupled effects; they
are now--sql-partition-count,--promql-fetch-fanout, and
--store-get-concurrency, each independently sizeable.--fetch-concurrency
still sets all three together for a config that predates the split (source
legacy-flagin the startup log). Combining it with any of the three new
flags is a startup error naming both flags, and a value of0in any of the
four is a startup error naming that flag, raised before any fetcher, engine,
or SQL session exists. -
GET concurrency is process-wide, not per engine (ADR-1195):
ravel-server
now builds exactly oneArc<GetLimiter>where it assembles its shared state
and hands that sameArcto every fetcher- and engine-construction site in
the process (the PromQL query path, the SQL executor's RSEG/RLOG/RSPAN
fetchers, the distributed fragment path, cache warming, exemplars, and
alerting). Before this, each RSEG and RLOG fetcher held its own semaphore, so
N fetchers each configured to "8 concurrent GETs" could together put 8N GETs
in flight against the store. Two behaviour changes follow. RSEG fetchers now
honour the configured limit instead of the compiled default of 16, so a host
whose derived value is below 16 issues fewer concurrent GETs than before.
RSPAN fetchers are bounded for the first time: span reads previously ran with
no GET limit at all, so a span-heavy deployment can see lower read
concurrency and should size--store-get-concurrencyfor it. No fetcher in
the server process owns a private limiter anymore.
Fixed
- The RLOG plan phase's whole-object read is carried into the scan. On the
whole-object fallback,plan_segmentfetched each object to plan it and the
scan fetched the same object again. The plan phase now hands its bytes to
the scan, which short-circuits on them before any GET and charges them to a
bytesReusedfigure rather than a cache hit. Retention is bounded: the
first segments to complete their plan keep their buffer, up to the SQL
partition count, and every later segment is re-fetched exactly as before, so
peak retained bytes are the partition count times the object size, never
the corpus. The saving is therefore about one duplicate read per unit of
plan fan-out; removing the rest needs the carry to stream per partition
instead of being held at the plan barrier, tracked separately. Correction. This change landed after the 0.14.0 tag and ships in 0.15.0, where the retention bound is the plan fan-out rather than the SQL partition count. - The RLOG raw prefetch is gated on the cursor budget before
try_join!
(ADR-0979 decision 4). The merge cursor's refill fetched the next two
row-group blocks before the budget had been checked, so up to twice the
group size was allocated and only accounted for on the following iteration.
The pending fetch window is now priced from resident metadata and reserved
before the fetch is issued.
What's Changed
- feat(promql): pin evaluator contract for equal-timestamp samples by @pmoust in #1149
- docs(adr): amend ADR-0873 for the .cstat identity and STR rule by @pmoust in #1178
- feat(query): add the PromQL-over-logs series source by @pmoust in #1158
- fix(promql): compute rate-family intervals directly from nanoseconds by @pmoust in #1183
- feat(sql): register the alerts and audit tables on both SQL transports by @pmoust in #1186
- docs: describe the alerts and audit SQL tables by @pmoust in #1189
- feat(query): answer PromQL log selectors from the logs signal by @pmoust in #1188
- docs: mark ADR-1101 accepted by @pmoust in https...