11#!/usr/bin/env python
22# -*- coding: utf-8 -*-
33import os
4+ import sys
45import datetime
6+ import logging
57from functools import lru_cache
8+ from contextlib import contextmanager
69
710import duckdb
811import polars as pl
912from attrs import define , field
10- from filelock import FileLock
1113
1214from .base import DataLoader
1315from ..utils import get_subclasses , load_subclasses
1416
17+ logger = logging .getLogger (__name__ )
1518CUSTOM_DATA_LOADER_PATH = os .getenv (
1619 "DW_CUSTOM_DATA_LOADER_PATH" , "dw_custom.data_loaders"
1720)
2023_CACHE_TABLE = "cache_data"
2124_META_TABLE = "cache_meta"
2225
26+ _IS_WINDOWS = sys .platform == "win32"
27+
28+ if _IS_WINDOWS :
29+ from filelock import FileLock
30+
31+ @contextmanager
32+ def _read_lock (lock_path ):
33+ """Windows: 独占锁(不支持共享锁)"""
34+ with FileLock (lock_path ):
35+ yield
36+
37+ @contextmanager
38+ def _write_lock (lock_path ):
39+ """Windows: 独占锁"""
40+ with FileLock (lock_path ):
41+ yield
42+
43+ else :
44+ import fcntl
45+
46+ @contextmanager
47+ def _read_lock (lock_path ):
48+ """Linux/macOS: 共享锁,允许多进程并发读"""
49+ with open (lock_path , "a+" ) as f :
50+ fcntl .flock (f , fcntl .LOCK_SH )
51+ try :
52+ yield
53+ finally :
54+ fcntl .flock (f , fcntl .LOCK_UN )
55+
56+ @contextmanager
57+ def _write_lock (lock_path ):
58+ """Linux/macOS: 独占锁,阻塞直到所有读锁释放"""
59+ with open (lock_path , "a+" ) as f :
60+ fcntl .flock (f , fcntl .LOCK_EX )
61+ try :
62+ yield
63+ finally :
64+ fcntl .flock (f , fcntl .LOCK_UN )
65+
2366
2467@define ()
2568class DatabaseLoader (DataLoader ):
@@ -52,7 +95,7 @@ def _cache_paths(self):
5295 def _is_cache_valid (self , db_path , lock_path ) -> bool :
5396 if not os .path .exists (db_path ):
5497 return False
55- with FileLock (lock_path ):
98+ with _read_lock (lock_path ):
5699 with duckdb .connect (db_path , read_only = True ) as conn :
57100 row = conn .execute (f"SELECT cached_at FROM { _META_TABLE } " ).fetchone ()
58101 if row is None :
@@ -61,12 +104,13 @@ def _is_cache_valid(self, db_path, lock_path) -> bool:
61104 return (datetime .datetime .now () - cached_at ).total_seconds () < self .ttl_cache
62105
63106 def _read_cache (self , db_path , lock_path ) -> pl .DataFrame :
64- with FileLock (lock_path ):
107+ with _read_lock (lock_path ):
65108 with duckdb .connect (db_path , read_only = True ) as conn :
66109 return conn .execute (f"SELECT * FROM { _CACHE_TABLE } " ).pl ()
67110
68111 def _write_cache (self , db_path , lock_path , df : pl .DataFrame ):
69- with FileLock (lock_path ):
112+ logger .info ("About to update data, starting to acquire lock" )
113+ with _write_lock (lock_path ):
70114 with duckdb .connect (db_path ) as conn :
71115 conn .execute (
72116 f"CREATE OR REPLACE TABLE { _CACHE_TABLE } AS SELECT * FROM df"
0 commit comments