Repository navigation
docs(issues): [#2466] specify bounding announce intervals to what every delivery protocol can encode #2468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
josecelano
merged 15 commits into
torrust:develop
from
josecelano:2466-1978-announce-interval-upper-bound-spec
Oct 7, 2026
+1,022
−17
Merged
docs(issues): [#2466] specify bounding announce intervals to what every delivery protocol can encode #2468
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
2fcfb36
docs(adrs): [#2466] bound protocol-agnostic values by the tightest de…
josecelano aaa63f5
docs(issues): [#1978] reopen configuration EPIC to add subissue #2466
josecelano dde863a
docs(issues): [#2466] add issue specification for bounding announce i…
josecelano 816d6d9
docs(issues): [#2243] link #2245 configuration-load follow-up #2466
josecelano fd0af8f
docs(skills): add reopen-issue skill from reopening EPIC #1978
josecelano 6eceade
docs(issues): [#2466] address round-1 spec review findings
josecelano fa20f21
docs(adrs): [#2466] quote issue markers and add the affected code
josecelano 5227b1d
docs(issues): [#1978] date the reopen at the GitHub reopen, not the d…
josecelano 188a524
docs(skills): [#2466] couple, register, and complete the reopen-issue…
josecelano 5fbd5a5
docs(pr-reviews): [#2466] record round-1 review findings on #2468
josecelano f1e75f8
docs(issues): [#2466] justify the shared interval_min type by meaning…
josecelano 43be393
docs(pr-reviews): [#2466] record the interval_min decision on #2468
josecelano 1736f0d
docs(pr-reviews): [#2466] copy template blocks verbatim and log the s…
josecelano fb62846
docs(pr-reviews): [#2466] record round-2 review findings on #2468
josecelano 3ba843a
docs(pr-reviews): [#2466] record da2ce7 round-3 approval on #2468
josecelano File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| --- | ||
| name: reopen-issue | ||
| description: Guide for reopening a closed GitHub issue or EPIC whose specification was archived in docs/issues/closed/. Covers deciding whether to reopen or file new work, reopening on GitHub, linking a new subissue, moving the spec back to docs/issues/open/, migrating legacy frontmatter, resetting closure checkpoints, and repairing live references. Use when new work belongs to a closed EPIC or a closed issue regressed or was incomplete. Triggers on "reopen issue", "reopen EPIC", "add subissue to closed EPIC", "unarchive issue spec", or "move spec back to open". | ||
| metadata: | ||
| author: torrust | ||
| version: "1.1" | ||
| --- | ||
|
|
||
| <!-- skill-link: reopen-issue --> | ||
|
|
||
| # Reopening a Closed Issue or EPIC | ||
|
|
||
| This is the inverse of [cleanup-completed-issues](../cleanup-completed-issues/SKILL.md). It was | ||
| first used to reopen configuration EPIC #1978 for subissue #2466 (2026-10-07). The EPIC path is | ||
| exercised; the single-issue path is derived from it and should be refined on first use. | ||
|
josecelano marked this conversation as resolved.
|
||
|
|
||
| ## Step 1: Decide Whether to Reopen | ||
|
|
||
| Reopen only with explicit maintainer approval; reopening changes shared GitHub state. | ||
|
|
||
| Reopen when the new work belongs to the original goal and its deliverable is not yet released (for | ||
| example, #1978's configuration schema v3.0.0 was active but not published on crates.io). Otherwise | ||
| create a new issue or EPIC and reference the closed one. | ||
|
|
||
| For a closed single issue, reopen when the closing PR did not meet its acceptance criteria or the | ||
| fix regressed before release. A regression after release is a new bug issue. | ||
|
|
||
| ## Step 2: Prepare the Branch | ||
|
|
||
| Update `develop` first, check that it is not behind, and only then create the branch. Run the | ||
| behind count as its own step and read it: a skipped or failed `git pull --ff-only` leaves `develop` | ||
| behind, and only the count shows it. Chained with `&&`, `git checkout -b` runs even when the count | ||
| is non-zero. | ||
|
|
||
| ```bash | ||
| UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-torrust}" | ||
| git checkout develop | ||
| git pull --ff-only "$UPSTREAM_REMOTE" develop | ||
| git rev-list --count HEAD.."$UPSTREAM_REMOTE"/develop # must print 0 | ||
| git checkout -b <branch> | ||
| ``` | ||
|
|
||
| When reopening an EPIC to add a subissue, reuse that subissue's specification branch | ||
| (`<issue>-<epic>-<description>-spec`), so the reopen, the subissue spec, and any ADR land in one PR. | ||
|
|
||
| ## Step 3: Update GitHub | ||
|
|
||
| 1. When adding a subissue, create it first (see [create-issue](../create-issue/SKILL.md)), so the | ||
| reopen comment can cite it. | ||
| 2. Reopen with the reason: | ||
|
|
||
| ```bash | ||
| gh issue reopen <number> --repo torrust/torrust-tracker --comment "Reopened to <reason>." | ||
| ``` | ||
|
|
||
| 3. Link a new subissue natively; the REST endpoint needs the child's database `id`, not its number: | ||
|
|
||
| ```bash | ||
| CHILD_ID=$(gh api repos/torrust/torrust-tracker/issues/<child> --jq .id) | ||
| gh api -X POST repos/torrust/torrust-tracker/issues/<epic>/sub_issues -F sub_issue_id="$CHILD_ID" | ||
| ``` | ||
|
|
||
| 4. Check the reopened issue's body for a `Specification` link and point it at the `open/` path. | ||
| Repository searches do not see issue bodies, and older bodies may still name a legacy | ||
| single-file path: | ||
|
|
||
| ```bash | ||
| gh issue view <number> --repo torrust/torrust-tracker --json body --jq .body > .tmp/issue-<number>-body.md | ||
| # edit the link, then: | ||
| gh issue edit <number> --repo torrust/torrust-tracker --body-file .tmp/issue-<number>-body.md | ||
| ``` | ||
|
|
||
| ## Step 4: Move the Specification Back | ||
|
|
||
| ```bash | ||
| git mv docs/issues/closed/<folder> docs/issues/open/ | ||
| ``` | ||
|
|
||
| Then update the moved spec: | ||
|
|
||
| - **Frontmatter**: set `status` to `planned` or `in-progress`, `spec-path` to the `open/` path, and | ||
| `last-updated-utc` to the current time. Specs archived before the v1 schema must be migrated to | ||
| v1 in `open/` (`schema-version: 1`, `epic: null` where applicable, quoted `last-updated-utc`, no | ||
| trailing slash in `related-artifacts`). Follow the migration checklist in | ||
| [`contrib/dev-tools/checks/frontmatter-validator/README.md`](../../../../../contrib/dev-tools/checks/frontmatter-validator/README.md). | ||
| - **Archive-time edits**: revert body paths the archive rewrote to `closed/`, such as the | ||
| "spec drafted in" checkpoint. | ||
| - **Checkpoints**: clear the "issue closed and spec moved" and "acceptance criteria reviewed" boxes. | ||
|
josecelano marked this conversation as resolved.
|
||
| - **Acceptance Verification**: set rows that depended on all work being done back to `TODO`, clear | ||
| the matching Acceptance Criteria checkboxes (for example, "Epic status reflects actual state of | ||
| linked subissues"), and correct stale counts (for example, the number of linked subissues). | ||
| - **Subissues table** (EPIC): add the new row with `TODO`. A `related-artifacts` entry must name a | ||
| tracked file, so add the new child spec path in the commit that adds the spec. | ||
| - **Progress Log**: add an entry with the reason and the remaining work. Date each entry at the | ||
| action it records: the maintainer's decision, the GitHub reopen, and the move are separate | ||
| events. | ||
| - **References**: add the new subissue, the issue it came from, and any new ADR. | ||
| - **Parent EPIC** (single issue): set its row back to `IN_PROGRESS` and point it at `open/`. | ||
|
|
||
| The frontmatter validator checks **staged** content. Stage the edits, not only the rename, before | ||
| running it: | ||
|
|
||
| ```bash | ||
| git add -A docs/issues/open/<folder> | ||
| cargo run --quiet --package frontmatter-validator --bin frontmatter-validator -- --staged | ||
| ``` | ||
|
|
||
| ## Step 5: Repair Live References | ||
|
|
||
| ```bash | ||
| rg '<folder>' --glob '!target/**' --glob '!storage/**' | ||
| ``` | ||
|
|
||
| - Update live navigational references outside `docs/issues/closed/` (for example, package docs) to | ||
| the `open/` path. | ||
|
josecelano marked this conversation as resolved.
|
||
| - Leave links inside other closed specs and historical PR review records unchanged. They are | ||
| archived records, the [local link checker](../../../../../lychee.toml) excludes | ||
| `docs/issues/closed/`, and the spec returns | ||
| there on closure. | ||
|
|
||
| When the reopened issue closes again, archive it with | ||
| [cleanup-completed-issues](../cleanup-completed-issues/SKILL.md); its reference search flips the | ||
| live references back. | ||
|
|
||
| ## Step 6: Commit and Open the PR | ||
|
|
||
| Commit the reopen separately from the new subissue spec. When the reopened spec references a new | ||
| ADR or spec in `related-artifacts`, commit that artifact first, because the validator requires | ||
| tracked paths. | ||
|
|
||
| ```bash | ||
| git commit -S -m "docs(issues): [#<number>] reopen <short title> to <reason>" | ||
| ``` | ||
|
|
||
| Use `Related to #<number>` in the PR body, never a closing keyword. | ||
|
|
||
| ## Skill Links | ||
|
|
||
| Artifacts this skill depends on; the first three carry a `skill-link: reopen-issue` marker: | ||
|
|
||
| - [`cleanup-completed-issues`](../cleanup-completed-issues/SKILL.md): the inverse workflow; a | ||
| change to how specs are archived changes what a reopen must undo. | ||
| - [`frontmatter-validator/README.md`](../../../../../contrib/dev-tools/checks/frontmatter-validator/README.md): | ||
| the v1 migration checklist used in Step 4. | ||
| - [`lychee.toml`](../../../../../lychee.toml): the `docs/issues/closed/` exclusion that Step 5 | ||
| relies on. | ||
| - [`docs/templates/EPIC.md`](../../../../../docs/templates/EPIC.md) and | ||
| [`docs/templates/ISSUE.md`](../../../../../docs/templates/ISSUE.md): the checkpoint wording reset | ||
| in Step 4. They carry no marker, because specs copy template markers. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
...61007082938_bound_protocol_agnostic_values_by_the_tightest_delivery_protocol.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| --- | ||
| semantic-links: | ||
| skill-links: | ||
| - create-adr | ||
| related-artifacts: | ||
| - "issue #1978" | ||
| - "issue #2245" | ||
| - packages/primitives/src/announce.rs | ||
|
josecelano marked this conversation as resolved.
|
||
| - docs/adrs/20260723184019_separate_configuration_value_invariants_from_consistency_validation.md | ||
| - docs/adrs/20260721100000_use_newtypes_for_constrained_configuration_field_types.md | ||
| --- | ||
|
|
||
| <!-- skill-link: create-adr --> | ||
|
|
||
| # Bound Protocol-Agnostic Values by the Tightest Delivery Protocol | ||
|
|
||
| ## Scope | ||
|
|
||
| Repository-level. The decision spans the configuration schema, the domain types in `primitives`, | ||
| and every delivery-protocol package (`udp-server`, `http-protocol`, `axum-http-server`), so it | ||
| belongs in `docs/adrs/`. | ||
|
|
||
| ## Description | ||
|
|
||
| The tracker serves the same swarm through several delivery protocols: UDP (BEP 15), HTTP (BEP 3 | ||
| and BEP 23), and possibly others later, such as WebTorrent. Some configured values are sent to | ||
| clients by every protocol, but each protocol encodes them differently. The announce interval is the | ||
| first case: BEP 15 encodes it as a signed 32-bit integer, while HTTP encodes it as a bencode | ||
| integer (`i64` in `http-protocol`). | ||
|
|
||
| The configuration and domain types held the interval as a plain `u32`, so a value above | ||
| `i32::MAX` was accepted. Before #2245, UDP sent such a value as a negative number. #2245 clamped | ||
| it on the UDP wire, which hid the problem: UDP and HTTP clients then received different intervals | ||
| for the same configuration. | ||
|
|
||
| The [value-invariant ADR](20260723184019_separate_configuration_value_invariants_from_consistency_validation.md) | ||
| says *how* to reject a single-value bound: a typed newtype, rejected during deserialization. It | ||
| does not say *which* bound to choose when the delivery protocols disagree, or where such a type | ||
| lives. | ||
|
|
||
| ## Agreement | ||
|
|
||
| A configuration value that every delivery protocol sends to clients is **one protocol-agnostic | ||
| value**, not one value per protocol. | ||
|
|
||
| Its bound is the **tightest limit among the supported delivery protocols**. For the announce | ||
| interval that is `i32::MAX` seconds (about 68 years), from BEP 15; realistic values are minutes to | ||
| hours. | ||
|
|
||
| The bound is a domain rule (the value must be representable on every supported protocol), so it is | ||
| enforced by a validated newtype in `primitives`. The configuration uses that domain type directly, | ||
| as it already uses `AnnouncePolicy`. Following the value-invariant ADR, construction and | ||
| `Deserialize` reject out-of-range values, so an invalid configuration fails at load. | ||
|
|
||
| Delivery adapters then convert the value to their wire type without clamping or failing. Clamping | ||
| remains appropriate only for values that no configuration bounds, such as peer counts. | ||
|
|
||
| When a new delivery protocol has a tighter limit, the shared bound is lowered. That is a breaking | ||
| configuration change and must be released as one. | ||
|
|
||
| This rule covers values the operator configures once and the tracker sends through every protocol. | ||
| It does not cover limits on what clients send, which differ per protocol for their own reasons; see | ||
| [Cap scrape info hashes per protocol](20261005124222_cap_scrape_info_hashes_per_protocol.md). | ||
|
|
||
| ## Alternatives Considered | ||
|
|
||
| ### One configuration value per delivery protocol | ||
|
|
||
| Rejected. It duplicates a single concept across protocols and lets operators configure different | ||
| intervals for the same swarm by accident. | ||
|
|
||
| ### Validate the shared value per delivery protocol | ||
|
|
||
| Rejected. A value could be valid for HTTP and invalid for UDP, so enabling a protocol would turn a | ||
| working configuration into an invalid one. It also needs consistency rules that depend on which | ||
| protocols are enabled. | ||
|
|
||
| ### Clamp on the wire only | ||
|
|
||
| Rejected. This was the state after #2245. It silently changes the configured value for some | ||
| clients, and the protocols then disagree. | ||
|
|
||
| ### An arbitrary "sensible" cap | ||
|
|
||
| Rejected. A cap such as one day has no source to derive it from, and choosing it is a separate | ||
| policy decision. The protocol limit is objective. | ||
|
|
||
| ### A bounded schema type only in `configuration` | ||
|
|
||
| Rejected. It keeps `primitives` unaware of the bound, but the domain type stays unbounded, so the | ||
| delivery adapters still need a fallible conversion or a clamp. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - **Positive**: one unambiguous value per concept; all delivery protocols report the same value. | ||
| - **Positive**: invalid configurations fail at load with a clear error instead of being clamped. | ||
| - **Positive**: delivery adapters convert infallibly, with no clamp to keep in sync. | ||
| - **Negative**: the bound is lower than some protocols need. For the announce interval this costs | ||
| nothing in practice. | ||
| - **Negative**: adding a protocol with a tighter limit is a breaking configuration change. | ||
| - **Negative**: changing a `primitives` field type is a breaking public API change for its | ||
| consumers. | ||
|
|
||
| ## Affected Code | ||
|
|
||
| - [`packages/primitives/src/announce.rs`](../../packages/primitives/src/announce.rs): | ||
| `AnnouncePolicy::interval` and `interval_min`, which #2466 changes to the bounded type. | ||
| - [`packages/udp-server/src/handlers/announce.rs`](../../packages/udp-server/src/handlers/announce.rs): | ||
| the UDP response encodes the interval through `saturating_wire_i32` until #2466 converts it | ||
| without clamping. | ||
|
|
||
| Issue #2466 adds module-level doc comments in both places that link back to this ADR. | ||
|
|
||
| ## Date | ||
|
|
||
| 2026-10-07 | ||
|
|
||
| ## References | ||
|
|
||
| - [Configuration Overhaul EPIC #1978](https://github.com/torrust/torrust-tracker/issues/1978) | ||
| - [Issue #2245](https://github.com/torrust/torrust-tracker/issues/2245) — review numeric protocol wire conversions (introduced the UDP clamp) | ||
| - [BEP 15: UDP Tracker Protocol](https://www.bittorrent.org/beps/bep_0015.html) | ||
| - [Separate Configuration Value Invariants from Consistency Validation](20260723184019_separate_configuration_value_invariants_from_consistency_validation.md) | ||
| - [Use Newtypes for Domain-Constrained Configuration Field Types](20260721100000_use_newtypes_for_constrained_configuration_field_types.md) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.