22
33from __future__ import annotations
44
5+ import errno
56import hashlib
7+ import logging
68import os
79import re
810import threading
1315from dotenv import dotenv_values
1416
1517_ASSIGNMENT_PATTERN = re .compile (r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$" )
18+ _FALLBACK_REWRITE_ERRNOS = {errno .EBUSY , errno .EXDEV }
19+
20+ logger = logging .getLogger (__name__ )
1621
1722
1823class ConfigManager :
@@ -64,7 +69,7 @@ def apply_updates(
6469 sensitive_keys : Set [str ],
6570 mask_token : str ,
6671 ) -> Tuple [List [str ], List [str ], str ]:
67- """Apply updates into `.env` file using atomic replace semantics ."""
72+ """Apply updates into `.env` file using atomic replace when possible ."""
6873 with self ._lock :
6974 current_values = self .read_config_map ()
7075 mutable_updates : Dict [str , str ] = {}
@@ -90,7 +95,7 @@ def apply_updates(
9095 return list (mutable_updates .keys ()), skipped_masked , self .get_config_version ()
9196
9297 def _atomic_upsert (self , updates : Dict [str , str ]) -> None :
93- """Write updates with temp file + fsync + rename strategy ."""
98+ """Write updates with atomic rename and in-place fallback for mounted files ."""
9499 lines = self ._read_lines ()
95100 key_to_index = self ._find_last_key_indexes (lines )
96101
@@ -115,7 +120,27 @@ def _atomic_upsert(self, updates: Dict[str, str]) -> None:
115120 file_obj .flush ()
116121 os .fsync (file_obj .fileno ())
117122
118- os .replace (temp_path , self ._env_path )
123+ try :
124+ os .replace (temp_path , self ._env_path )
125+ except OSError as exc :
126+ if exc .errno not in _FALLBACK_REWRITE_ERRNOS :
127+ raise
128+
129+ logger .warning (
130+ "Atomic replace for .env failed with errno=%s, falling back to in-place rewrite" ,
131+ exc .errno ,
132+ )
133+ self ._rewrite_in_place (content )
134+ finally :
135+ if temp_path .exists ():
136+ temp_path .unlink ()
137+
138+ def _rewrite_in_place (self , content : str ) -> None :
139+ """Rewrite `.env` content in place when rename is unsupported by mount type."""
140+ with self ._env_path .open ("w" , encoding = "utf-8" , newline = "\n " ) as file_obj :
141+ file_obj .write (content )
142+ file_obj .flush ()
143+ os .fsync (file_obj .fileno ())
119144
120145 def _read_lines (self ) -> List [str ]:
121146 if not self ._env_path .exists ():
0 commit comments