|
@@ -132,6 +132,9 @@ class _AirbnbClient:
|
|
|
self._fetched_months: set[tuple[int, int]] = set()
|
|
self._fetched_months: set[tuple[int, int]] = set()
|
|
|
self._raw_months: dict[tuple[int, int], list[dict]] = {}
|
|
self._raw_months: dict[tuple[int, int], list[dict]] = {}
|
|
|
self._months_inflight: dict[tuple[int, int], asyncio.Task] = {}
|
|
self._months_inflight: dict[tuple[int, int], asyncio.Task] = {}
|
|
|
|
|
+ # Tracks months where we've seen at least one booked day; survives clear_cache() so we can
|
|
|
|
|
+ # detect when a freshly-fetched month drops from N booked days to 0 (stale replica signal).
|
|
|
|
|
+ self._had_bookings: set[tuple[int, int]] = set()
|
|
|
self._refresh_lock = asyncio.Lock()
|
|
self._refresh_lock = asyncio.Lock()
|
|
|
self._refresh_task: asyncio.Task | None = None
|
|
self._refresh_task: asyncio.Task | None = None
|
|
|
self._http = httpx.AsyncClient(timeout=10.0)
|
|
self._http = httpx.AsyncClient(timeout=10.0)
|
|
@@ -321,8 +324,24 @@ class _AirbnbClient:
|
|
|
|
|
|
|
|
async def _get_month_days(self, month: int, year: int) -> list[dict]:
|
|
async def _get_month_days(self, month: int, year: int) -> list[dict]:
|
|
|
key = (month, year)
|
|
key = (month, year)
|
|
|
|
|
+ forced_refetch = False
|
|
|
if key in self._raw_months:
|
|
if key in self._raw_months:
|
|
|
- return self._raw_months[key]
|
|
|
|
|
|
|
+ cached = self._raw_months[key]
|
|
|
|
|
+ # If we've seen bookings for this month before but the cache now shows none,
|
|
|
|
|
+ # the data is likely from a stale eventual-consistency replica — force a refetch.
|
|
|
|
|
+ if key in self._had_bookings and not any(d.get('reservationData') for d in cached):
|
|
|
|
|
+ _log.warning(
|
|
|
|
|
+ 'Airbnb: %d-%02d cache shows 0 booked days but month had bookings; forcing refetch',
|
|
|
|
|
+ year, month,
|
|
|
|
|
+ )
|
|
|
|
|
+ del self._raw_months[key]
|
|
|
|
|
+ self._fetched_months.discard(key)
|
|
|
|
|
+ prefix = f'{year}-{month:02d}'
|
|
|
|
|
+ for d in [k for k in self._cache if k.startswith(prefix)]:
|
|
|
|
|
+ del self._cache[d]
|
|
|
|
|
+ forced_refetch = True
|
|
|
|
|
+ else:
|
|
|
|
|
+ return cached
|
|
|
if key in self._months_inflight:
|
|
if key in self._months_inflight:
|
|
|
return await self._months_inflight[key]
|
|
return await self._months_inflight[key]
|
|
|
task = asyncio.create_task(self._fetch_month_days(month, year))
|
|
task = asyncio.create_task(self._fetch_month_days(month, year))
|
|
@@ -331,6 +350,11 @@ class _AirbnbClient:
|
|
|
result = await task
|
|
result = await task
|
|
|
finally:
|
|
finally:
|
|
|
self._months_inflight.pop(key, None)
|
|
self._months_inflight.pop(key, None)
|
|
|
|
|
+ if any(d.get('reservationData') for d in result):
|
|
|
|
|
+ self._had_bookings.add(key)
|
|
|
|
|
+ elif forced_refetch:
|
|
|
|
|
+ # Forced refetch confirmed no bookings — stop guarding this month
|
|
|
|
|
+ self._had_bookings.discard(key)
|
|
|
self._raw_months[key] = result
|
|
self._raw_months[key] = result
|
|
|
return result
|
|
return result
|
|
|
|
|
|