mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-07-29 04:52:48 +02:00
Merge pull request #317 from ipnet-mesh/chore/routes-default-thresholds
chore(routes): default packet_count_threshold=5 and 3× clear multiplier
This commit is contained in:
+4
-4
@@ -17,10 +17,10 @@ Each route carries these knobs:
|
||||
| Field | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `match_width` | `1` | Path-hash prefix width in bytes (1/2/3). Higher widths disambiguate nodes that share a short public-key prefix. |
|
||||
| `window_hours` | `24` | Rolling lookback window for the live status card. |
|
||||
| `packet_count_threshold` | `3` | Distinct matching packets at/above which the route is `healthy`. "Distinct" is per underlying event, not per transmission — see [How health is evaluated](#how-health-is-evaluated) above. |
|
||||
| `clear_threshold` | _(2× threshold)_ | Comfort bar for the `clear`/`marginal` split. Omit/null to use twice the threshold. |
|
||||
| `max_hop_span` | _(unlimited)_ | Caps the position gap between the first and last matched node, to reject matches that wander too far. |
|
||||
| `window_hours` | `48` | Rolling lookback window for the live status card. |
|
||||
| `packet_count_threshold` | `5` | Distinct matching packets at/above which the route is `healthy`. "Distinct" is per underlying event, not per transmission — see [How health is evaluated](#how-health-is-evaluated) above. |
|
||||
| `clear_threshold` | _(3× threshold)_ | Comfort bar for the `clear`/`marginal` split. Omit/null to use three times the threshold. |
|
||||
| `max_hop_span` | `8` | Caps the position gap between the first and last matched node, to reject matches that wander too far. |
|
||||
| `reversible` | `true` | Also match the path in reverse direction. |
|
||||
| `enabled` | `true` | When `false`, the route is skipped by the evaluator and reports `unknown`/`no_coverage`. |
|
||||
|
||||
|
||||
+3
-3
@@ -126,9 +126,9 @@ routes:
|
||||
description: A140 corridor route
|
||||
visibility: community
|
||||
match_width: 1
|
||||
window_hours: 24
|
||||
packet_count_threshold: 3
|
||||
# clear_threshold: 10 # optional; omit/null = 2x threshold
|
||||
window_hours: 48
|
||||
packet_count_threshold: 5
|
||||
# clear_threshold: 15 # optional; omit/null = 3x threshold
|
||||
# max_hop_span: 8 # optional; omit/null = unlimited
|
||||
enabled: true
|
||||
reversible: true # match both directions (A->B and B->A)
|
||||
|
||||
@@ -348,7 +348,7 @@ def collect_metrics(session: Any) -> bytes:
|
||||
)
|
||||
route_clear = Gauge(
|
||||
"meshcore_route_clear_threshold",
|
||||
"Effective clear threshold (2x threshold when unset)",
|
||||
"Effective clear threshold (3x threshold when unset)",
|
||||
["route"],
|
||||
registry=registry,
|
||||
)
|
||||
|
||||
@@ -806,7 +806,7 @@ def _import_routes(
|
||||
route.match_width = match_width
|
||||
route.window_hours = value.get("window_hours", 48)
|
||||
route.packet_count_threshold = value.get(
|
||||
"packet_count_threshold", 3
|
||||
"packet_count_threshold", 5
|
||||
)
|
||||
route.clear_threshold = value.get("clear_threshold")
|
||||
route.max_hop_span = value.get("max_hop_span", 8)
|
||||
@@ -827,7 +827,7 @@ def _import_routes(
|
||||
visibility=visibility,
|
||||
match_width=match_width,
|
||||
window_hours=value.get("window_hours", 48),
|
||||
packet_count_threshold=value.get("packet_count_threshold", 3),
|
||||
packet_count_threshold=value.get("packet_count_threshold", 5),
|
||||
clear_threshold=value.get("clear_threshold"),
|
||||
max_hop_span=value.get("max_hop_span", 8),
|
||||
enabled=value.get("enabled", True),
|
||||
|
||||
@@ -27,8 +27,8 @@ from meshcore_hub.common.models.route_result_history import RouteResultHistory
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Multiplier for the relative default comfort bar (``clear_threshold = None``
|
||||
#: means ``effective_clear = 2 × packet_count_threshold``).
|
||||
CLEAR_DEFAULT_MULTIPLIER = 2
|
||||
#: means ``effective_clear = 3 × packet_count_threshold``).
|
||||
CLEAR_DEFAULT_MULTIPLIER = 3
|
||||
|
||||
#: Cap on candidate receptions for preview to bound work per call.
|
||||
PREVIEW_CANDIDATE_CAP = 5000
|
||||
@@ -54,7 +54,7 @@ def derive_expected_hash(public_key: str, match_width: int) -> str:
|
||||
|
||||
|
||||
def effective_clear_threshold(route: Route) -> int:
|
||||
"""The effective comfort bar: explicit value or ``2 × threshold``."""
|
||||
"""The effective comfort bar: explicit value or ``3 × threshold``."""
|
||||
return route.clear_threshold or (
|
||||
route.packet_count_threshold * CLEAR_DEFAULT_MULTIPLIER
|
||||
)
|
||||
@@ -1239,7 +1239,7 @@ def preview_route(
|
||||
match_width: int = config.get("match_width") or 1
|
||||
observer_ids: Optional[list[str]] = config.get("observer_ids") or None
|
||||
max_hop_span: Optional[int] = config.get("max_hop_span")
|
||||
threshold: int = config.get("packet_count_threshold") or 3
|
||||
threshold: int = config.get("packet_count_threshold") or 5
|
||||
clear_bar: Optional[int] = config.get("clear_threshold")
|
||||
reversible: bool = config.get("reversible", True)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class Route(Base, UUIDMixin, TimestampMixin):
|
||||
match_width: Path-hash prefix width in bytes (1/2/3)
|
||||
window_hours: Evaluation lookback window in hours
|
||||
packet_count_threshold: Minimum distinct packets for healthy
|
||||
clear_threshold: Comfort bar for the clear/marginal split (null = 2x threshold)
|
||||
clear_threshold: Comfort bar for the clear/marginal split (null = 3x threshold)
|
||||
max_hop_span: Max hops between first and last configured node (null = unlimited)
|
||||
enabled: Whether this route is actively evaluated
|
||||
"""
|
||||
@@ -74,7 +74,7 @@ class Route(Base, UUIDMixin, TimestampMixin):
|
||||
)
|
||||
packet_count_threshold: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=3,
|
||||
default=5,
|
||||
nullable=False,
|
||||
)
|
||||
clear_threshold: Mapped[Optional[int]] = mapped_column(
|
||||
|
||||
@@ -67,10 +67,10 @@ class RouteCreate(BaseModel):
|
||||
default=48, ge=1, le=720, description="Evaluation window in hours"
|
||||
)
|
||||
packet_count_threshold: int = Field(
|
||||
default=3, ge=1, le=10000, description="Minimum distinct packets for healthy"
|
||||
default=5, ge=1, le=10000, description="Minimum distinct packets for healthy"
|
||||
)
|
||||
clear_threshold: Optional[int] = Field(
|
||||
default=None, description="Comfort bar (null = 2x threshold)"
|
||||
default=None, description="Comfort bar (null = 3x threshold)"
|
||||
)
|
||||
max_hop_span: Optional[int] = Field(
|
||||
default=8, description="Max hop distance between first and last node"
|
||||
@@ -233,7 +233,7 @@ class RoutePreviewRequest(BaseModel):
|
||||
)
|
||||
match_width: int = Field(default=1, ge=1, le=3)
|
||||
window_hours: int = Field(default=48, ge=1, le=720)
|
||||
packet_count_threshold: int = Field(default=3, ge=1, le=10000)
|
||||
packet_count_threshold: int = Field(default=5, ge=1, le=10000)
|
||||
clear_threshold: Optional[int] = None
|
||||
max_hop_span: Optional[int] = Field(default=8)
|
||||
observer_public_keys: Optional[list[str]] = None
|
||||
|
||||
@@ -405,13 +405,13 @@ function renderRouteModal({ modalState, onSave, onCancel, saving }) {
|
||||
<div>
|
||||
<label class="text-sm opacity-70">${t('routes.threshold_label')}</label>
|
||||
<input type="number" id="route-modal-threshold" class="input input-sm w-full"
|
||||
.value=${route.packet_count_threshold || 3} min="1" max="10000" />
|
||||
.value=${route.packet_count_threshold || 5} min="1" max="10000" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm opacity-70">${t('routes.clear_label')}</label>
|
||||
<input type="number" id="route-modal-clear" class="input input-sm w-full"
|
||||
.value=${route.clear_threshold || ''}
|
||||
placeholder="${2 * (route.packet_count_threshold || 3)}" min="1" />
|
||||
placeholder="${3 * (route.packet_count_threshold || 5)}" min="1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm opacity-70">${t('routes.span_label')}</label>
|
||||
@@ -633,7 +633,7 @@ export async function render(container, params, router) {
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
modalState = _newModalState('add', { visibility: 'community', enabled: true, match_width: 1, window_hours: 48, max_hop_span: 8 });
|
||||
modalState = _newModalState('add', { visibility: 'community', enabled: true, match_width: 1, window_hours: 48, max_hop_span: 8, packet_count_threshold: 5 });
|
||||
renderPage(routes);
|
||||
}
|
||||
|
||||
@@ -804,7 +804,7 @@ export async function render(container, params, router) {
|
||||
visibility: visEl.value,
|
||||
match_width: parseInt(widthEl.value, 10) || 1,
|
||||
window_hours: parseInt(windowEl.value, 10) || 48,
|
||||
packet_count_threshold: parseInt(thresholdEl.value, 10) || 3,
|
||||
packet_count_threshold: parseInt(thresholdEl.value, 10) || 5,
|
||||
max_hop_span: spanEl.value ? parseInt(spanEl.value, 10) : null,
|
||||
enabled: enabledEl.checked,
|
||||
reversible: reversibleEl.checked,
|
||||
|
||||
@@ -390,7 +390,7 @@ class TestRouteQualityAvg:
|
||||
quality="clear",
|
||||
matched_count=1,
|
||||
threshold=route.packet_count_threshold,
|
||||
effective_clear=route.packet_count_threshold * 2,
|
||||
effective_clear=route.packet_count_threshold * 3,
|
||||
quality_avg=value,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@ class TestRunEvaluation:
|
||||
route = _make_route(
|
||||
db_session, "R1", [node_a, node_b], packet_count_threshold=3
|
||||
)
|
||||
for i in range(7):
|
||||
for i in range(10):
|
||||
_make_reception(db_session, f"pkt{i}", ["AA", "BB"])
|
||||
db_session.commit()
|
||||
|
||||
@@ -117,7 +117,7 @@ class TestRunEvaluation:
|
||||
assert result.state == RouteState.HEALTHY.value
|
||||
assert result.quality == RouteQuality.CLEAR.value
|
||||
assert result.threshold == 3
|
||||
assert result.effective_clear == 6
|
||||
assert result.effective_clear == 9
|
||||
|
||||
def test_evaluation_error_logged(self, db_manager, db_session, monkeypatch):
|
||||
"""An exception evaluating one route is caught; count stays 0."""
|
||||
|
||||
@@ -212,14 +212,14 @@ class TestEffectiveClear:
|
||||
)
|
||||
assert effective_clear_threshold(route) == 20
|
||||
|
||||
def test_default_2x(self, db_session):
|
||||
def test_default_3x(self, db_session):
|
||||
route = Route(
|
||||
from_label="t",
|
||||
to_label="t",
|
||||
packet_count_threshold=5,
|
||||
clear_threshold=None,
|
||||
)
|
||||
assert effective_clear_threshold(route) == 10
|
||||
assert effective_clear_threshold(route) == 15
|
||||
|
||||
|
||||
class TestDeriveExpectedHash:
|
||||
@@ -681,7 +681,7 @@ class TestPreviewRoute:
|
||||
node_a = _make_node(db_session, "aa" + "0" * 62)
|
||||
node_b = _make_node(db_session, "bb" + "0" * 62)
|
||||
|
||||
for i in range(7):
|
||||
for i in range(10):
|
||||
_make_reception(db_session, None, f"pkt{i}", ["AA", "BB"])
|
||||
db_session.commit()
|
||||
|
||||
@@ -695,7 +695,7 @@ class TestPreviewRoute:
|
||||
},
|
||||
since,
|
||||
)
|
||||
assert result["matched_count"] == 7
|
||||
assert result["matched_count"] == 10
|
||||
assert result["quality"] == RouteQuality.CLEAR.value
|
||||
assert result["truncated"] is False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user