66from contextlib import suppress
77import logging
88from os import (
9+ O_RDONLY as os_O_RDONLY ,
10+ close as os_close ,
911 fsync as os_fsync ,
1012 getenv as os_getenv ,
1113 getpid as os_getpid ,
1214 name as os_name ,
15+ open as os_open ,
1316)
1417from os .path import expanduser as os_path_expand_user , join as os_path_join
1518from pathlib import Path
2730_LOGGER = logging .getLogger (__name__ )
2831
2932
33+ def _fsync_parent_dir (path : Path ) -> None :
34+ """Ensure persistence on POSIX."""
35+ fd = os_open (str (path ), os_O_RDONLY )
36+ try :
37+ os_fsync (fd )
38+ finally :
39+ os_close (fd )
40+
41+
3042class PlugwiseCache :
3143 """Base class to cache plugwise information."""
3244
@@ -87,7 +99,9 @@ def _get_writable_os_dir(self) -> str:
8799 )
88100 return os_path_join (os_path_expand_user ("~" ), CACHE_DIR )
89101
90- async def write_cache (self , data : dict [str , str ], rewrite : bool = False ) -> None :
102+ async def write_cache (
103+ self , data : dict [str , str ], rewrite : bool = False
104+ ) -> None : # noqa: PLR0912
91105 """Save information to cache file atomically using aiofiles + temp file."""
92106 if not self ._initialized :
93107 raise CacheError (
@@ -98,15 +112,15 @@ async def write_cache(self, data: dict[str, str], rewrite: bool = False) -> None
98112 if not rewrite :
99113 current_data = await self .read_cache ()
100114
101- processed_keys : list [str ] = []
115+ processed_keys : set [str ] = set ()
102116 data_to_write : list [str ] = []
103117
104118 # Prepare data exactly as in original implementation
105119 for _cur_key , _cur_val in current_data .items ():
106120 _write_val = _cur_val
107121 if _cur_key in data :
108122 _write_val = data [_cur_key ]
109- processed_keys .append (_cur_key )
123+ processed_keys .add (_cur_key )
110124 data_to_write .append (f"{ _cur_key } { CACHE_KEY_SEPARATOR } { _write_val } \n " )
111125
112126 # Write remaining new data
@@ -129,16 +143,27 @@ async def write_cache(self, data: dict[str, str], rewrite: bool = False) -> None
129143 file = str (temp_path ),
130144 mode = "w" ,
131145 encoding = UTF8 ,
146+ newline = "\n " ,
132147 ) as temp_file :
133148 await temp_file .writelines (data_to_write )
134149 await temp_file .flush ()
135150 # Ensure data reaches disk before rename
136- loop = get_running_loop ()
137- await loop .run_in_executor (None , os_fsync , temp_file .fileno ())
151+ try :
152+ loop = get_running_loop ()
153+ await loop .run_in_executor (None , os_fsync , temp_file .fileno ())
154+ except (OSError , TypeError , AttributeError ):
155+ # If fsync fails due to fileno() issues or other problems,
156+ # continue without it. flush() provides reasonable durability.
157+ pass
138158
139159 # Atomic rename (overwrites atomically on all platforms)
140160 temp_path .replace (cache_file_path )
141161 temp_path = None # Successfully renamed
162+ if os_name != "nt" :
163+ # Ensure directory entry is persisted on POSIX
164+ await loop .run_in_executor (
165+ None , _fsync_parent_dir , cache_file_path .parent
166+ )
142167
143168 if not self ._cache_file_exists :
144169 self ._cache_file_exists = True
@@ -165,7 +190,7 @@ async def read_cache(self) -> dict[str, str]:
165190 """Return current data from cache file."""
166191 if not self ._initialized :
167192 raise CacheError (
168- f"Unable to save cache. Initialize cache file '{ self ._file_name } ' first."
193+ f"Unable to read cache. Initialize cache file '{ self ._file_name } ' first."
169194 )
170195 current_data : dict [str , str ] = {}
171196 if self ._cache_file is None :
0 commit comments