Nebulixlabs commited on
Commit
55bd237
·
verified ·
1 Parent(s): 813b849

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -35
app.py CHANGED
@@ -51,16 +51,19 @@ DB_PATH = STATE_DIR / "crawler.db"
51
  TRANCO_URL = os.getenv("TRANCO_URL", "https://tranco-list.eu/top-1m.csv.zip")
52
  SEED_COUNT = int(os.getenv("SEED_COUNT", "100000"))
53
 
54
- GLOBAL_CONCURRENCY = int(os.getenv("GLOBAL_CONCURRENCY", "128")) # HF Space ke liye safe limit
55
- PER_HOST_CONCURRENCY = int(os.getenv("PER_HOST_CONCURRENCY", "4"))
56
- REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", "15"))
 
57
  CONNECT_TIMEOUT = float(os.getenv("CONNECT_TIMEOUT", "5"))
58
- MAX_REDIRECTS = int(os.getenv("MAX_REDIRECTS", "5"))
59
 
60
  MAX_RESPONSE_BYTES = int(os.getenv("MAX_RESPONSE_BYTES", str(4 * 1024 * 1024)))
61
  MAX_TEXT_CHARS = int(os.getenv("MAX_TEXT_CHARS", "20000"))
62
  MAX_KEYWORDS = int(os.getenv("MAX_KEYWORDS", "30"))
63
- MAX_LINKS_PER_PAGE = int(os.getenv("MAX_LINKS_PER_PAGE", "500"))
 
 
64
 
65
  DB_FLUSH_INTERVAL = float(os.getenv("DB_FLUSH_INTERVAL", "1.0"))
66
  HF_UPLOAD_INTERVAL = int(os.getenv("HF_UPLOAD_INTERVAL", str(3 * 60 * 60)))
@@ -76,14 +79,14 @@ USER_AGENT = os.getenv("USER_AGENT", "NebulixSearchBot/1.0 (+https://nebulixlabs
76
  # APP & RUNTIME STATE
77
  # ============================================================
78
 
79
- app = FastAPI(title="Nebulix Search Crawler", version="4.1.0")
80
 
81
  db: Optional[aiosqlite.Connection] = None
82
  http_session: Optional[aiohttp.ClientSession] = None
83
 
84
  frontier = asyncio.PriorityQueue()
85
- record_queue = asyncio.Queue(maxsize=5000)
86
- db_event_queue = asyncio.Queue(maxsize=20000)
87
 
88
  db_lock = asyncio.Lock()
89
  host_state_lock = asyncio.Lock()
@@ -170,6 +173,7 @@ async def init_db():
170
  await db.execute("PRAGMA journal_mode=WAL")
171
  await db.execute("PRAGMA synchronous=NORMAL")
172
  await db.execute("PRAGMA temp_store=MEMORY")
 
173
 
174
  await db.execute("""
175
  CREATE TABLE IF NOT EXISTS urls (
@@ -198,7 +202,7 @@ async def db_writer():
198
  batch.append(first)
199
  except asyncio.TimeoutError: pass
200
 
201
- while len(batch) < 5000:
202
  try: batch.append(db_event_queue.get_nowait())
203
  except asyncio.QueueEmpty: break
204
 
@@ -238,10 +242,15 @@ async def db_writer():
238
 
239
  async def frontier_feeder():
240
  while True:
 
 
 
 
 
241
  try:
242
  rows = []
243
  async with db_lock:
244
- cursor = await db.execute("SELECT url_hash, url, priority, depth FROM urls WHERE status = 0 ORDER BY priority ASC, discovered_at ASC LIMIT 2000")
245
  rows = await cursor.fetchall()
246
 
247
  if rows:
@@ -260,7 +269,7 @@ async def frontier_feeder():
260
  await asyncio.sleep(1)
261
 
262
  # ============================================================
263
- # TRANCO SEED (RESTORED - VERY IMPORTANT)
264
  # ============================================================
265
 
266
  async def seed_tranco():
@@ -437,15 +446,12 @@ async def fetch_page(session, url):
437
  async with semaphore:
438
  await respect_host_delay(host, DEFAULT_HOST_DELAY)
439
  timeout = ClientTimeout(total=REQUEST_TIMEOUT, connect=CONNECT_TIMEOUT)
440
- for attempt in range(2):
 
 
441
  try:
442
  async with session.get(url, timeout=timeout, allow_redirects=True, max_redirects=MAX_REDIRECTS) as response:
443
  status = response.status
444
- if status in {429, 500, 502, 503, 504}:
445
- if attempt == 0:
446
- await asyncio.sleep(0.5)
447
- continue
448
- return None
449
  if status != 200: return None
450
 
451
  content_type = response.headers.get("Content-Type", "").lower()
@@ -463,10 +469,7 @@ async def fetch_page(session, url):
463
  body = b"".join(chunks)
464
  async with stats_lock: stats["bytes_downloaded"] += len(body)
465
  return {"url": str(response.url), "status": status, "html": body, "headers": {str(k).lower(): str(v) for k, v in response.headers.items()}}
466
- except (asyncio.TimeoutError, aiohttp.ClientError):
467
- if attempt == 0:
468
- await asyncio.sleep(0.25)
469
- continue
470
  return None
471
  return None
472
 
@@ -529,7 +532,7 @@ async def record_writer():
529
  batch.append(first)
530
  except asyncio.TimeoutError: pass
531
 
532
- while len(batch) < 500:
533
  try: batch.append(record_queue.get_nowait())
534
  except asyncio.QueueEmpty: break
535
 
@@ -605,7 +608,7 @@ async def hf_upload_loop():
605
  except Exception: pass
606
 
607
  # ============================================================
608
- # CRAWLER WORKER
609
  # ============================================================
610
 
611
  async def crawl_worker(worker_id):
@@ -618,7 +621,8 @@ async def crawl_worker(worker_id):
618
  response = await fetch_page(http_session, url)
619
 
620
  if response is None:
621
- await db_event_queue.put(("failed", page_hash))
 
622
  async with stats_lock: stats["failed"] += 1
623
  continue
624
 
@@ -627,11 +631,13 @@ async def crawl_worker(worker_id):
627
  record, discovered_urls, noindex = parsed
628
 
629
  if noindex:
630
- await db_event_queue.put(("done", page_hash))
 
631
  async with stats_lock: stats["noindex"] += 1
632
  else:
633
  await persist_record(record)
634
- await db_event_queue.put(("done", page_hash))
 
635
  async with stats_lock:
636
  stats["successful"] += 1
637
  recent_successes.append(time.monotonic())
@@ -641,20 +647,26 @@ async def crawl_worker(worker_id):
641
 
642
  for discovered_url in discovered_urls:
643
  target_host = urllib.parse.urlsplit(discovered_url).hostname or ""
644
- next_priority = priority + 10 if target_host == source_host else priority + 100
 
 
 
 
645
  normalized = normalize_url(discovered_url)
646
  if normalized:
647
- await db_event_queue.put(("discover", normalized, next_priority, next_depth))
648
- async with stats_lock: stats["urls_discovered"] += 1
 
649
 
650
  except Exception as exc:
651
- await db_event_queue.put(("failed", page_hash))
 
652
  async with stats_lock: stats["failed"] += 1
653
  finally:
654
  frontier.task_done()
655
 
656
  # ============================================================
657
- # API ENDPOINTS (RESTORED FULL STATUS)
658
  # ============================================================
659
 
660
  async def get_db_counts():
@@ -740,16 +752,15 @@ async def force_save_data():
740
  async def start_app():
741
  global http_session
742
 
743
- # HF Space safe CPU limit (Max 16 threads to avoid RAM crash)
744
  loop = asyncio.get_running_loop()
745
- safe_threads = min(16, (os.cpu_count() or 2) * 4)
746
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=safe_threads)
747
  loop.set_default_executor(executor)
748
 
749
  await init_db()
750
  await reset_in_progress()
751
 
752
- # Check if DB is empty, then seed Tranco
753
  async with db_lock:
754
  async with db.execute("SELECT COUNT(*) FROM urls") as cursor:
755
  count = (await cursor.fetchone())[0]
@@ -773,7 +784,7 @@ async def start_app():
773
  for i in range(GLOBAL_CONCURRENCY):
774
  worker_tasks.append(asyncio.create_task(crawl_worker(i)))
775
 
776
- print(f"Crawler started with {GLOBAL_CONCURRENCY} workers and {safe_threads} CPU threads.")
777
 
778
  @asynccontextmanager
779
  async def lifespan(_app):
 
51
  TRANCO_URL = os.getenv("TRANCO_URL", "https://tranco-list.eu/top-1m.csv.zip")
52
  SEED_COUNT = int(os.getenv("SEED_COUNT", "100000"))
53
 
54
+ # Speed badhane ke liye concurrency 256 kar di gayi hai
55
+ GLOBAL_CONCURRENCY = int(os.getenv("GLOBAL_CONCURRENCY", "256"))
56
+ PER_HOST_CONCURRENCY = int(os.getenv("PER_HOST_CONCURRENCY", "2")) # Alag domains pe force karne ke liye kam kiya
57
+ REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", "10")) # 15 se 10 kiya taki slow sites time waste na karein
58
  CONNECT_TIMEOUT = float(os.getenv("CONNECT_TIMEOUT", "5"))
59
+ MAX_REDIRECTS = int(os.getenv("MAX_REDIRECTS", "3")) # 5 se 3 kiya
60
 
61
  MAX_RESPONSE_BYTES = int(os.getenv("MAX_RESPONSE_BYTES", str(4 * 1024 * 1024)))
62
  MAX_TEXT_CHARS = int(os.getenv("MAX_TEXT_CHARS", "20000"))
63
  MAX_KEYWORDS = int(os.getenv("MAX_KEYWORDS", "30"))
64
+
65
+ # Queue block hone se bachane ke liye 500 se 50 links per page kiya
66
+ MAX_LINKS_PER_PAGE = int(os.getenv("MAX_LINKS_PER_PAGE", "50"))
67
 
68
  DB_FLUSH_INTERVAL = float(os.getenv("DB_FLUSH_INTERVAL", "1.0"))
69
  HF_UPLOAD_INTERVAL = int(os.getenv("HF_UPLOAD_INTERVAL", str(3 * 60 * 60)))
 
79
  # APP & RUNTIME STATE
80
  # ============================================================
81
 
82
+ app = FastAPI(title="Nebulix Search Crawler", version="4.2.0")
83
 
84
  db: Optional[aiosqlite.Connection] = None
85
  http_session: Optional[aiohttp.ClientSession] = None
86
 
87
  frontier = asyncio.PriorityQueue()
88
+ record_queue = asyncio.Queue(maxsize=10000)
89
+ db_event_queue = asyncio.Queue(maxsize=100000) # Queue limit badha di gayi hai
90
 
91
  db_lock = asyncio.Lock()
92
  host_state_lock = asyncio.Lock()
 
173
  await db.execute("PRAGMA journal_mode=WAL")
174
  await db.execute("PRAGMA synchronous=NORMAL")
175
  await db.execute("PRAGMA temp_store=MEMORY")
176
+ await db.execute("PRAGMA mmap_size=3000000000") # Speed up SQLite
177
 
178
  await db.execute("""
179
  CREATE TABLE IF NOT EXISTS urls (
 
202
  batch.append(first)
203
  except asyncio.TimeoutError: pass
204
 
205
+ while len(batch) < 10000:
206
  try: batch.append(db_event_queue.get_nowait())
207
  except asyncio.QueueEmpty: break
208
 
 
242
 
243
  async def frontier_feeder():
244
  while True:
245
+ # RAM ko free rakhne ke liye memory queue ko 5000 par limit kiya
246
+ if frontier.qsize() > 5000:
247
+ await asyncio.sleep(0.5)
248
+ continue
249
+
250
  try:
251
  rows = []
252
  async with db_lock:
253
+ cursor = await db.execute("SELECT url_hash, url, priority, depth FROM urls WHERE status = 0 ORDER BY priority ASC, discovered_at ASC LIMIT 3000")
254
  rows = await cursor.fetchall()
255
 
256
  if rows:
 
269
  await asyncio.sleep(1)
270
 
271
  # ============================================================
272
+ # TRANCO SEED
273
  # ============================================================
274
 
275
  async def seed_tranco():
 
446
  async with semaphore:
447
  await respect_host_delay(host, DEFAULT_HOST_DELAY)
448
  timeout = ClientTimeout(total=REQUEST_TIMEOUT, connect=CONNECT_TIMEOUT)
449
+
450
+ # Dead websites par rukne ke bajaye aage badhne ke liye sirf 1 attempt
451
+ for attempt in range(1):
452
  try:
453
  async with session.get(url, timeout=timeout, allow_redirects=True, max_redirects=MAX_REDIRECTS) as response:
454
  status = response.status
 
 
 
 
 
455
  if status != 200: return None
456
 
457
  content_type = response.headers.get("Content-Type", "").lower()
 
469
  body = b"".join(chunks)
470
  async with stats_lock: stats["bytes_downloaded"] += len(body)
471
  return {"url": str(response.url), "status": status, "html": body, "headers": {str(k).lower(): str(v) for k, v in response.headers.items()}}
472
+ except Exception:
 
 
 
473
  return None
474
  return None
475
 
 
532
  batch.append(first)
533
  except asyncio.TimeoutError: pass
534
 
535
+ while len(batch) < 1000:
536
  try: batch.append(record_queue.get_nowait())
537
  except asyncio.QueueEmpty: break
538
 
 
608
  except Exception: pass
609
 
610
  # ============================================================
611
+ # CRAWLER WORKER (FIXED PRIORITY & BLOCKING)
612
  # ============================================================
613
 
614
  async def crawl_worker(worker_id):
 
621
  response = await fetch_page(http_session, url)
622
 
623
  if response is None:
624
+ if not db_event_queue.full():
625
+ db_event_queue.put_nowait(("failed", page_hash))
626
  async with stats_lock: stats["failed"] += 1
627
  continue
628
 
 
631
  record, discovered_urls, noindex = parsed
632
 
633
  if noindex:
634
+ if not db_event_queue.full():
635
+ db_event_queue.put_nowait(("done", page_hash))
636
  async with stats_lock: stats["noindex"] += 1
637
  else:
638
  await persist_record(record)
639
+ if not db_event_queue.full():
640
+ db_event_queue.put_nowait(("done", page_hash))
641
  async with stats_lock:
642
  stats["successful"] += 1
643
  recent_successes.append(time.monotonic())
 
647
 
648
  for discovered_url in discovered_urls:
649
  target_host = urllib.parse.urlsplit(discovered_url).hostname or ""
650
+
651
+ # FIX: Breadth-First Search. Alag domain ko +10 (High Priority), Same domain ko +100 (Low Priority)
652
+ # Isse workers alag-alag websites pe jayenge aur wait nahi karenge.
653
+ next_priority = priority + 10 if target_host != source_host else priority + 100
654
+
655
  normalized = normalize_url(discovered_url)
656
  if normalized:
657
+ if not db_event_queue.full():
658
+ db_event_queue.put_nowait(("discover", normalized, next_priority, next_depth))
659
+ async with stats_lock: stats["urls_discovered"] += 1
660
 
661
  except Exception as exc:
662
+ if not db_event_queue.full():
663
+ db_event_queue.put_nowait(("failed", page_hash))
664
  async with stats_lock: stats["failed"] += 1
665
  finally:
666
  frontier.task_done()
667
 
668
  # ============================================================
669
+ # API ENDPOINTS
670
  # ============================================================
671
 
672
  async def get_db_counts():
 
752
  async def start_app():
753
  global http_session
754
 
755
+ # CPU ka 70-80% use karne ke liye ThreadPool badha diya gaya hai
756
  loop = asyncio.get_running_loop()
757
+ safe_threads = min(48, (os.cpu_count() or 4) * 8)
758
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=safe_threads)
759
  loop.set_default_executor(executor)
760
 
761
  await init_db()
762
  await reset_in_progress()
763
 
 
764
  async with db_lock:
765
  async with db.execute("SELECT COUNT(*) FROM urls") as cursor:
766
  count = (await cursor.fetchone())[0]
 
784
  for i in range(GLOBAL_CONCURRENCY):
785
  worker_tasks.append(asyncio.create_task(crawl_worker(i)))
786
 
787
+ print(f"Crawler started with {GLOBAL_CONCURRENCY} workers and {safe_threads} CPU threads for maximum speed.")
788
 
789
  @asynccontextmanager
790
  async def lifespan(_app):