added atomic writer action for ctr c abort
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// src/corporate/update.rs - ABORT-SAFE VERSION WITH JSONL LOG
|
||||
// src/corporate/update.rs - UPDATED WITH DATA INTEGRITY FIXES
|
||||
use super::{scraper::*, storage::*, helpers::*, types::*, openfigi::*, yahoo::*};
|
||||
use crate::config::Config;
|
||||
use crate::corporate::update_parallel::build_companies_jsonl_streaming_parallel;
|
||||
@@ -11,12 +11,13 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// UPDATED: Main corporate update entry point with shutdown awareness
|
||||
pub async fn run_full_update(
|
||||
_config: &Config,
|
||||
pool: &Arc<ChromeDriverPool>,
|
||||
shutdown_flag: &Arc<AtomicBool>,
|
||||
) -> anyhow::Result<()> {
|
||||
logger::log_info("=== Corporate Update (STREAMING MODE) ===").await;
|
||||
logger::log_info("=== Corporate Update (STREAMING MODE WITH DATA INTEGRITY) ===").await;
|
||||
|
||||
let paths = DataPaths::new(".")?;
|
||||
|
||||
@@ -33,6 +34,7 @@ pub async fn run_full_update(
|
||||
};
|
||||
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
logger::log_warn("Shutdown detected after GLEIF download").await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -41,6 +43,7 @@ pub async fn run_full_update(
|
||||
logger::log_info(" ✓ OpenFIGI metadata loaded").await;
|
||||
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
logger::log_warn("Shutdown detected after OpenFIGI load").await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -54,6 +57,7 @@ pub async fn run_full_update(
|
||||
}
|
||||
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
logger::log_warn("Shutdown detected after LEI-FIGI mapping").await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -69,10 +73,11 @@ pub async fn run_full_update(
|
||||
}
|
||||
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
logger::log_warn("Shutdown detected after securities map build").await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logger::log_info("Step 5: Building companies.jsonl (streaming with abort-safe persistence)...").await;
|
||||
logger::log_info("Step 5: Building companies.jsonl with parallel processing and validation...").await;
|
||||
let count = build_companies_jsonl_streaming_parallel(&paths, pool, shutdown_flag).await?;
|
||||
logger::log_info(&format!(" ✓ Saved {} companies", count)).await;
|
||||
|
||||
@@ -80,40 +85,32 @@ pub async fn run_full_update(
|
||||
logger::log_info("Step 6: Processing events (using index)...").await;
|
||||
let _event_index = build_event_index(&paths).await?;
|
||||
logger::log_info(" ✓ Event index built").await;
|
||||
} else {
|
||||
logger::log_warn("Shutdown detected, skipping event index build").await;
|
||||
}
|
||||
|
||||
logger::log_info("✓ Corporate update complete").await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Abort-safe incremental JSONL persistence with atomic checkpoints
|
||||
/// UPDATED: Serial version with validation (kept for compatibility/debugging)
|
||||
///
|
||||
/// Implements the data_updating_rule.md specification:
|
||||
/// - Append-only JSONL log for all updates
|
||||
/// - Batched fsync for performance (configurable batch size)
|
||||
/// - Time-based fsync for safety (max 10 seconds without fsync)
|
||||
/// - Atomic checkpoints via temp file + rename
|
||||
/// - Crash recovery by loading checkpoint + replaying log
|
||||
/// - Partial lines automatically ignored by .lines() iterator
|
||||
/// This is the non-parallel version that processes companies sequentially.
|
||||
/// Updated with same validation and shutdown checks as parallel version.
|
||||
///
|
||||
/// # Error Handling & Crash Safety
|
||||
///
|
||||
/// If any write or fsync fails:
|
||||
/// - Function returns error immediately
|
||||
/// - Partial line may be in OS buffer but not fsynced
|
||||
/// - On next startup, .lines() will either:
|
||||
/// a) Skip partial line (if no \n written)
|
||||
/// b) Fail to parse malformed JSON (logged and skipped)
|
||||
/// - No data corruption, at most last batch entries lost
|
||||
async fn build_companies_jsonl_streaming(
|
||||
/// Use this for:
|
||||
/// - Debugging issues with specific companies
|
||||
/// - Environments where parallel processing isn't desired
|
||||
/// - Testing validation logic without concurrency complexity
|
||||
async fn build_companies_jsonl_streaming_serial(
|
||||
paths: &DataPaths,
|
||||
pool: &Arc<ChromeDriverPool>,
|
||||
shutdown_flag: &Arc<AtomicBool>,
|
||||
) -> anyhow::Result<usize> {
|
||||
// Configuration constants
|
||||
const CHECKPOINT_INTERVAL: usize = 50; // Create checkpoint every N updates
|
||||
const FSYNC_BATCH_SIZE: usize = 10; // fsync every N writes for performance
|
||||
const FSYNC_INTERVAL_SECS: u64 = 10; // Also fsync every N seconds for safety
|
||||
const CHECKPOINT_INTERVAL: usize = 50;
|
||||
const FSYNC_BATCH_SIZE: usize = 10;
|
||||
const FSYNC_INTERVAL_SECS: u64 = 10;
|
||||
|
||||
let path = DataPaths::new(".")?;
|
||||
let corporate_path = path.data_dir().join("corporate").join("by_name");
|
||||
@@ -134,7 +131,7 @@ async fn build_companies_jsonl_streaming(
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
// === RECOVERY PHASE 1: Load last checkpoint ===
|
||||
// === RECOVERY PHASE: Load checkpoint + replay log ===
|
||||
let mut existing_companies: HashMap<String, CompanyCrossPlatformInfo> = HashMap::new();
|
||||
let mut processed_names: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
@@ -142,8 +139,6 @@ async fn build_companies_jsonl_streaming(
|
||||
logger::log_info("Loading checkpoint from companies.jsonl...").await;
|
||||
let existing_content = tokio::fs::read_to_string(&companies_path).await?;
|
||||
|
||||
// Note: .lines() only returns complete lines terminated with \n
|
||||
// Partial lines (incomplete writes from crashes) are automatically skipped
|
||||
for line in existing_content.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
@@ -155,7 +150,6 @@ async fn build_companies_jsonl_streaming(
|
||||
existing_companies.insert(company.name.clone(), company);
|
||||
}
|
||||
Err(e) => {
|
||||
// This catches both malformed JSON and partial lines
|
||||
logger::log_warn(&format!("Skipping invalid checkpoint line: {}", e)).await;
|
||||
}
|
||||
}
|
||||
@@ -163,14 +157,11 @@ async fn build_companies_jsonl_streaming(
|
||||
logger::log_info(&format!("Loaded checkpoint with {} companies", existing_companies.len())).await;
|
||||
}
|
||||
|
||||
// === RECOVERY PHASE 2: Replay log after checkpoint ===
|
||||
if log_path.exists() {
|
||||
logger::log_info("Replaying update log...").await;
|
||||
let log_content = tokio::fs::read_to_string(&log_path).await?;
|
||||
let mut replayed = 0;
|
||||
|
||||
// Note: .lines() only returns complete lines terminated with \n
|
||||
// Partial lines from crashes are automatically skipped
|
||||
for line in log_content.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
@@ -183,7 +174,6 @@ async fn build_companies_jsonl_streaming(
|
||||
replayed += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
// This catches both malformed JSON and partial lines
|
||||
logger::log_warn(&format!("Skipping invalid log line: {}", e)).await;
|
||||
}
|
||||
}
|
||||
@@ -193,225 +183,143 @@ async fn build_companies_jsonl_streaming(
|
||||
}
|
||||
}
|
||||
|
||||
// === APPEND-ONLY LOG: Open in append mode with O_APPEND semantics ===
|
||||
// === OPEN LOG FILE ===
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut log_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true) // O_APPEND - atomic append operations
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.await?;
|
||||
|
||||
let mut count = existing_companies.len();
|
||||
let mut updated_count = 0;
|
||||
let mut new_count = 0;
|
||||
let mut updates_since_checkpoint = 0;
|
||||
|
||||
// Batched fsync tracking for performance
|
||||
let mut writes_since_fsync = 0;
|
||||
let mut last_fsync = std::time::Instant::now();
|
||||
let mut updates_since_checkpoint = 0;
|
||||
let mut count = 0;
|
||||
let mut new_count = 0;
|
||||
let mut updated_count = 0;
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
logger::log_info(&format!("Processing {} companies sequentially...", securities.len())).await;
|
||||
|
||||
for (name, company_info) in securities.iter() {
|
||||
// === PROCESS COMPANIES SEQUENTIALLY ===
|
||||
for (name, company_info) in securities.clone() {
|
||||
// Check shutdown before each company
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
logger::log_info("Shutdown requested - stopping company processing").await;
|
||||
logger::log_warn(&format!(
|
||||
"Shutdown detected at company: {} (progress: {}/{})",
|
||||
name, count, count + securities.len()
|
||||
)).await;
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip if already processed (from checkpoint or log replay)
|
||||
if processed_names.contains(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let existing_entry = existing_companies.get(name).cloned();
|
||||
|
||||
let existing_entry = existing_companies.get(&name).cloned();
|
||||
let is_update = existing_entry.is_some();
|
||||
|
||||
let mut isin_tickers_map: HashMap<String, Vec<String>> =
|
||||
existing_entry
|
||||
.as_ref()
|
||||
.map(|e| e.isin_tickers_map.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut sector = existing_entry.as_ref().and_then(|e| e.sector.clone());
|
||||
let mut exchange = existing_entry.as_ref().and_then(|e| e.exchange.clone());
|
||||
|
||||
let mut unique_isin_ticker_pairs: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
for figi_infos in company_info.securities.values() {
|
||||
for figi_info in figi_infos {
|
||||
if !figi_info.isin.is_empty() {
|
||||
let tickers = unique_isin_ticker_pairs
|
||||
.entry(figi_info.isin.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
|
||||
if !figi_info.ticker.is_empty() && !tickers.contains(&figi_info.ticker) {
|
||||
tickers.push(figi_info.ticker.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (isin, figi_tickers) in unique_isin_ticker_pairs {
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
let tickers = isin_tickers_map
|
||||
.entry(isin.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
|
||||
for figi_ticker in figi_tickers {
|
||||
if !tickers.contains(&figi_ticker) {
|
||||
tickers.push(figi_ticker);
|
||||
}
|
||||
}
|
||||
|
||||
let has_yahoo_ticker = tickers.iter().any(|t| t.starts_with("YAHOO:"));
|
||||
|
||||
if !has_yahoo_ticker && !shutdown_flag.load(Ordering::SeqCst) {
|
||||
logger::log_info(&format!("Fetching Yahoo details for {} (ISIN: {})", name, isin)).await;
|
||||
// Process company with validation
|
||||
match process_single_company_serial(
|
||||
name.clone(),
|
||||
company_info,
|
||||
existing_entry,
|
||||
pool,
|
||||
shutdown_flag,
|
||||
).await {
|
||||
Ok(Some(company_entry)) => {
|
||||
// Write to log
|
||||
let line = serde_json::to_string(&company_entry)?;
|
||||
log_file.write_all(line.as_bytes()).await?;
|
||||
log_file.write_all(b"\n").await?;
|
||||
|
||||
match scrape_company_details_by_isin(pool, &isin).await {
|
||||
Ok(Some(details)) => {
|
||||
logger::log_info(&format!("✓ Found Yahoo ticker {} for ISIN {}", details.ticker, isin)).await;
|
||||
|
||||
tickers.push(format!("YAHOO:{}", details.ticker));
|
||||
|
||||
if sector.is_none() && details.sector.is_some() {
|
||||
sector = details.sector.clone();
|
||||
logger::log_info(&format!(" Sector: {}", details.sector.as_ref().unwrap())).await;
|
||||
}
|
||||
|
||||
if exchange.is_none() && details.exchange.is_some() {
|
||||
exchange = details.exchange.clone();
|
||||
logger::log_info(&format!(" Exchange: {}", details.exchange.as_ref().unwrap())).await;
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
logger::log_warn(&format!("◯ No search results for ISIN {}", isin)).await;
|
||||
tickers.push("YAHOO:NO_RESULTS".to_string());
|
||||
},
|
||||
Err(e) => {
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
logger::log_warn(&format!("✗ Yahoo lookup error for ISIN {}: {}", isin, e)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
if !isin_tickers_map.is_empty() {
|
||||
let company_entry = CompanyCrossPlatformInfo {
|
||||
name: name.clone(),
|
||||
isin_tickers_map,
|
||||
sector,
|
||||
exchange,
|
||||
};
|
||||
|
||||
// === APPEND-ONLY: Write single-line JSON with batched fsync ===
|
||||
// Write guarantees the line is either fully written or not at all
|
||||
let line = serde_json::to_string(&company_entry)?;
|
||||
log_file.write_all(line.as_bytes()).await?;
|
||||
log_file.write_all(b"\n").await?;
|
||||
writes_since_fsync += 1;
|
||||
|
||||
// Batched fsync for performance + time-based fsync for safety
|
||||
// fsync if: batch size reached OR time interval exceeded
|
||||
let should_fsync = writes_since_fsync >= FSYNC_BATCH_SIZE
|
||||
|| last_fsync.elapsed().as_secs() >= FSYNC_INTERVAL_SECS;
|
||||
|
||||
if should_fsync {
|
||||
log_file.flush().await?;
|
||||
// Critical: fsync to ensure durability before considering writes successful
|
||||
// This prevents data loss on power failure or kernel panic
|
||||
log_file.sync_data().await?;
|
||||
writes_since_fsync = 0;
|
||||
last_fsync = std::time::Instant::now();
|
||||
}
|
||||
|
||||
// Update in-memory state ONLY after write (fsync happens in batches)
|
||||
// This is safe because we fsync before checkpoints and at end of processing
|
||||
processed_names.insert(name.clone());
|
||||
existing_companies.insert(name.clone(), company_entry);
|
||||
|
||||
count += 1;
|
||||
updates_since_checkpoint += 1;
|
||||
|
||||
if is_update {
|
||||
updated_count += 1;
|
||||
} else {
|
||||
new_count += 1;
|
||||
}
|
||||
|
||||
// === ATOMIC CHECKPOINT: Periodically create checkpoint ===
|
||||
// This reduces recovery time by snapshotting current state
|
||||
if updates_since_checkpoint >= CHECKPOINT_INTERVAL {
|
||||
// Ensure any pending writes are fsynced before checkpoint
|
||||
if writes_since_fsync > 0 {
|
||||
writes_since_fsync += 1;
|
||||
|
||||
// Batched + time-based fsync
|
||||
let should_fsync = writes_since_fsync >= FSYNC_BATCH_SIZE
|
||||
|| last_fsync.elapsed().as_secs() >= FSYNC_INTERVAL_SECS;
|
||||
|
||||
if should_fsync {
|
||||
log_file.flush().await?;
|
||||
log_file.sync_data().await?;
|
||||
writes_since_fsync = 0;
|
||||
last_fsync = std::time::Instant::now();
|
||||
}
|
||||
|
||||
logger::log_info(&format!("Creating checkpoint at {} companies...", count)).await;
|
||||
// Update in-memory state
|
||||
processed_names.insert(name.clone());
|
||||
existing_companies.insert(name.clone(), company_entry);
|
||||
|
||||
let checkpoint_tmp = companies_path.with_extension("jsonl.tmp");
|
||||
let mut checkpoint_file = tokio::fs::File::create(&checkpoint_tmp).await?;
|
||||
count += 1;
|
||||
updates_since_checkpoint += 1;
|
||||
|
||||
// Write all current state to temporary checkpoint file
|
||||
for company in existing_companies.values() {
|
||||
let line = serde_json::to_string(company)?;
|
||||
checkpoint_file.write_all(line.as_bytes()).await?;
|
||||
checkpoint_file.write_all(b"\n").await?;
|
||||
if is_update {
|
||||
updated_count += 1;
|
||||
} else {
|
||||
new_count += 1;
|
||||
}
|
||||
|
||||
checkpoint_file.flush().await?;
|
||||
checkpoint_file.sync_all().await?;
|
||||
drop(checkpoint_file);
|
||||
// Periodic checkpoint
|
||||
if updates_since_checkpoint >= CHECKPOINT_INTERVAL {
|
||||
if writes_since_fsync > 0 {
|
||||
log_file.flush().await?;
|
||||
log_file.sync_data().await?;
|
||||
writes_since_fsync = 0;
|
||||
last_fsync = std::time::Instant::now();
|
||||
}
|
||||
|
||||
logger::log_info(&format!("Creating checkpoint at {} companies...", count)).await;
|
||||
|
||||
let checkpoint_tmp = companies_path.with_extension("jsonl.tmp");
|
||||
let mut checkpoint_file = tokio::fs::File::create(&checkpoint_tmp).await?;
|
||||
|
||||
for company in existing_companies.values() {
|
||||
let line = serde_json::to_string(company)?;
|
||||
checkpoint_file.write_all(line.as_bytes()).await?;
|
||||
checkpoint_file.write_all(b"\n").await?;
|
||||
}
|
||||
|
||||
checkpoint_file.flush().await?;
|
||||
checkpoint_file.sync_all().await?;
|
||||
drop(checkpoint_file);
|
||||
|
||||
tokio::fs::rename(&checkpoint_tmp, &companies_path).await?;
|
||||
|
||||
drop(log_file);
|
||||
tokio::fs::remove_file(&log_path).await.ok();
|
||||
log_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.await?;
|
||||
|
||||
updates_since_checkpoint = 0;
|
||||
logger::log_info("✓ Checkpoint created and log cleared").await;
|
||||
}
|
||||
|
||||
// Atomic rename - this is the commit point
|
||||
// After this succeeds, the checkpoint is visible
|
||||
tokio::fs::rename(&checkpoint_tmp, &companies_path).await?;
|
||||
|
||||
// Clear log after successful checkpoint
|
||||
// Any entries before this point are now captured in the checkpoint
|
||||
drop(log_file);
|
||||
tokio::fs::remove_file(&log_path).await.ok();
|
||||
log_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.await?;
|
||||
|
||||
updates_since_checkpoint = 0;
|
||||
logger::log_info("✓ Checkpoint created and log cleared").await;
|
||||
if count % 10 == 0 {
|
||||
logger::log_info(&format!(
|
||||
"Progress: {} companies ({} new, {} updated)",
|
||||
count, new_count, updated_count
|
||||
)).await;
|
||||
}
|
||||
}
|
||||
|
||||
if count % 10 == 0 {
|
||||
logger::log_info(&format!("Progress: {} companies ({} new, {} updated)", count, new_count, updated_count)).await;
|
||||
tokio::task::yield_now().await;
|
||||
Ok(None) => {
|
||||
// Company had no ISINs or was skipped
|
||||
logger::log_info(&format!("Skipped company: {} (no ISINs)", name)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
logger::log_warn(&format!("Error processing company {}: {}", name, e)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Time-based fsync: Even if this company didn't result in a write,
|
||||
// fsync any pending writes if enough time has passed
|
||||
// This reduces data loss window during long Yahoo lookup operations
|
||||
// Time-based fsync
|
||||
if writes_since_fsync > 0 && last_fsync.elapsed().as_secs() >= FSYNC_INTERVAL_SECS {
|
||||
log_file.flush().await?;
|
||||
log_file.sync_data().await?;
|
||||
writes_since_fsync = 0;
|
||||
last_fsync = std::time::Instant::now();
|
||||
logger::log_info("Time-based fsync completed").await;
|
||||
}
|
||||
}
|
||||
|
||||
// === FSYNC PENDING WRITES: Even if shutdown requested, save what we have ===
|
||||
// === FSYNC PENDING WRITES ===
|
||||
if writes_since_fsync > 0 {
|
||||
logger::log_info(&format!("Fsyncing {} pending writes...", writes_since_fsync)).await;
|
||||
log_file.flush().await?;
|
||||
@@ -419,9 +327,7 @@ async fn build_companies_jsonl_streaming(
|
||||
logger::log_info("✓ Pending writes saved").await;
|
||||
}
|
||||
|
||||
// === FINAL CHECKPOINT: Write complete final state ===
|
||||
// This ensures we don't need to replay the log on next startup
|
||||
// (Pending writes were already fsynced above)
|
||||
// === FINAL CHECKPOINT ===
|
||||
if !shutdown_flag.load(Ordering::SeqCst) && updates_since_checkpoint > 0 {
|
||||
logger::log_info("Creating final checkpoint...").await;
|
||||
|
||||
@@ -438,21 +344,172 @@ async fn build_companies_jsonl_streaming(
|
||||
checkpoint_file.sync_all().await?;
|
||||
drop(checkpoint_file);
|
||||
|
||||
// Atomic rename makes final checkpoint visible
|
||||
tokio::fs::rename(&checkpoint_tmp, &companies_path).await?;
|
||||
|
||||
// Clean up log
|
||||
drop(log_file);
|
||||
tokio::fs::remove_file(&log_path).await.ok();
|
||||
|
||||
logger::log_info("✓ Final checkpoint created").await;
|
||||
}
|
||||
|
||||
logger::log_info(&format!("Completed: {} total companies ({} new, {} updated)", count, new_count, updated_count)).await;
|
||||
logger::log_info(&format!(
|
||||
"Completed: {} total companies ({} new, {} updated)",
|
||||
count, new_count, updated_count
|
||||
)).await;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// UPDATED: Process single company serially with validation
|
||||
async fn process_single_company_serial(
|
||||
name: String,
|
||||
company_info: CompanyInfo,
|
||||
existing_entry: Option<CompanyCrossPlatformInfo>,
|
||||
pool: &Arc<ChromeDriverPool>,
|
||||
shutdown_flag: &Arc<AtomicBool>,
|
||||
) -> anyhow::Result<Option<CompanyCrossPlatformInfo>> {
|
||||
// Check shutdown at start
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut isin_tickers_map: HashMap<String, Vec<String>> =
|
||||
existing_entry
|
||||
.as_ref()
|
||||
.map(|e| e.isin_tickers_map.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut sector = existing_entry.as_ref().and_then(|e| e.sector.clone());
|
||||
let mut exchange = existing_entry.as_ref().and_then(|e| e.exchange.clone());
|
||||
|
||||
// Collect unique ISIN-ticker pairs
|
||||
let mut unique_isin_ticker_pairs: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
for figi_infos in company_info.securities.values() {
|
||||
for figi_info in figi_infos {
|
||||
if !figi_info.isin.is_empty() {
|
||||
let tickers = unique_isin_ticker_pairs
|
||||
.entry(figi_info.isin.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
|
||||
if !figi_info.ticker.is_empty() && !tickers.contains(&figi_info.ticker) {
|
||||
tickers.push(figi_info.ticker.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process each ISIN with validation
|
||||
for (isin, figi_tickers) in unique_isin_ticker_pairs {
|
||||
// Check shutdown before each ISIN
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let tickers = isin_tickers_map
|
||||
.entry(isin.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
|
||||
for figi_ticker in figi_tickers {
|
||||
if !tickers.contains(&figi_ticker) {
|
||||
tickers.push(figi_ticker);
|
||||
}
|
||||
}
|
||||
|
||||
let has_yahoo_ticker = tickers.iter().any(|t| t.starts_with("YAHOO:"));
|
||||
|
||||
if !has_yahoo_ticker {
|
||||
logger::log_info(&format!("Fetching Yahoo details for {} (ISIN: {})", name, isin)).await;
|
||||
|
||||
// Use validated scraping with retry
|
||||
match scrape_with_retry_serial(pool, &isin, 3, shutdown_flag).await {
|
||||
Ok(Some(details)) => {
|
||||
logger::log_info(&format!(
|
||||
"✓ Found Yahoo ticker {} for ISIN {} (company: {})",
|
||||
details.ticker, isin, name
|
||||
)).await;
|
||||
|
||||
tickers.push(format!("YAHOO:{}", details.ticker));
|
||||
|
||||
if sector.is_none() && details.sector.is_some() {
|
||||
sector = details.sector.clone();
|
||||
}
|
||||
|
||||
if exchange.is_none() && details.exchange.is_some() {
|
||||
exchange = details.exchange.clone();
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
logger::log_warn(&format!("◯ No search results for ISIN {} (company: {})", isin, name)).await;
|
||||
tickers.push("YAHOO:NO_RESULTS".to_string());
|
||||
},
|
||||
Err(e) => {
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
return Ok(None);
|
||||
}
|
||||
logger::log_warn(&format!(
|
||||
"✗ Yahoo lookup error for ISIN {} (company: {}): {}",
|
||||
isin, name, e
|
||||
)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final shutdown check
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !isin_tickers_map.is_empty() {
|
||||
Ok(Some(CompanyCrossPlatformInfo {
|
||||
name,
|
||||
isin_tickers_map,
|
||||
sector,
|
||||
exchange,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// UPDATED: Scrape with retry for serial processing
|
||||
async fn scrape_with_retry_serial(
|
||||
pool: &Arc<ChromeDriverPool>,
|
||||
isin: &str,
|
||||
max_retries: u32,
|
||||
shutdown_flag: &Arc<AtomicBool>,
|
||||
) -> anyhow::Result<Option<YahooCompanyDetails>> {
|
||||
let mut retries = 0;
|
||||
|
||||
loop {
|
||||
if shutdown_flag.load(Ordering::SeqCst) {
|
||||
return Err(anyhow::anyhow!("Aborted due to shutdown"));
|
||||
}
|
||||
|
||||
match scrape_company_details_by_isin(pool, isin, shutdown_flag).await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) => {
|
||||
if retries >= max_retries {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let backoff_ms = 1000 * 2u64.pow(retries);
|
||||
let jitter_ms = random_range(0, 500);
|
||||
let total_delay = backoff_ms + jitter_ms;
|
||||
|
||||
logger::log_warn(&format!(
|
||||
"Retry {}/{} for ISIN {} after {}ms: {}",
|
||||
retries + 1, max_retries, isin, total_delay, e
|
||||
)).await;
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(total_delay)).await;
|
||||
retries += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_most_recent_figi_date_dir(paths: &DataPaths) -> anyhow::Result<Option<std::path::PathBuf>> {
|
||||
let map_cache_dir = paths.cache_gleif_openfigi_map_dir();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user