libparitypp (20260731+18) unstable; urgency=high

  * server::run(): call socket_->setStartDispatchPromoted(true) (new in
    libnetplus 20260731+29) so a connection's first dispatch is promoted
    to quicDispatchPool() instead of running inline on the single-threaded
    accept loop — fixes the self-sustaining DEGRADED/CRITICAL flapping
    where an in-flight STORE_BLOCK/FETCH_BLOCK during scrub/rebalance
    starved GET_STATUS health probes on other connections long enough for
    health_loop() to mark the peer non-responsive.
  * server.h/server.cpp: guard peer_status_fn_ with a new peer_status_mutex_
    — was an unguarded std::function reassigned every health-loop cycle
    while GET_STATUS concurrently read it, a data race made more likely to
    hit by the above change actually running health checks concurrently
    with data-plane dispatch.

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 31 Jul 2026 07:50:00 +0200

libparitypp (20260728+17) unstable; urgency=high

  * client::mark_connection_failed(): replace the flat 10s dead-node
    cooldown with an adaptive one (1s/2s/4s/8s, capped at the old 10s from
    the 5th consecutive failure) tracked per node. Also fixes a re-entry
    bug where ensure_connected()'s "still in cooldown" recheck threw an
    exception that the same generic catch blocks treated as a *new*
    failure, re-extending dead_until on every retry attempt so a node that
    failed once could never actually see its cooldown expire. Centralized
    all 15 call sites that used to inline "reset + flat 10s" into this one
    helper. New test: test/reconnect_test.cpp, verified to fail (hang past
    the old flat 10s) against the pre-fix build and pass against this one.
  * client::retrieve_stripe_once(): widen the per-attempt gather window
    from 5s to 10s. A node idle >20s needs a fresh QUIC/TLS handshake
    before it can answer FETCH_BLOCK; that handshake plus the round trip
    must both fit inside this window under real cross-host latency, and
    5s was tight enough that ordinary reconnect overhead looked like a
    connectivity failure.
  * client::retrieve_stripe_once(): add per-node failure-reason diagnostics
    (gated behind PARITYPP_DIAG, same as the existing store-side
    diagnostics) so a stripe-unreachable failure reports *why* each node
    didn't contribute (timeout, send failure, exception text, or
    definitive not-found) instead of just the aggregate "unreachable"
    summary.
  * client::connect_to_node(): fix quic::connect(..., false) blocking
    internally in completeHandshake() (hardcoded 5s, unrelated to any of
    this library's own deadline/retry tuning) and throwing directly on
    timeout — bypassing this function's own non-blocking poll loop right
    below it, which was written to drive the handshake via pumpNetwork()
    and integrate with mark_connection_failed's adaptive cooldown, but was
    dead code on every failure path as a result. Pass nonblock=true so
    that loop actually runs.

 -- Jan Koester <jan.koester@tuxist.de>  Tue, 28 Jul 2026 17:10:00 +0200

libparitypp (20260727+16) unstable; urgency=high

  * server: FETCH_BLOCK handler no longer forward-fetches from peers when a
    block isn't in the local store. That path constructed a brand-new
    `client` (fresh connections, no reuse) synchronously inside the
    request's own dispatch and blocked on it — including that new client's
    own per-node 5s timeouts — before replying, so any slowness in the
    side-fetch silently became latency on the original, unrelated request.
    It was added for the full-cluster-restart/rebuild case, but is
    redundant with what the *caller* already does: retrieve_stripe_once()
    (client.cpp) queries every node in parallel and only needs k of n, so
    it finds a block on whichever other node actually holds it without
    this node needing to hop on its behalf. Now replies BLOCK_NOT_FOUND
    immediately and lets the requester's own parallel fetch handle it.
    Root-caused as the last piece of mediadb's persistent "index"/
    "tombstones" fetch failures: `client::retrieve()` correctly reported
    these as a connectivity issue (20260727+15's fix), but the underlying
    connectivity hit was this node-to-node forwarding call stacking
    latency onto FETCH_BLOCK requests for the small, frequently-synced
    keys most likely to hit a momentarily-missing local block — not an
    actual network problem (0% ICMP loss, sub-ms RTT between all three
    production nodes, confirmed before concluding this).
    Verified: paritypp_test and paritypp_consistency_test both pass
    unchanged, including the loss-pattern/reconstruction-from-missing-
    blocks cases — confirms the client-side k-of-n path this now relies on
    exclusively already covers what the removed forwarding did.

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 27 Jul 2026 08:19:39 +0200

libparitypp (20260727+15) unstable; urgency=high

  * client::retrieve_stripe()/retrieve(): stop conflating "every node gave
    an explicit not-found answer" with "retries exhausted while some node
    stayed ambiguous (never responded at all)" — both used to collapse to
    the same plain `false`/empty-vector result, which retrieve() then
    reported as "key does not exist". New retrieve_stripe() out-param
    definitively_not_found carries the real distinction from
    retrieve_stripe_once()'s existing should_retry logic (which already
    computed it internally and then discarded it). retrieve(uint64_t,
    deadline) now throws instead of silently returning {} when stripe 0
    couldn't be confirmed absent — this is what a caller actually needs to
    tell "genuinely missing" apart from "the cluster's degraded right now".
    Root-caused via mediadb's recurring, contentless "[CLUSTER-SYNC] index
    fetch failed (exception in cluster layer)" — the "index" key
    unquestionably exists (core metadata, written since cluster setup) and
    Cluster::fetch()'s own read_pool/pclient exception logging
    (cluster.cpp) was never firing for it, meaning retrieve() was
    returning empty *without* raising anything: every attempt was hitting
    the ambiguous-retries-exhausted case, mislabeled as non-existence, with
    zero diagnostic trail. Cluster::fetch() already has a
    catch (const std::exception&) that logs e.what() — this alone makes
    the underlying connectivity failure visible in the logs from now on,
    which is a diagnostic fix first and a behavior fix second: the
    `retrieve(group_id, size_t original_size, deadline)` overload (used by
    the raw-media-read path, not this one) has the identical latent gap
    and was deliberately left alone here — not exercised by the symptom
    this was root-caused from, kept in scope.
    Verified: paritypp_test and paritypp_consistency_test both pass
    unchanged, including the loss-pattern/reconstruction-from-missing-
    blocks cases in the consistency suite — confirms the genuine
    not-found path (data actually lost/never written) is untouched.

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 27 Jul 2026 08:00:01 +0200

libparitypp (20260727+14) unstable; urgency=high

  * client::ensure_connected(): reconnect proactively once a per-node
    connection has sat idle 20s+, instead of only discovering it's dead by
    eating a full 5s send_recv timeout on next use. Nothing pumps an idle
    connection's network processing in the background — every
    pumpNetwork() call in this file runs synchronously inside an active
    RPC — so the QUIC layer's own PING keepalive (fires after 15s
    inactivity, socket.h) never gets a chance to run between infrequent
    calls. Past _max_idle_timeout (30s, socket.h) with no traffic either
    way, one or both sides may silently discard connection state per RFC
    9000 SS10.1 with no notification — the peer just doesn't answer the
    next stream opened on it. This is the root cause behind the repeated
    "[PARITY-DIAG] send_recv empty response: node=N type=6" (LIST_GROUPS)
    reports in mediadb: that RPC only runs once per CLUSTER-SYNC cycle
    (MEDIADB_SYNC_INTERVAL_SEC=30, matching the 30s idle timeout almost
    exactly), so its connection is idle just long enough, just often
    enough, to keep silently expiring between cycles — while the
    FETCH_BLOCK data path (retrieve_stripe_once) is kept naturally warm by
    constant live traffic and doesn't show the symptom. New
    quic_connection::last_activity (client.h) is set on successful
    connect and refreshed on every reuse within the threshold, so a
    connection kept genuinely busy (repeated calls under 20s apart) is
    never needlessly reconnected — only one that's actually gone idle
    long enough to be at real risk of having been silently reaped.
    Verified: paritypp_test and paritypp_consistency_test both pass
    unchanged (this only changes when a reconnect is triggered, not the
    protocol or erasure-coding logic).

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 27 Jul 2026 06:57:43 +0200

libparitypp (20260727+13) unstable; urgency=medium

  * client::retrieve_stripe_once(): break out of the response-collection
    loop as soon as reaching k_ successes becomes mathematically
    impossible (total_ok + remaining < k_), instead of always waiting out
    the full 5s retr_deadline once at least one node stops responding.
    Every node counted in `remaining` hasn't answered yet, so it's still a
    possible future success; once enough nodes have already given a
    definitive BLOCK_NOT_FOUND or connection-error answer to drop that
    ceiling below k_, no amount of further waiting changes the outcome —
    only the mandatory retry-with-backoff in retrieve_stripe() (unchanged)
    can still help. Found via mediadb: a genuine L2 preview-cache miss
    (PreviewCache::get(), store_id=1) with one slow/unresponsive node was
    costing a full 5s — or multiples of it across retries — per
    never-before-rendered preview, even though 2 of 3 nodes had already
    answered "not found" well before the deadline. Verified: paritypp_test
    and paritypp_consistency_test both pass unchanged (this only affects
    how early a doomed attempt gives up, not the success/failure outcome
    or the erasure-coding math).

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 27 Jul 2026 05:59:14 +0200

libparitypp (20260711+12) unstable; urgency=high

  * client::store(): write stripes in reverse order (last stripe first,
    stripe 0 — the one carrying the 8-byte length header — last) instead
    of forward. store() never truncates stale trailing stripes left over
    from a previous, longer store() at the same group_id, and retrieve()
    only checks that the reconstructed length fits the header it read —
    it has no way to tell a stripe apart from an older store generation.
    Writing forward, a partial failure (a later stripe throws — including
    the intentional no-retry "response timeout" fast-fail path) left an
    already-written new header pointing past the failure into those
    leftover old stripes: retrieve() silently spliced new+stale bytes
    into one length-valid but corrupted result, with no error raised
    anywhere. Root-caused as the source of repeated preview-cache
    corruption in mediadb (PreviewCache re-stores rendered previews at a
    stable group_id on every re-render). Writing backwards means a
    partial failure always leaves the last-committed header pointing at
    a fully intact version — either the untouched old one, if stripe 0
    never got rewritten, or the complete new one, if every other stripe
    already succeeded — never a torn mix. store_stripe() addresses each
    stripe independently by (group_id, stripe_index), so the write order
    is a pure reordering with no protocol change. Existing test/
    consistency suites pass unchanged.

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 18 Jul 2026 07:11:57 +0200

libparitypp (20260711+11) unstable; urgency=medium

  * client::retrieve_stripe_once(): track BLOCK_NOT_FOUND as a distinct
    per-node outcome (and treat a local store miss the same way, since
    that has no network ambiguity) instead of collapsing it into the
    same "not ok" bucket as a node that never responded at all.
    20260711+6 made any zero-block stripe retry, to give a transient
    multi-node connectivity blip a chance to clear — correct for a
    group that genuinely exists but is momentarily unreachable, but it
    also meant a key that has *never* been written anywhere (every
    node explicitly confirms this) burned the full ~30s retry/backoff
    budget on every single lookup. Now only retries when the shortfall
    is ambiguous (some node gave no answer at all); when every node's
    answer is a definitive BLOCK_NOT_FOUND, returns immediately.
    Confirmed in production (authdb): creating a new domain — a
    read-before-write existence check plus a fresh backend's own
    cluster prefetch, both against a group_id that has never been
    stored — took ~30s each time purely from this retry loop.

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 13 Jul 2026 14:30:00 +0200

libparitypp (20260711+10) unstable; urgency=high

  * client::check_groups(): fix completeness check that compared each
    node's local block count to the union of blocks across all nodes
    (blocks.size() != union_indices.size()). Storage is erasure-coded —
    node i only ever holds its own shard per stripe, never a full copy
    — so for T stripes each node legitimately holds T blocks while the
    union across n nodes is n*T; that comparison can never succeed for
    n>1, so every multi-stripe group was flagged incomplete on every
    single check_groups() call, forever. Restored the pre-regression
    check (block_index % n == node_index, total block count a multiple
    of n) that this had replaced in 22469cb. Confirmed in production
    (authdb 3-node cluster): rebalance() re-stored the same 2 groups on
    every health cycle indefinitely, burning the cluster client's
    mutex nonstop and starving real request-driven fetches behind it.

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 13 Jul 2026 14:00:00 +0200

libparitypp (20260711+9) unstable; urgency=high

  * client::list(): retry once (300ms gap) when a node gives no usable
    LIST_BLOCKS response, instead of silently omitting it from the
    result on the first miss. list() feeds check_groups()'s
    completeness verification — with no retry, a single momentary
    non-response from one node made an otherwise fully-replicated group
    look incomplete, and since the same transient blip recurs on the
    next sweep, rebalance() could repair the same already-correct
    groups forever without ever converging. Confirmed in production
    (authdb cluster): rebalance re-stored the same 2 groups on every
    single cycle with no store failures reported, i.e. the data was
    fine and only the verification was flaky.

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 12 Jul 2026 13:10:00 +0200

libparitypp (20260711+8) unstable; urgency=medium

  * client::retrieve()/retrieve_range()/retrieve_stripe()/
    retrieve_stripe_once(): accept an optional overall deadline
    (defaults to none — existing callers are unaffected). Without this,
    a caller like Cluster::fetch() that tries a read-pool client and
    then falls back to pclient_ could not actually bound its own total
    time: each retrieve() call has its own independent ~30s retry
    budget that nothing above it could interrupt, so two stacked calls
    cost ~60-70s instead of the ~30s the caller intended. Confirmed in
    production: index-fetch retries during cluster sync were taking
    ~130s per cycle (two ~65s Cluster::fetch() calls), matching this
    exactly.

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 12 Jul 2026 12:15:00 +0200

libparitypp (20260711+7) unstable; urgency=high

  * client::rebalance(): drop the remove()+store() "cleanup" pass, same
    fix as mediadb 20260711+26 applied to its own (separate, hand-rolled)
    copy of this logic in Cluster::scrub(). client::remove() is
    best-effort across all nodes and never throws on partial failure, so
    it reliably deleted a group's just-repaired blocks before the second
    store() re-wrote them; if that second store() then hit a transient
    node failure, the group was left with zero valid blocks anywhere.
    The first store() alone already places every block on its canonical
    node, which is sufficient to repair the group. Caught live: a
    production rebalance re-storing gid 522665934817728990 (the exact
    group flagged missing earlier) survived only because the transient
    node[1] QUIC handshake timeout it hit landed on both the first *and*
    second store() (each individually within the m=1 failure tolerance)
    — a single node failing only on the second store() would have wiped
    the group's only good copy.

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 12 Jul 2026 06:00:00 +0200

libparitypp (20260711+6) unstable; urgency=high

  * client::retrieve_stripe_once(): a stripe with zero blocks retrieved
    from any node now sets should_retry (previously only "some blocks
    but fewer than k_" did). A live node's explicit BLOCK_NOT_FOUND and
    a node that simply never responds both collapse into the same "not
    ok" state here, with nothing recorded to tell them apart — so giving
    up immediately on zero responses meant a transient multi-node
    connectivity blip (this cluster has shown these clear within
    10-120s) was indistinguishable from genuine data loss, and got
    reported as the latter without ever getting the ~30s retry budget
    every other partial-failure case already receives.

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 11 Jul 2026 22:30:00 +0200

libparitypp (20260711+5) unstable; urgency=medium

  * client::list_groups_on_node() split into a locking wrapper and an
    unlocked list_groups_on_node_impl(), mirroring the existing
    get_node_status/get_node_status_impl split.
  * Added client::list_groups_on_nodes(): probes several nodes' LIST_GROUPS
    reachability concurrently via std::async, same pattern as
    get_cluster_status(). Lets callers checking multiple peers bound total
    latency by the slowest single node instead of the sum of all of them.

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 11 Jul 2026 18:30:00 +0200

libparitypp (20260711+4) unstable; urgency=high

  * client::check_groups(): replace the group-ID-presence check introduced
    in 20260412+40 with a real per-node block-index comparison via
    list(). A node only needs to hold ONE block of a group to count as
    "present" for it, so the presence-only check could not detect a node
    silently missing a subset of a group's stripe blocks — under-replicated
    data went unrepaired indefinitely since rebalance()/scrub() both
    treated such groups as already_ok.
  * client::rebalance() now uses check_groups() instead of duplicating the
    presence-only discovery logic.
  * client::get_cluster_status(): flag a reachable node as DEGRADED when
    its self-reported block_count trails its healthiest peer's, so the
    cluster health view no longer reports OK for a node that is quietly
    behind on replication.

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 11 Jul 2026 12:00:00 +0200

libparitypp (20260515+3) unstable; urgency=medium

  * Rebuild

 -- Jan Koester <jan.koester@tuxist.de>  Thu, 15 May 2026 12:00:00 +0200

libparitypp (20260505+2) unstable; urgency=high

  * Rebuild against libnetplus 20260505+12 (ABI change: _closed_streams
    added to quic class; fixes crash in quic::~quic(), flow control
    double-counting, stream resurrection, silent data loss, uni-stream
    dispatch, FIN-only frame)

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 05 May 2026 17:00:00 +0200

libparitypp (20260504+1) unstable; urgency=high

  * client/store_stripe Phase 2: pump ALL pending QUIC connections every
    loop iteration, not just sockets with incoming data — this ensures
    PTO (Probe Timeout) retransmissions fire even when the peer hasn't
    responded due to tail packet loss

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 04 May 2026 19:45:00 +0200

libparitypp (20260503+7) unstable; urgency=high

  * client/store_stripe: reset remote QUIC connections after response timeout
    so retries do not reuse poisoned half-open sessions
  * client: improves recovery from repeated "remote_sent=2" + no-response
    patterns seen during cluster import writes

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 03 May 2026 20:55:00 +0200

libparitypp (20260503+6) unstable; urgency=high

  * client: add opt-in deep diagnostics for store_stripe/send_recv timeout
    analysis via PARITYPP_DIAG=1 (header/body progress, timing, per-node
    response state) to isolate cross-node response timeout root causes

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 03 May 2026 19:45:00 +0200

libparitypp (20260503+5) unstable; urgency=high

  * client: reduce import stall time by failing fast on response-timeout
    dominated store() errors instead of running a full second global store
    attempt after store_stripe timeout failures
  * client: keep retry-once behavior for non-timeout transient errors

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 03 May 2026 19:35:00 +0200

libparitypp (20260503+4) unstable; urgency=high

  * client: fix logic bug in response parsing for send_recv() and
    store_stripe() when QUIC stream data arrives fragmented
  * client: handle partial response header/body reads statefully instead of
    discarding short reads; prevents false "response timeout" and
    "too many node failures" under import load

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 03 May 2026 19:05:00 +0200

libparitypp (20260503+3) unstable; urgency=medium

  * client: split store_stripe timeout into separate send and response
    deadlines; keep a much larger response window for remote block-store
    ACKs under import load to avoid false peer timeouts
  * client: apply the same longer type-specific timeout model to
    send_recv(STORE_BLOCK) so direct block writes do not still fail at 5s

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 03 May 2026 18:10:00 +0200

libparitypp (20260502+1) unstable; urgency=medium

  * Rebuild against libnetplus 20260502+1 (QUIC performance improvements)

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 02 May 2026 12:00:00 +0200

libparitypp (20260426+2) unstable; urgency=high

  * file_block_store: fix CRC32 PCLMULQDQ fold constants (k1k2, k3k4),
    poly constant byte-order, and add missing 96→64 fold step in
    Barrett reduction — the broken PCLMUL path caused all records to
    fail CRC verification on restart, leading to complete data loss
  * vacuum_impl: re-scan index from data file when index is empty but
    data file is non-empty; refuse to vacuum if re-scan finds nothing
  * crc32: promote to public API; add comprehensive unit test comparing
    PCLMUL vs table-driven across 24 sizes, 16 alignments, and 64 KB

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 26 Apr 2026 17:00:00 +0200

libparitypp (20260426+1) unstable; urgency=high

  * file_block_store: fix CRC32 PCLMULQDQ fold constants (k1k2, k3k4),
    poly constant byte-order, and add missing 96→64 fold step in
    Barrett reduction — the broken PCLMUL path caused all records to
    fail CRC verification on restart, leading to complete data loss
  * vacuum_impl: refuse to run when index is empty but data file is
    non-empty, preventing data wipe from future CRC/scan bugs
  * crc32: promote to public API; add comprehensive unit test comparing
    PCLMUL vs table-driven across 24 sizes, 16 alignments, and 64 KB

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 26 Apr 2026 16:00:00 +0200

libparitypp (20260425+4) unstable; urgency=medium

  * client::store_stripe: interleave remote QUIC sends via round-robin
    waitWrite/pump instead of sequential per-node blocking. All remote
    connections now grow their congestion windows concurrently, reducing
    per-stripe latency from sum(RTTs) to max(RTTs).

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 25 Apr 2026 22:00:00 +0200

libparitypp (20260425+3) unstable; urgency=medium

  * Rebuild against libnetplus 20260425+5 (RSA Montgomery CIOS
    heap over-read fix)

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 25 Apr 2026 21:00:00 +0200

libparitypp (20260425+2) unstable; urgency=medium

  * file_block_store: add PCLMULQDQ-accelerated CRC32 path with
    runtime CPU dispatch via __builtin_cpu_supports("pclmul"),
    processing 64 bytes per fold iteration

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 25 Apr 2026 16:00:00 +0200

libparitypp (20260425+1) unstable; urgency=medium

  * file_block_store: replace bit-by-bit CRC32 with table-driven
    lookup, reducing per-byte cost from 8 branches to a single
    table fetch

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 25 Apr 2026 12:00:00 +0200

libparitypp (20260424+6) unstable; urgency=medium

  * memory_block_store: add optional byte-budget constructor with LRU
    eviction. Groups are evicted least-recently-used when used bytes
    exceed the configured maximum. Default constructor remains unbounded.

 -- Jan Koester <jan.koester@tuxist.de>  Thu, 24 Apr 2026 00:00:00 +0200

libparitypp (20260423+5) unstable; urgency=high

  * file_block_store: fix WAL commit ordering — wal_commit() is now called
    AFTER data is durably flushed, preventing data loss on crash between
    WAL commit and flush.
  * file_block_store: defer old-record tombstoning until replacement record
    is durably flushed. Crash between tombstone and flush can no longer
    destroy the only copy of a block.
  * file_block_store: handle duplicate LIVE records in scan_and_build_index
    (recovery after crash with deferred tombstoning).
  * file_block_store: clear pending_tombstones_ in vacuum_impl().
  * client::rebalance(): store data BEFORE removing old copies to prevent
    data loss when re-store fails after remove.

 -- Jan Koester <jan.koester@tuxist.de>  Wed, 23 Apr 2026 00:00:00 +0200

libparitypp (20260420+4) unstable; urgency=medium

  * store_stripe: replace std::async parallel sends with sequential sends.
    Eliminates thousands of short-lived OS threads during bulk imports that
    caused OOM kills and SEGV crashes on memory-constrained nodes.

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 20 Apr 2026 00:00:00 +0200

libparitypp (20260420+3) unstable; urgency=medium

  * store_stripe: log warning (stderr) when nodes fail but within parity
    tolerance — makes partial replication visible in Release builds.
  * connect_to_node: increase QUIC handshake timeout from 2s to 5s to
    improve reliability under load (e.g. during bulk imports).

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 20 Apr 2026 00:00:00 +0200

libparitypp (20260418+2) unstable; urgency=medium

  * Reduce mutex hold time in client::store(), retrieve(), retrieve_range()
    and remove() — release/reacquire between stripes/nodes for concurrency

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 18 Apr 2026 22:00:00 +0200

libparitypp (20260418+1) unstable; urgency=medium

  * Add scrub_running flag to status_info protocol (backward-compatible)
  * Add set_rebuilding() setter to server for external scrub signaling

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 18 Apr 2026 21:00:00 +0200

libparitypp (20260417+1) unstable; urgency=medium

  * Fix lock group inclusion in client::rebalance()

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 17 Apr 2026 12:00:00 +0200

libparitypp (20260412+43) unstable; urgency=medium

  * Add client::warmup() to pre-establish QUIC connections + auth
    on all nodes, eliminating first-request latency (2-3s per node)
  * Revert store_stripe deadline back to 5s (was 15s)

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 12 Apr 2026 23:00:00 +0200

libparitypp (20260412+42) unstable; urgency=medium

  * Add distributed scrub lock: try_scrub_lock() / release_scrub_lock()
    using a well-known parity group with timestamp-based TTL (5 min)
  * Prevents concurrent scrub/rebalance across cluster nodes

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 12 Apr 2026 18:00:00 +0200

libparitypp (20260412+41) unstable; urgency=medium

  * Increase store_stripe deadline from 5s to 15s
  * Optimize rebalance(): skip per-group list() calls, use
    list_groups_on_node() presence check only

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 12 Apr 2026 16:00:00 +0200

libparitypp (20260412+40) unstable; urgency=medium

  * Optimize rebalance(): skip expensive per-group list() calls by using
    list_groups_on_node() presence check — groups on all n nodes are
    assumed OK, only under-replicated groups get retrieve/remove/re-store

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 12 Apr 2026 14:00:00 +0200

libparitypp (20260412+39) unstable; urgency=medium

  * Increase store_stripe deadline from 5s to 15s — the shared deadline
    covers Phase 1 (send), Phase 1b (retry), and Phase 2 (collect),
    leaving too little time for response collection after QUIC handshakes

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 12 Apr 2026 12:00:00 +0200

libparitypp (20260411+38) unstable; urgency=medium

  * Add client::rebalance() method: detects misplaced blocks
    (block_index % n != node_index), re-stores groups with correct
    placement, then vacuums stale blocks

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 11 Apr 2026 20:00:00 +0200

libparitypp (20260411+37) unstable; urgency=medium

  * Fix client global mutex blocking: release mutex during retrieve_stripe
    retry backoff sleep so other operations are not serialized behind retries
  * Change retrieve/retrieve_range from lock_guard to unique_lock to allow
    unlock/relock around retry sleeps

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 11 Apr 2026 18:00:00 +0200

libparitypp (20260411+36) unstable; urgency=medium

  * Add per-record CRC32 checksums to file_block_store (record header 17→21 bytes)
  * Add WAL journal (blocks.wal) with STORE/TOMBSTONE/COMMIT for crash safety
  * Add fdatasync after flush and fsync directory after vacuum rename
  * Add WAL recovery on startup in scan_and_build_index
  * Add CRC32 validation on load, truncate trailing corrupt records
  * Add client retrieve_stripe retry with exponential backoff (up to 6 retries)
  * Add server peer-forwarding on FETCH_BLOCK when block not local
  * Add configurable startup_peer_wait_secs for cluster full-restart resilience
  * Add degraded mode flag with GET_STATUS reporting and auto-clear on rebuild
  * Add file_block_store test suite

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 11 Apr 2026 00:00:00 +0200

libparitypp (20260410+35) unstable; urgency=medium

  * No library changes — version bump for dependent package rebuilds
    (authdb, mediadb cluster scrub and health monitor improvements)

 -- Jan Koester <jan.koester@tuxist.de>  Thu, 10 Apr 2026 00:00:00 +0200

libparitypp (20260409+34) unstable; urgency=medium

  * Make file_block_store write buffer threshold configurable via
    constructor parameter (default 256KB, 0 = immediate flush)

 -- Jan Koester <jan.koester@tuxist.de>  Wed, 09 Apr 2026 20:00:00 +0200

libparitypp (20260409+33) unstable; urgency=medium

  * Add batch API to client: store_batch(), retrieve_batch(),
    retrieve_batch_item(), encode_batch(), decode_batch() for packing
    multiple keyed items into a single parity group
  * Add write coalescing to file_block_store: buffer small writes in
    memory (up to 256KB) and flush as a single pwrite(), reducing
    syscall overhead for many small block stores
  * file_block_store::fetch() reads from write buffer for unflushed data
  * file_block_store::remove_group() tombstones in write buffer when
    applicable, avoiding premature disk I/O
  * Flush write buffer before vacuum to ensure data consistency
  * Add public flush() method to file_block_store

 -- Jan Koester <jan.koester@tuxist.de>  Wed, 09 Apr 2026 18:00:00 +0200

libparitypp (20260409+32) unstable; urgency=medium

  * Rebuild against libnetplus 20260409+14 (BLOCKSIZE 65536)

 -- Jan Koester <jan.koester@tuxist.de>  Wed, 09 Apr 2026 00:00:00 +0200

libparitypp (20260407+31) unstable; urgency=medium

  * New release

 -- Jan Koester <jan.koester@tuxist.de>  Tue, 07 Apr 2026 00:00:00 +0200

libparitypp (20260406+30) unstable; urgency=medium

  * Add mark_node_dead() public method: allows external callers to propagate
    dead-node info between client instances (e.g. session_client_ syncs from
    pclient_ health monitor), avoiding redundant connect timeouts on dead nodes

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 06 Apr 2026 00:00:00 +0000

libparitypp (20260406+29) unstable; urgency=medium

  * Make client thread-safe: add internal mutex_ to all public methods
    (store, retrieve, remove, list, replicate_to_peers, fetch_from_peers,
    get_node_status, get_cluster_status, list_groups_on_node,
    vacuum_all_nodes, set_local_node, begin_store/flush_stripe)
  * External callers no longer need their own mutex around client calls

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 06 Apr 2026 00:00:00 +0000

libparitypp (20260406+28) unstable; urgency=medium

  * rebuild

 -- Jan Koester <jan.koester@tuxist.de>  Mon, 06 Apr 2026 00:00:00 +0000

libparitypp (20260405+27) unstable; urgency=medium

  * Parallelize store_stripe and retrieve_stripe Phase 1: send to all
    remote nodes concurrently via std::async instead of sequentially
  * Parallelize store_stripe Phase 1b retry with fresh connections

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 05 Apr 2026 00:00:00 +0000

libparitypp (20260405+26) unstable; urgency=medium

  * retrieve(): return empty vector instead of throwing when no stripes
    exist for a group_id (fixes spam on fresh/empty clusters)

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 05 Apr 2026 00:00:00 +0000

libparitypp (20260405+25) unstable; urgency=medium

  * Add retrieve_range(group_id, offset, length): fetch only the stripes
    needed for a byte range, accounting for 8-byte framing header

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 05 Apr 2026 00:00:00 +0000

libparitypp (20260405+24) unstable; urgency=medium

  * Pipeline retrieve_stripe: send FETCH_BLOCK to all nodes simultaneously,
    collect responses with waitReadMulti (parallelize block fetches per stripe)
  * Auto-retry in client::store on stale QUIC connections: reset all
    connections and retry once on first stripe failure
  * Gate all debug logging behind NDEBUG macro (no output in Release builds)

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 05 Apr 2026 00:00:00 +0000

libparitypp (20260405+23) unstable; urgency=medium

  * Increase STRIPE_SIZE from 16KB to 4MB to reduce roundtrips per store
    (e.g. 188MB: 11490 stripes -> 46 stripes)

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 05 Apr 2026 00:00:00 +0000

libparitypp (20260405+22) unstable; urgency=medium

  * Add diagnostic stderr logging to client::store and store_stripe for
    debugging import hangs (stripe progress, failure diagnostics)

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 05 Apr 2026 00:00:00 +0000

libparitypp (20260405+21) unstable; urgency=medium

  * Replace per-socket waitRead polling loop in store_stripe Phase 2 with
    waitReadMulti: single epoll/kqueue/poll call waits on all pending nodes
    at once, eliminating CPU-burning spin when nodes are slow to respond

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 05 Apr 2026 00:00:00 +0000

libparitypp (20260405+20) unstable; urgency=medium

  * Add VACUUM protocol command (0x07) for remote block store compaction
  * Move vacuum() to block_store base class (default no-op, file_block_store
    overrides)
  * Add client::vacuum_all_nodes() to broadcast VACUUM to all cluster peers

 -- Jan Koester <jan.koester@tuxist.de>  Sun, 05 Apr 2026 00:00:00 +0000

libparitypp (20260404+19) unstable; urgency=medium

  * Replace busy-spin accept loop with poll-based waitRead (fixes 100% CPU
    usage on one core when idle)

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 04 Apr 2026 00:00:00 +0000

libparitypp (20260404+18) unstable; urgency=medium

  * Replace per-file block storage with append-only binary file
    (single blocks.bin per node instead of thousands of .blk files)
  * Add vacuum() method to compact file by dropping deleted records
  * In-memory index rebuilt on startup by scanning the data file

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 04 Apr 2026 00:00:00 +0000

libparitypp (20260404+17) unstable; urgency=medium

  * Catch netplus::NetException explicitly in store_stripe Phase 1 and
    Phase 2 (does not inherit std::exception, was falling through to
    catch(...) losing the error message)

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 04 Apr 2026 00:00:00 +0000

libparitypp (20260404+16) unstable; urgency=medium

  * Flush UDP datagrams immediately after sendStreamData (pumpNetwork)
  * Verify connection state after ensure_connected before using socket
  * Per-node diagnostic in store_stripe error: address, port, failure reason
    (connect failed, send timeout, response timeout, server non-OK, exception)

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 04 Apr 2026 00:00:00 +0000

libparitypp (20260404+15) unstable; urgency=medium

  * Add error_message() virtual method to store_session interface
  * Capture stripe failure details in client_store_session instead of
    logging to cerr (lost when daemonized)
  * Retry failed remote nodes once with a fresh connection in store_stripe
    (fixes stale QUIC connections between streaming sessions)

 -- Jan Koester <jan.koester@tuxist.de>  Sat, 04 Apr 2026 00:00:00 +0000

libparitypp (20260404+14) unstable; urgency=medium

  * Remove all std::cerr debug logging to eliminate data races
  * Replace ostringstream with snprintf in file_block_store to fix
    std::ctype<char>::_M_widen_init() BSS race

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 00:00:00 +0000

libparitypp (20260404+13) unstable; urgency=medium

  * Fix store_stripe Phase 2: check hasStreamData before waitRead (buffered responses)
  * Close leaked streams when Phase 1 send fails

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 23:00:00 +0200

libparitypp (20260404+12) unstable; urgency=medium

  * Fix pipelined response collection: use waitRead() instead of busy-poll
  * Increase timeout to 15s, handle null sockets gracefully

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 23:00:00 +0200

libparitypp (20260404+11) unstable; urgency=medium

  * Pipeline store_stripe: send all blocks in parallel, then collect responses
  * Reduces per-stripe latency from O(n*RTT) to O(RTT)

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 21:30:00 +0200

libparitypp (20260404+10) unstable; urgency=medium

  * Rebuild against libnetplus 20260404+6

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 21:00:00 +0200

libparitypp (20260404+9) unstable; urgency=medium

  * Stripe-based store/retrieve: fixed 16KB stripes with independent parity
  * Add store_session for incremental streaming store (feed chunks, finalize)
  * Add begin_store(group_id, total_size) to client
  * Peak RAM reduced from full payload to ~16KB per stripe

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 18:00:00 +0200

libparitypp (20260403+8) unstable; urgency=medium

  * Add retry logic (3 attempts) to store(gid) for transient connection failures
  * Add set_local_node() to bypass QUIC for self-node via direct block_store
  * Add erasure-coded store/retrieve with deterministic group_id
  * store(group_id, data) prepends 8-byte length header for padding removal
  * retrieve(group_id) reconstructs data with automatic size detection

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 09:00:00 +0200

libparitypp (20260403+5) unstable; urgency=medium

  * replicate_to_peers now returns success count instead of void

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 08:00:00 +0200

libparitypp (20260403+4) unstable; urgency=medium

  * Add periodic resync every 60s in server accept loop

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 06:00:00 +0200

libparitypp (20260403+3) unstable; urgency=medium

  * Rebuild against libnetplus 20260403+2 (client handshake debug logging)

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 02:00:00 +0200

libparitypp (20260403+2) unstable; urgency=medium

  * Rebuild against libnetplus 20260403+1 (udp::sendTo, clean sendPacket)

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 01:00:00 +0200

libparitypp (20260403+1) unstable; urgency=medium

  * Rebuild against libnetplus 20260403 (QUIC debug logging)

 -- Jan Koester <jan.koester@tuxist.de>  Fri, 04 Apr 2026 00:10:00 +0200

libparitypp (20260403) unstable; urgency=medium

  * Add debug logging to load_certs: warn on cert/key load failure,
    log loaded certificate CN and key size

 -- Jan Koester <jan.koester@tuxist.de>  Thu, 03 Apr 2026 23:55:00 +0200

libparitypp (20260401) unstable; urgency=medium

  * Initial Debian packaging with multiarch support.

 -- Jan Koester <jan.koester@tuxist.de>  Wed, 01 Apr 2026 00:00:00 +0200
