5.7 KiB
RustProxy 0.24.6 memory and cache-preset audit
Scope
0.24.6 persists cache-preset intent and hardens temporary/background memory use. The cache numeric limits remain the runtime source of truth; memory_preset and cold_preset are metadata stored in the same revisioned JSON configuration row in SQLite so the administration UI can restore the selected state after reload.
This audit is source/static in the packaging workspace because Rust/Cargo and heap profilers are unavailable here. It adds compiler/runtime tests to the source tree and an offline memory-contract gate, but sustained RSS/heap acceptance must still run on a compiler-enabled release host.
Cache preset persistence
| Area | Contract |
|---|---|
| Cache model | Settings.memory_preset accepts conservative/recommended/performance; Settings.cold_preset accepts recommended; old configurations deserialize both as None. |
| SQLite | Store::replace serializes the complete cache settings into configuration.document; the database reopen test now verifies both preset values survive close/reopen. |
| Cache identity | Preset metadata is excluded from cache_revision(), so changing only the selected label cannot purge HOT/COLD data. Effective numeric/policy changes still rotate cache identity normally. |
| UI | Settings restores the selected button/badge from configuration. Manual edits clear the affected preset metadata. |
| Recommended COLD | Enables COLD, idle/LRU/shutdown demotion, disables age demotion, sets 300 s idle, 3600 s max-hot, 60 s GC and 256 max-delete, and chooses a queue around half transient RAM. Disk path, disk bytes and entry/object limits are preserved. |
Transformation memory review
| Path | 0.24.6 behavior |
|---|---|
| Concurrent transforms | A process-wide 128 MiB semaphore budget accounts for collected input plus expected decoded/output buffers. Transform is fail-open with memory_pressure when the budget is busy. |
| Response aggregation | http_body_util::Limited enforces max_body_bytes even when upstream Content-Length is false; over-limit aggregation becomes a body error instead of allocating without bound. |
| Decompression | Fixed 16 KiB reads and exact bounded reserve checks reject expansion beyond max_body_bytes. |
| JSON | IgnoredAny validates syntax without materializing serde_json::Value; whitespace compaction is one pass and preserves number lexemes/key order. |
| HTML | Raw element detection compares tag slices case-insensitively instead of allocating/lowercasing a tag name for every element. |
| CSS | Tracks the last output character instead of repeatedly scanning the full output string. |
| Large-buffer release | Existing glibc malloc_trim(0) throttling remains after successful large transforms. |
Background-worker memory/task review
| Facility | Bound/ownership reviewed |
|---|---|
| Cache COLD writer | Existing object-count and byte budgets remain; delete fallback is limited to 2 detached tasks and otherwise applies bounded synchronous backpressure. |
| COLD cleanup | Manual/automatic cleanup share a single-flight semaphore. Candidate selection uses a bounded heap sized to the requested deletion limit; delete path metadata is flushed per 32-entry batch. |
| Stale-while-revalidate | Distinct background revalidations are capped at 8, limiting retained cached Bytes when objects are concurrently evicted/replaced. |
| Proactive refresh | Sleeping scheduled-key tasks are capped at 64..256 depending on configured refresh concurrency; actual refreshes keep the existing proactive_refresh_max_inflight bound. |
| Logging | History and broadcast buffers are fixed at 4096; sink delivery uses a bounded channel and try_send. |
| Diagnostics | One cancellation-aware sampler loop; each blocking sample is awaited before the next iteration. |
| DNS | Shared semaphore of 16 blocking resolver workers plus timeout; a timed-out OS resolver still owns its permit until the worker exits. |
| Backend health | One supervisor with a JoinSet, maximum 16 probes, at most one claimed probe per member, abort/drain on shutdown. |
| TCP/UDP streams | Per-stream connection/session semaphore, bounded UDP channel, JoinSet ownership and cancellation. |
| HTTP listeners | Accepted connections are gated by max_connections; connection tasks are owned by the listener JoinSet. App-owned tunnel/background tasks use TaskTracker. |
| Configuration writes | Validation/publication work is bounded by 8 update slots and serialized publication lock. |
| ACME | Issuance/renewal is single-flight through a semaphore; the scheduler is one cancellation-aware loop. |
| Docker discovery / update checker | One cancellation-aware periodic loop each; no per-tick detached task accumulation. |
| Admin live streams | Viewer permits and bounded broadcast/snapshot semantics prevent per-client unbounded queues. |
| Tunnel registry | Stores weak references and removes dead sessions on drop/list cleanup. |
Added regression coverage
Rust tests cover preset serialization/database reopen, preset-only cache identity stability, transform memory-budget math, bounded decompression, false Content-Length aggregation, proactive pending-task limits, stale revalidation limits and single-flight COLD cleanup. tools/checks/memory_contracts.py independently verifies the source-level bounds above when Cargo is unavailable and is part of ./dev.sh test.
Release-host acceptance gate
Run ./dev.sh fmt, ./dev.sh check and ./dev.sh test, then exercise representative transform/cache traffic while recording RSS/heap. Acceptance target: RSS reaches a plateau after warm-up; transform concurrency never exceeds the 128 MiB temporary budget estimate; COLD cleanup and revalidation counts remain bounded; no monotonically growing task/map/queue is observed after traffic stops.