diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-19 16:02:04 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-19 16:02:04 +0000 |
| commit | 1adbd93e5bb8e14403ba64a76d5dc93209227514 (patch) | |
| tree | cf21d9ec7958ac1918143b72649c53eb14804283 | |
| parent | 02a90c109d4d08c6a54184f821c100f4eba92545 (diff) | |
docs: cleanup
| -rw-r--r-- | docs/explanation/state-structure-redesign-proposal.md | 373 | ||||
| -rw-r--r-- | work/phase1-baseline.md | 159 |
2 files changed, 0 insertions, 532 deletions
diff --git a/docs/explanation/state-structure-redesign-proposal.md b/docs/explanation/state-structure-redesign-proposal.md deleted file mode 100644 index 0a27cf4..0000000 --- a/docs/explanation/state-structure-redesign-proposal.md +++ /dev/null | |||
| @@ -1,373 +0,0 @@ | |||
| 1 | # State Structure Redesign Proposal v2 | ||
| 2 | |||
| 3 | ## The Core Problem | ||
| 4 | |||
| 5 | We need to transform: | ||
| 6 | - **Repo Announcements** (30617) that list relays | ||
| 7 | - **Root Events** (1617/1618/1619/1621) that tag repos | ||
| 8 | |||
| 9 | Into: | ||
| 10 | - **Per-relay subscriptions**: which repos and root events to sync from each relay | ||
| 11 | |||
| 12 | And generate **RelayActions** when this mapping changes. | ||
| 13 | |||
| 14 | --- | ||
| 15 | |||
| 16 | ## Proposed Data Model | ||
| 17 | |||
| 18 | ### 1. RepoIndex (Primary Source of Truth) | ||
| 19 | |||
| 20 | ```rust | ||
| 21 | /// Everything we know about repos we're tracking | ||
| 22 | /// Key: repo addressable ref ("30617:pubkey:identifier") | ||
| 23 | pub type RepoIndex = Arc<RwLock<HashMap<String, RepoInfo>>>; | ||
| 24 | |||
| 25 | #[derive(Debug, Clone, Default)] | ||
| 26 | pub struct RepoInfo { | ||
| 27 | /// Relay URLs listed in the repo's announcement | ||
| 28 | pub relays: HashSet<String>, | ||
| 29 | /// Root event IDs that reference this repo | ||
| 30 | pub root_events: HashSet<EventId>, | ||
| 31 | } | ||
| 32 | ``` | ||
| 33 | |||
| 34 | **Updated by:** Database init, batch processing of new announcements/root events | ||
| 35 | |||
| 36 | ### 2. RelayIndex (Applied State) | ||
| 37 | |||
| 38 | ```rust | ||
| 39 | /// What we've told each relay to sync | ||
| 40 | /// Key: relay URL | ||
| 41 | pub type RelayIndex = Arc<RwLock<HashMap<String, SyncTarget>>>; | ||
| 42 | |||
| 43 | #[derive(Debug, Clone, Default, PartialEq)] | ||
| 44 | pub struct SyncTarget { | ||
| 45 | /// Repos we're syncing for this relay | ||
| 46 | pub repos: HashSet<String>, | ||
| 47 | /// Root events we're tracking | ||
| 48 | pub root_events: HashSet<EventId>, | ||
| 49 | } | ||
| 50 | ``` | ||
| 51 | |||
| 52 | **Updated by:** SyncManager after RelayActions are applied | ||
| 53 | |||
| 54 | --- | ||
| 55 | |||
| 56 | ## The Transformation | ||
| 57 | |||
| 58 | ```mermaid | ||
| 59 | flowchart LR | ||
| 60 | subgraph Input | ||
| 61 | RA[Repo Announcements] | ||
| 62 | RE[Root Events] | ||
| 63 | end | ||
| 64 | |||
| 65 | subgraph RepoIndex | ||
| 66 | R1[repo_a: relays=X,Y events=1,2] | ||
| 67 | R2[repo_b: relays=Y,Z events=3] | ||
| 68 | end | ||
| 69 | |||
| 70 | subgraph Derived Target | ||
| 71 | T1[relay_X: repos=a events=1,2] | ||
| 72 | T2[relay_Y: repos=a,b events=1,2,3] | ||
| 73 | T3[relay_Z: repos=b events=3] | ||
| 74 | end | ||
| 75 | |||
| 76 | subgraph RelayIndex Applied | ||
| 77 | A1[relay_X: repos=a events=1,2] | ||
| 78 | A2[relay_Y: repos=a events=1,2] | ||
| 79 | end | ||
| 80 | |||
| 81 | RA --> R1 | ||
| 82 | RA --> R2 | ||
| 83 | RE --> R1 | ||
| 84 | RE --> R2 | ||
| 85 | |||
| 86 | R1 --> T1 | ||
| 87 | R1 --> T2 | ||
| 88 | R2 --> T2 | ||
| 89 | R2 --> T3 | ||
| 90 | ``` | ||
| 91 | |||
| 92 | The **diff** between Derived Target and RelayIndex produces RelayActions: | ||
| 93 | - relay_Y needs AddFilters for repo_b and event 3 | ||
| 94 | - relay_Z needs SpawnRelay | ||
| 95 | |||
| 96 | --- | ||
| 97 | |||
| 98 | ## Algorithm: derive_target_from_repo_index | ||
| 99 | |||
| 100 | ```rust | ||
| 101 | /// Derive what we SHOULD be syncing from the repo data | ||
| 102 | fn derive_relay_targets(repo_index: &HashMap<String, RepoInfo>) -> HashMap<String, SyncTarget> { | ||
| 103 | let mut targets: HashMap<String, SyncTarget> = HashMap::new(); | ||
| 104 | |||
| 105 | for (repo_ref, info) in repo_index { | ||
| 106 | // For each relay that lists this repo | ||
| 107 | for relay_url in &info.relays { | ||
| 108 | let target = targets.entry(relay_url.clone()).or_default(); | ||
| 109 | target.repos.insert(repo_ref.clone()); | ||
| 110 | target.root_events.extend(info.root_events.iter().cloned()); | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 114 | targets | ||
| 115 | } | ||
| 116 | ``` | ||
| 117 | |||
| 118 | --- | ||
| 119 | |||
| 120 | ## Algorithm: process_batch | ||
| 121 | |||
| 122 | ```rust | ||
| 123 | async fn process_batch(&self, pending: &mut PendingUpdates) { | ||
| 124 | // ============================================ | ||
| 125 | // STEP 1: Update RepoIndex from batch | ||
| 126 | // ============================================ | ||
| 127 | |||
| 128 | let mut repo_index = self.repo_index.write().await; | ||
| 129 | |||
| 130 | // 1a. Process root events - add to repo's root_events set | ||
| 131 | for event in pending.root_events.drain(..) { | ||
| 132 | for repo_ref in extract_repo_refs(&event) { | ||
| 133 | repo_index.entry(repo_ref) | ||
| 134 | .or_default() | ||
| 135 | .root_events | ||
| 136 | .insert(event.id); | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | // 1b. Process announcements - update repo's relay set | ||
| 141 | for event in pending.announcements.drain(..) { | ||
| 142 | if !lists_our_service(&event) { | ||
| 143 | continue; | ||
| 144 | } | ||
| 145 | let repo_ref = build_repo_ref(&event); | ||
| 146 | let relay_urls: HashSet<String> = extract_relay_urls(&event) | ||
| 147 | .into_iter() | ||
| 148 | .filter(|url| !is_own_relay(url)) | ||
| 149 | .collect(); | ||
| 150 | |||
| 151 | // Replace relay set (handles updates that change relays) | ||
| 152 | repo_index.entry(repo_ref) | ||
| 153 | .or_default() | ||
| 154 | .relays = relay_urls; | ||
| 155 | } | ||
| 156 | |||
| 157 | // ============================================ | ||
| 158 | // STEP 2: Derive target state from RepoIndex | ||
| 159 | // ============================================ | ||
| 160 | |||
| 161 | let target = derive_relay_targets(&repo_index); | ||
| 162 | drop(repo_index); // Release write lock | ||
| 163 | |||
| 164 | // ============================================ | ||
| 165 | // STEP 3: Diff target vs applied (RelayIndex) | ||
| 166 | // ============================================ | ||
| 167 | |||
| 168 | let applied = self.relay_index.read().await; | ||
| 169 | let actions = compute_relay_actions(&target, &applied); | ||
| 170 | drop(applied); // Release read lock | ||
| 171 | |||
| 172 | // ============================================ | ||
| 173 | // STEP 4: Send actions & update RelayIndex | ||
| 174 | // ============================================ | ||
| 175 | |||
| 176 | for action in actions { | ||
| 177 | match &action { | ||
| 178 | RelayAction::SpawnRelay { relay_url, repos_and_root_events } => { | ||
| 179 | // Update RelayIndex with new relay | ||
| 180 | let mut applied = self.relay_index.write().await; | ||
| 181 | applied.insert(relay_url.clone(), SyncTarget { | ||
| 182 | repos: repos_and_root_events.keys().cloned().collect(), | ||
| 183 | root_events: repos_and_root_events.values() | ||
| 184 | .flat_map(|e| e.iter().cloned()) | ||
| 185 | .collect(), | ||
| 186 | }); | ||
| 187 | } | ||
| 188 | RelayAction::AddFilters { relay_url, repos_and_new_root_event } => { | ||
| 189 | // Update RelayIndex with additions | ||
| 190 | let mut applied = self.relay_index.write().await; | ||
| 191 | if let Some(target) = applied.get_mut(relay_url) { | ||
| 192 | for (repo, events) in repos_and_new_root_event { | ||
| 193 | target.repos.insert(repo.clone()); | ||
| 194 | target.root_events.extend(events.iter().cloned()); | ||
| 195 | } | ||
| 196 | } | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | // Send action to SyncManager | ||
| 201 | let _ = self.action_tx.send(action).await; | ||
| 202 | } | ||
| 203 | } | ||
| 204 | ``` | ||
| 205 | |||
| 206 | --- | ||
| 207 | |||
| 208 | ## Algorithm: compute_relay_actions | ||
| 209 | |||
| 210 | ```rust | ||
| 211 | fn compute_relay_actions( | ||
| 212 | target: &HashMap<String, SyncTarget>, | ||
| 213 | applied: &HashMap<String, SyncTarget>, | ||
| 214 | ) -> Vec<RelayAction> { | ||
| 215 | let mut actions = Vec::new(); | ||
| 216 | |||
| 217 | for (relay_url, target_state) in target { | ||
| 218 | match applied.get(relay_url) { | ||
| 219 | None => { | ||
| 220 | // New relay - spawn it | ||
| 221 | let mut repos_and_events = HashMap::new(); | ||
| 222 | for repo in &target_state.repos { | ||
| 223 | // Get events for this specific repo | ||
| 224 | let events = target_state.root_events.clone(); // simplified | ||
| 225 | repos_and_events.insert(repo.clone(), events); | ||
| 226 | } | ||
| 227 | actions.push(RelayAction::SpawnRelay { | ||
| 228 | relay_url: relay_url.clone(), | ||
| 229 | repos_and_root_events: repos_and_events, | ||
| 230 | }); | ||
| 231 | } | ||
| 232 | Some(applied_state) => { | ||
| 233 | // Existing relay - check for new repos/events | ||
| 234 | let new_repos: HashSet<_> = target_state.repos | ||
| 235 | .difference(&applied_state.repos) | ||
| 236 | .cloned() | ||
| 237 | .collect(); | ||
| 238 | let new_events: HashSet<_> = target_state.root_events | ||
| 239 | .difference(&applied_state.root_events) | ||
| 240 | .cloned() | ||
| 241 | .collect(); | ||
| 242 | |||
| 243 | if !new_repos.is_empty() || !new_events.is_empty() { | ||
| 244 | let mut repos_and_events = HashMap::new(); | ||
| 245 | for repo in &new_repos { | ||
| 246 | repos_and_events.insert(repo.clone(), new_events.clone()); | ||
| 247 | } | ||
| 248 | // Also handle new events for existing repos | ||
| 249 | if !new_events.is_empty() && new_repos.is_empty() { | ||
| 250 | for repo in &applied_state.repos { | ||
| 251 | repos_and_events.insert(repo.clone(), new_events.clone()); | ||
| 252 | } | ||
| 253 | } | ||
| 254 | |||
| 255 | actions.push(RelayAction::AddFilters { | ||
| 256 | relay_url: relay_url.clone(), | ||
| 257 | repos_and_new_root_event: repos_and_events, | ||
| 258 | }); | ||
| 259 | } | ||
| 260 | } | ||
| 261 | } | ||
| 262 | } | ||
| 263 | |||
| 264 | // Future: detect relay removal (in applied but not in target) | ||
| 265 | |||
| 266 | actions | ||
| 267 | } | ||
| 268 | ``` | ||
| 269 | |||
| 270 | --- | ||
| 271 | |||
| 272 | ## Handling Announcement Updates | ||
| 273 | |||
| 274 | When an announcement is **updated** and changes its relay list: | ||
| 275 | |||
| 276 | ```mermaid | ||
| 277 | flowchart TD | ||
| 278 | A[repo_a announcement updated] --> B[Old: relays X,Y] | ||
| 279 | B --> C[New: relays Y,Z] | ||
| 280 | C --> D[RepoIndex updated: repo_a.relays = Y,Z] | ||
| 281 | D --> E[derive_relay_targets] | ||
| 282 | E --> F[Target: X=empty, Y=repo_a, Z=repo_a] | ||
| 283 | F --> G[Diff with Applied: X=repo_a, Y=repo_a] | ||
| 284 | G --> H1[X: repo_a removed - future RemoveFilters] | ||
| 285 | G --> H2[Z: new relay - SpawnRelay] | ||
| 286 | ``` | ||
| 287 | |||
| 288 | The current RelayAction types only support growth (SpawnRelay, AddFilters). Removal would need a new `RemoveFilters` action type - this is a future enhancement. | ||
| 289 | |||
| 290 | --- | ||
| 291 | |||
| 292 | ## Name Mappings | ||
| 293 | |||
| 294 | | Current | Proposed | Semantics | | ||
| 295 | |---------|----------|-----------| | ||
| 296 | | `FollowingRepoRootEvents` | `RepoIndex` | Per-repo: relays + root events | | ||
| 297 | | `SyncRelays` | `RelayIndex` | Per-relay: what we're syncing (applied state) | | ||
| 298 | | - | `SyncTarget` | Struct for repos + events | | ||
| 299 | | - | `RepoInfo` | Struct for relay set + event set | | ||
| 300 | |||
| 301 | --- | ||
| 302 | |||
| 303 | ## Data Flow Summary | ||
| 304 | |||
| 305 | ```mermaid | ||
| 306 | flowchart TB | ||
| 307 | subgraph Batch Input | ||
| 308 | RA[30617 Announcements] | ||
| 309 | RE[Root Events 1617-1621] | ||
| 310 | end | ||
| 311 | |||
| 312 | subgraph Step 1: Update Source | ||
| 313 | RI[RepoIndex] | ||
| 314 | end | ||
| 315 | |||
| 316 | subgraph Step 2: Derive Target | ||
| 317 | DT[derive_relay_targets] | ||
| 318 | TGT[Target HashMap] | ||
| 319 | end | ||
| 320 | |||
| 321 | subgraph Step 3: Diff | ||
| 322 | RLI[RelayIndex - Applied] | ||
| 323 | DIFF[compute_relay_actions] | ||
| 324 | end | ||
| 325 | |||
| 326 | subgraph Step 4: Apply | ||
| 327 | ACT[RelayActions] | ||
| 328 | SM[SyncManager] | ||
| 329 | end | ||
| 330 | |||
| 331 | RA --> RI | ||
| 332 | RE --> RI | ||
| 333 | RI --> DT | ||
| 334 | DT --> TGT | ||
| 335 | TGT --> DIFF | ||
| 336 | RLI --> DIFF | ||
| 337 | DIFF --> ACT | ||
| 338 | ACT --> SM | ||
| 339 | ACT --> |update| RLI | ||
| 340 | ``` | ||
| 341 | |||
| 342 | --- | ||
| 343 | |||
| 344 | ## Files to Modify | ||
| 345 | |||
| 346 | | File | Changes | | ||
| 347 | |------|---------| | ||
| 348 | | [`src/sync/mod.rs`](src/sync/mod.rs) | Replace type aliases with RepoIndex/RelayIndex + structs | | ||
| 349 | | [`src/sync/self_subscriber.rs`](src/sync/self_subscriber.rs) | Rewrite process_batch with new algorithm | | ||
| 350 | |||
| 351 | --- | ||
| 352 | |||
| 353 | ## Questions for Approval | ||
| 354 | |||
| 355 | 1. **Naming**: Are `RepoIndex`/`RelayIndex` and `RepoInfo`/`SyncTarget` clear enough? | ||
| 356 | |||
| 357 | 2. **When to update RelayIndex**: Should we: | ||
| 358 | - (a) Update immediately when generating action (optimistic) ← proposed above | ||
| 359 | - (b) Update only after SyncManager confirms action succeeded | ||
| 360 | |||
| 361 | 3. **Bootstrap relay**: Keep special-casing it in RelayIndex (always present)? | ||
| 362 | |||
| 363 | 4. **Future work**: Add `RemoveFilters` action for relay removal, or defer? | ||
| 364 | |||
| 365 | --- | ||
| 366 | |||
| 367 | ## Benefits | ||
| 368 | |||
| 369 | 1. **Logical flow**: Source → Derived → Diff → Actions | ||
| 370 | 2. **Single source of truth**: RepoIndex is the authoritative data | ||
| 371 | 3. **Clear transformation**: `derive_relay_targets()` is a pure function | ||
| 372 | 4. **Handles updates**: Replacing `repo.relays` naturally handles announcement changes | ||
| 373 | 5. **Testable**: Each step can be unit tested independently \ No newline at end of file | ||
diff --git a/work/phase1-baseline.md b/work/phase1-baseline.md deleted file mode 100644 index 8bd3902..0000000 --- a/work/phase1-baseline.md +++ /dev/null | |||
| @@ -1,159 +0,0 @@ | |||
| 1 | # Phase 1: Sync Test Baseline | ||
| 2 | |||
| 3 | **Timestamp:** 2025-12-18T16:50:07Z (UTC) | ||
| 4 | **Git Commit:** (pre-refactoring baseline) | ||
| 5 | |||
| 6 | ## Test Execution Command | ||
| 7 | ```bash | ||
| 8 | cargo test --test sync | ||
| 9 | ``` | ||
| 10 | |||
| 11 | ## Summary Statistics | ||
| 12 | |||
| 13 | - **Total Tests:** 40 | ||
| 14 | - **Passed:** 38 (95%) | ||
| 15 | - **Failed:** 2 (5%) | ||
| 16 | - **Ignored:** 0 | ||
| 17 | - **Filtered Out:** 0 | ||
| 18 | - **Execution Time:** 8.05s | ||
| 19 | |||
| 20 | ## Passing Tests (38) | ||
| 21 | |||
| 22 | ### Common Module Tests (7) | ||
| 23 | - `common::relay::tests::test_find_free_port` | ||
| 24 | - `common::sync_helpers::tests::test_parse_empty_metrics` | ||
| 25 | - `common::sync_helpers::tests::test_parse_counter_with_labels` | ||
| 26 | - `common::sync_helpers::tests::test_parse_gauge_without_labels` | ||
| 27 | - `common::sync_helpers::tests::test_parse_metric_with_relay_url_label` | ||
| 28 | - `common::sync_helpers::tests::test_repo_coord_format` | ||
| 29 | - `common::sync_helpers::tests::test_build_layer3_comment_with_uppercase_e` | ||
| 30 | |||
| 31 | ### Sync Helper Builder Tests (6) | ||
| 32 | - `common::sync_helpers::tests::test_build_layer3_comment_kind_1` | ||
| 33 | - `common::sync_helpers::tests::test_build_layer3_quote_with_q` | ||
| 34 | - `common::sync_helpers::tests::test_build_layer3_comment_kind_1111` | ||
| 35 | - `common::sync_helpers::tests::test_build_layer2_issue_event` | ||
| 36 | - `common::sync_helpers::tests::test_build_layer3_reply_with_e_tag` | ||
| 37 | - `common::sync_helpers::tests::test_build_layer2_issue_with_uppercase_a` | ||
| 38 | - `common::sync_helpers::tests::test_build_layer2_issue_with_q_tag` | ||
| 39 | |||
| 40 | ### Metrics Tests (6 passing) | ||
| 41 | - `sync::metrics::test_metric_values_are_numeric` | ||
| 42 | - `sync::metrics::test_concurrent_metrics_requests` | ||
| 43 | - `sync::metrics::test_metrics_availability_during_sync` | ||
| 44 | - `sync::metrics::test_connection_failure_increments_counter` | ||
| 45 | - `sync::metrics::test_prometheus_format_valid` | ||
| 46 | - `sync::metrics::test_relay_connected_status` | ||
| 47 | - `sync::metrics::test_health_state_degrades_on_failure` | ||
| 48 | - `sync::metrics::test_startup_sync_event_count` | ||
| 49 | |||
| 50 | ### Live Sync Tests (3) | ||
| 51 | - `sync::live_sync::test_live_sync_layer2_events` | ||
| 52 | - `sync::live_sync::test_live_sync_layer3_events` | ||
| 53 | - `sync::live_sync::test_live_sync_event_ordering` | ||
| 54 | |||
| 55 | ### Bootstrap Tests (3) | ||
| 56 | - `sync::bootstrap::test_announcement_not_listing_relay_is_not_synced` | ||
| 57 | - `sync::bootstrap::test_history_sync_without_negentropy` | ||
| 58 | - `sync::bootstrap::test_bootstrap_syncs_existing_layer2_events` | ||
| 59 | - `sync::bootstrap::test_relay_replays_events_after_restart` | ||
| 60 | |||
| 61 | ### Discovery Tests (3) | ||
| 62 | - `sync::discovery::test_layer2_discovery_with_chain` | ||
| 63 | - `sync::discovery::test_discovers_layer3_via_layer2` | ||
| 64 | - `sync::discovery::test_recursive_relay_discovery_syncs_announcement` | ||
| 65 | |||
| 66 | ### Tag Variations Tests (6) | ||
| 67 | - `sync::tag_variations::test_layer2_sync_with_lowercase_a_tag` | ||
| 68 | - `sync::tag_variations::test_layer2_sync_with_q_tag` | ||
| 69 | - `sync::tag_variations::test_layer2_sync_with_uppercase_a_tag` | ||
| 70 | - `sync::tag_variations::test_layer3_sync_with_lowercase_e_tag` | ||
| 71 | - `sync::tag_variations::test_layer3_sync_with_q_tag` | ||
| 72 | - `sync::tag_variations::test_layer3_sync_with_uppercase_e_tag` | ||
| 73 | |||
| 74 | ## Failing Tests (2) | ||
| 75 | |||
| 76 | ### 1. sync::metrics::test_live_sync_event_count | ||
| 77 | |||
| 78 | **Location:** `tests/sync/metrics.rs:444` | ||
| 79 | |||
| 80 | **Error Type:** Assertion failure | ||
| 81 | |||
| 82 | **Details:** | ||
| 83 | ``` | ||
| 84 | assertion `left == right` failed: Should have 2 live events | ||
| 85 | left: None | ||
| 86 | right: Some(2) | ||
| 87 | ``` | ||
| 88 | |||
| 89 | **Root Cause:** Live event counting metric is not being populated. The metric parser is returning `None` when it should find a count of 2 live synced events. | ||
| 90 | |||
| 91 | **Output Sample:** | ||
| 92 | ``` | ||
| 93 | Live events synced: None | ||
| 94 | ``` | ||
| 95 | |||
| 96 | **Impact:** This suggests that the `sync_events_total{sync_type="live"}` metric either: | ||
| 97 | - Is not being incremented correctly during live sync | ||
| 98 | - Is using a different metric name/label than expected | ||
| 99 | - Is not being exposed in the metrics endpoint | ||
| 100 | |||
| 101 | --- | ||
| 102 | |||
| 103 | ### 2. sync::metrics::test_multi_source_aggregate_counts | ||
| 104 | |||
| 105 | **Location:** `tests/sync/metrics.rs:603` | ||
| 106 | |||
| 107 | **Error Type:** Assertion failure | ||
| 108 | |||
| 109 | **Details:** | ||
| 110 | ``` | ||
| 111 | assertion `left == right` failed: Should have 0 connected | ||
| 112 | left: Some(1) | ||
| 113 | right: Some(0) | ||
| 114 | ``` | ||
| 115 | |||
| 116 | **Root Cause:** After stopping a relay connection, the `sync_relays_connected_total` metric is not being decremented. The test expects 0 connected relays after calling stop, but the metric still shows 1. | ||
| 117 | |||
| 118 | **Output Sample:** | ||
| 119 | ``` | ||
| 120 | Tracked total: Some(1) | ||
| 121 | Connected total: Some(1) | ||
| 122 | After stop - Tracked total: Some(1) | ||
| 123 | After stop - Connected total: Some(1) | ||
| 124 | ``` | ||
| 125 | |||
| 126 | **Impact:** This indicates that relay disconnection is not properly updating the connection count metric. This could be: | ||
| 127 | - A lifecycle issue where the metric update happens asynchronously after the test assertion | ||
| 128 | - A bug where the disconnect handler doesn't decrement the counter | ||
| 129 | - A race condition in the test timing | ||
| 130 | |||
| 131 | --- | ||
| 132 | |||
| 133 | ## Analysis | ||
| 134 | |||
| 135 | ### Test Health | ||
| 136 | The sync test suite is in relatively good shape with a 95% pass rate. The failures are both isolated to the metrics module and appear to be either timing/synchronization issues or metric collection bugs rather than fundamental sync logic problems. | ||
| 137 | |||
| 138 | ### Pre-existing Issues | ||
| 139 | Both failing tests appear to be pre-existing issues unrelated to the planned refactoring work. They should be tracked separately and not conflated with any issues introduced during the refactor. | ||
| 140 | |||
| 141 | ### Refactoring Risk Assessment | ||
| 142 | - **Low Risk Areas:** Bootstrap, discovery, live_sync, tag_variations modules are all passing | ||
| 143 | - **Medium Risk Area:** Metrics tests have 2 failures, but they're specific to metric collection, not sync functionality | ||
| 144 | - **Safe to Refactor:** The core sync logic tests are passing, so structural refactoring of test helpers and organization should not affect test outcomes | ||
| 145 | |||
| 146 | ## Next Steps | ||
| 147 | |||
| 148 | This baseline will be used to: | ||
| 149 | 1. Verify that refactoring doesn't introduce new failures | ||
| 150 | 2. Distinguish pre-existing failures from regressions | ||
| 151 | 3. Track if the refactoring inadvertently fixes the existing failures | ||
| 152 | 4. Ensure that after refactoring, we still have 38 passing tests (or more if we fix the failing ones) | ||
| 153 | |||
| 154 | ## Notes | ||
| 155 | |||
| 156 | - Both failures are in `tests/sync/metrics.rs` | ||
| 157 | - The failures appear to be metric collection/timing issues rather than sync logic bugs | ||
| 158 | - All functional sync tests (bootstrap, discovery, live_sync, tag_variations) are passing | ||
| 159 | - The refactoring should not affect these test results unless we accidentally change metric collection timing \ No newline at end of file | ||