Repository navigation
feat(config): expose the frame-memory budget and per-IP cap as settings (#989) - #995
chappie-daemon wants to merge 14 commits into
Conversation
…catalogue does not
…he number is typed
🦋 Changeset detectedLatest commit: 60aa7e9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
[Medium impact] The change affects relay resource limits and configuration checks in one server workflow. Fix the overflowing ceiling before merging so the relay and command-line programs still build on 32-bit targets. Findings
|
| /// above any deployment each way, and far below `usize::MAX`, so a stray digit cannot reach | ||
| /// [`FrameBudget::admit`]'s arithmetic. A value above it is treated like a zero: the default | ||
| /// stands (ADR 0008). | ||
| pub const MAX_SETTING_BOUND: usize = 1024 * 1024 * 1024 * 1024; |
There was a problem hiding this comment.
If built for a 32-bit target such as ARMv7 or i686, MAX_SETTING_BOUND overflows usize while the compiler evaluates it. The relay and both command-line programs cannot build, even with default settings.
Store the ceiling as u64 and check that accepted values fit usize before converting them. Update the ceiling tests to avoid the same overflowing usize expression.
| | mirroring.static[].limits.event | Event limit overrides for this mirror. See configurations under limits.event. | | ||
| | mirroring.static[].secret | Secret to pass to relays. Nostream relays only. Optional. | | ||
| | mirroring.static[].skipAdmissionCheck | Disable the admission fee check for events coming from this mirror. | | ||
| | network.frameMemoryBudgetBytes | The queued outbound payload bytes every socket of this relay shares. Unit: bytes. Defaults to 268435456 (256 MiB). Zero, or a value above 1099511627776 (1 TiB), means the default. | |
There was a problem hiding this comment.
The published TypeScript CLI reads the shared defaults, so it accepts and saves both new keys. But the TypeScript relay never reads either one. Operators can receive a successful update while neither limit takes effect.
Mark both settings as Rust-only here and in the shared defaults comments.
Knowledge Base Used: Configuration, CLI, and deployment
| **Why a ceiling, and not just `> 0`.** `usize::MAX` *is* a `usize`, so a bare positivity check | ||
| passes it through, and the budget then stops bounding anything: the fit guard subtracts with | ||
| ` if bytes > self.limit_bytes.saturating_sub(accounted) {` | ||
| (`crates/nostream-relay/src/sink.rs:107`), which can never be true against a limit of | ||
| `usize::MAX`, while the accounting that follows adds unchecked — | ||
| " accounted + bytes," (`crates/nostream-relay/src/sink.rs:113`) — so it panics in a | ||
| debug build and wraps in a release one, and a wrapped counter refills the budget it was meant to | ||
| exhaust. The ceiling is what keeps an operator's number out of that arithmetic: |
There was a problem hiding this comment.
Overflow explanation is incorrect
For a limit of usize::MAX, FrameBudget::admit still rejects a frame larger than usize::MAX - accounted. An accepted frame therefore cannot make accounted + bytes overflow.
Correct this explanation and the matching from_settings comment. They currently describe a nonexistent counter wrap as the reason for the ceiling, giving future maintainers the wrong safety model.
| bad += 1 | ||
| continue | ||
| body = open(real, errors="replace").read().split("\n") | ||
| if not (1 <= first <= len(body)) or end > len(body): |
There was a problem hiding this comment.
The check accepts reversed ranges such as file.rs:10-1 and ranges ending at zero. These references print EXISTS and return success; an end of zero even reads the last line through Python's negative indexing.
Require 1 <= first <= end <= len(body) so the checker rejects invalid ranges.
| if not (1 <= first <= len(body)) or end > len(body): | |
| if not (1 <= first <= end <= len(body)): |
Coverage Report for CI Build 40176091065Warning No base build found for commit Coverage: 73.979%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats💛 - Coveralls |
frame_memory_budget_bytesandmax_connections_per_ipareRelayConfigfields with the policy's numbers compiled in. A daemon builds its config at one site, so an operator cannot retune either bound without a rebuild — a policy constant wearing a config field's name. This exposes both as settings keys, in the shape the repository's other limits already use.One check is red and cannot be fixed by this branch:
Greptile Review. It fails because this PR's base —factory/016-frame-memory-budget— is unresolvable to that app, which then reviews the whole tree and reports six findings in files this 29-path diff does not touch (zero paths underwebsite/orsrc/; they last changed in #806, #807, #799). It clears when the retarget torewrite/mainmakes the base resolvable. PR 997, stacked on this branch, isCLEANwithGreptile Reviewgreen — so this is one unresolvable base name, not a property of the stack.Closes #989. Stacked on #990 (the branch that adds the two bounds); see the base note at the end.
What it does
network.frameMemoryBudgetBytes268435456(256 MiB)limits.connection.maxConnectionsPerIp64RelayConfig::from_settings(&Settings)is the one place either leaf is interpreted: present and non-zero wins, absent or zero takes the compiled default.nostreamdholds it in alet— inline construction isuse of moved value: settings— and keeps only itsdrain_timeoutenvironment read, so noDefault::default()path survives that would ignore an operator's number.Two bounds, one ceiling, one question. An operator-supplied number feeds
FrameBudget::admit, whose guard usessaturating_subwhile its accounting uses an unchecked add: a value nearusize::MAXwould panic in debug, wrap in release, and refill the budget.MAX_SETTING_BOUND(1 TiB) keeps that state unreachable from settings, andbound_refusalasksin_rangefirst so an accepted value can never be reported as refused. A boundary test asserts ceiling, one over, and zero through both functions by value — and it fails if the derivation is bypassed.A zero is documented, not policed. ADR 0008 deferred this question here; the record now answers it after the last code commit:
0means the default, followingtransport.rs's read-time filter rather than the capture's "disabled at zero", because a bound whose zero silently disabled it would be a bound an operator can lose with a typo.Both read-only doors say the same thing
nostreamd --checkandnostream-cli config validatenow print the same verdicts from one producer (refusal_message), because a refusal that reads as a network fault is indistinguishable from one. Two sentences, not one:0 means the default, and the relay runs with itis not the same statement asabove the … a bound accepts; the key's value is ignored, and reporting the first as the second was a round of review in this job.A document the loader rejected is a third case:
loadreplaces it whole, so the walk has nothing of the operator's to describe — the schema fault is the whole report. The gate is guarded onused_defaultsto say so rather than imply it.Also here
CONFIGURATION.mdrows and the shipped document's comments name the unit, the default, the zero meaning and the ceiling — the ceiling being the one thing an operator cannot discover from--check.(sockets, 0 means the default)), which the ADR claimed for both labels and only one had.RelayConfig, not settings" is reversed in place, with its reason answered and what still stands kept; ADR 0008's deferral is closed and its observability gap re-stated.scripts/citation-meaning.pyis committed: it prints each citing sentence beside its cited line. The oracle proves a citation resolves; this proves it says what the sentence claims, and the difference cost this job three rounds.Verification
All five repository commands green at head (57 targets, 935 passed); both audits reproduce (
citation-oracleexit 0 on the two ADRs and the changeset;citation-meaning82 citations, 0 bad). Protected tests: the shipped document's numbers equalRelayConfig::default()(fails when either number is changed), and the constructor's three pass-through/zero/absent cases (fail whenfrom_settingsreturnsSelf::default()).Cost
Five rounds against a $1.50 estimate. Rounds 1, 3 and 4 bought correctness — an escape above
usize::MAX, a documented zero reported as a violation, a re-typed comparison no test could see. Rounds 3 and 5 also bought prose that should not have needed a round: the reason is in this PR's shape, not in its scope.Base
This branch is cut from
04186d4, the head of #990, because #989's subject exists only there. It must merge after #990, and GitHub retargets a pull request only when its base branch is deleted — so whoever merges #990 must also move this one:then rebase if #990's merge added anything.