5 minute read

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

  1. Конкурентный доступ: Если один скрипт читает строки, а второй в это время добавляет новые строки, может произойти ситуация, когда оба скрипта попытаются одновременно изменить файл. Это может привести к повреждению данных или к ошибкам записи.
  2. Заблокированность файла: В некоторых операционных системах, если один процесс читает или записывает файл, другие процессы могут не иметь возможности получить доступ к этому файлу, пока первый процесс не завершит свою работу. Это может вызвать ошибки в другом процессе.
  3. Ошибки записи/чтения: Если второй скрипт добавляет данные, пока первый читает их, это может привести к ошибкам синхронизации, например, неверно прочитанным данным.

Чтобы избежать таких проблем, можно использовать следующие подходы:

  • Блокировки: Можно использовать механизмы блокировки файлов (например, через модули как flock или аналогичные). Когда один скрипт работает с файлом, он блокирует его для других, и другие процессы ждут, пока файл станет доступным.
  • Очередь в памяти или база данных: Вместо того чтобы работать напрямую с текстовым файлом, можно использовать очередь в памяти или базу данных, которые обеспечивают лучшую синхронизацию при параллельной работе с данными.
  • Периодическое чтение/запись: В случае записи в файл можно настроить оба скрипта так, чтобы они не взаимодействовали одновременно с ним (например, один может периодически записывать данные в файл, а другой — читать с интервалами).

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


flock — это системный механизм блокировки файлов, который используется для синхронизации доступа к файлам между несколькими процессами. Он позволяет предотвратить одновременную запись в файл несколькими процессами, что может привести к повреждению данных.

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

Основные типы блокировок:

  1. Блокировка для чтения (shared lock): Несколько процессов могут читать файл одновременно, но никто не может записывать в файл.
  2. Блокировка для записи (exclusive lock): Только один процесс может записывать в файл, и никакой другой процесс не может его читать или записывать, пока блокировка не будет снята.

Как использовать flock в Node.js:

В Node.js можно использовать модуль flock или его аналоги для работы с блокировками файлов. Например, можно использовать модуль fs-ext или библиотеку flock для блокировки файла. Вот пример с использованием flock в Node.js:

const fs = require("fs");
const { promisify } = require("util");
const flock = promisify(require("flock").flock);

async function writeFileWithLock(filePath, data) {
  const fd = fs.openSync(filePath, "a"); // открываем файл для дозаписи
  try {
    // Получаем эксклюзивную блокировку для записи
    await flock(fd, "ex");

    // Пишем данные в файл
    fs.writeSync(fd, data);
  } finally {
    // Снимаем блокировку и закрываем файл
    fs.closeSync(fd);
  }
}

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

Важные замечания:

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

Если вы попытаетесь прочитать файл, который уже заблокирован с помощью flock для записи (эксклюзивная блокировка), то результат будет зависеть от типа блокировки, установленной для вашего процесса и того, какую блокировку установил другой процесс:

  1. Если файл заблокирован для записи (exclusive lock):
    • При попытке другого процесса прочитать этот файл с помощью flock, который также пытается захватить блокировку (например, для чтения или записи), процесс, пытающийся получить доступ к файлу, будет ожидать, пока блокировка не будет снята.
    • То есть другой процесс не получит доступ к файлу, пока блокировка не будет освобождена.
  2. Если файл заблокирован для чтения (shared lock):
    • В случае, если файл заблокирован для чтения (shared lock), другие процессы также смогут захватить блокировку для чтения и продолжить читать файл. Однако, если вы пытаетесь записывать в файл (установив эксклюзивную блокировку), то другие процессы не смогут захватить блокировку для записи.

Поведение:

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

Для управления такими ситуациями можно использовать параметр timeout, чтобы процесс не ждал бесконечно долго и мог обработать ошибку в случае долгого ожидания блокировки.


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

Вот пример:

Пример ожидания с тайм-аутом:

const fs = require("fs");
const { promisify } = require("util");
const flock = promisify(require("fs").flock); // flock через стандартный `fs`
const sleep = promisify(setTimeout);

async function waitForLock(filePath, mode, timeout = 5000, interval = 100) {
  const startTime = Date.now();
  const fd = fs.openSync(filePath, "r+"); // Открываем файл для чтения/записи

  while (true) {
    try {
      // Попытка захватить блокировку
      await flock(fd, mode);
      console.log(`Lock acquired on ${filePath}`);
      return fd;
    } catch (err) {
      if (Date.now() - startTime > timeout) {
        // Если превышен тайм-аут, выбрасываем ошибку
        fs.closeSync(fd);
        throw new Error(`Failed to acquire lock on ${filePath} within timeout (${timeout}ms)`);
      }
      // Ждём перед новой попыткой
      await sleep(interval);
    }
  }
}

async function writeFileWithTimeout(filePath, data, timeout = 5000) {
  let fd;
  try {
    // Ожидаем захвата блокировки
    fd = await waitForLock(filePath, "ex", timeout);
    fs.writeSync(fd, data + "\n");
    console.log(`Data written to ${filePath}`);
  } finally {
    if (fd) {
      fs.closeSync(fd); // Освобождаем ресурс
    }
  }
}

// Пример использования
(async () => {
  const filePath = "./example.txt";
  try {
    await writeFileWithTimeout(filePath, "Hello, world!", 5000);
  } catch (err) {
    console.error(err.message);
  }
})();

Объяснение:

  1. waitForLock: Этот метод реализует цикл ожидания блокировки.
    • Он пробует захватить блокировку с помощью flock и, если не удается, ожидает заданный интервал (interval).
    • Если время ожидания превышает лимит (timeout), выбрасывается ошибка.
  2. writeFileWithTimeout: Использует waitForLock для захвата блокировки и записывает данные в файл.
  3. Параметры:
    • filePath: Путь к файлу.
    • timeout: Максимальное время ожидания блокировки (в миллисекундах).
    • interval: Интервал между попытками захвата блокировки (по умолчанию 100 мс).

Вывод:

  • Если блокировка доступна, скрипт сразу захватит её и выполнит операцию.
  • Если блокировка недоступна, скрипт будет ожидать и пробовать снова, пока не истечет тайм-аут.
  • Если превышен тайм-аут, выбрасывается ошибка.