Статьи

Простота и надежность — продемонстрировано при обработке файла блокировки

Сегодня мы обсудим конфликт между проектными ценностями сохранения простоты, глупости (KISS) и надежности, между недостаточным дизайном и чрезмерным дизайном.

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

Как недавно объяснил Ойвинд Бакксйо из Comoyo, инженер-программист отличается от простого программиста продумыванием и заботой не только о счастливом пути в коде, но и о несчастных случаях. Хорошие инженеры думают о возможных проблемах и стараются изящно решать их, чтобы код, который зависит от них и их пользователей, быстрее справлялся с проблемной ситуацией. Надежность включает в себя ранний отлов ошибок, их правильную обработку и предоставление полезных и полезных сообщений об ошибках. С другой стороны, простота [TBD: Hickey] является важной характеристикой систем. Всегда слишком легко тратить слишком много времени на то, чтобы сделать код пуленепробиваемым, вместо того, чтобы сосредоточить усилия там, где это было бы более ценно для бизнеса.

Чрезмерно простая реализация

Первоначальная реализация была довольно простой:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SimpleSingletonBatchJob {
    private static boolean getLock() {
        File file = new File(LOCK_DIRECTORY+File.separatorChar+Configuration.getGroupPrefix());
        try {
            return file.createNewFile();
        } catch (IOException e) {
            return false;
        }
    }
 
    private static void releaseLock() {
        File file = new File(LOCK_DIRECTORY+File.separatorChar+Configuration.getGroupPrefix());
        file.delete();
    }
 
 
    public static void exit(int nr) {
        releaseLock();
        System.exit(nr);
    }
 
    public static void main(String[] args) throws IOException {
        ...
        if (! getLock()) { // #1 try to create lock
            System.out.println("Already running");
            return;
        }
        ... // do the job (may throw exceptions)
         
        releaseLock(); // #2 release lock when done
    }
}

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

Утверждалось, что такие сбои и преднамеренные убийства будут происходить настолько редко, что попытка сделать код более надежным не будет оправдана. Однако нам нужно приложить очень мало усилий, чтобы сделать код более дружественным и надежным, например. включив путь к файлу блокировки в сообщение об ошибке и объяснив, почему он может быть там и как его исправить (например, «если приложение не запущено, блокировка является остатком неудачного запуска и может быть удалена»). Убедиться в том, что файл удален при сбое, — несколько простых строк кода, которые могут сэкономить время и путаницу. Кроме того, стоит сделать его немного более надежным, чтобы не требовалось так много ручных вмешательств — будьте добры к своим сотрудникам. (Надеюсь, это ты.)

Более надежная реализация

Это улучшенная версия с полезным сообщением об ошибке и снятием блокировки при сбое:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class RobustSingletonBatchJob {
     
    // Note: We could use File.deleteOnExit() but the docs says it is not 100% reliable and recommends to
    // use java.nio.channels.FileLock; however this code works well enough for us
    static synchronized boolean getLock() {
        File file = new File(LOCK_DIRECTORY, StaticConfiguration.getGroupPrefix());
        try {
            // Will try to create path to lockfile if it does not exist.
            file.getParentFile().mkdirs(); // #1 Create the lock dir if it doesn't exist
            if (file.createNewFile()) {
                return true;
            } else {
                log.info("Lock file " + file.getAbsolutePath() + " already exists."); // #2 Helpful error msg w/ path
                return false;
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to create lock file " + file.getAbsolutePath()
               + " due to " + e + ". Fix the problem and retry."
               , e); // #3 Helpful error message with context (file path)
        }
    }
 
    private synchronized static void releaseLock() {
        File file = new File(LOCK_DIRECTORY, StaticConfiguration.getGroupPrefix());
        file.delete();
    }
     
    public static void main(String[] args) throws Exception {
        boolean releaseLockUponCompletion = true;
        try {
            ...
            if (! getLock() {
                releaseLockUponCompletion = false;
                log.error("Lock file is present, exiting."); // Lock path already logged
                throw new RuntimeException("Lock file is present"); // throwing is nicer than System.exit/return
            }
            ... // do the job (may throw exceptions)
        } finally {
            if (releaseLockUponCompletion) {
                releaseLock(); // #4 Always release the lock, even upon exceptions
            }
        }
}

Улучшения:

  1. Создайте каталог, в котором хранятся блокировки, если он не существует (его несуществование и, как следствие, сбивающее с толку сообщение об ошибке «Уже запущено»), укусило нас
  2. Полезное сообщение об ошибке «Файл блокировки <абсолютный путь к файлу> уже существует». => Легко копировать и вставлять INT RM .
  3. Полезное сообщение об ошибке с путем к файлу и ошибкой, когда мы не можем создать блокировку (нехватка места, недостаточные права доступа к каталогу,…)
  4. Завершение всего основного в попытке — наконец, и убедитесь, что всегда удаляете файл блокировки

Код все еще не совершенен — ​​если вы убьете приложение, файл блокировки все равно останется позади. Есть способы справиться с этим (например, включить pid приложения в файл, при запуске проверить не только его существование, но и то, что pid действительно существует / является приложением), но затраты на их реализацию с точки зрения времени и увеличения сложность действительно выше, чем выгода.

Вывод

Как KISS, так и надежность являются важными целями и часто будут конфликтовать. Создание более надежного кода, чем необходимо, делает его слишком сложным, требует затрат времени и (упущенных) альтернативных издержек. Создание слишком простого кода будет стоить вам или его пользователям времени из-за устранения неполадок. Достижение правильного баланса требует опыта и итерации к нему. Если ваша команда не может найти консенсус, возможно, лучше начать с более простого кода и собирать точные данные о его реальных потребностях в надежности, а не перерабатывать его заранее. Не будь перфекционистом, как я, но будь добр к своим пользователям и коллегам-разработчикам. Если вы можете сделать свое приложение более надежным без особых усилий, сделайте это. Если это требует больше работы, иди и собирай данные, чтобы оправдать (или нет) работу.