Threadsafe и отказоустойчивые записи файлов
У меня есть длительный процесс, который записывает много всего в файл. Результатом должно быть все или ничего, поэтому я пишу во временный файл и переименовываю его в настоящее имя в конце. В настоящее время мой код выглядит следующим образом:
filename = 'whatever'
tmpname = 'whatever' + str(time.time())
with open(tmpname, 'wb') as fp:
fp.write(stuff)
fp.write(more stuff)
if os.path.exists(filename):
os.unlink(filename)
os.rename(tmpname, filename)
Я не доволен этим по нескольким причинам:
- он не очищается должным образом, если возникает исключение.
- он игнорирует проблемы concurrency
- он не может использоваться повторно (мне это нужно в разных местах моей программы)
Любые предложения по улучшению кода? Есть ли библиотека, которая может мне помочь?
Ответы
Ответ 1
Вы можете использовать модуль Python tempfile
, чтобы дать вам временное имя файла. Он может создать временный файл в потокобезопасном режиме, а не использовать time.time()
, который может возвращать одно и то же имя, если он используется одновременно в нескольких потоках.
Как указано в комментарии к вашему вопросу, это может быть связано с использованием диспетчера контекстов. Вы можете получить некоторые идеи о том, как реализовать то, что вы хотите сделать, посмотрев на источники Python tempfile.py
.
Следующий фрагмент кода может делать то, что вы хотите. Он использует некоторые внутренние элементы объектов, возвращаемых из tempfile
.
- Создание временных файлов является потокобезопасным.
- Переименование файлов при успешном завершении является атомарным, по крайней мере, в Linux. Между
os.path.exists()
и os.rename()
нет отдельной проверки, которая могла бы привести к состоянию гонки. Для атомного переименования в Linux источник и адресаты должны находиться в одной файловой системе, поэтому этот код помещает временный файл в тот же каталог, что и целевой файл.
- Класс
RenamedTemporaryFile
должен вести себя как a NamedTemporaryFile
для большинства целей, кроме случаев, когда он закрыт с помощью диспетчера контекстов, файл переименовывается.
Пример:
import tempfile
import os
class RenamedTemporaryFile(object):
"""
A temporary file object which will be renamed to the specified
path on exit.
"""
def __init__(self, final_path, **kwargs):
tmpfile_dir = kwargs.pop('dir', None)
# Put temporary file in the same directory as the location for the
# final file so that an atomic move into place can occur.
if tmpfile_dir is None:
tmpfile_dir = os.path.dirname(final_path)
self.tmpfile = tempfile.NamedTemporaryFile(dir=tmpfile_dir, **kwargs)
self.final_path = final_path
def __getattr__(self, attr):
"""
Delegate attribute access to the underlying temporary file object.
"""
return getattr(self.tmpfile, attr)
def __enter__(self):
self.tmpfile.__enter__()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.tmpfile.delete = False
result = self.tmpfile.__exit__(exc_type, exc_val, exc_tb)
os.rename(self.tmpfile.name, self.final_path)
else:
result = self.tmpfile.__exit__(exc_type, exc_val, exc_tb)
return result
Затем вы можете использовать его следующим образом:
with RenamedTemporaryFile('whatever') as f:
f.write('stuff')
Во время записи содержимое переходит во временный файл, после чего файл переименовывается. Этот код, вероятно, потребует некоторых настроек, но общая идея должна помочь вам приступить к работе.
Ответ 2
Чтобы написать все или ничего в файл надежно:
import os
from contextlib import contextmanager
from tempfile import NamedTemporaryFile
if not hasattr(os, 'replace'):
os.replace = os.rename #NOTE: it won't work for existing files on Windows
@contextmanager
def FaultTolerantFile(name):
dirpath, filename = os.path.split(name)
# use the same dir for os.rename() to work
with NamedTemporaryFile(dir=dirpath, prefix=filename, suffix='.tmp') as f:
yield f
f.flush() # libc -> OS
os.fsync(f) # OS -> disc (note: on OSX it is not enough)
f.delete = False # don't delete tmp file if `replace()` fails
f.close()
os.replace(f.name, name)
См. также Переименован() без fsync() безопасен? (упоминается @Mihai Stan)
Использование
with FaultTolerantFile('very_important_file') as file:
file.write('either all ')
file.write('or nothing is written')
Чтобы реализовать отсутствующий os.replace()
, вы можете вызвать MoveFileExW(src, dst, MOVEFILE_REPLACE_EXISTING)
(через модули win32file или ctypes) в Windows.
В случае нескольких потоков вы можете вызвать queue.put(data)
из
разные потоки и записывать в файл в выделенном потоке:
for data in iter(queue.get, None):
file.write(data)
queue.put(None)
прерывает цикл.
В качестве альтернативы вы можете использовать блокировки (потоки, многопроцессорность,
filelock) для синхронизации доступа:
def write(self, data):
with self.lock:
self.file.write(data)
Ответ 3
Вы можете использовать файл блокировки файла, чтобы заблокировать файл во время написания. Любая последующая попытка блокировки блокируется до тех пор, пока не будет освобождена блокировка из предыдущего процесса/потока.
from lockfile import FileLock
with FileLock(filename):
#open your file here....
Таким образом, вы обойдете свои проблемы concurrency и не должны очищать оставшийся файл, если возникает исключение.
Ответ 4
Конструкция with
полезна для очистки при выходе, но не для системы фиксации/отката, которую вы хотите. Для этого может использоваться блок try/except/else.
Вы также должны использовать стандартный способ создания временного имени файла, например, с помощью модуля tempfile.
И помните fsync перед переименованием
Ниже приведен полный код:
import time, os, tempfile
def begin_file(filepath):
(filedir, filename) = os.path.split(filepath)
tmpfilepath = tempfile.mktemp(prefix=filename+'_', dir=filedir)
return open(os.path.join(filedir, tmpfilepath), 'wb')
def commit_file(f):
tmppath = f.name
(filedir, tmpname) = os.path.split(tmppath)
origpath = os.path.join(filedir,tmpname.split('_')[0])
os.fsync(f.fileno())
f.close()
if os.path.exists(origpath):
os.unlink(origpath)
os.rename(tmppath, origpath)
def rollback_file(f):
tmppath = f.name
f.close()
os.unlink(tmppath)
fp = begin_file('whatever')
try:
fp.write('stuff')
except:
rollback_file(fp)
raise
else:
commit_file(fp)