Skip to content

Releases: NOFireAI/ravel

v0.23.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 23:44

[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.json and
    maintain.json now allow writing a tenant's key-epoch record t/<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-cli does changes. Operators must re-apply the three templates.
  • /metrics says why a flush was handed back (issue #2600).
    ravel_ingest_rerouted_flushes_total gains a reason label,
    retired_index or generation_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-rows Arrow 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 decodes ceil(--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-rows not
    divisible by K, short and empty partitions, --skip-rows and 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 (ClickBench hits.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_columnar used 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 of ravel-cli load on ClickBench hits.parquet at --batch-rows 500000 --read-cursors 16 --shards 4 fell from 15.48 GB to 11.37 GB.
  • Columnar log batches store attribute cells as typed columns (issue
    #2625). A ColumnarLogBatch dynamic column used to hold one AttrValue
    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 load fills 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 pass u32::MAX bytes 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 ClickBench hits.parquet at --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 1000000 the load ran its 360 seconds without running out
    of memory, at a 13.33 GB peak.
  • ravel-cli load holds 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-bytes above 1 lets 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 25000000 stored
    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 500000 and 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 load documents a measured recipe for large log objects
    (issues #2627 and #2651). The --target-bytes and --pipeline-depth help
    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 6500000000 stored 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-bytes was 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-delay must 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_hours of 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 on ravel_catalog_fold_refold_requests_dropped_total; an
    evicted hour, or one lost when the process restarts, ...
Read more

v0.22.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 22:51

[0.22.0] - 2026-10-05

Changed

  • SQL spill is on by default when ravel-server runs 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
    source cache-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_DIR with
    RAVEL_SQL_SPILL_MAX_BYTES still wins, and RAVEL_SQL_SPILL_MAX_BYTES set
    alone replaces the derived ceiling. The new --sql-spill off flag disables
    spill whatever the environment or --cache-dir says. Startup logs two more
    performance default resolved lines, sql_spill_dir and
    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-dir is set, --sql-spill is not off and 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_DIR pair's directory or off for lack of space. A process
    whose spill resolves under --cache-dir then 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-dir must be a
    local volume unless --sql-spill off is set: the sweep runs wherever
    spill resolves.
  • An ORDER BY over a spill-eligible aggregation can now spill (ADR-0954
    amendment, issue #2416). Previously any Sort kept a query from spilling.
    Whenever a statement's plan has a Sort directly over an aggregation of
    only COUNT, SUM and AVG with no float GROUP BY column (or over a
    projection directly over it), its sort key gets the aggregation's
    GROUP BY columns 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 whose AVG
    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. A Sort over any other aggregate, over
    a float group column, over grouping sets, over a HAVING filter, 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-phase decompressed_bytes of 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 in prefetch_memory_reopens and 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_micros and
    per_connection_throughput_bytes_per_s (set together or not at all), and
    cost-based takes 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-threshold is no longer overridden on that profile. The
    logs fetch policy resolved startup line gains rate_term,
    projection_break_even_bytes (the break-even in force, the routing threshold
    when the policy derives none) and break_even_source (profile or
    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 readable MemAvailable in
    /proc/meminfo, memory_budget_bytes resolves to min(MemTotal - RESERVE, max(FLOOR, MemAvailable + own RSS - RESERVE)), logged with
    source="derived-available". FLOOR is 1 GiB and floors only the
    MemAvailable + own RSS - RESERVE term; when it binds, a WARN names the
    MemAvailable reading and --memory-budget-bytes as the remedy. The outer
    min keeps the budget at or below MemTotal - RESERVE, so a host whose
    MemTotal is 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", MemAvailable not consulted), including when
    /proc/meminfo cannot be read, since the limit is then the memory figure.
    An unreadable MemAvailable on a host whose MemTotal parses keeps the
    MemTotal-minus-reserve rule (source="derived"). With neither a readable
    MemTotal nor a cgroup limit (every non-Linux build, for example) no budget
    is derived: source="fallback", budget unlimited, as before. The new
    --memory-budget-bytes flag overrides every derivation except gateway mode,
    which clai...
Read more

v0.21.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 16:26

[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 less max_flush_lifetime, less the
    slowest flush trigger (the largest of --max-flush-delay,
    --max-flush-delay-idle and 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 new DeferralCapReached error (HTTP 429 with
    Retry-After, gRPC RESOURCE_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 on ravel_ingest_deferral_cap_refused_total by 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 merges l1_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 under compact-bucket derives 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-bytes sets the log and metrics caps together. Span merges
    keep the fixed 256 MiB, since span parts have no stored-size cap.
    ravel-server derives it from its memory budget and
    --maintain-unit-concurrency and takes the new
    --maintain-l1-part-memory-target-bytes flag; ravel-cli maintain derives
    it from the host's memory and --bucket-concurrency, and compact-bucket
    gains --l1-part-memory-target-bytes and --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_bytes and max_l1_part_bytes, and the
    server's startup log carries the same fields; the unprefixed
    l1_part_memory_target_bytes line is gone. The server's startup lease check
    now sizes the largest part as the larger of the two stored-size caps.
    ravel-server and ravel-cli share 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 returns AlreadyCompacted, issue #2370), so the bucket's L0
    inputs should be held with ravel-cli hold set meanwhile.
  • ravel-server refuses 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 3600s with 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-idle so 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-bytes derives 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-bytes still wins: over an explicit
    --sql-tenant-max-bytes it is clamped to that ceiling, and over a derived
    tenant ceiling it raises the ceiling to match, as before. An explicit
    --sql-tenant-max-bytes between 25% and 50% of MemTotal now clamps the
    derived per-query pool, which the resolved line reports as clamped=true
    with a startup warning naming --sql-max-query-bytes. To keep the earlier
    split, set --sql-max-query-bytes to 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 with SnapshotInvalidated
    when the next sweep deleted it. The sweeps now write an unnamed-since marker
    under t/<tenant_hash>/<signal>/maint/unn/ the first time they find a
    candidate unnamed, and delete it only once max_query_duration + head_cache_ttl + 4 * clock_skew_allowance has passed, holding it until then
    under the new pinned_window reason. The maintain IAM template gains delete
    and list on t/*/*/maint/*, an erasure request's .dreq lives longer by
    the same window, and gc-config set and 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 migrate is fenced the same way, and takes --no-claim.
    The claim_min_input_bytes setting (--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_version 0, 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...
Read more

v0.20.0

Choose a tag to compare

@github-actions github-actions released this 02 Oct 16:03

[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-protection reads the S3 bucket's protection configuration at startup and refuses on more conditions
  • ravel-server sends a CRC64-NVME upload checksum on every S3 PUT by default
  • ravel-cli --store s3 checksums every PUT with CRC-64/NVME by default, and store qualify reads 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_ts equal to ts, stored as a reference to it)
  • s3_e2e_bench counts 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 migrate names every bucket whose refusal no re-run clears
  • ravel-cli maintain migrate names 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-bucket and compact-tenant take 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-versions now 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/gc format version 2 records the HEAD cache TTL, and ravel-cli maintain sweep reads sys/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/gc at 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 logs loads a dictionary-encoded hex trace_id or span_id column
  • ravel-cli load --signal logs and --signal metrics refuse 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 show and ravel-cli bloom-scope show, and the clustering fields in ravel-cli rlog inspect
  • ravel-cli clustering-key set, ravel-cli clustering-key clear and ravel-cli bloom-scope set write a tenant's clustering key and bloom scope
  • ravel-cli store verify-protection checks a bucket's protection configuration
  • /metrics exports the Flight SQL slice capability rejects
  • /metrics exports the fragment capability rejects
  • /metrics exports the superseded-input sweep's hold counters
  • --audit-retention sets the query-audit retention window
  • ravel-server exports 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 metrics writes a tenant's stored metric samples to a Parquet file load --signal metrics reads back onto the same series
  • /metrics renders 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_rewrite refuses a non-canonical superseded_record_key
  • ravel-cli maintain inspect prints a compaction record's superseded_record_key
  • Advisory compaction claims report five /metrics counters and gain three server flags
  • ravel-cli rlog footprint attributes 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 spans loads the spans signal
  • /metrics renders ravel_health_heartbeat_age_seconds
  • ravel-server has an off-by-default heap-profiling cargo feature
  • ravel-cli load takes --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-operator Deployment 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_seconds and ravel_maintain_units_scan_failed render on /metrics
  • A full-object GET now verifies the body against the checksum the store recorded at upload
  • ravel-ingest has an opt-in idle flush byte floor, off by default
  • The S3 adapter observes the store's own clock from response Date headers
  • ravel-server --idle-flush-byte-floor exposes that floor, and /metrics reports what it holds
  • ObjectStoreBackend::get_pinned reads exactly the bytes a recorded pin names, and ravel-object-store gains read-only external stores per credential profile
  • Parquet table location grants and in-place manifest format
  • ravel-cli tenant parquet-grant add|remove|ls and ravel-cli parquet ls|sweep
  • ravel-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/sql and Flight SQL (ADR-2040 D3, D4
  • ravel-parquet: snapshot::snapshot_location, the file list CREATE EXTERNAL TABLE will pin
  • Tenant resolution carries an optional ddl capability
  • 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 GetFlightInfo resolved
  • ravel-sql gains 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

Read more

v0.19.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 19:30

[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). The scripts/chaos/
    parser now scans a sample's label set quote-aware, reads the value before
    any timestamp, and takes label=value selectors, and the mid-flush trigger
    selects signal="metrics". A comma-joined x-ravel-commit-token header is
    split into its opaque tokens, and the header name is matched without the
    gawk-only IGNORECASE. 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.sh covers 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-requests permit 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 and Retry-After on HTTP or
    DEADLINE_EXCEEDED for 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 answers UNIMPLEMENTED on 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 the ravel_ingest_concurrency_shed_total counter are unchanged,
    and a refusal keeps the status and message it had: 429 with Retry-After
    and 401 for bad credentials on HTTP, RESOURCE_EXHAUSTED and
    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 failed bench-smoke, quickstart and
    object-store-contract. Those jobs and the MetricsBench nightly now create
    their RustFS bucket with the runner's own aws CLI 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 turns createbucket into a
    no-op on the locally built image; the documented quickstart command is
    unchanged. demo/kill-and-recover.sh now restarts ravel-server with
    --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's alertname stays the rule id, or the rule's own alertname label
    when it sets one; a series label named alertname never replaces it and,
    when the rule sets no alertname, is sent as exported_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_interval applies to each alert on its own. A rule matching more
    than 1000 series fails the tick with TooManyAlerts, and two series
    merging to one label set fail it with DuplicateAlertIdentity, whose
    message names the label names and the colliding alert_id but 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 a rule_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_notifications gauge 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 in ravel_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
      DuplicateAlertIdentity when 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
      with TooManyAlerts when 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_id now
      fails startup with rule id "<id>" is used by more than one rule in tenant "<tenant>". Give each rule its own rule_id before 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. Raise repeat_interval, or set it to 0s, on rules that match many
      series.

Added

  • ravel-server builds 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-permits and --cpu-gate-write-permits size the two gates.
    Unset, they are max(1, cores - 1) and max(1, cores / 2), and 0 is
    refused at startup. /metrics renders ravel_cpu_gate_permits,
    ravel_cpu_gate_running, ravel_cpu_gate_queued, the
    ravel_cpu_gate_wait_seconds and ravel_cpu_gate_run_seconds sum and count
    pairs and ravel_cpu_gate_abandoned_total per gate, plus
    ravel_cpu_gate_jobs_total and ravel_cpu_gate_inline_total per gate and
    site, and the tokio runtime's ravel_runtime_workers,
    ravel_runtime_alive_tasks, ravel_runtime_global_queue_depth and
    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 logs writes a tenant's stored logs back out to
    a Parquet file ravel-cli load reads in
    (ADR-1751, issue #1712). The
    command takes the store and tenancy flags the other read commands take, plus
    --tenant, an RFC 3339 --start/--end window (Z or any numeric
    offset, converted to UTC), --parquet for the output path, and ...
Read more

v0.18.0

Choose a tag to compare

@github-actions github-actions released this 26 Sep 08:26

[0.18.0] - 2026-09-26

Changed

  • ravel-server defaults to byte-minimal logs fetching against a
    loopback S3 endpoint
    (ADR-2014). Unset, --logs-fetch-policy used to
    always resolve to cost-based; now, a --store s3 deployment whose
    --s3-endpoint is loopback (localhost or a loopback IPv4/IPv6 literal)
    resolves to byte-minimal instead, because on such a store the cold path
    is disk-bound rather than network-bound and the whole-object reads
    cost-based picks 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 explicit cost-based on 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 resolved startup 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.0

One binary per platform, named <binary>-<os>-<arch>:

  • ravel-server-linux-amd64, ravel-server-linux-arm64
  • ravel-cli-linux-amd64, ravel-cli-linux-arm64
  • ravel-operator-linux-amd64, ravel-operator-linux-arm64
  • ravel-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' \
  SHA256SUMS

Debug 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-amd64

gdb 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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 22:29

[0.17.0] - 2026-09-25

Fixed

  • ravel-bench passes clippy -D warnings under --all-features
    (issue #1925). The read_path_accounting binary's Backend enum carried
    an unboxed S3Config in its S3 variant next to a data-less variant, so
    clippy::large_enum_variant failed cargo 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. The RAVEL_MINIO_* variables that gate the
    S3 contract suite and the bench smoke tests are now RAVEL_RUSTFS_*, and
    the old names are no longer read, so anyone running those suites locally
    must rename them. The tests minio_contract and minio_ingest_read_smoke
    are now rustfs_contract and rustfs_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's Backend::Minio is Backend::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.0

One binary per platform, named <binary>-<os>-<arch>:

  • ravel-server-linux-amd64, ravel-server-linux-arm64
  • ravel-cli-linux-amd64, ravel-cli-linux-arm64
  • ravel-operator-linux-amd64, ravel-operator-linux-arm64
  • ravel-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' \
  SHA256SUMS

Debug 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-amd64

gdb 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

Choose a tag to compare

@github-actions github-actions released this 25 Sep 14:30

[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 at gh release create with "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.yml now 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
    tagged CHANGELOG.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.1

One binary per platform, named <binary>-<os>-<arch>:

  • ravel-server-linux-amd64, ravel-server-linux-arm64
  • ravel-cli-linux-amd64, ravel-cli-linux-arm64
  • ravel-operator-linux-amd64, ravel-operator-linux-arm64
  • ravel-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' \
  SHA256SUMS

Debug 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-amd64

gdb 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

Choose a tag to compare

@github-actions github-actions released this 08 Sep 17:02

[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>-qualify Job running
    ravel-cli store qualify with 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
    StoreQualified status 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 by activeDeadlineSeconds
    (1400 s across both attempts), so a hung attempt fails rather than holding
    StoreQualified at 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: ConcurrentCreateIfAbsentSingleWinner races
    eight writers on one absent key and checks for exactly one winner,
    LexicographicListingOrder and CrossPageListing check listing order and
    start_after resumption across page boundaries, and DeleteVisibility
    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 gains s3:DeleteObject on
    sys/qualify/* so the delete probe can run under it; no other resource
    gets a delete grant. CONFORMANCE_SUITE_VERSION stays at 1 on purpose, so
    a bucket that already carries a sys/qualification record 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::Alerts transition 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/latest now 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 duplicate alert_id is 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 k on a min/max aggregate 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 on RavelCluster.
    Every
    rendered gateway, query, maintain, and ingest-router container now carries
    a SecurityContext: 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-dir flag). 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 on RavelCluster objects stays
    cluster-wide so a cluster outside ravel-system still reconciles. Serving
    a namespace other than ravel-system also 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-server in mode all or query now 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 set RAVEL_AUDIT_TOKEN_KEY to 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 malformed match[] 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_values audit
    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 SQL resolve_snapshot path.
  • 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 to bytesReused,
    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 failing ravel-cli store qualify Job 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
    ...
Read more

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 22:19

[0.14.0]

Added

  • A latency-first logs fetch policy (ADR-0996 amendment, superseded by
    ADR-1196). Measured on a reference cold-cache corpus, the cost-based
    default resolves to whole-object reads and moves 3x the bytes of
    byte-minimal at a deployment where transfer and retrieval are free,
    because the derived per-request rate saturates. --logs-fetch-policy latency-first resolves the same byte quantities as byte-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-fanout exactly as every other policy does. cost-based
    stays the default; latency-first is 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 under latency-first make
    that precondition operator-visible.
  • The alerts and audit SQL tables (ADR-1101). POST /api/v1/sql and
    Flight SQL serve five tables, and Flight GetTables lists all five; naming
    two in one query is still rejected before any listing. alerts is 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.
    audit reads 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. An audit query on
    a --shards 1 deployment 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-memory is 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 a has_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 by gates.sh and CI, fails on
    thread::sleep, tokio::time::sleep, tokio::time::timeout, Instant::,
    SystemTime, a bare sleep(...) or .elapsed() inside a helper that takes
    a TestClock or FixedClock, unless the line carries
    // allow-wall-clock: <reason>. Its default scope is the loader's test
    module in ravel-cli; the one wait it found there was made clock-driven
    rather than exempted.
  • scripts/verify-dispatch-gates.sh --with-gates runs gates.sh itself
    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 in Catalog::resolve_impl passed 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
    as ResourcesExhausted naming 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-concurrency unbundled 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-flag in the startup log). Combining it with any of the three new
    flags is a startup error naming both flags, and a value of 0 in 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 one Arc<GetLimiter> where it assembles its shared state
    and hands that same Arc to 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-concurrency for 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_segment fetched 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
    bytesReused figure 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...
Read more