Обработка пакета транзакций

Разбираем, как COBOL-программа обрабатывает список транзакций одну за другой — основа любой ночной банковской обработки.

Пакетная обработка (batch processing) — это когда программа не ждёт, пока человек нажмёт кнопку, а сама проходит по большому списку операций подряд, без остановки, и обрабатывает каждую по одинаковым правилам.

Зачем это нужно банку

Представь: за день в банке прошло два миллиона операций — переводы, списания, пополнения. Ночью, пока клиенты спят, специальная программа должна пройти по каждой из этих операций, обновить остатки на счетах, отфильтровать подозрительные транзакции и сформировать итоговый отчёт для утра. Это и есть пакетная обработка (batch processing) — именно то, для чего COBOL создавался изначально и в чём он до сих пор силён на реальных мейнфреймах.

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

Как выглядит список транзакций

Сначала опишем, что вообще такое одна транзакция — то есть одна запись в нашем списке операций.

01  WS-TRANSAKCIYA.
    05  WS-NOMER-SCHETA    PIC 9(10).
    05  WS-TIP-OPERACII    PIC X(1).
        88  POPOLNENIE     VALUE "P".
        88  SPISANIE       VALUE "S".
    05  WS-SUMMA           PIC 9(7)V99.

01  WS-FLAG-KONEC-FAYLA    PIC X(1) VALUE "N".
    88  KONEC-FAYLA        VALUE "Y".

01  WS-ITOGO-POPOLNENIY    PIC 9(9)V99 VALUE ZERO.
01  WS-ITOGO-SPISANIY      PIC 9(9)V99 VALUE ZERO.
01  WS-SCHETCHIK           PIC 9(6)    VALUE ZERO.

Здесь появилась новая конструкция — уровень 88, который называется условное имя. Это одна из самых полезных придумок COBOL: вместо того чтобы потом в коде постоянно писать IF WS-TIP-OPERACII = "P", мы один раз объявляем, что «POPOLNENIE — это когда поле равно значению P», и дальше по всему коду можно писать просто IF POPOLNENIE — гораздо понятнее для человека, который читает программу спустя пять лет после её написания (а в банках такое случается сплошь и рядом, банковский COBOL-код нередко живёт десятилетиями). Точно так же WS-FLAG-KONEC-FAYLA с условным именем KONEC-FAYLA будет нашим флагом «список закончился» — стандартный приём в пакетной обработке.

Цикл обработки: PERFORM UNTIL

Теперь главное — сам цикл, который проходит по всем транзакциям. В COBOL для этого используют PERFORM ... UNTIL, что переводится примерно как «выполняй, пока не выполнится условие».

PERFORM UNTIL KONEC-FAYLA
    PERFORM CHITAT-SLEDUYUSHCHUYU-TRANSAKCIYU

    IF NOT KONEC-FAYLA
        EVALUATE TRUE
            WHEN POPOLNENIE
                ADD WS-SUMMA TO WS-ITOGO-POPOLNENIY
            WHEN SPISANIE
                ADD WS-SUMMA TO WS-ITOGO-SPISANIY
            WHEN OTHER
                DISPLAY "Неизвестный тип операции по счёту "
                        WS-NOMER-SCHETA
        END-EVALUATE

        ADD 1 TO WS-SCHETCHIK
    END-IF
END-PERFORM.

DISPLAY "Обработано транзакций: " WS-SCHETCHIK.
DISPLAY "Итого пополнений: "     WS-ITOGO-POPOLNENIY.
DISPLAY "Итого списаний: "       WS-ITOGO-SPISANIY.

Разберём по шагам. PERFORM UNTIL KONEC-FAYLA — это заголовок цикла: «повторяй тело цикла, пока условное имя KONEC-FAYLA не станет истинным». Внутри цикла первым делом вызывается PERFORM CHITAT-SLEDUYUSHCHUYU-TRANSAKCIYU — это вызов отдельного именованного блока кода (представь его как вызов функции), который читает следующую транзакцию из списка и, если транзакций больше нет, устанавливает флаг WS-FLAG-KONEC-FAYLA в значение Y. Дальше идёт проверка IF NOT KONEC-FAYLA — она нужна, чтобы не пытаться обработать «пустую» транзакцию, если чтение как раз обнаружило конец списка на этом шаге.

Внутри — уже знакомая нам конструкция EVALUATE TRUE с условными именами POPOLNENIE и SPISANIE: для пополнений сумма прибавляется к общему итогу пополнений через ADD WS-SUMMA TO WS-ITOGO-POPOLNENIY, для списаний — к итогу списаний. Оператор ADD ... TO ... — это простое накопительное сложение, читается буквально «прибавь сумму к итогу». И снова ветка WHEN OTHER ловит операции с неожиданным типом — на реальном мейнфрейме в батче из двух миллионов записей рано или поздно попадётся кривая запись, и программа обязана на неё отреагировать, а не упасть или молча пропустить.

Практический пример: небольшой пакет вручную

Чтобы окончательно закрепить логику, разберём тот же принцип на маленьком «игрушечном» пакете из трёх транзакций — так же, как это работало бы с настоящим файлом, только данные мы зададим прямо в коде.

01  WS-INDEKS         PIC 9(1) VALUE 1.
01  WS-VSEGO-ZAPISEY  PIC 9(1) VALUE 3.

01  WS-TIPY-MASSIV.
    05  WS-TIP OCCURS 3 TIMES PIC X(1) VALUE "P".
01  WS-SUMMY-MASSIV.
    05  WS-SUMMA-EL OCCURS 3 TIMES PIC 9(5)V99.

MOVE "P" TO WS-TIP(1).   MOVE 5000.00 TO WS-SUMMA-EL(1).
MOVE "S" TO WS-TIP(2).   MOVE 1200.50 TO WS-SUMMA-EL(2).
MOVE "P" TO WS-TIP(3).   MOVE 3300.00 TO WS-SUMMA-EL(3).

PERFORM VARYING WS-INDEKS FROM 1 BY 1
    UNTIL WS-INDEKS > WS-VSEGO-ZAPISEY

    IF WS-TIP(WS-INDEKS) = "P"
        DISPLAY "Пополнение №" WS-INDEKS ": " WS-SUMMA-EL(WS-INDEKS)
    ELSE
        DISPLAY "Списание №" WS-INDEKS ": " WS-SUMMA-EL(WS-INDEKS)
    END-IF
END-PERFORM.

Здесь мы увидели ещё один вид цикла — PERFORM VARYING ... FROM ... BY ... UNTIL, это классический счётный цикл (аналог for в других языках): переменная WS-INDEKS начинается с 1, каждый шаг увеличивается на 1, и цикл идёт, пока индекс не превысит количество записей. Конструкция OCCURS 3 TIMES в описании поля превращает его в массив из трёх элементов — так COBOL хранит повторяющиеся однотипные данные, а обращение к конкретному элементу идёт через скобки, например WS-TIP(2) — второй элемент массива.

Вывод:

Пополнение №1: 05000.00
Списание №2: 01200.50
Пополнение №3: 03300.00

Частые ошибки

Самая опасная ошибка в пакетной обработке — забыть проверить флаг конца списка сразу после чтения очередной записи, до того как эта запись пойдёт в обработку. Если пропустить проверку IF NOT KONEC-FAYLA, последняя «пустая» итерация может попасть в расчёт как настоящая транзакция с мусорными данными — и итоговая сумма в отчёте окажется неверной. Вторая частая ошибка — не сбросить накопительные поля в ноль перед началом обработки (у нас это сделано через VALUE ZERO в описании), иначе, если программа перезапустится или итоги пересчитываются повторно, к старым суммам прибавятся новые, и получится задвоение. Третья ошибка, характерная именно для новичков в пакетной обработке, — не предусмотреть ветку WHEN OTHER для непредвиденных типов операций: на живых данных всегда найдётся запись с опечаткой или битым полем, и без защитной ветки такая запись либо тихо потеряется, либо уронит всю программу посреди обработки миллионов записей.

Итоги

  • Пакетная обработка — это цикл, который проходит по всем записям файла без участия человека, поэтому код должен быть особенно надёжным.
  • Условные имена (уровень 88) делают код читаемым: вместо сравнений с «магическими» символами используются осмысленные имена вроде POPOLNENIE или KONEC-FAYLA.
  • PERFORM UNTIL — цикл «пока условие не выполнено», а PERFORM VARYING — классический счётный цикл с индексом; оба активно используются в реальных программах.
  • Всегда проверяй флаг конца данных сразу после чтения и обнуляй накопительные поля перед стартом — это две самые частые причины неверных итогов в банковских отчётах.
Проверьте себя
1. Зачем в COBOL используют условные имена уровня 88, например 88 POPOLNENIE VALUE "P"?
AЭто обязательное требование компилятора для всех полей
BЧтобы вместо сравнения с символом писать понятное имя вроде IF POPOLNENIE
CЧтобы ускорить выполнение программы
DУровень 88 нужен только для чисел, а не для текста
2. Что произойдёт, если не проверять флаг конца данных сразу после чтения очередной записи в цикле PERFORM UNTIL?
AПрограмма обработает файл быстрее
BПоследняя пустая итерация может попасть в расчёт как настоящая транзакция
CCOBOL сам пропустит лишнюю запись автоматически
DНичего страшного, это просто стиль кода