Skip to content

feat(config): expose the frame-memory budget and per-IP cap as settings (#989) - #995

Open
chappie-daemon wants to merge 14 commits into
factory/016-frame-memory-budgetfrom
factory/017-frame-budget-settings
Open

chappie-daemon wants to merge 14 commits into
factory/016-frame-memory-budgetfrom
factory/017-frame-budget-settings

Conversation

@chappie-daemon

@chappie-daemon chappie-daemon commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

frame_memory_budget_bytes and max_connections_per_ip are RelayConfig fields 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 under website/ or src/; they last changed in #806, #807, #799). It clears when the retarget to rewrite/main makes the base resolvable. PR 997, stacked on this branch, is CLEAN with Greptile Review green — 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

key default means
network.frameMemoryBudgetBytes 268435456 (256 MiB) the process-wide budget for queued outbound frame text
limits.connection.maxConnectionsPerIp 64 live sockets one address may hold

RelayConfig::from_settings(&Settings) is the one place either leaf is interpreted: present and non-zero wins, absent or zero takes the compiled default. nostreamd holds it in a let — inline construction is use of moved value: settings — and keeps only its drain_timeout environment read, so no Default::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 uses saturating_sub while its accounting uses an unchecked add: a value near usize::MAX would panic in debug, wrap in release, and refill the budget. MAX_SETTING_BOUND (1 TiB) keeps that state unreachable from settings, and bound_refusal asks in_range first 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: 0 means the default, following transport.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 --check and nostream-cli config validate now 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 it is not the same statement as above 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: load replaces it whole, so the walk has nothing of the operator's to describe — the schema fault is the whole report. The gate is guarded on used_defaults to say so rather than imply it.

Also here

  • CONFIGURATION.md rows 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.
  • The cap's guided label gained its unit ((sockets, 0 means the default)), which the ADR claimed for both labels and only one had.
  • ADR 0005's "such knobs are 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.py is 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-oracle exit 0 on the two ADRs and the changeset; citation-meaning 82 citations, 0 bad). Protected tests: the shipped document's numbers equal RelayConfig::default() (fails when either number is changed), and the constructor's three pass-through/zero/absent cases (fail when from_settings returns Self::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:

gh api -X PATCH repos/cameri/nostream/pulls/<n> -f base=rewrite/main

then rebase if #990's merge added anything.

@changeset-bot

changeset-bot Bot commented Oct 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 60aa7e9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When 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

@greptile-apps

greptile-apps Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[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

  1. P1 32-bit builds fail ▶
  2. P2 Settings appear to work ▶
  3. P2 Overflow explanation is incorrect ▶
  4. P2 Invalid citation ranges pass ▶

Summary

Exposes the frame-memory budget and per-IP connection cap as Rust settings.

  • Relay limits can now be set in the settings file.
  • The daemon and CLI explain when a configured limit uses its default.
  • Decision notes explain the limit settings and gain a citation-audit script.

Acknowledgments: chappie-daemon explicitly documents zero and above-ceiling values falling back to defaults, and rejected documents reporting only their schema fault. These intentional behaviors are not findings. The changed decision record also defers guided range checks and refusal metrics, and accepts the added CLI build cost.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  YAML[Shared defaults and operator settings] --> Typed[Rust typed settings]
  Typed --> Check[Shared refusal report]
  Check --> DaemonCheck[nostreamd --check]
  Check --> CLIValidate[nostream-cli config validate]
  Typed --> Config[RelayConfig::from_settings]
  Config --> Budget[FrameBudget]
  Config --> Cap[ConnectionCap]
  YAML --> TSCLI[TypeScript CLI accepts both keys]
  TSCLI --> TSRelay[TypeScript relay does not read them]
Loading

Reviews (1) · Last reviewed commit: "docs(rewrite): state the two mechanisms ..." · Reviewed by Greptile

/// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 32-bit builds fail

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.

Comment thread CONFIGURATION.md
| 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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Settings appear to work

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

Comment on lines +409 to +416
**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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Invalid citation ranges pass

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.

Suggested change
if not (1 <= first <= len(body)) or end > len(body):
if not (1 <= first <= end <= len(body)):

@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 40176091065

Warning

No base build found for commit 04186d4 on factory/016-frame-memory-budget.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 73.979%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 10291
Covered Lines: 7992
Line Coverage: 77.66%
Relevant Branches: 5362
Covered Branches: 3588
Branch Coverage: 66.92%
Branches in Coverage %: Yes
Coverage Strength: 867.27 hits per line

💛 - Coveralls

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants