Я пытаюсь реализовать класс "менеджер записей" в Python 3x и Linux/MacOS. Класс относительно прост и понятен, единственная "трудная" вещь, которую я хочу, - иметь возможность доступа к одному и тому же файлу (где сохраняются результаты) в нескольких процессах.
Концептуально это казалось довольно простым: при сохранении получайте эксклюзивную блокировку файла. Обновите свою информацию, сохраните новую информацию, снимите эксклюзивную блокировку файла. Достаточно просто.
Я использую fcntl.lockf(file, fcntl.LOCK_EX)
для получения эксклюзивной блокировки. Проблема в том, что, просматривая Интернет, я нахожу множество различных веб-сайтов, которые говорят, что это ненадежно, что оно не будет работать на Windows, что поддержка NFS ненадежна и что между macOS и линукс.
Я согласился с тем, что код не будет работать на Windows, но я надеялся, что смогу заставить его работать на MacOS (на одной машине) и на Linux (на нескольких серверах с NFS).
Проблема в том, что я не могу заставить это работать; и после некоторого времени отладки и после прохождения тестов на macOS они потерпели неудачу, как только я попробовал их на NFS с linux (ubuntu 16.04). Проблема заключается в несоответствии информации, сохраняемой несколькими процессами - в некоторых процессах отсутствуют их модификации, что означает, что что-то пошло не так в процедуре блокировки и сохранения.
Я уверен, что что-то не так делаю, и подозреваю, что это может быть связано с проблемами, о которых я читал в Интернете. Итак, как правильно организовать множественный доступ к одному и тому же файлу, который работает в macOS и linux через NFS?
редактировать
Вот как выглядит типичный метод записи новой информации на диск:
sf = open(self._save_file_path, 'rb+')
try:
fcntl.lockf(sf, fcntl.LOCK_EX) # acquire an exclusive lock - only one writer
self._raw_update(sf) #updates the records from file (other processes may have modified it)
self._saved_records[name] = new_info
self._raw_save() #does not check for locks (but does *not* release the lock on self._save_file_path)
finally:
sf.flush()
os.fsync(sf.fileno()) #forcing the OS to write to disk
sf.close() #release the lock and close
Хотя вот как выглядит типичный метод, который только читает информацию с диска:
sf = open(self._save_file_path, 'rb')
try:
fcntl.lockf(sf, fcntl.LOCK_SH) # acquire shared lock - multiple writers
self._raw_update(sf) #updates the records from file (other processes may have modified it)
return self._saved_records
finally:
sf.close() #release the lock and close
Кроме того, вот так выглядит _raw_save:
def _raw_save(self):
#write to temp file first to avoid accidental corruption of information.
#os.replace is guaranteed to be an atomic operation in POSIX
with open('temp_file', 'wb') as p:
p.write(self._saved_records)
os.replace('temp_file', self._save_file_path) #pretty sure this does not release the lock
Сообщение об ошибке
Я написал unit тест, в котором я создал 100 различных процессов, 50 из которых читают, а 50 - пишут в один и тот же файл. Каждый процесс выполняет случайное ожидание, чтобы избежать последовательного доступа к файлам.
Проблема в том, что некоторые записи не хранятся; в итоге пропадает 3-4 случайных записи, поэтому я получаю только 46-47 записей, а не 50.
Редактировать 2
Я изменил приведенный выше код и получаю блокировку не для самого файла, а для отдельного файла блокировки. Это предотвращает проблему, заключающуюся в том, что закрытие файла снимет блокировку (как предложено @janneb), и заставляет код корректно работать на Mac. Тот же код не работает на Linux с NFS, хотя.