فهرست مقاله
برنامهٔ ثبت دادهٔ مهندسی ممکن است هنگام ذخیرهٔ آخرین snapshot خطا بدهد؛ اما از دید کاربر، مهم است بداند نسخهٔ قبلی مانده یا نسخهٔ تازه جای آن نشسته است. یک پیام کلیِ «ذخیره نشد» همیشه این وضعیت را درست توصیف نمیکند. در این درس، ذخیرهٔ یک فایل بایتی کوچک را در پوشهٔ موقتِ اختصاصی اجرا میکنیم و خطای قبل و بعد از جایگزینی را جدا میسنجیم. دادهها ساختگیاند و آزمایش قطع برق انجام نمیدهیم.
۱. مرز ذخیره را پیش از پیام خطا تعریف کنیم

هدف، نوشتن یک snapshot کامل با نام مشخص است؛ ثبت افزایشی رویدادها یا تراکنش چندفایلی نداریم. ابتدا نسخهٔ تازه در فایل موقت ساخته میشود و سپس نام هدف به آن جایگزین میشود. قرارداد ما فقط یک نویسنده در پوشهٔ خصوصی و مورداعتماد دارد. رقابت نویسندگان، تغییر خصمانهٔ مسیرها و نسخهبندی همزمان خارج از دامنهاند؛ کنترل سادهٔ نام، جای طراحی امنیت مسیر یا قفل را نمیگیرد.
payload باید bytes دقیق و حداکثر ۶۵۵۳۶ بایت باشد. دادهٔ خالی مجاز است و معنای فایل خالی دارد؛ عملیاتِ بیاثر نیست. نام باید ساده، ASCII و با پسوند bin باشد و مسیر نسبیِ دارای پوشه پذیرفته نمیشود. این محدودیتها سیاست همین مثالاند. تابع محتوای JSON، واحد حسگر یا معتبر بودن مقدار اندازهگیری را بررسی نمیکند؛ فقط قرارداد بایتیِ ذخیره را میداند.
سه نتیجهٔ خطا را تفکیک میکنیم: not_attempted یعنی هنوز فراخوانی جایگزینی آغاز نشده؛ unconfirmed یعنی خطا در مرز فراخوانیِ وابستگی جایگزینی رخ داده؛ completed_locally یعنی آن فراخوانی برگشته ولی مرحلهٔ بعد خطا داده است. نام سوم تنها دربارهٔ روند محلی برنامه است و به معنای دوام در قطع برق یا تأیید دریافت در دستگاه دیگر نیست. خودِ خطا نیز نباید بدون بررسی نتیجه، دستور تکرار کور عملیات بسازد.
۲. فایل موقت همپوشه و محدودهٔ fsync

مرجع NamedTemporaryFile پایتون نام قابلمشاهده و سیاست حذف فایل موقت را توضیح میدهد. فایل را در همان پوشهٔ هدف، با delete=False میسازیم و پیش از جایگزینی میبندیم؛ این ترتیب برای قواعد دستهٔ فایل در ویندوز مهم است. نام تصادفیِ ساختهشده را خودمان حدس نمیزنیم و پاکسازی فقط همان مسیرِ ثبتشده را هدف میگیرد.
پس از write، ابتدا flush بافر پایتون و بعد fsync روی توصیفگر همان فایل فراخوانی میشود. مرجع fsync تفاوت فراخوانی بومی یونیکس و _commit در ویندوز را بیان میکند. در این اجرا، بازگشت موفقِ fsync فایل ثبت شده است؛ همگامسازی پوشه انجام نمیدهیم. رفتار کش سختافزار، ثبت متادیتای نام و بازیابی پس از خرابی در این آزمون سنجیده نشدهاند.
مستندات os.replace جایگزینی فایل موجود، احتمال شکست بین فایلسیستمها و اتمیبودن تغییرنام موفق را با اشاره به الزام POSIX بیان میکند. مثال ما در ویندوز اجرا شده و تنها بایتهای قبل و بعد را بهترتیب میخواند. این مشاهدهٔ ترتیبی، آزمونِ خوانندهٔ همزمان یا تضمین رفتار همهٔ سیستمعاملها و فایلسیستمها نیست؛ اتمیبودن مشاهده را نیز با دوام در قطع برق یکی نمیگیریم.
۳. کد کامل و نقطههای تزریق خطا

کد با Python 3.14.5 اجرا شده و مسیرهای آزمایش در یک پوشهٔ یکتای اختصاصی کنار اسکریپت ساخته میشوند. هیچ فایل کاربر یا پایگاه دادهای هدف نیست. نام، نوع، اندازه، وابستگیها و پوشه پیش از ساخت فایل موقت کنترل میشوند. تابع از خطای معمول Exception یک خطای دارای stage میسازد و علت اولیه را بهصورت cause حفظ میکند؛ اعتبارسنجی اولیه مسیر جدا دارد.
"""Original local snapshot replacement/error-outcome lesson; no crash testing."""
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import tempfile
class SnapshotWriteError(OSError):
def __init__(self, stage):
self.stage = stage
self.outcome = ('unconfirmed' if stage == 'replace' else
'completed_locally' if stage == 'after_replace' else 'not_attempted')
self.cleanup_error = None
super().__init__('snapshot write failed at ' + stage)
def write_snapshot(directory, name, payload, *, inject=None, observer=None, replace_file=os.replace):
"""Private trusted directory, one writer, exact bytes <=65536; no durability claim."""
if type(name) is not str or re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,31}\.bin', name) is None:
raise ValueError('simple ASCII .bin name required')
if type(payload) is not bytes:
raise TypeError('payload must be exact bytes')
if len(payload) > 65536:
raise ValueError('payload exceeds authored byte limit')
if inject not in (None, 'before_replace', 'after_replace'):
raise ValueError('unknown authored injection stage')
if observer is not None and not callable(observer):
raise TypeError('observer must be callable or None')
if not callable(replace_file):
raise TypeError('replacement dependency must be callable')
base = Path(directory).resolve(strict=True)
if not base.is_dir():
raise ValueError('existing trusted directory required')
candidate = base / name
if candidate.is_symlink():
raise ValueError('target symlink excluded from this lesson')
target = candidate.resolve()
if target.parent != base or (target.exists() and not target.is_file()):
raise ValueError('target must remain a regular-file path in the chosen directory')
digest = hashlib.sha256(payload).hexdigest()
temporary, stage, failure, cause = None, 'staging', None, None
receipt = None
try:
with tempfile.NamedTemporaryFile(mode='wb', dir=base, prefix='.stage-', suffix='.bin', delete=False) as stream:
temporary = Path(stream.name).resolve()
if temporary.parent != base:
raise RuntimeError('unexpected temporary-file parent')
if stream.write(payload) != len(payload):
raise OSError('short staged write')
stream.flush()
os.fsync(stream.fileno())
# The named file is closed before replacement, including on Windows.
stage = 'before_replace'
if observer is not None:
observer(stage, temporary, target)
if inject == stage:
raise RuntimeError('authored before-replace injection')
stage = 'replace'
replace_file(temporary, target)
stage = 'after_replace'
if observer is not None:
observer(stage, temporary, target)
if inject == stage:
raise RuntimeError('authored after-replace injection')
receipt = {'bytes': len(payload), 'sha256': digest, 'replacement_returned': True,
'file_fsync_returned': True, 'directory_fsync_performed': False}
except Exception as error:
cause, failure = error, SnapshotWriteError(stage)
finally:
if temporary is not None:
try:
temporary.unlink(missing_ok=True) # only this one tracked file, no recursive deletion
except OSError as error:
if failure is None:
cause, failure = error, SnapshotWriteError(stage)
failure.cleanup_error = type(error).__name__
if failure is not None:
raise failure from cause
return receipt
def _raises(expected, function, *args, **kwargs):
try:
function(*args, **kwargs)
except expected:
return True
return False
def _failure(function, *args, **kwargs):
try:
function(*args, **kwargs)
except SnapshotWriteError as error:
return error
raise AssertionError('expected authored failure')
def run_example():
# Every execution owns one unique directory beneath this script's fixture root.
fixture_root = (Path(__file__).resolve().parent / 'file-replace-fixtures').resolve()
fixture_root.mkdir(exist_ok=True)
work = Path(tempfile.mkdtemp(prefix='run-', dir=fixture_root)).resolve()
if work.parent != fixture_root:
raise RuntimeError('fixture directory outside intended parent')
old, new = b'OLD-v1', b'NEW-v2'
try:
target = work / 'snapshot.bin'
target.write_bytes(old)
observations = []
def observe(stage, temporary, destination):
observations.append({'stage': stage, 'same_parent': temporary.parent == destination.parent == work,
'target_hex': destination.read_bytes().hex(),
'temporary_hex': temporary.read_bytes().hex() if temporary.exists() else None})
receipt = write_snapshot(work, 'snapshot.bin', new, observer=observe)
success_bytes = target.read_bytes()
first_receipt = write_snapshot(work, 'first.bin', b'FIRST')
target.write_bytes(old)
before = _failure(write_snapshot, work, 'snapshot.bin', new, inject='before_replace')
before_bytes = target.read_bytes()
after = _failure(write_snapshot, work, 'snapshot.bin', new, inject='after_replace')
after_bytes = target.read_bytes()
binary = b'\x00\xff\r\n' + 'ولتاژ'.encode('utf-8')
write_snapshot(work, 'binary.bin', binary)
write_snapshot(work, 'empty.bin', b'')
largest = b'Z' * 65536
maximum_receipt = write_snapshot(work, 'maximum.bin', largest)
oversize_rejected = _raises(ValueError, write_snapshot, work, 'maximum.bin', b'Z' * 65537)
repeat_receipt = write_snapshot(work, 'snapshot.bin', new)
repeated_bytes = target.read_bytes()
bad_names = ('../escape.bin', 'sub/file.bin', 'sub\\file.bin', '/abs.bin', '', 'no_extension', '.hidden.bin', 1)
name_checks = [_raises(ValueError, write_snapshot, work, name, new) for name in bad_names]
payload_checks = [_raises(TypeError, write_snapshot, work, 'snapshot.bin', value)
for value in ('text', bytearray(b'x'), memoryview(b'x'), None, True, float('nan'))]
dependency_checks = [
_raises(ValueError, write_snapshot, work, 'snapshot.bin', new, inject='crash'),
_raises(TypeError, write_snapshot, work, 'snapshot.bin', new, observer=1),
_raises(TypeError, write_snapshot, work, 'snapshot.bin', new, replace_file=None),
]
missing = _raises(FileNotFoundError, write_snapshot, work / 'missing', 'snapshot.bin', new)
blocked = work / 'blocked.bin'
blocked.mkdir()
directory_target_rejected = _raises(ValueError, write_snapshot, work, 'blocked.bin', new)
oracle_cases = []
for number, data in enumerate((b'X', b'\x00\xff', b'\r\n', b'\x00' * 64)):
name = 'oracle' + str(number) + '.bin'
path = work / name
for fault in (None, 'before_replace', 'after_replace'):
path.write_bytes(old)
if fault is None:
write_snapshot(work, name, data)
else:
_failure(write_snapshot, work, name, data, inject=fault)
# Independent table: only the known pre-boundary injection leaves old bytes.
expected = old if fault == 'before_replace' else data
oracle_cases.append(path.read_bytes() == expected)
def fail_before_replacement(source, destination):
raise OSError('mock dependency failed before calling real replacement')
def replace_then_fail(source, destination):
os.replace(source, destination)
raise OSError('mock dependency raised after real replacement')
target.write_bytes(old)
unconfirmed_old = _failure(write_snapshot, work, 'snapshot.bin', new, replace_file=fail_before_replacement)
old_seen = target.read_bytes()
unconfirmed_new = _failure(write_snapshot, work, 'snapshot.bin', new, replace_file=replace_then_fail)
new_seen = target.read_bytes()
temporary_leftovers = [p.name for p in work.iterdir() if p.name.startswith('.stage-')]
checks = {
'normal_existing_target_replaced_with_complete_expected_bytes': success_bytes == new
and receipt['bytes'] == len(new) and receipt['sha256'] == hashlib.sha256(new).hexdigest(),
'first_publication_creates_previously_absent_target': (work / 'first.bin').read_bytes() == b'FIRST'
and first_receipt['replacement_returned'] is True,
'injected_before_replace_preserves_old_target': before.stage == 'before_replace'
and before.outcome == 'not_attempted' and before_bytes == old,
'injected_after_replace_error_still_leaves_new_target': after.stage == 'after_replace'
and after.outcome == 'completed_locally' and after_bytes == new,
'independent_boundary_observer_sees_old_then_new_bytes': observations == [
{'stage': 'before_replace', 'same_parent': True, 'target_hex': old.hex(), 'temporary_hex': new.hex()},
{'stage': 'after_replace', 'same_parent': True, 'target_hex': new.hex(), 'temporary_hex': None}],
'staging_same_parent_and_file_sync_receipt_not_directory_sync': all(x['same_parent'] for x in observations)
and receipt['file_fsync_returned'] is True and receipt['directory_fsync_performed'] is False,
'binary_nul_high_byte_crlf_and_utf8_preserved_exactly': (work / 'binary.bin').read_bytes() == binary,
'empty_payload_is_explicit_empty_snapshot_not_no_op': (work / 'empty.bin').read_bytes() == b'',
'maximum_payload_inclusive_and_larger_rejected_before_change': maximum_receipt['bytes'] == 65536
and oversize_rejected and (work / 'maximum.bin').read_bytes() == largest,
'repeated_identical_payload_matches_bytes_and_content_digest_only': repeated_bytes == new
and repeat_receipt['sha256'] == receipt['sha256'],
'tracked_temporary_files_cleaned_for_selected_success_and_exception_paths': temporary_leftovers == [],
'simple_filename_policy_rejects_paths_and_invalid_names': all(name_checks),
'payload_type_contract_rejects_text_buffers_bool_and_nan': all(payload_checks),
'unknown_fault_stage_and_invalid_dependencies_rejected': all(dependency_checks),
'missing_parent_rejected_without_creating_directory': missing and not (work / 'missing').exists(),
'existing_directory_is_not_a_snapshot_file': directory_target_rejected and blocked.is_dir()
and list(blocked.iterdir()) == [],
'independent_expected_outcome_table_twelve_cases': len(oracle_cases) == 12 and all(oracle_cases),
'replace_dependency_error_unconfirmed_despite_opposite_observed_results': unconfirmed_old.outcome == unconfirmed_new.outcome == 'unconfirmed'
and old_seen == old and new_seen == new
and type(unconfirmed_old.__cause__) is OSError and type(unconfirmed_new.__cause__) is OSError
and type(before.__cause__) is RuntimeError and type(after.__cause__) is RuntimeError,
}
assert len(checks) == 18 and all(type(v) is bool and v for v in checks.values())
return {'runtime': {'python': platform.python_version(), 'platform_system': platform.system(), 'libraries': ['stdlib local filesystem; no crash/power failure or concurrent reader test']},
'contract': {'payload': 'exact bytes0..65536, private trusted existing directory, simple ASCII .bin filename',
'publication': 'closed same-parent named temp -> os.replace; file flush/fsync returned before replace',
'error_outcomes': ['not_attempted', 'unconfirmed', 'completed_locally'],
'scope': 'single writer; no hostile path mutation, concurrent update, directory metadata sync, permission preservation or live database'},
'metrics': {'normal_result_hex': success_bytes.hex(), 'before_injection_result_hex': before_bytes.hex(),
'after_injection_result_hex': after_bytes.hex(), 'before_injection_outcome': before.outcome,
'after_injection_outcome': after.outcome, 'unconfirmed_dependency_old_hex': old_seen.hex(),
'unconfirmed_dependency_new_hex': new_seen.hex(), 'same_error_outcome_for_dependency_cases': unconfirmed_old.outcome,
'receipt': receipt, 'independent_expected_table_cases': len(oracle_cases), 'temporary_leftovers': len(temporary_leftovers)},
'checks': checks,
'limitations': ['Injected Python exceptions and controlled replacement dependencies are NOT actual OS crash, process kill or power-failure tests.',
'Sequential before/after byte observations do not test concurrent reader atomicity; consult OS/filesystem guarantees separately.',
'fsync called only on staged file; no directory sync, hardware-cache or power-loss durability guarantee.',
'No concurrency control/CAS, metadata/ACL preservation, backups, multi-file transaction, hostile-directory security or SQLite copying claim.',
'Cleanup covers selected normal Exception paths; abrupt termination/BaseException or unlink failures may leave artifacts; cleanup_error is recorded.']}
finally:
# Checked absolute unique fixture scope; remove only direct known fixture entries, never recursively.
if work.parent != fixture_root or not work.name.startswith('run-'):
raise RuntimeError('unexpected cleanup scope')
for entry in work.iterdir():
if entry.parent.resolve() != work:
raise RuntimeError('unexpected fixture entry')
if entry.is_file():
entry.unlink()
elif entry.name == 'blocked.bin' and entry.is_dir():
entry.rmdir()
else:
raise RuntimeError('unexpected nested fixture object')
work.rmdir()
if __name__ == '__main__':
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
در before_replace، فایل موقت کامل و بسته شده، اما نام هدف هنوز دستنخورده است. خطای تزریقشدهٔ این نقطه باید نسخهٔ OLD-v1 را نگه دارد. در after_replace، os.replace قبلاً برگشته است؛ خطای تزریقشده مانع بازگشت رسید میشود، ولی نسخهٔ NEW-v2 اکنون در هدف دیده میشود. این دو خطا نشان میدهند نبود رسید موفق، لزوماً نبود اثر ذخیره نیست.
دو وابستگی ساختگی هم داریم: یکی پیش از فراخوانی جایگزینی واقعی خطا میدهد و دیگری ابتدا جایگزینی واقعی را انجام میدهد، سپس خطا میدهد. هر دو از دید تابع در stage برابر replace هستند و unconfirmed گزارش میشوند، اما بایت نهایی متفاوت است. این تزریق، سیاست محافظهکارانهٔ تفسیر خطا را میسنجد؛ ادعا نمیکند خطای واقعیِ هر سیستمعامل دقیقاً یکی از همین مسیرهاست.
۴. مرجع مستقلِ بایتها و هجده گروه بررسی

در اجرای عادی، مشاهدهگر مستقل پیش از جایگزینی، OLD-v1 را در هدف و NEW-v2 را در فایل موقت میخواند. پس از جایگزینی، هدف NEW-v2 است و نام موقت دیگر وجود ندارد. خود مشاهدهگر از رسیدِ تابع برای ساخت جواب استفاده نمیکند. خواندنهایش قبل از ادامه بسته میشوند؛ باز نگهداشتن دستهٔ فایل یا رفتار اشتراک دسترسی ویندوز را در این سناریو وارد نکردهایم.
{
"runtime": {
"python": "3.14.5",
"platform_system": "Windows",
"libraries": [
"stdlib local filesystem; no crash/power failure or concurrent reader test"
]
},
"contract": {
"payload": "exact bytes0..65536, private trusted existing directory, simple ASCII .bin filename",
"publication": "closed same-parent named temp -> os.replace; file flush/fsync returned before replace",
"error_outcomes": [
"not_attempted",
"unconfirmed",
"completed_locally"
],
"scope": "single writer; no hostile path mutation, concurrent update, directory metadata sync, permission preservation or live database"
},
"metrics": {
"normal_result_hex": "4e45572d7632",
"before_injection_result_hex": "4f4c442d7631",
"after_injection_result_hex": "4e45572d7632",
"before_injection_outcome": "not_attempted",
"after_injection_outcome": "completed_locally",
"unconfirmed_dependency_old_hex": "4f4c442d7631",
"unconfirmed_dependency_new_hex": "4e45572d7632",
"same_error_outcome_for_dependency_cases": "unconfirmed",
"receipt": {
"bytes": 6,
"sha256": "78ee777bd4fdc162ba71bc7327ac501314ef8640d3bc9b39b19dfc6e95fdfb75",
"replacement_returned": true,
"file_fsync_returned": true,
"directory_fsync_performed": false
},
"independent_expected_table_cases": 12,
"temporary_leftovers": 0
},
"checks": {
"normal_existing_target_replaced_with_complete_expected_bytes": true,
"first_publication_creates_previously_absent_target": true,
"injected_before_replace_preserves_old_target": true,
"injected_after_replace_error_still_leaves_new_target": true,
"independent_boundary_observer_sees_old_then_new_bytes": true,
"staging_same_parent_and_file_sync_receipt_not_directory_sync": true,
"binary_nul_high_byte_crlf_and_utf8_preserved_exactly": true,
"empty_payload_is_explicit_empty_snapshot_not_no_op": true,
"maximum_payload_inclusive_and_larger_rejected_before_change": true,
"repeated_identical_payload_matches_bytes_and_content_digest_only": true,
"tracked_temporary_files_cleaned_for_selected_success_and_exception_paths": true,
"simple_filename_policy_rejects_paths_and_invalid_names": true,
"payload_type_contract_rejects_text_buffers_bool_and_nan": true,
"unknown_fault_stage_and_invalid_dependencies_rejected": true,
"missing_parent_rejected_without_creating_directory": true,
"existing_directory_is_not_a_snapshot_file": true,
"independent_expected_outcome_table_twelve_cases": true,
"replace_dependency_error_unconfirmed_despite_opposite_observed_results": true
},
"limitations": [
"Injected Python exceptions and controlled replacement dependencies are NOT actual OS crash, process kill or power-failure tests.",
"Sequential before/after byte observations do not test concurrent reader atomicity; consult OS/filesystem guarantees separately.",
"fsync called only on staged file; no directory sync, hardware-cache or power-loss durability guarantee.",
"No concurrency control/CAS, metadata/ACL preservation, backups, multi-file transaction, hostile-directory security or SQLite copying claim.",
"Cleanup covers selected normal Exception paths; abrupt termination/BaseException or unlink failures may leave artifacts; cleanup_error is recorded."
]
}
جدول مرجع برای چهار payload متفاوت و سه وضعیتِ عادی، خطای پیشین و خطای پسین، دوازده نتیجهٔ معلوم دارد. همهٔ این موردها یک گروه از هجده گروه نامدارند. گروههای دیگر ایجاد هدفِ قبلاً ناموجود، بایت صفر و FF، CRLF، متنِ ازقبل UTF-8شده، فایل خالی، مرز اندازه و رد نام مسیر را کنترل میکنند. هیچ تبدیل خودکار متن یا پایانخطی در نوشتن باینری انجام نمیشود.
رسید، طول بایت و SHA-256 همان payload را دارد؛ این digest برای مقایسهٔ محتوای مورد انتظار است و امضای احراز اصالت نیست. نوشتن دوبارهٔ همان payload در این آزمایش بایت و digest یکسان میدهد، ولی هویت فایل، زمانها، مجوزها یا اثر همزمانی را ثابت نمیکند. در سامانهٔ واقعی، مشاهدهٔ نسخهٔ تازه باید با سیاست نسخهبندی و وضعیت عملیات تطبیق داده شود؛ این تابع آن سامانه را پیادهسازی نمیکند.
پاکسازی مسیرهای معمولِ موفق و خطا در این نمونه بررسی شده و فایل موقتِ باقیمانده صفر است. اگر unlink خطا بدهد، نوع خطای پاکسازی ثبت میشود؛ از این مسیر، rollback اعلام نمیکنیم. مسیرهای BaseException آزمایش نشدهاند؛ کشتهشدن ناگهانی فرایند یا قطع برق نیز ممکن است فرصت اجرای finally ندهد. همچنین مالکیت، ACL، مجوزهای نسخهٔ قبلی و نسخهٔ پشتیبان حفظ یا بازسازی نمیشوند.
گزارش رویداد حسگر به ثبت رخداد با فیلدهای محدود کمک میکند و قرارداد JSON حسگر اعتبار داده را پیش از ذخیره روشن میسازد. نتیجهٔ این درس برای فایل snapshot مستقل است؛ کپیکردن فایل پایگاه دادهٔ زنده یا تراکنش SQLite توصیه نمیشود. منابع در ۲۴ مهر ۱۴۰۵، با مستندات مرجع 3.14.8 خوانده شدند؛ نسخهٔ اجرای محلی جدا ثبت شده است.
۵. چهار پرسش با پاسخ کامل

پرسش اول: خطای before_replace در مثال کدام نسخه را باقی میگذارد؟ پاسخ: OLD-v1؛ فایل موقت آماده شده، اما فراخوانی جایگزینی هنوز انجام نشده است. فایل موقت در مسیر خطای معمولِ آزمودهشده پاک میشود.
پرسش دوم: خطای after_replace یعنی NEW-v2 ذخیره نشده؟ پاسخ: خیر؛ در این تزریق، جایگزینی قبلاً برگشته و بایت تازه در هدف دیده میشود. رسید موفق برنگشته، ولی اثر محلی وجود دارد؛ تکرار باید پس از بررسی نتیجه و سیاست عملیات باشد.
پرسش سوم: چرا دو خطای وابستگی، هر دو unconfirmed هستند؟ پاسخ: چون هر دو هنگام فراخوانی جایگزینی بالا آمدهاند. مشاهدهٔ مستقل در یکی نسخهٔ قدیمی و در دیگری تازه را نشان میدهد؛ پیام خطا بهتنهایی برای فرض بازگردانی کافی نیست.
پرسش چهارم: flush و fsync فایل، دوام قطعیِ snapshot را ثابت میکنند؟ پاسخ: خیر؛ این اجرا همگامسازی پوشه و خرابی واقعیِ فرایند، برق یا رسانه را نسنجیده است. بازگشت محلیِ عملیات را باید از تضمین دوام و مشاهدهٔ همزمان جدا گزارش کرد.
منابع و مطالعهٔ بیشتر
بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

