[DEPRECATED] Go implementation of plcbundle

fix fetchnextbundle

+90 -63
+58 -17
bundle/manager.go
··· 1076 1076 return result, nil 1077 1077 } 1078 1078 1079 - // FetchNextBundle delegates to sync.Fetcher 1079 + // FetchNextBundle fetches operations and creates a bundle, looping until caught up 1080 1080 func (m *Manager) FetchNextBundle(ctx context.Context, quiet bool) (*Bundle, error) { 1081 1081 if m.plcClient == nil { 1082 1082 return nil, fmt.Errorf("PLC client not configured") ··· 1115 1115 m.logger.Printf("Preparing bundle %06d (mempool: %d ops)...", nextBundleNum, m.mempool.Count()) 1116 1116 } 1117 1117 1118 - // ✨ Fetch operations if needed (FetchToMempool loops internally) 1119 - if m.mempool.Count() < types.BUNDLE_SIZE { 1118 + // ✨ NEW: Loop until we have enough OR catch up to latest 1119 + maxAttempts := 50 // Safety limit 1120 + attempt := 0 1121 + caughtUp := false 1122 + 1123 + for m.mempool.Count() < types.BUNDLE_SIZE && attempt < maxAttempts { 1124 + attempt++ 1125 + 1126 + needed := types.BUNDLE_SIZE - m.mempool.Count() 1127 + 1128 + if !quiet && attempt > 1 { 1129 + m.logger.Printf(" Attempt %d: Need %d more ops...", attempt, needed) 1130 + } 1131 + 1120 1132 newOps, err := m.syncer.FetchToMempool( 1121 1133 ctx, 1122 1134 afterTime, 1123 1135 prevBoundaryCIDs, 1124 - types.BUNDLE_SIZE-m.mempool.Count(), 1136 + needed, 1125 1137 quiet, 1126 1138 m.mempool.Count(), 1127 1139 ) ··· 1135 1147 } 1136 1148 1137 1149 if !quiet && added > 0 { 1138 - m.logger.Printf("Added %d new operations (mempool now: %d)", added, m.mempool.Count()) 1150 + m.logger.Printf(" Added %d new operations (mempool now: %d)", added, m.mempool.Count()) 1151 + } 1152 + 1153 + // Update cursor for next fetch 1154 + if len(newOps) > 0 { 1155 + afterTime = newOps[len(newOps)-1].CreatedAt.Format(time.RFC3339Nano) 1156 + } 1157 + } 1158 + 1159 + // Check if we caught up (got incomplete batch) 1160 + if err != nil || len(newOps) == 0 { 1161 + caughtUp = true 1162 + if !quiet { 1163 + m.logger.Printf(" Caught up to latest PLC data") 1164 + } 1165 + break 1166 + } 1167 + 1168 + // If we got a full batch but still need more, continue looping 1169 + if len(newOps) > 0 && m.mempool.Count() < types.BUNDLE_SIZE { 1170 + if !quiet { 1171 + m.logger.Printf(" Got full batch, fetching more...") 1139 1172 } 1173 + continue 1140 1174 } 1141 1175 1142 - // If fetch failed AND we don't have enough, return error 1143 - if err != nil && m.mempool.Count() < types.BUNDLE_SIZE { 1144 - m.mempool.Save() 1145 - return nil, err 1176 + // If we have enough, break 1177 + if m.mempool.Count() >= types.BUNDLE_SIZE { 1178 + break 1146 1179 } 1147 1180 } 1148 1181 1182 + // Save mempool state 1183 + m.mempool.Save() 1184 + 1149 1185 // Check if we have enough for a bundle 1150 1186 if m.mempool.Count() < types.BUNDLE_SIZE { 1151 - m.mempool.Save() 1152 - return nil, fmt.Errorf("insufficient operations: have %d, need %d (no more available)", 1153 - m.mempool.Count(), types.BUNDLE_SIZE) 1187 + if caughtUp { 1188 + return nil, fmt.Errorf("insufficient operations: have %d, need %d (caught up to latest PLC data)", 1189 + m.mempool.Count(), types.BUNDLE_SIZE) 1190 + } else { 1191 + return nil, fmt.Errorf("insufficient operations: have %d, need %d (max attempts reached)", 1192 + m.mempool.Count(), types.BUNDLE_SIZE) 1193 + } 1154 1194 } 1155 1195 1156 1196 // Create bundle 1157 1197 operations, err := m.mempool.Take(types.BUNDLE_SIZE) 1158 1198 if err != nil { 1159 - m.mempool.Save() 1160 1199 return nil, err 1161 1200 } 1162 1201 1163 - // Create bundle structure directly 1202 + // Create bundle structure 1164 1203 syncBundle := internalsync.CreateBundle(nextBundleNum, operations, afterTime, prevBundleHash, m.operations) 1165 1204 1166 - // Convert from sync.Bundle to bundle.Bundle inline 1205 + // Convert to bundle.Bundle 1167 1206 bundle := &Bundle{ 1168 1207 BundleNumber: syncBundle.BundleNumber, 1169 1208 StartTime: syncBundle.StartTime, ··· 1175 1214 BoundaryCIDs: syncBundle.BoundaryCIDs, 1176 1215 Compressed: syncBundle.Compressed, 1177 1216 CreatedAt: syncBundle.CreatedAt, 1178 - // Note: Hash fields are empty here, will be calculated in SaveBundle 1179 1217 } 1180 1218 1181 - m.mempool.Save() 1219 + if !quiet { 1220 + m.logger.Printf("✓ Bundle %06d ready (%d ops, %d DIDs)", 1221 + bundle.BundleNumber, len(bundle.Operations), bundle.DIDCount) 1222 + } 1182 1223 1183 1224 return bundle, nil 1184 1225 }
+9 -33
cmd/plcbundle/commands/server.go
··· 310 310 startBundle = lastBundle.BundleNumber + 1 311 311 } 312 312 313 - isInitialSync := (lastBundle == nil || lastBundle.BundleNumber < 10) 314 - 315 - if isInitialSync && !verbose { 316 - fmt.Fprintf(os.Stderr, "[Sync] Initial sync - fast loading mode (bundle %06d → ...)\n", startBundle) 317 - } else if verbose { 318 - fmt.Fprintf(os.Stderr, "[Sync] Checking for new bundles (current: %06d)...\n", startBundle-1) 319 - } 320 - 321 313 mempoolBefore := mgr.GetMempoolStats()["count"].(int) 322 314 fetchedCount := 0 323 - consecutiveErrors := 0 324 315 316 + // ✨ SIMPLIFIED: Just keep calling FetchNextBundle until it fails 325 317 for { 326 - currentBundle := startBundle + fetchedCount 327 - 328 318 b, err := mgr.FetchNextBundle(ctx, !verbose) 329 319 if err != nil { 330 320 if isEndOfDataError(err) { ··· 334 324 335 325 if fetchedCount > 0 { 336 326 fmt.Fprintf(os.Stderr, "[Sync] ✓ Bundle %06d | Synced: %d | Mempool: %d (+%d) | %dms\n", 337 - currentBundle-1, fetchedCount, mempoolAfter, addedOps, duration.Milliseconds()) 338 - } else if !isInitialSync { 327 + startBundle+fetchedCount-1, fetchedCount, mempoolAfter, addedOps, duration.Milliseconds()) 328 + } else { 339 329 fmt.Fprintf(os.Stderr, "[Sync] ✓ Bundle %06d | Up to date | Mempool: %d (+%d) | %dms\n", 340 330 startBundle-1, mempoolAfter, addedOps, duration.Milliseconds()) 341 331 } 342 332 break 343 333 } 344 334 345 - consecutiveErrors++ 346 - if verbose { 347 - fmt.Fprintf(os.Stderr, "[Sync] Error fetching bundle %06d: %v\n", currentBundle, err) 348 - } 349 - 350 - if consecutiveErrors >= 3 { 351 - fmt.Fprintf(os.Stderr, "[Sync] Too many errors, stopping\n") 352 - break 353 - } 354 - 355 - time.Sleep(5 * time.Second) 356 - continue 335 + // Real error 336 + fmt.Fprintf(os.Stderr, "[Sync] Error: %v\n", err) 337 + break 357 338 } 358 339 359 - consecutiveErrors = 0 360 - 340 + // Save bundle 361 341 if err := mgr.SaveBundle(ctx, b, !verbose); err != nil { 362 342 fmt.Fprintf(os.Stderr, "[Sync] Error saving bundle %06d: %v\n", b.BundleNumber, err) 363 343 break ··· 366 346 fetchedCount++ 367 347 368 348 if !verbose { 369 - fmt.Fprintf(os.Stderr, "[Sync] ✓ %06d | hash=%s | content=%s | %d ops, %d DIDs\n", 370 - b.BundleNumber, 371 - b.Hash[:16]+"...", 372 - b.ContentHash[:16]+"...", 373 - len(b.Operations), 374 - b.DIDCount) 349 + fmt.Fprintf(os.Stderr, "[Sync] ✓ %06d | %d ops, %d DIDs\n", 350 + b.BundleNumber, len(b.Operations), b.DIDCount) 375 351 } 376 352 377 353 time.Sleep(500 * time.Millisecond)
+23 -13
internal/sync/fetcher.go
··· 27 27 } 28 28 29 29 // FetchToMempool fetches operations and returns them 30 + // Returns empty slice + nil error when caught up (incomplete batch received) 30 31 func (f *Fetcher) FetchToMempool( 31 32 ctx context.Context, 32 33 afterTime string, ··· 59 60 } 60 61 61 62 if !quiet { 62 - f.logger.Printf(" Fetch #%d: requesting %d operations", fetchNum+1, batchSize) 63 + f.logger.Printf(" Fetch #%d: requesting %d operations (after: %s)", 64 + fetchNum+1, batchSize, currentAfter[:19]) 63 65 } 64 66 65 67 batch, err := f.plcClient.Export(ctx, plcclient.ExportOptions{ 66 68 Count: batchSize, 67 69 After: currentAfter, 68 70 }) 71 + 69 72 if err != nil { 70 73 return allNewOps, fmt.Errorf("export failed: %w", err) 71 74 } ··· 74 77 if !quiet { 75 78 f.logger.Printf(" No more operations available from PLC") 76 79 } 77 - break 80 + // Return what we have (might be incomplete) 81 + return allNewOps, nil 78 82 } 79 83 80 - // Deduplicate against boundary CIDs only 81 - // Mempool will handle deduplication of operations already in mempool 84 + // Deduplicate 85 + beforeDedup := len(allNewOps) 82 86 for _, op := range batch { 83 87 if !seenCIDs[op.CID] { 84 88 seenCIDs[op.CID] = true ··· 86 90 } 87 91 } 88 92 93 + if !quiet && len(batch) > 0 { 94 + deduped := len(batch) - (len(allNewOps) - beforeDedup) 95 + if deduped > 0 { 96 + f.logger.Printf(" Received %d ops (%d duplicates filtered)", len(batch), deduped) 97 + } 98 + } 99 + 89 100 // Update cursor 90 101 if len(batch) > 0 { 91 102 currentAfter = batch[len(batch)-1].CreatedAt.Format(time.RFC3339Nano) 92 103 } 93 104 94 - // Stop if we got less than requested (caught up) 105 + // ✨ KEY: Stop if we got incomplete batch (caught up!) 95 106 if len(batch) < batchSize { 96 107 if !quiet { 97 - f.logger.Printf(" Received incomplete batch (%d/%d), caught up to latest", len(batch), batchSize) 108 + f.logger.Printf(" Received incomplete batch (%d/%d) → caught up to latest", 109 + len(batch), batchSize) 98 110 } 99 - break 111 + return allNewOps, nil 100 112 } 101 - } 102 113 103 - if len(allNewOps) > 0 { 104 - if !quiet { 105 - f.logger.Printf("✓ Fetch complete: %d operations", len(allNewOps)) 114 + // If we have enough, stop 115 + if len(allNewOps) >= target { 116 + break 106 117 } 107 - return allNewOps, nil 108 118 } 109 119 110 - return nil, fmt.Errorf("no operations available (reached latest data)") 120 + return allNewOps, nil 111 121 }