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)

Я не доволен по нескольким причинам:

  • он не очищается должным образом, если происходит исключение
  • он игнорирует проблемы параллелизма
  • это не многоразовые (мне нужно это в разных местах в моей программе)

любые предложения, как улучшить мой код? Есть библиотека, которая может мне помочь?

4 ответов


вы можете использовать Python в tempfile модуль, чтобы дать вам имя временного файла. Он может создать временный файл потокобезопасным способом, а не создавать его с помощью time.time() который может возвращать одно и то же имя, если используется в нескольких потоках одновременно.

как предложено в комментарии к вашему вопросу, это может быть связано с использованием контекстного менеджера. Вы можете получить некоторые идеи о том, как реализовать то, что вы хотите сделать, посмотрев на Python tempfile.py источники.

следующий фрагмент кода может делать то, что вы хотите. Он использует некоторые из внутренних объектов, возвращенных из tempfile.

  • создание временных файлов является потокобезопасным.
  • переименование файлов после успешного завершения является атомарным, по крайней мере, в Linux. Нет отдельной проверки между os.path.exists() и os.rename() который может ввести условие гонки. Для атомарного переименования в Linux источник и назначения должны быть одинаковыми файловая система, поэтому этот код помещает временный файл в тот же каталог, что и целевой файл.
  • на RenamedTemporaryFile класс должен вести себя как NamedTemporaryFile для большинства целей, за исключением случаев, когда он закрыт с помощью context manager, файл переименовывается.

пример:

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')

во время записи содержимое переходит во временный файл, при выходе файл переименовывается. Этот код, вероятно, будет нужны некоторые настройки, но общая идея должна помочь вам начать работу.


записать все или ничего в файл надежно:

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)

см. также является ли rename () без 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) разрыв петли.

в качестве альтернативы вы можете использовать блокировки (резьба, многопроцессорная обработка, забыл) для синхронизации доступа:

def write(self, data):
    with self.lock:
        self.file.write(data)

можно использовать модуль lockfile чтобы заблокировать файл во время записи в него. Любая последующая попытка заблокировать его будет блокироваться до тех пор, пока блокировка от предыдущего процесса/потока не будет освобождена.

from lockfile import FileLock
with FileLock(filename):
    #open your file here....

таким образом, вы обходите свои проблемы параллелизма и не должны очищать любой оставшийся файл, если происходит исключение.


на with construct полезен для очистки при выходе, но не для системы фиксации/отката, которую вы хотите. А попробуйте / кроме / 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)