Является ли этот метод блокировки файлов приемлемым? - PullRequest
3 голосов
/ 24 июля 2010

У нас есть 10 Linux-боксов, которые должны запускать 100 различных задач каждую неделю.Эти компьютеры работают над этими задачами в основном ночью, когда мы дома.Один из моих коллег работает над проектом по оптимизации времени выполнения, автоматизируя запуск задач с помощью Python.Его программа собирается прочитать список задач, получить открытую задачу, пометить эту задачу как выполняющуюся в файле, а затем, как только задача будет завершена, пометить задачу как завершенную в файле.Файлы задач будут находиться в нашей сети.

Мы понимаем, что не рекомендуется иметь несколько экземпляров программы, обращающихся к одному и тому же файлу, но мы не видим никакой другой опции.Пока он искал способ предотвратить одновременную запись в файл двумя компьютерами, я разработал собственный метод, который, казалось, был проще реализовать, чем методы, которые мы нашли в Интернете.Мой метод - проверить, существует ли файл, подождать несколько секунд, если он не существует, а затем временно переместить файл, если он есть.Я написал скрипт для проверки этого метода:

#!/usr/bin/env python

import time, os, shutil
from shutil import move
from os import path


fh = "testfile"
fhtemp = "testfiletemp"


while os.path.exists(fh) == False:
    time.sleep(3)

move(fh, fhtemp)
f = open(fhtemp, 'w')
line = raw_input("type something: ")
print "writing to file"
f.write(line)
raw_input("hit enter to close file.")
f.close()
move(fhtemp, fh)

В наших тестах этот метод работал, но мне было интересно, не возникнут ли у нас некоторые проблемы, которые мы не видим при использовании этого.Я понимаю, что катастрофа может возникнуть в результате одновременного существования двух компьютеров ().Маловероятно, что два компьютера дойдут до этой точки одновременно, поскольку задачи выполняются где-то между 20 минутами и 8 часами.

Ответы [ 7 ]

8 голосов
/ 24 июля 2010

Вы в основном разработали версию двоичного семафора (или мьютекса) для файловой системы. Это хорошо изученная структура, используемая для блокировки, поэтому, если вы правильно поняли детали реализации, она должна работать. Хитрость заключается в том, чтобы операция «проверить и установить» или, в вашем случае, «проверить существование и движение», была действительно атомарной. Для этого я бы использовал что-то вроде этого:

lock_acquired = False
while not lock_acquired:
    try:
        move(fh, fhtemp)
    except:
        sleep(3)
    else:
        lock_acquired = True
# do your writing
move(fhtemp, fh)
lock_acquired = False

Программа, которая у вас была, работала бы большую часть времени, но, как уже упоминалось, у вас могут возникнуть проблемы, если другой процесс переместит файл между проверкой его существования и вызовом move. Я полагаю, вы могли бы обойти это, но я лично рекомендую придерживаться хорошо проверенного алгоритма мьютекса. (Я перевел / портировал приведенный выше пример кода из Современные операционные системы Эндрю Таненбаума, но возможно, что я внес ошибки при преобразовании - просто честное предупреждение)

Кстати, справочная страница для функции open в Linux предлагает следующее решение для блокировки файлов:

Решение для выполнения атомарной блокировки файлов с использованием файла блокировки состоит в том, чтобы создать уникальный файл в той же файловой системе (например, включающий имя хоста и pid), используя ссылку (2), чтобы создать ссылку на файл блокировки. Если link () возвращает 0, блокировка успешна. В противном случае используйте stat (2) для уникального файла, чтобы проверить, увеличилось ли количество ссылок до 2, и в этом случае блокировка также будет успешной.

Чтобы реализовать это в Python, вы можете сделать что-то вроде этого:

# each instance of the process should have a different filename here
process_lockfile = '/path/to/hostname.pid.lock'
# all processes should have the same filename here
global_lockfile = '/path/to/lockfile'
# create the file if necessary (only once, at the beginning of each process)
with open(process_lockfile, 'w') as f:
    f.write('\n') # or maybe write the hostname and pid

# now, each time you have to lock the file:
lock_acquired = False
while not lock_acquired:
    try:
        link(process_lockfile, global_lockfile)
    except:
        lock_acquired = (stat(process_lockfile).st_nlinks == 2)
    else:
        lock_acquired = True
# do your writing
unlink(global_lockfile)
lock_acquired = False
2 голосов
/ 24 июля 2010

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

А как насчет того, чтобы вместо этого ставить задачу в каталог, где каждая ожидающая задача представляет собой файл? Затем этот процесс так же прост, как выбор задачи из каталога «Ожидание», перемещение ее в каталог (скажем) «Выполнение», и после этого переместите файл задачи в каталог «Завершено». Поскольку перемещение файла является атомарной операцией, условия гонки не будут возникать (если перемещение завершится неудачно, это означает, что другой работник просто схватил его первым, поэтому возьмите следующую задачу).

Кроме того, проверка прогресса так же проста, как и выдача ls в один из каталогов: -)

1 голос
/ 24 июля 2010

РЕДАКТИРОВАТЬ : Обычной практикой также является определение процесса, который переименовал файл. Таким образом, если процесс прекратит работу, прежде чем восстановить его первоначальное имя, можно будет отследить владение файлом и определить, вмешиваться ли.

Я вставил (едва протестирован) вызовы os и socket, чтобы добавить эту функцию. Используйте на свой страх и риск.


Если два процесса конкурируют за переименование файла, то проверка их существования в первую очередь не предотвратит состояние гонки; это только задержит время, когда это произойдет.

Документы для shutil.move (к сожалению) не содержат явных указаний об ошибке IOError, если файл не существует, но это кажется разумным ожиданием - и я обнаружил, что это происходит на практике: 1014 *

import shutil
import os
import socket

oldname = "foobar.txt"
newname = (oldname + "." + socket.gethostbyaddr(socket.gethostname())[0]
           + "." + str(os.getpid()))
i_win = True
try:
    shutil.move(oldname, newname)
except IOError, e:
    print "File does not exist"
    i_win = False
except Exception, e:
    print e
    i_win = False

if i_win:
    print "I got it!"

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

1 голос
/ 24 июля 2010

Если вы не отслеживаете свои move вызовы, чтобы увидеть, были ли они успешными или нет, вы никогда не узнаете, станете ли вы жертвой окна синхронизации. Помните, что если что-то может пойти не так, то это будет в самое неподходящее время .

Вместо того, чтобы использовать содержимое файла в качестве флага, может быть, вы могли бы использовать само имя файла? Для каждой задачи переименуйте файл «task_waiting_to_run» в «task_running» в «task_complete». Если переименование из «task_waiting_to_run» в «task_running» завершается неудачно, это означает, что другой ящик попал туда первым.

1 голос
/ 24 июля 2010

Перемещение / переименование файлов, как правило, является атомарной операцией в большинстве операционных систем, поэтому, вероятно, это работоспособное решение.

Вы, возможно, захотите добавить проверку исключения для вызовов move и open, хотя, в случае, если какой-то другой процесс переместил файл между проверкой вашего существования и move (или если move не удалось завершить).

Edit

Чтобы суммировать правильный поток, который будетработа:

  1. Выпуск move от А до А. [myID]
  2. Попробуйте open A. [myID]
  3. Если # 1 или #2 не удалось, мы не получили блокировку;немного подождите, а затем вернитесь к # 1.В противном случае мы получили блокировку, продолжаем.
  4. Внесите изменения.
  5. Выпустите move из A. [myID] в A. (Никогда не выйдет из строя.) Это снимет блокировку.

Хорошим вариантом для [myID] является PID процесса (возможно, также включает хост, если он запущен в нескольких системах).

0 голосов
/ 11 июня 2018

Вот пример с тайм-аутом, реализованным как менеджер контекста, поэтому вы можете использовать его следующим образом:

with NetworkFileLock(r"\\machine\path\lockfile", 60):

...

@contextmanager
def NetworkFileLock(sharedFilePath, timeoutSeconds):

    # Try to acquire the lock here, by moving the file to a unique path for this process/thread
    uniqueFilePath = "{}-{}-{}-{}".format(sharedFilePath, socket.gethostname(), os.getpid(), threading.get_ident())

    startTime = time.time()
    while True:
        try:
            shutil.move(sharedFilePath, uniqueFilePath)
            # Check temp file now exists
            with open(uniqueFilePath, "r"):
                pass
            break
        except:
            if (time.time() - startTime) > timeoutSeconds:
                raise TimeoutError("Timed out after {} seconds waiting for network lock on file {}".format(time.time() - startTime, networkFilePath))
            time.sleep(3)

    try:
        # Yield to the body of the "with" statement
        yield
    except:
        # Move the file back to release the lock
        shutil.move(uniqueFilePath, sharedFilePath)
        raise
    else:
        # Move the file back to release the lock
        shutil.move(uniqueFilePath, sharedFilePath)
0 голосов
/ 24 июля 2010

Доверие к сетевым файловым системам для блокировки - это проблема, которая преследует системы в течение многих лет (и все еще часто не работает так, как вы ожидаете)как система баз данных?(Лично мне нравится Postgres ...)

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...