Skip to content

Commit 9ad6942

Browse files
Fix(lib.cache): apply fallback cache fix, fix tests (ansible#656)
Apply fix for Fallback cache premature file creation. AAP-36305
1 parent a65e8f7 commit 9ad6942

File tree

2 files changed

+60
-68
lines changed

2 files changed

+60
-68
lines changed

ansible_base/lib/cache/fallback_cache.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,6 @@
1717
PRIMARY_CACHE = 'primary'
1818
FALLBACK_CACHE = 'fallback'
1919

20-
_temp_path = get_setting('ANSIBLE_BASE_FALLBACK_CACHE_FILE_PATH', tempfile.gettempdir())
21-
_temp_file = Path().joinpath(_temp_path, 'gw_primary_cache_failed')
22-
23-
24-
def create_temp_file():
25-
_temp_file.touch()
26-
_temp_file.chmod(mode=0o660)
27-
2820

2921
class DABCacheWithFallback(BaseCache):
3022
_instance = None
@@ -44,15 +36,17 @@ def __init__(self, location, params):
4436

4537
self._primary_cache = django_cache.caches.create_connection(PRIMARY_CACHE)
4638
self._fallback_cache = django_cache.caches.create_connection(FALLBACK_CACHE)
39+
self._temp_path = get_setting('ANSIBLE_BASE_FALLBACK_CACHE_FILE_PATH', tempfile.gettempdir())
40+
self._temp_file = Path().joinpath(self._temp_path, 'gw_primary_cache_failed')
4741
self.thread_pool = ThreadPoolExecutor()
4842

49-
if _temp_file.exists():
50-
_temp_file.unlink()
43+
if self._temp_file.exists():
44+
self._temp_file.unlink()
5145

5246
self.__initialized = True
5347

5448
def get_active_cache(self):
55-
return FALLBACK_CACHE if _temp_file.exists() else PRIMARY_CACHE
49+
return FALLBACK_CACHE if self._temp_file.exists() else PRIMARY_CACHE
5650

5751
# Main cache interface
5852
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
@@ -71,7 +65,7 @@ def clear(self):
7165
return self._op_with_fallback("clear")
7266

7367
def _op_with_fallback(self, operation, *args, **kwargs):
74-
if _temp_file.exists():
68+
if self._temp_file.exists():
7569
response = getattr(self._fallback_cache, operation)(*args, **kwargs)
7670
self.start_recovery()
7771
else:
@@ -83,9 +77,10 @@ def _op_with_fallback(self, operation, *args, **kwargs):
8377
# Attempt to ensure one thread/process goes first
8478
# dynamic settings especially are read in a batch very quickly
8579
time.sleep(random.uniform(10, 100) / 100.0)
86-
if not _temp_file.exists():
80+
if not self._temp_file.exists():
8781
logger.error("Primary cache unavailable, switching to fallback cache.")
88-
create_temp_file()
82+
self.create_temp_file()
83+
# _temp_file.touch()
8984
response = getattr(self._fallback_cache, operation)(*args, **kwargs)
9085

9186
return response
@@ -104,14 +99,18 @@ def check_primary_cache(self):
10499
try:
105100
self._primary_cache.get('up_test')
106101
with multiprocessing.Lock():
107-
if _temp_file.exists():
102+
if self._temp_file.exists():
108103
logger.warning("Primary cache recovered, clearing and resuming use.")
109104
# Clear the primary cache
110105
self._primary_cache.clear()
111106
# Clear the backup cache just incase we need to fall back again (don't want it out of sync)
112107
self._fallback_cache.clear()
113-
_temp_file.unlink()
108+
self._temp_file.unlink()
114109
except Exception:
115110
pass
116111
finally:
117112
self._fallback_cache.delete('RECOVERY_IN_PROGRESS')
113+
114+
def create_temp_file(self):
115+
self._temp_file.touch()
116+
self._temp_file.chmod(mode=0o660)

test_app/tests/lib/cache/test_fallback_cache.py

Lines changed: 45 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,9 @@ def test_dead_primary():
142142

143143
@override_settings(CACHES=cache_settings)
144144
def test_ensure_temp_file_is_removed_on_init():
145-
temp_file = Path(tempfile.NamedTemporaryFile().name)
146-
with mock.patch('ansible_base.lib.cache.fallback_cache._temp_file', temp_file):
145+
tmp_path = tempfile.gettempdir()
146+
temp_file = Path().joinpath(tmp_path, 'gw_primary_cache_failed')
147+
with override_settings(ANSIBLE_BASE_FALLBACK_CACHE_FILE_PATH=tmp_path):
147148
temp_file.touch()
148149
# Remove singleton instance
149150
DABCacheWithFallback._instance = None
@@ -196,72 +197,64 @@ def test_all_methods_are_overwritten(method):
196197
@override_settings(CACHES=cache_settings)
197198
def test_check_primary_cache(file_exists):
198199
temp_file = Path(tempfile.NamedTemporaryFile().name)
199-
with mock.patch('ansible_base.lib.cache.fallback_cache._temp_file', temp_file):
200-
# Remove singleton instance
201-
DABCacheWithFallback._instance = None
202-
# Initialization of the cache will clear the temp file so do this first
203-
cache = DABCacheWithFallback(None, {})
204-
# Ensure cache is working
205-
cache._primary_cache.fixit()
206-
207-
# Create the temp file if needed
208-
if file_exists:
209-
temp_file.touch()
210-
else:
211-
try:
212-
temp_file.unlink()
213-
except Exception:
214-
pass
215-
mocked_function = mock.MagicMock(return_value=None)
216-
cache._primary_cache.clear = mocked_function
217-
cache.check_primary_cache()
218-
if file_exists:
219-
mocked_function.assert_called_once()
220-
else:
221-
mocked_function.assert_not_called()
222-
assert temp_file.exists() is False
200+
# Remove singleton instance
201+
DABCacheWithFallback._instance = None
202+
# Initialization of the cache will clear the temp file so do this first
203+
cache = DABCacheWithFallback(None, {})
204+
cache._temp_file = temp_file
205+
# Ensure cache is working
206+
cache._primary_cache.fixit()
207+
208+
# Create the temp file if needed
209+
if file_exists:
210+
temp_file.touch()
211+
else:
212+
try:
213+
temp_file.unlink()
214+
except Exception:
215+
pass
216+
mocked_function = mock.MagicMock(return_value=None)
217+
cache._primary_cache.clear = mocked_function
218+
cache.check_primary_cache()
219+
if file_exists:
220+
mocked_function.assert_called_once()
221+
else:
222+
mocked_function.assert_not_called()
223+
assert temp_file.exists() is False
223224

224225

225226
@override_settings(CACHES=cache_settings)
226227
def test_file_unlink_exception_does_not_cause_failure():
227228
temp_file = Path(tempfile.NamedTemporaryFile().name)
228-
with mock.patch('ansible_base.lib.cache.fallback_cache._temp_file', temp_file):
229-
cache = DABCacheWithFallback(None, {})
230-
# We can't do: temp_file.unlink = mock.MagicMock(side_effect=Exception('failed to unlink exception'))
231-
# Because unlink is marked as read only so we will just mock the cache.clear to raise in its place
232-
mocked_function = mock.MagicMock(side_effect=Exception('failed to delete a file exception'))
233-
cache._primary_cache.clear = mocked_function
229+
cache = DABCacheWithFallback(None, {})
230+
cache._temp_file = temp_file
231+
# We can't do: temp_file.unlink = mock.MagicMock(side_effect=Exception('failed to unlink exception'))
232+
# Because unlink is marked as read only so we will just mock the cache.clear to raise in its place
233+
mocked_function = mock.MagicMock(side_effect=Exception('failed to delete a file exception'))
234+
cache._primary_cache.clear = mocked_function
234235

235-
temp_file.touch()
236-
cache.check_primary_cache()
237-
# No assertion needed because we just want to make sure check_primary_cache does not raise
236+
temp_file.touch()
237+
cache.check_primary_cache()
238+
# No assertion needed because we just want to make sure check_primary_cache does not raise
238239

239240

241+
@override_settings(CACHES=cache_settings)
240242
def test_temp_file_setting(tmp_path):
241-
with override_settings(ANSIBLE_BASE_FALLBACK_CACHE_FILE_PATH=tmp_path):
242-
import importlib
243-
244-
from ansible_base.lib.cache import fallback_cache
245-
246-
importlib.reload(fallback_cache)
243+
import os
247244

245+
with override_settings(ANSIBLE_BASE_FALLBACK_CACHE_FILE_PATH=tmp_path):
246+
# Remove singleton instance
247+
DABCacheWithFallback._instance = None
248+
fallback_cache = DABCacheWithFallback(None, {})
248249
# Assert that the temp file does not exist yet
249250
assert not fallback_cache._temp_file.exists()
250251

251252
fallback_cache.create_temp_file()
252253

253254
# The temp file should now exist at the specified cache path
254255
assert fallback_cache._temp_file.exists()
256+
255257
expected_path = Path().joinpath(tmp_path, 'junk.txt')
256258
assert fallback_cache._temp_file.parent in expected_path.parents
257-
258-
259-
def test_temp_file_permissions():
260-
import os
261-
262-
from ansible_base.lib.cache.fallback_cache import _temp_file, create_temp_file
263-
264-
create_temp_file()
265-
assert _temp_file.exists
266-
status = os.stat(_temp_file)
267-
assert oct(status.st_mode)[-3:] == "660"
259+
status = os.stat(fallback_cache._temp_file)
260+
assert oct(status.st_mode)[-3:] == "660"

0 commit comments

Comments
 (0)