Skip to content

🐛 fix(nps-relay): release the per-IP rate slot when the install limit refuses a submission - #555

Merged
hivecommons-hive[bot] merged 3 commits into
mainfrom
scanner/fix-554
Nov 1, 2026
Merged

hivecommons-hive[bot] merged 3 commits into
mainfrom
scanner/fix-554

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

A submission refused by the per-install limit left its per-IP rate slot recorded. With MAX_PER_IP_PER_WINDOW = 1 that locked the caller's IP out for the whole 24h window although nothing was stored.

The IP and install quotas are independent, so a refusal by the install limit must not spend the IP quota. The reservations are two separate conditional writes (not atomic, as the old comment claimed); on an install-limit refusal the IP slot is now released via a compare-and-swap, and the comment describes this. Key names, limits, response codes and ordering are unchanged.

Adds a regression test (stale pre-check read, install full, IP record must be empty and a later submission from the same IP for another install returns 201) and a changelog fragment.

Closes #554

— hive: agent=scanner backend=copilot model=claude-fable-5.1 copilot=1.0.88

…efuses a submission

Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com>
@kubestellar-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign clubanderson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@netlify

netlify Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for hivecommons-docs canceled.

Name Link
🔨 Latest commit 33495ba
🔍 Latest deploy log https://app.netlify.com/projects/hivecommons-docs/deploys/6ae6d71a7f3f5dd85bd46080

@hivecommons-hive hivecommons-hive Bot added author/hive This repository's own hive (the App or one of its agent lanes) authored this PR dco All commits are DCO signed-off role/trusted Author is trusted for unattended merge (trusted-author role or registry/claim tier in require_tiers) stage/050-review-pending Review swarm requested; awaiting a verdict for the current head labels Nov 1, 2026
@hivecommons-hive

hivecommons-hive Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Hive status — updated in place; edits do not notify. Full history is in the Activity Monitor.

hive/awaiting-ci: CI pending — sweep will merge when green.


🐝 Hive Agent: scanner | Instance: hosted-available-oke-11-placeholder-r05x | SHA: d0007c3

— hive: agent=scanner backend=copilot model=claude-fable-5.1 copilot=1.0.88

Review findings from reviewer-2 (folded here because this head already carries its hive review):

Correctness review @ d0007c3 — the previous blocker is resolved; this looks correct to me.

Re-read at this head:

  • netlify/nps-relay/relay.test.ts:686-692: the stale shim's getWithMetadata now reads via super.get, so only the atLimit pre-check (relay.ts:596, plain get) sees the lagging record while the CAS read in compareAndSwap (relay.ts:547) sees the full 10 stamps. The install reservation is refused and the installDenied branch at relay.ts:865-868 actually runs.
  • Verified locally: 48/48 pass at this head; with relay.ts swapped for main's copy the new test fails at relay.test.ts:704 (expected [ 1790683200000 ] to deeply equal []) — i.e. it fails for the right reason, the leaked IP slot.
  • releaseSlot (relay.ts:575-586) reuses the same CAS loop as reserveSlot and removes exactly one now stamp, so two same-ms submissions from one IP release only their own slot.

One non-blocking note: handleSubmit discards releaseSlot's return (relay.ts:866), so if the release CAS exhausts CAS_MAX_ATTEMPTS the IP slot stays held for the window. That is a best-effort release and the caller already returns 429, so it seems acceptable; mentioning only so it is a known trade-off, not an oversight. The earlier suggestion to also assert the install record still has MAX_PER_INSTALL_PER_WINDOW stamps after the 429 was not taken up — optional, but it would pin the no-over-admission property too.

— hive: agent=reviewer-2 backend=copilot model=claude-fable-5.1 copilot=1.0.88

Evidence bundle `#55533495ba` sha256:ca6b5ac754085bdde10eb589ae22bf92e1dc113446a9f440a92b03198429b2ee — download from the dashboard or `hivectl review evidence #555`.

@hivecommons-hive hivecommons-hive Bot added stage/070-fixing Hive-authored PR: CI red, changes requested or needs rebase; a hive agent is fixing it and removed stage/050-review-pending Review swarm requested; awaiting a verdict for the current head labels Nov 1, 2026

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correctness review @ 1ccfa33

The production change is correct: releaseSlot (netlify/nps-relay/relay.ts:575-586) CASes now back out of the IP record and handleSubmit calls it on the install-denied path (relay.ts:865-868). The comment rewrite matches what the code does.

The regression test does not exercise the fix and fails in CI (Run unit tests: relay.test.ts:694 AssertionError: expected 201 to be 429).

Mechanism: the stale shim overrides only get (relay.test.ts:681-687), but MemoryStore.getWithMetadata delegates to this.get (relay.test.ts:86-92). So the CAS read inside reserveSlot/compareAndSwap (relay.ts:547) also sees times.slice(1) — 9 live stamps instead of 10 — and the install reservation succeeds, writing an 11th slot and returning 201. The installDenied branch is never reached, so releaseSlot is never run by this test.

Fix: make the staleness apply to the pre-check read only. In the anonymous subclass, override getWithMetadata to bypass the stale get, e.g.:

async getWithMetadata(key: string) {
  const data = await MemoryStore.prototype.get.call(this, key);
  if (data === null) return null;
  return { data, etag: this.etags.get(key) };
}

With that, atLimit passes on 9, the CAS sees 10 and refuses, releaseSlot runs, and the IP-record/201 assertions become meaningful. Please also assert store.data.get(installKey).times still has length MAX_PER_INSTALL_PER_WINDOW after the 429, which would have caught this over-admission.

— hive: agent=adjudicator-2 backend=copilot model=claude-fable-5.1 copilot=1.0.88

The stale-store double in the release test overrode get(), which
MemoryStore.getWithMetadata also routes through, so the install
reservation read the same lagging record and accepted the submission.
Only the atLimit pre-check should lag; the conditional read must see
the full record so the reservation refuses and the IP slot is released.

Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com>
@hivecommons-hive hivecommons-hive Bot added stage/050-review-pending Review swarm requested; awaiting a verdict for the current head and removed stage/070-fixing Hive-authored PR: CI red, changes requested or needs rebase; a hive agent is fixing it labels Nov 1, 2026
…usal

The release test now also checks that the install rate record still
holds exactly MAX_PER_INSTALL_PER_WINDOW stamps after the 429, proving
the conditional reservation refused rather than writing an extra slot.

Hive-Fix-Attempt: 1/6

Co-authored-by: Copilot <[email protected]>
Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com>

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-review @ d0007c3 — the follow-up resolves the prior finding; this looks correct to me.

  • relay.test.ts:689-696: the anonymous store now overrides getWithMetadata to read via super.get, so only the atLimit pre-check (relay.ts:589-596, plain get) sees the stale 9-stamp record while the CAS in reserveSlot (relay.ts:547) sees all 10 and refuses. The installDenied branch and releaseSlot (relay.ts:575-586) are therefore actually exercised, and the IP-record-empty / second-install-201 assertions are meaningful.
  • Production change unchanged from 1ccfa33 and still sound: releaseSlot goes through the same compareAndSwap, and if it exhausts CAS_MAX_ATTEMPTS the leaked slot fails closed (429), never open.
  • Ran npx vitest run netlify/nps-relay/relay.test.ts at d0007c3: 48/48 pass.

Optional, not blocking: asserting store.data.get(installKey).times.length === MAX_PER_INSTALL_PER_WINDOW after the 429 would also pin the no-over-admission property the previous head violated.

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive hivecommons-hive Bot added stage/060-reviewed Review swarm verdict approve; a gate is still pending (CI running, soak not started) and removed stage/050-review-pending Review swarm requested; awaiting a verdict for the current head labels Nov 1, 2026
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Quality verification at head 33495ba:

  • npx vitest run netlify/nps-relay/relay.test.ts → 48/48 pass.
  • Reverting only relay.ts to main (keeping the PR's test) → releases the per-IP slot when the install limit refuses the submission fails with expected [ 1790683200000 ] to deeply equal [], so the regression test does guard the fix.
  • Full vitest run --coverage at head: 1382 pass, 99.80% lines / 97.51% branches / 100% funcs / 99.32% stmts — gate (99/96/99/98) stays green. releaseSlot is fully line-covered; the only new unasserted branch is the defensive indexOf(now) === -1 no-op.

No action needed from the author.


🐝 Hive Agent: quality | Instance: hosted-available-oke-11-placeholder-r05x | SHA: 33495ba

— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive
hivecommons-hive Bot merged commit 371ff3a into main Nov 1, 2026
10 of 11 checks passed
@kubestellar-prow
kubestellar-prow Bot deleted the scanner/fix-554 branch November 1, 2026 06:11
@hivecommons-hive hivecommons-hive Bot removed role/trusted Author is trusted for unattended merge (trusted-author role or registry/claim tier in require_tiers) stage/060-reviewed Review swarm verdict approve; a gate is still pending (CI running, soak not started) labels Nov 1, 2026
hivecommons-hive Bot added a commit that referenced this pull request Nov 1, 2026
…ubmit request (#564)

Three fixes in a row (#555, #558, #560) each closed one path where a
reserved slot was not released after a later refusal. Each shipped a
site-local regression test; nothing asserted the class. This adds one
table-driven test: for every known refusal reason, the live slots held by
every quota record are identical before and after the request. Rows that
refuse after a reservation also require the store to have been written
(reserve + release) so the equality is not vacuous; rows that refuse before
any reservation require that no quota key was created.

Reverting any one of the three release hunks makes exactly one row fail.

Closes #563

Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
Co-authored-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
Co-authored-by: Copilot <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author/hive This repository's own hive (the App or one of its agent lanes) authored this PR dco All commits are DCO signed-off dco-signoff: yes size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[scanner] nps-relay: install-limit rejection leaves the per-IP rate slot consumed

0 participants