Fix booking scrape date selection — oldest-first tiered distribution
Medium and low priority tiers were generating full static date ranges (150 and 185 dates) every day. With a queue limit of 200, high (31) + medium (150) consumed the entire budget, leaving only ~19 slots for low priority — causing the observed ~6 month cap. Medium now selects the 60 oldest-scraped (or never-scraped) dates from the days 31-180 window; low selects the 30 oldest from days 181-365. Per-run budget drops from ~365 to ~121 dates, and coverage naturally cycles through the full year: medium every ~2-3 days, low every ~6-7 days. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9a0dc58b11
commit
d5ce38859d
1 changed files with 71 additions and 24 deletions
|
|
@ -1,16 +1,23 @@
|
||||||
"""
|
"""
|
||||||
Scheduled Booking.com Rate Scraping Job
|
Scheduled Booking.com Rate Scraping Job
|
||||||
|
|
||||||
Priority-based scheduling for 365-day coverage (all queued daily):
|
Tiered scheduling for 365-day coverage:
|
||||||
- High (priority 10): next 30 days
|
- High (priority 10): next 30 days — all 31 dates scraped every day
|
||||||
- Medium (priority 5): days 31-180
|
- Medium (priority 5): days 31-180 — up to 60 oldest-scraped dates per day
|
||||||
- Low (priority 2): days 181-365
|
- Low (priority 2): days 181-365 — up to 30 oldest-scraped dates per day
|
||||||
|
|
||||||
Queue processes in priority order. If rate-limited/blocked, lower priority
|
Per-run budget: ~121 dates (30 high + 60 medium + 30 low).
|
||||||
dates remain queued for the next run.
|
Medium and low tiers select whichever dates in their window have the oldest
|
||||||
|
(or missing) scrape data first, naturally distributing coverage across the
|
||||||
|
full year without hammering all 365 dates every day.
|
||||||
|
|
||||||
|
Approximate refresh cadence:
|
||||||
|
- High: daily
|
||||||
|
- Medium (150 dates / 60 per day): every ~2-3 days
|
||||||
|
- Low (185 dates / 30 per day): every ~6 days
|
||||||
|
|
||||||
Uses a queue-based approach:
|
Uses a queue-based approach:
|
||||||
1. Populate the queue with dates and priorities
|
1. Populate the queue with the selected dates and priorities
|
||||||
2. Process the queue in priority order
|
2. Process the queue in priority order
|
||||||
3. Failed dates are retried (up to 3 attempts)
|
3. Failed dates are retried (up to 3 attempts)
|
||||||
4. On blocking, the queue pauses and resumes after cooldown
|
4. On blocking, the queue pauses and resumes after cooldown
|
||||||
|
|
@ -22,6 +29,7 @@ import logging
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
from database import SyncSessionLocal
|
from database import SyncSessionLocal
|
||||||
from services.booking_scraper import (
|
from services.booking_scraper import (
|
||||||
populate_queue,
|
populate_queue,
|
||||||
|
|
@ -38,32 +46,69 @@ PRIORITY_HIGH = 10 # 0-30 days
|
||||||
PRIORITY_MEDIUM = 5 # 31-180 days
|
PRIORITY_MEDIUM = 5 # 31-180 days
|
||||||
PRIORITY_LOW = 2 # 181-365 days
|
PRIORITY_LOW = 2 # 181-365 days
|
||||||
|
|
||||||
|
# Per-run date budget for medium and low tiers
|
||||||
|
MEDIUM_DATES_PER_RUN = 60 # out of ~150 in window
|
||||||
|
LOW_DATES_PER_RUN = 30 # out of ~185 in window
|
||||||
|
|
||||||
|
|
||||||
def get_high_priority_dates() -> list[date]:
|
def get_high_priority_dates() -> list[date]:
|
||||||
"""High priority: today + 30 days."""
|
"""High priority: all of today through +30 days, scraped every run."""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
return [today + timedelta(days=i) for i in range(31)]
|
return [today + timedelta(days=i) for i in range(31)]
|
||||||
|
|
||||||
|
|
||||||
def get_medium_priority_dates() -> list[date]:
|
def _get_oldest_scraped_dates(db: Session, start: date, end: date, limit: int) -> list[date]:
|
||||||
"""Medium priority: days 31-180."""
|
"""
|
||||||
today = date.today()
|
From the date window [start, end], return up to `limit` dates ordered by
|
||||||
return [today + timedelta(days=i) for i in range(31, 181)]
|
oldest last-scrape first (never-scraped dates come first via NULLS FIRST).
|
||||||
|
|
||||||
|
Joining against booking_com_rates means we naturally pick whichever dates
|
||||||
|
in the window are most stale or have no data yet.
|
||||||
|
"""
|
||||||
|
rows = db.execute(
|
||||||
|
text("""
|
||||||
|
SELECT s.rate_date::date AS rate_date,
|
||||||
|
MAX(r.scraped_at) AS last_scraped
|
||||||
|
FROM generate_series(:start::date, :end::date, '1 day'::interval) AS s(rate_date)
|
||||||
|
LEFT JOIN booking_com_rates r ON r.rate_date = s.rate_date::date
|
||||||
|
GROUP BY s.rate_date
|
||||||
|
ORDER BY last_scraped ASC NULLS FIRST
|
||||||
|
LIMIT :limit
|
||||||
|
"""),
|
||||||
|
{'start': start, 'end': end, 'limit': limit}
|
||||||
|
).fetchall()
|
||||||
|
return [row.rate_date for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def get_low_priority_dates() -> list[date]:
|
def get_medium_priority_dates(db: Session, limit: int = MEDIUM_DATES_PER_RUN) -> list[date]:
|
||||||
"""Low priority: days 181-365."""
|
"""Medium priority: up to `limit` oldest-scraped dates in the days 31-180 window."""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
return [today + timedelta(days=i) for i in range(181, 366)]
|
return _get_oldest_scraped_dates(
|
||||||
|
db,
|
||||||
|
start=today + timedelta(days=31),
|
||||||
|
end=today + timedelta(days=180),
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_low_priority_dates(db: Session, limit: int = LOW_DATES_PER_RUN) -> list[date]:
|
||||||
|
"""Low priority: up to `limit` oldest-scraped dates in the days 181-365 window."""
|
||||||
|
today = date.today()
|
||||||
|
return _get_oldest_scraped_dates(
|
||||||
|
db,
|
||||||
|
start=today + timedelta(days=181),
|
||||||
|
end=today + timedelta(days=365),
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def compute_next_scrape_for_date(target_date: date) -> tuple[str, date | None]:
|
def compute_next_scrape_for_date(target_date: date) -> tuple[str, date | None]:
|
||||||
"""
|
"""
|
||||||
For a target date, determine its priority tier and when it will next be scraped.
|
For a target date, determine its priority tier and approximate next scrape date.
|
||||||
|
|
||||||
Returns (tier, next_scrape_date) where tier is 'high'/'medium'/'low'/'none'.
|
Returns (tier, next_scrape_date) where tier is 'high'/'medium'/'low'/'none'.
|
||||||
All dates are queued daily, so next scrape is always today (or tomorrow if
|
High dates are scraped every run; medium/low estimates reflect typical cadence
|
||||||
today's run has passed).
|
based on the per-run budget vs window size.
|
||||||
"""
|
"""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
offset = (target_date - today).days
|
offset = (target_date - today).days
|
||||||
|
|
@ -73,13 +118,15 @@ def compute_next_scrape_for_date(target_date: date) -> tuple[str, date | None]:
|
||||||
if offset > 365:
|
if offset > 365:
|
||||||
return ('none', None)
|
return ('none', None)
|
||||||
|
|
||||||
# All tiers run daily - next scrape is today
|
|
||||||
if offset <= 30:
|
if offset <= 30:
|
||||||
|
# Scraped every day
|
||||||
return ('high', today)
|
return ('high', today)
|
||||||
elif offset <= 180:
|
elif offset <= 180:
|
||||||
return ('medium', today)
|
# ~150 dates, 60/day → ~2-3 day cadence
|
||||||
|
return ('medium', today + timedelta(days=3))
|
||||||
else:
|
else:
|
||||||
return ('low', today)
|
# ~185 dates, 30/day → ~6-7 day cadence
|
||||||
|
return ('low', today + timedelta(days=7))
|
||||||
|
|
||||||
|
|
||||||
def run_scheduled_booking_scrape():
|
def run_scheduled_booking_scrape():
|
||||||
|
|
@ -104,10 +151,10 @@ def run_scheduled_booking_scrape():
|
||||||
cleanup_stale_batches(db, max_age_minutes=120)
|
cleanup_stale_batches(db, max_age_minutes=120)
|
||||||
clear_old_queue_items(db, days=3)
|
clear_old_queue_items(db, days=3)
|
||||||
|
|
||||||
# Gather dates with priorities
|
# Gather dates with priorities — medium/low select oldest-scraped first
|
||||||
high = get_high_priority_dates()
|
high = get_high_priority_dates()
|
||||||
medium = get_medium_priority_dates()
|
medium = get_medium_priority_dates(db)
|
||||||
low = get_low_priority_dates()
|
low = get_low_priority_dates(db)
|
||||||
|
|
||||||
priorities = {}
|
priorities = {}
|
||||||
for d in high:
|
for d in high:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue