Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .markdownlintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.tmp/**
2 changes: 1 addition & 1 deletion .taplo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Used by the "Even Better TOML" VS Code extension

# Exclude generated and runtime folders from linting
exclude = [ ".coverage/**", "storage/**", "target/**" ]
exclude = [ ".coverage/**", ".tmp/**", "storage/**", "target/**" ]

[formatting]
# Preserve blank lines that exist
Expand Down
1 change: 1 addition & 0 deletions .yamllint-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ rules:

# Ignore generated/runtime directories
ignore: |
.tmp/**
target/**
storage/**
.coverage/**
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"toml"
],
"ignorePaths": [
".tmp/**",
"target",
"docs/media/*.svg",
"contrib/bencode/benches/*.bencode",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
semantic-links:
related-artifacts:
- docs/adrs/index.md
- docs/events-architecture.md
- packages/http-core/src/container.rs
- packages/udp-core/src/container.rs
- packages/udp-server/src/container.rs
- packages/udp-core/src/services/banning.rs
- packages/tracker-core/src/container.rs
- packages/configuration/src/v3_0_0/udp_tracker_server.rs
Expand All @@ -21,8 +24,9 @@ Each listener binds to a different address/port but shares core infrastructure:
multiple listeners: they serve the same swarm.
- **Ban service** (`BanService` in `UdpTrackerCoreServices`) — all UDP instances
share the same IP-ban state. An IP banned on one UDP listener is banned on all.
- **Event buses** — core-layer events (`UdpTrackerCoreServices`) are shared;
server-layer events (`UdpTrackerServerContainer`) are per-instance.
- **Event buses and repositories** — HTTP core, UDP core, and UDP server events
are aggregate application services. The UDP server container is shared by all
UDP listeners.

This ADR documents the shared-services design and the rationale for keeping
certain services global rather than per-instance.
Expand All @@ -41,8 +45,21 @@ same type:
| UDP ban service | `UdpTrackerCoreServices::ban_service` | Yes | Resource protection: an attacker should not be able to consume N× resources by attacking N listeners independently |
| UDP core event bus | `UdpTrackerCoreServices::event_bus` | Yes | Core events (connect, announce, scrape) are objective facts about the swarm, not about a specific listener |
| UDP core services (connect, announce, scrape) | `UdpTrackerCoreServices` | Yes | Stateless service objects; they read from the shared peer repository |
| UDP server event bus | `UdpTrackerServerContainer::event_bus` | **No** | Per-instance; server events (request accepted, banned, error) are specific to one listener |
| UDP server stats repository | `UdpTrackerServerContainer::stats_repository` | **No** | Per-instance; each listener has its own statistics |
| HTTP core event bus and repository | `HttpTrackerCoreServices` | Yes | Aggregate HTTP metrics are collected in one application-wide event path |
| UDP server event bus | `UdpTrackerServerContainer::event_bus` | Yes | One application-wide bus is passed to every UDP listener |
| UDP server stats repository | `UdpTrackerServerContainer::stats_repository` | Yes | One aggregate server repository receives events from every UDP listener |

The UDP server's shared bus and repository do not conflict with per-listener
metrics policy. Events are objective facts and must be emitted independently of
metrics configuration. The target design filters events in the metrics listener
by stable runtime listener identity before mutating the shared aggregate
repository. A configured `SocketAddr` is not sufficient identity because two
listeners may validly use `0.0.0.0:0`.

UDP banning remains independent of metrics. Its listener receives every
security-relevant event from the shared UDP server bus and updates the shared
ban service regardless of whether the originating listener contributes to
metrics. See [events-architecture.md](../events-architecture.md).

### Why the ban service is shared

Expand Down
120 changes: 120 additions & 0 deletions docs/events-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
semantic-links:
related-artifacts:
- docs/adrs/20260727000000_events_are_objective_facts.md
- docs/adrs/20260727180000_shared_services_across_tracker_instances.md
- packages/events/src/bus.rs
- packages/http-core/src/container.rs
- packages/udp-core/src/container.rs
- packages/udp-server/src/container.rs
- src/container.rs
- src/bootstrap/jobs/
---

# Events Architecture

## Purpose

This guide describes the tracker event topology as it exists on `develop` and
the direction for normalizing per-listener metrics policy. It distinguishes an
event producer's responsibility to report an objective fact from a listener's
responsibility to apply a policy to that fact.

## Current Topology

`EventBus` wraps a Tokio broadcast channel and can return no sender when its
`SenderStatus` is disabled. Today, the HTTP core, UDP core, and UDP server each
create one event bus and one aggregate statistics repository during application
container initialization. Their senders and listeners are gated by the global
`core.tracker_usage_statistics` setting.

The HTTP and UDP tracker instance containers share their respective core
services. The UDP server differs from both core layers: `AppContainer` creates
one application-wide `UdpTrackerServerContainer`, then passes a clone of that
container to every UDP listener. Consequently, the UDP server has one shared
event bus and one shared server statistics repository, not a bus or repository
per UDP listener.

| Layer | Current event bus and repository ownership | Event consumers |
| ---------- | -------------------------------------------------------------------------- | ------------------------------------------------------- |
| HTTP core | One application-wide bus and aggregate repository shared by HTTP listeners | HTTP core statistics listener |
| UDP core | One application-wide bus and aggregate repository shared by UDP listeners | UDP core statistics listener |
| UDP server | One application-wide bus and aggregate repository shared by UDP listeners | UDP server statistics listener and UDP banning listener |

The REST metrics adapter exposes TCP announce counts from HTTP core statistics
and UDP announce counts from UDP server statistics. UDP request handling thus
has two event layers: a server lifecycle event stream and a core protocol event
stream.

## Producer and Listener Responsibilities

Events are objective facts. As established by
[ADR 20260727000000](adrs/20260727000000_events_are_objective_facts.md), an
event must not encode whether a particular consumer should act on it.

The current `SenderStatus` gate applies a metrics setting at the producer. That
is too broad when an event has more than one consumer. In particular, UDP server
cookie-error events are observed by both the metrics listener and the banning
listener. Suppressing production for metrics also prevents the banning listener
from observing the error.

The target division of responsibility is:

```text
listener instance
-> always emit an objective event with stable runtime identity
-> shared layer event bus
-> metrics listener filters by that listener's metrics policy
-> shared aggregate repository
-> UDP banning listener receives every relevant security event
-> shared ban service
```

Metrics filtering is therefore a consumer-side decision. Banning is independent
of metrics configuration and continues to enforce against the shared ban state.

## HTTP and UDP Asymmetry

The user-facing intent from [#1263][1263] and [#1401][1401] is aggregate
statistics with a per-public-listener `tracker_usage_statistics` policy. The
current implementation cannot express that intent consistently:

- HTTP and UDP core event production is gated globally, even though listeners
are configured independently.
- UDP server metrics are also generated through one global event path and are
the source of public UDP request counters.
- UDP server events additionally feed the banning listener, so a metrics gate
cannot safely decide whether those facts exist.

This is an asymmetry of event ownership and consumers, not a reason to create a
repository per listener. A shared aggregate repository remains the desired
topology when the metrics listener filters events by stable listener identity.

## Proposed Normalization

The normalization work depends on [#2036][2036] defining canonical runtime
service and configuration-instance identity. That identity must travel with
metric-relevant events; configured socket addresses are not suitable because
multiple configuration blocks may use `0.0.0.0:0`.

The implementation will make producers emit objective facts regardless of
`tracker_usage_statistics`, then provide each metrics listener with an immutable
identity-to-policy lookup. The listener ignores disabled-listener events before
updating its shared aggregate repository. The UDP banning listener does not use
that lookup.

This approach preserves aggregate metrics, fixes the server-layer UDP gap, and
lets future non-metrics consumers receive complete facts without coupling them
to metrics configuration.

## Related Work

- Approved specification: [normalize per-instance event metrics policy](issues/open/2039-normalize-per-instance-event-metrics-policy/ISSUE.md)
- Bootstrap bug: [#2035][2035]
- Runtime identity prerequisite: [#2036][2036]
- Shared-services decision: [ADR 20260727180000](adrs/20260727180000_shared_services_across_tracker_instances.md)

[1263]: https://github.com/torrust/torrust-tracker/issues/1263
[1401]: https://github.com/torrust/torrust-tracker/issues/1401
[2035]: https://github.com/torrust/torrust-tracker/issues/2035
[2036]: https://github.com/torrust/torrust-tracker/issues/2036
17 changes: 9 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ source code, see the [crate docs on docs.rs][docs].

Operational and development guides for working with the tracker.

| Document | Description |
| ------------------------------------------ | -------------------------------------------------------------------- |
| [application-jobs.md](application-jobs.md) | Current background-job ownership, lifecycle, and shutdown behavior |
| [benchmarking.md](benchmarking.md) | How to run and interpret the torrent-repository benchmarks |
| [containers.md](containers.md) | Building and running the tracker with Docker / Podman |
| [packages.md](packages.md) | Workspace package catalog, architecture layers, and dependency rules |
| [profiling.md](profiling.md) | CPU and memory profiling with Valgrind / kcachegrind |
| [release_process.md](release_process.md) | Branch strategy, versioning, and the staging → main release pipeline |
| Document | Description |
| ------------------------------------------------ | -------------------------------------------------------------------- |
| [application-jobs.md](application-jobs.md) | Current background-job ownership, lifecycle, and shutdown behavior |
| [benchmarking.md](benchmarking.md) | How to run and interpret the torrent-repository benchmarks |
| [containers.md](containers.md) | Building and running the tracker with Docker / Podman |
| [events-architecture.md](events-architecture.md) | Event topology, consumers, and per-listener metrics policy |
| [packages.md](packages.md) | Workspace package catalog, architecture layers, and dependency rules |
| [profiling.md](profiling.md) | CPU and memory profiling with Valgrind / kcachegrind |
| [release_process.md](release_process.md) | Branch strategy, versioning, and the staging → main release pipeline |

## Architecture Decisions (ADRs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,17 @@ which creates isolated workspaces with separate config and storage directories f
**Strategy**: First prove the scaffolding is broken by adding a second test case that would conflict,
then fix the scaffolding infrastructure, then expand coverage.

| ID | Status | Task | Notes |
| --- | ------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| T1 | DONE | Create `tests/AGENTS.md` with guidelines and TODO list | Document what belongs in main-level integration tests vs package tests; include prioritized TODO list of future valuable integration tests |
| T2 | TODO | Add second assertion to existing stats test | Check another global stat field (e.g., `tcp4_scrapes_handled` or `tcp6_announces_handled`) to prove need for parallel test capability |
| T3 | TODO | Create test utilities module | `tests/helpers.rs` with utilities for temp workspace creation (config + storage dirs) and port extraction |
| T4 | TODO | Add utility to create isolated temp workspace | Returns `TempDir` with subdirectories for config and storage; writes TOML config; sets `TORRUST_TRACKER_CONFIG_TOML_PATH` env var |
| T5 | TODO | Add utility to extract bound addresses from `AppContainer` | Query `Registar` or job handles to get actual bound `SocketAddr` for HTTP API, trackers, etc. |
| T6 | TODO | Update existing stats test to use port 0 and temp workspace | Replace `env::set_var` with isolated temp workspace, use port 0, extract actual ports for requests; fixes scaffolding to support parallelism |
| T7 | TODO | Expand global stats test coverage | Add tests for more global stat metrics now that scaffolding supports it (scrape counters, different IP families, etc.) |
| T8 | TODO | Run automatic verification | `cargo test --test stats` must pass with all tests running concurrently |
| ID | Status | Task | Notes |
| --- | ------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| T1 | DONE | Create `tests/AGENTS.md` with guidelines and TODO list | Document what belongs in main-level integration tests vs package tests; include prioritized TODO list of future valuable integration tests |
| T2 | TODO | Add second assertion to existing stats test | Check another global stat field (e.g., `tcp4_scrapes_handled` or `tcp6_announces_handled`) to prove need for parallel test capability |
| T3 | TODO | Create test utilities module | `tests/helpers.rs` with utilities for temp workspace creation (config + storage dirs) and port extraction |
| T4 | TODO | Add utility to create isolated temp workspace | Returns `TempDir` with subdirectories for config and storage; writes TOML config; sets `TORRUST_TRACKER_CONFIG_TOML_PATH` env var |
| T5 | TODO | Add utility to extract bound addresses from `AppContainer` | Query `Registar` or job handles to get actual bound `SocketAddr` for HTTP API, trackers, etc. |
| T6 | TODO | Update existing stats test to use port 0 and temp workspace | Replace `env::set_var` with isolated temp workspace, use port 0, extract actual ports for requests; fixes scaffolding to support parallelism |
| T7 | TODO | Expand global stats test coverage | Add tests for more global stat metrics now that scaffolding supports it (scrape counters, different IP families, etc.) |
| T8 | TODO | Add shared-event-bus policy integration test | After #2036 and event-metrics normalization, verify enabled and disabled HTTP/UDP listeners share aggregate buses while only enabled traffic changes public metrics. |
| T9 | TODO | Run automatic verification | `cargo test --test stats` must pass with all tests running concurrently |

## Progress Tracking

Expand Down
Loading
Loading