Skip to content

Commit 2b7aa3a

Browse files
refactor: make route exploration deterministic
1 parent 309e802 commit 2b7aa3a

4 files changed

Lines changed: 209 additions & 69 deletions

File tree

WinMTR.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,7 @@
14161416
<ClInclude Include="WinMTRMain.h" />
14171417
<ClInclude Include="WinMTRNetworkData.h" />
14181418
<ClInclude Include="WinMTRProperties.h" />
1419+
<ClInclude Include="WinMTRRoutePolicy.h" />
14191420
<ClInclude Include="WinMTRSerialization.h" />
14201421
<ClCompile Include="WinMTRUtils.ixx">
14211422
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>

WinMTRNet-Tracing.cpp

Lines changed: 19 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ of the License.
1212
module;
1313
#pragma warning (disable : 4005)
1414
#include "targetver.h"
15+
#include "WinMTRRoutePolicy.h"
1516
#define WIN32_LEAN_AND_MEAN
1617
#define VC_EXTRALEAN
1718
#define NOMCX
@@ -670,6 +671,15 @@ WinMTRTraceResult WinMTRNet::DoTrace(std::stop_token stop_token, SOCKADDR_INET a
670671
auto now = monotonic_now();
671672
scheduler.start(this_session, current_epoch, now);
672673
const auto mandatory_ttl = std::max(trace_options.start_ttl, trace_options.minimum_ttl);
674+
winmtr::route::RoutePolicy route_policy({
675+
.start_ttl = trace_options.start_ttl,
676+
.minimum_ttl = trace_options.minimum_ttl,
677+
.max_hops = trace_options.max_hops,
678+
.unknown_host_limit = trace_options.unknown_host_limit,
679+
.exploration_period = WinMTRUtils::PATH_EXPLORATION_PERIOD,
680+
.exploration_frontier_ttls = WinMTRUtils::PATH_EXPLORATION_FRONTIER_TTLS,
681+
.shrink_confirmations = WinMTRUtils::PATH_SHRINK_CONFIRMATIONS,
682+
});
673683
// The first path discovery is complete but staggered over one interval. A
674684
// full initial sweep avoids hiding a destination behind an early run of
675685
// silent routers; subsequent cycles use the configured unknown tail.
@@ -682,13 +692,6 @@ WinMTRTraceResult WinMTRNet::DoTrace(std::stop_token stop_token, SOCKADDR_INET a
682692
bool session_had_usable_reply = false;
683693
bool stopping = false;
684694
std::optional<winmtr::probe::MonotonicMilliseconds> drain_deadline;
685-
bool exploration_cycle = true;
686-
bool initial_discovery = true;
687-
unsigned highest_response_ttl = 0;
688-
unsigned destination_ttl = 0;
689-
unsigned stable_ceiling = initial_ceiling;
690-
unsigned shrink_candidate = 0;
691-
unsigned shrink_confirmations = 0;
692695

693696
const auto notify_changed = [this] {
694697
if (options != nullptr) options->notifyTraceDataChanged();
@@ -704,50 +707,8 @@ WinMTRTraceResult WinMTRNet::DoTrace(std::stop_token stop_token, SOCKADDR_INET a
704707
&& minimum > reported_cycles) {
705708
reported_cycles = minimum;
706709
setCompletedCycles(reported_cycles, current_epoch);
707-
if (exploration_cycle) {
708-
const auto tail_origin = highest_response_ttl == 0
709-
? trace_options.start_ttl - 1u
710-
: highest_response_ttl;
711-
const auto normal_ceiling = destination_ttl != 0
712-
? std::max(mandatory_ttl, destination_ttl)
713-
: std::min(trace_options.max_hops,
714-
std::max(mandatory_ttl, tail_origin + trace_options.unknown_host_limit));
715-
if (initial_discovery) {
716-
stable_ceiling = normal_ceiling;
717-
initial_discovery = false;
718-
shrink_candidate = 0;
719-
shrink_confirmations = 0;
720-
}
721-
else if (normal_ceiling < stable_ceiling) {
722-
if (shrink_candidate == normal_ceiling) {
723-
++shrink_confirmations;
724-
}
725-
else {
726-
shrink_candidate = normal_ceiling;
727-
shrink_confirmations = 1;
728-
}
729-
if (shrink_confirmations >= WinMTRUtils::PATH_SHRINK_CONFIRMATIONS) {
730-
stable_ceiling = normal_ceiling;
731-
shrink_candidate = 0;
732-
shrink_confirmations = 0;
733-
}
734-
}
735-
else {
736-
stable_ceiling = normal_ceiling;
737-
shrink_candidate = 0;
738-
shrink_confirmations = 0;
739-
}
740-
scheduler.set_last_ttl(stable_ceiling, monotonic_now());
741-
exploration_cycle = false;
742-
}
743-
if (reported_cycles % WinMTRUtils::PATH_EXPLORATION_PERIOD == 0
744-
&& stable_ceiling < trace_options.max_hops) {
745-
exploration_cycle = true;
746-
highest_response_ttl = 0;
747-
destination_ttl = 0;
748-
const auto frontier_ceiling = std::min(trace_options.max_hops,
749-
stable_ceiling + WinMTRUtils::PATH_EXPLORATION_FRONTIER_TTLS);
750-
scheduler.set_last_ttl(frontier_ceiling, monotonic_now());
710+
if (const auto ceiling = route_policy.complete_cycle(reported_cycles)) {
711+
scheduler.set_last_ttl(*ceiling, monotonic_now());
751712
}
752713
}
753714
};
@@ -760,13 +721,7 @@ WinMTRTraceResult WinMTRNet::DoTrace(std::stop_token stop_token, SOCKADDR_INET a
760721
reported_cycles = 0;
761722
session_reached_destination = false;
762723
session_had_usable_reply = false;
763-
exploration_cycle = true;
764-
initial_discovery = true;
765-
highest_response_ttl = 0;
766-
destination_ttl = 0;
767-
stable_ceiling = initial_ceiling;
768-
shrink_candidate = 0;
769-
shrink_confirmations = 0;
724+
route_policy.reset();
770725
scheduler.restart(current_epoch, now);
771726
scheduler.set_last_ttl(initial_ceiling, now);
772727
}
@@ -776,8 +731,8 @@ WinMTRTraceResult WinMTRNet::DoTrace(std::stop_token stop_token, SOCKADDR_INET a
776731
if (found == requests.end()) continue;
777732
const auto disposition = scheduler.complete(request->token,
778733
request->completion_kind, request->completed_at);
779-
const auto accepted_after_destination = destination_ttl != 0
780-
&& request->token.ttl > std::max(mandatory_ttl, destination_ttl)
734+
const auto accepted_after_destination = route_policy.destination_ttl() != 0
735+
&& request->token.ttl > std::max(mandatory_ttl, route_policy.destination_ttl())
781736
&& (disposition == winmtr::probe::CompletionDisposition::accepted_reply
782737
|| disposition == winmtr::probe::CompletionDisposition::accepted_timeout
783738
|| disposition == winmtr::probe::CompletionDisposition::accepted_local_error
@@ -802,12 +757,9 @@ WinMTRTraceResult WinMTRNet::DoTrace(std::stop_token stop_token, SOCKADDR_INET a
802757
trace_options.resolve_hostnames,
803758
trace_options.lookup_asn_isp);
804759
session_had_usable_reply = true;
805-
highest_response_ttl = std::max(highest_response_ttl, request->token.ttl);
760+
route_policy.note_reply(request->token.ttl, request->destination_reply);
806761
if (request->destination_reply) {
807762
session_reached_destination = true;
808-
destination_ttl = destination_ttl == 0
809-
? request->token.ttl
810-
: std::min(destination_ttl, request->token.ttl);
811763
scheduler.set_last_ttl(std::max(mandatory_ttl, request->token.ttl), now);
812764
}
813765
notify_changed();
@@ -847,8 +799,8 @@ WinMTRTraceResult WinMTRNet::DoTrace(std::stop_token stop_token, SOCKADDR_INET a
847799
drain_deadline = now + trace_options.grace_ms;
848800
}
849801
for (const auto& expired : scheduler.expire(now)) {
850-
if (destination_ttl != 0
851-
&& expired.ttl > std::max(mandatory_ttl, destination_ttl)) {
802+
if (route_policy.destination_ttl() != 0
803+
&& expired.ttl > std::max(mandatory_ttl, route_policy.destination_ttl())) {
852804
commitPostDestinationCompletion(expired.ttl, expired.epoch);
853805
}
854806
else {
@@ -880,9 +832,7 @@ WinMTRTraceResult WinMTRNet::DoTrace(std::stop_token stop_token, SOCKADDR_INET a
880832
commitCacheSkipped(slot.token.ttl, slot.token.epoch);
881833
if (cached_destination) {
882834
session_reached_destination = true;
883-
destination_ttl = destination_ttl == 0
884-
? slot.token.ttl
885-
: std::min(destination_ttl, slot.token.ttl);
835+
route_policy.note_reply(slot.token.ttl, true);
886836
scheduler.set_last_ttl(std::max(mandatory_ttl, slot.token.ttl), now);
887837
}
888838
notify_changed();

WinMTRRoutePolicy.h

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <cstdint>
5+
#include <optional>
6+
7+
namespace winmtr::route {
8+
9+
struct RoutePolicyConfig final {
10+
unsigned start_ttl = 1;
11+
unsigned minimum_ttl = 0;
12+
unsigned max_hops = 30;
13+
unsigned unknown_host_limit = 12;
14+
unsigned exploration_period = 10;
15+
unsigned exploration_frontier_ttls = 5;
16+
unsigned shrink_confirmations = 2;
17+
};
18+
19+
class RoutePolicy final {
20+
public:
21+
explicit RoutePolicy(RoutePolicyConfig config) noexcept
22+
: config_(normalize(config))
23+
{
24+
reset();
25+
}
26+
27+
void reset() noexcept
28+
{
29+
exploring_ = true;
30+
initial_discovery_ = true;
31+
highest_response_ttl_ = 0;
32+
destination_ttl_ = 0;
33+
stable_ceiling_ = config_.max_hops;
34+
shrink_candidate_ = 0;
35+
shrink_confirmations_ = 0;
36+
}
37+
38+
void note_reply(unsigned ttl, bool destination) noexcept
39+
{
40+
if (ttl < config_.start_ttl || ttl > config_.max_hops) return;
41+
highest_response_ttl_ = std::max(highest_response_ttl_, ttl);
42+
if (destination) {
43+
destination_ttl_ = destination_ttl_ == 0
44+
? ttl
45+
: std::min(destination_ttl_, ttl);
46+
}
47+
}
48+
49+
// Returns a new active TTL ceiling only when the scheduler must change it.
50+
[[nodiscard]] std::optional<unsigned> complete_cycle(
51+
std::uint64_t completed_cycles) noexcept
52+
{
53+
std::optional<unsigned> next;
54+
if (exploring_) {
55+
const auto normal = normal_ceiling();
56+
if (initial_discovery_) {
57+
stable_ceiling_ = normal;
58+
initial_discovery_ = false;
59+
clear_shrink_candidate();
60+
}
61+
else if (normal < stable_ceiling_) {
62+
if (shrink_candidate_ == normal) ++shrink_confirmations_;
63+
else {
64+
shrink_candidate_ = normal;
65+
shrink_confirmations_ = 1;
66+
}
67+
if (shrink_confirmations_ >= config_.shrink_confirmations) {
68+
stable_ceiling_ = normal;
69+
clear_shrink_candidate();
70+
}
71+
}
72+
else {
73+
stable_ceiling_ = normal;
74+
clear_shrink_candidate();
75+
}
76+
exploring_ = false;
77+
next = stable_ceiling_;
78+
}
79+
80+
if (config_.exploration_period != 0 && completed_cycles != 0
81+
&& completed_cycles % config_.exploration_period == 0
82+
&& stable_ceiling_ < config_.max_hops) {
83+
exploring_ = true;
84+
highest_response_ttl_ = 0;
85+
destination_ttl_ = 0;
86+
next = std::min(config_.max_hops,
87+
stable_ceiling_ + config_.exploration_frontier_ttls);
88+
}
89+
return next;
90+
}
91+
92+
[[nodiscard]] unsigned destination_ttl() const noexcept { return destination_ttl_; }
93+
[[nodiscard]] unsigned stable_ceiling() const noexcept { return stable_ceiling_; }
94+
[[nodiscard]] bool exploring() const noexcept { return exploring_; }
95+
96+
private:
97+
[[nodiscard]] static RoutePolicyConfig normalize(RoutePolicyConfig value) noexcept
98+
{
99+
value.start_ttl = std::max(1u, value.start_ttl);
100+
value.max_hops = std::max(value.start_ttl, value.max_hops);
101+
value.minimum_ttl = std::min(value.minimum_ttl, value.max_hops);
102+
value.unknown_host_limit = std::max(1u, value.unknown_host_limit);
103+
value.shrink_confirmations = std::max(1u, value.shrink_confirmations);
104+
return value;
105+
}
106+
107+
[[nodiscard]] unsigned mandatory_ttl() const noexcept
108+
{
109+
return std::max(config_.start_ttl, config_.minimum_ttl);
110+
}
111+
112+
[[nodiscard]] unsigned normal_ceiling() const noexcept
113+
{
114+
if (destination_ttl_ != 0) {
115+
return std::max(mandatory_ttl(), destination_ttl_);
116+
}
117+
const auto tail_origin = highest_response_ttl_ == 0
118+
? config_.start_ttl - 1u
119+
: highest_response_ttl_;
120+
return std::min(config_.max_hops,
121+
std::max(mandatory_ttl(), tail_origin + config_.unknown_host_limit));
122+
}
123+
124+
void clear_shrink_candidate() noexcept
125+
{
126+
shrink_candidate_ = 0;
127+
shrink_confirmations_ = 0;
128+
}
129+
130+
RoutePolicyConfig config_;
131+
bool exploring_ = true;
132+
bool initial_discovery_ = true;
133+
unsigned highest_response_ttl_ = 0;
134+
unsigned destination_ttl_ = 0;
135+
unsigned stable_ceiling_ = 0;
136+
unsigned shrink_candidate_ = 0;
137+
unsigned shrink_confirmations_ = 0;
138+
};
139+
140+
} // namespace winmtr::route

tests/ProbeSchedulerTests.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "WinMTRAddressPolicy.h"
33
#include "WinMTRJson.h"
44
#include "WinMTRSerialization.h"
5+
#include "WinMTRRoutePolicy.h"
56

67
#include <array>
78
#include <cmath>
@@ -393,6 +394,53 @@ void test_deterministic_scheduler_resource_budget()
393394
"resource budget scheduler starved a TTL");
394395
}
395396

397+
void test_route_policy_scripted_changes()
398+
{
399+
using winmtr::route::RoutePolicy;
400+
using winmtr::route::RoutePolicyConfig;
401+
RoutePolicy policy(RoutePolicyConfig{
402+
.start_ttl = 1,
403+
.minimum_ttl = 0,
404+
.max_hops = 30,
405+
.unknown_host_limit = 12,
406+
.exploration_period = 10,
407+
.exploration_frontier_ttls = 5,
408+
.shrink_confirmations = 2,
409+
});
410+
411+
policy.note_reply(8, true);
412+
require(policy.complete_cycle(1) == 8u && policy.stable_ceiling() == 8u,
413+
"initial destination did not establish the route ceiling");
414+
require(policy.complete_cycle(10) == 13u && policy.exploring(),
415+
"periodic frontier did not extend five TTLs");
416+
policy.note_reply(12, true);
417+
require(policy.complete_cycle(11) == 12u && policy.stable_ceiling() == 12u,
418+
"longer route was not adopted after frontier discovery");
419+
420+
require(policy.complete_cycle(20) == 17u, "second frontier was not scheduled");
421+
policy.note_reply(6, true);
422+
require(policy.complete_cycle(21) == 12u && policy.stable_ceiling() == 12u,
423+
"route shrank without the required hysteresis confirmation");
424+
require(policy.complete_cycle(30) == 17u, "shrink confirmation frontier was not scheduled");
425+
policy.note_reply(6, true);
426+
require(policy.complete_cycle(31) == 6u && policy.stable_ceiling() == 6u,
427+
"confirmed shorter route was not adopted");
428+
429+
policy.reset();
430+
policy.note_reply(8, false);
431+
require(policy.complete_cycle(1) == 20u,
432+
"silent destination did not retain the configured unknown tail");
433+
434+
RoutePolicy minimumPolicy(RoutePolicyConfig{
435+
.start_ttl = 1, .minimum_ttl = 15, .max_hops = 30,
436+
.unknown_host_limit = 5, .exploration_period = 10,
437+
.exploration_frontier_ttls = 5, .shrink_confirmations = 2,
438+
});
439+
minimumPolicy.note_reply(6, true);
440+
require(minimumPolicy.complete_cycle(1) == 15u,
441+
"minimum TTL was not enforced after an early destination");
442+
}
443+
396444
void fuzz_json_parser_offline()
397445
{
398446
std::uint64_t state = 0x243f6a8885a308d3ull;
@@ -428,6 +476,7 @@ int main()
428476
test_global_rate_limit_is_fair();
429477
test_grace_cancellation_is_not_loss();
430478
test_deterministic_scheduler_resource_budget();
479+
test_route_policy_scripted_changes();
431480
fuzz_json_parser_offline();
432481
std::cout << "All probe scheduler tests passed.\n";
433482
return 0;

0 commit comments

Comments
 (0)