Запоминание/кэширование вывода командной строки

Вопрос или проблема

У меня есть bash-скрипт, который я использую для запуска нескольких программ на Python и C++ в последовательности.

Каждая программа принимает некоторые входные параметры, которые я определяю в bash-скрипте. Например, я запускаю программу так:

echo $param1 $param2 $param3 | python foo.py

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

Так что мой вопрос: существует ли какой-то универсальный инструмент, который реализует эту функцию? То есть, есть ли какая-то программа под названием ‘bar’, которую я мог бы запустить так:

bar $param1 $param2 $param3 "python foo.py"

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

Правка: Имя файла журнала также может быть передано в качестве входного параметра, конечно.

Я только что увлёкся созданием довольно полного скрипта для этого; последняя версия доступна по адресу https://gist.github.com/akorn/51ee2fe7d36fa139723c851d87e56096.

Преимущества по сравнению с реализацией оболочки sivann:

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

Недостаток: написан на zsh, а не на bash.

#!/bin/zsh
#
# Цель: выполнить указанную команду с указанными аргументами и кэшировать результат. Если кэш свежий, не выполнять команду снова, а вернуть кэшированный вывод.
# Также кэшируется код завершения и stderr.
# Авторские права (c) 2019-2020 Андраш Корн; Лицензия: GPLv3

# Используйте глупо длинные имена переменных, чтобы избежать конфликтов с тем, что может использовать вызываемая программа
RUNCACHED_MAX_AGE=${RUNCACHED_MAX_AGE:-300}
RUNCACHED_IGNORE_ENV=${RUNCACHED_IGNORE_ENV:-0}
RUNCACHED_IGNORE_PWD=${RUNCACHED_IGNORE_PWD:-0}
[[ -n "$HOME" ]] && RUNCACHED_CACHE_DIR=${RUNCACHED_CACHE_DIR:-$HOME/.runcached}
RUNCACHED_CACHE_DIR=${RUNCACHED_CACHE_DIR:-/var/cache/runcached}

function usage() {
    echo "Использование: runcached [--ttl <максимальный возраст кэша>] [--cache-dir <каталог кэша>]"
    echo "       [--ignore-env] [--ignore-pwd] [--help] [--prune-cache]"
    echo "       [--] command [arg1 [arg2 ...]]"
    echo
    echo "Выполнить 'command' с указанными аргументами и кэшировать stdout, stderr и завершение"
    echo "статус. Если вы снова запускаете ту же команду и кэш свежий, возвращаются"
    echo "данные из кэша, и команда на самом деле не запускается."
    echo
    echo "Обычно все экспортируемые переменные окружения, а также текущий рабочий"
    echo "каталог включены в ключ кэша. Опции --ignore отключают это."
    echo "Переменная OLDPWD всегда игнорируется."
    echo
    echo "--prune-cache удаляет все записи кэша, старше максимального возраста. Нет"
    echo "другого механизма, чтобы предотвратить неограниченный рост кэша."
    echo
    echo "Каталог кэша по умолчанию ${RUNCACHED_CACHE_DIR}."
    echo "Максимальный возраст кэша по умолчанию ${RUNCACHED_MAX_AGE}."
    echo
    echo "ОГРАНИЧЕНИЯ:"
    echo
    echo "Побочные эффекты 'command' явно не кэшируются."
    echo
    echo "Нет логики недействительности кэша, кроме возраста кэша (указанного в секундах)."
    echo
    echo "Если кэш не может быть создан, команда выполняется без кэша."
    echo
    echo "Этот скрипт всегда молчит; любой вывод поступает от вызываемой команды. Вы"
    echo "можете таким образом не заметить ошибки при создании кэша и тому подобное."
    echo
    echo "Потоки stdout и stderr сохраняются отдельно. Когда оба записываются в"
    echo "терминал из кэша, они, вероятно, будут перемешаны иначе"
    echo "чем оригинально. Порядок сообщений в двух потоках сохраняется."
    exit 0
}

while [[ -n "$1" ]]; do
    case "$1" in
        --ttl)      RUNCACHED_MAX_AGE="$2"; shift 2;;
        --cache-dir)    RUNCACHED_CACHE_DIR="$2"; shift 2;;
        --ignore-env)   RUNCACHED_IGNORE_ENV=1; shift;;
        --ignore-pwd)   RUNCACHED_IGNORE_PWD=1; shift;;
        --prune-cache)  RUNCACHED_PRUNE=1; shift;;
        --help)     usage;;
        --)     shift; break;;
        *)      break;;
    esac
done

zmodload zsh/datetime
zmodload zsh/stat
zmodload zsh/system
zmodload zsh/files

# встроенный mv не переходит к копированию, если переименование не удалось из-за EXDEV;
# так как каталог кэша, вероятно, находится на другой файловой системе, чем временный
# каталог, это важное ограничение, поэтому мы используем /bin/mv вместо
disable mv  

mkdir -p "$RUNCACHED_CACHE_DIR" >/dev/null 2>/dev/null

((RUNCACHED_PRUNE)) && find "$RUNCACHED_CACHE_DIR/." -maxdepth 1 -type f \! -newermt @$[EPOCHSECONDS-RUNCACHED_MAX_AGE] -delete 2>/dev/null

[[ -n "$@" ]] || exit 0 # если команда не указана, выйти тихо

(
    # Почти(?) ничего не использует OLDPWD, но учитывая это, потенциально снижается эффективность кэша.
    # Таким образом, мы игнорируем его для цели создания ключа кэша.
    unset OLDPWD
    ((RUNCACHED_IGNORE_PWD)) && unset PWD
    ((RUNCACHED_IGNORE_ENV)) || env
    echo -E "$@"
) | md5sum | read RUNCACHED_CACHE_KEY RUNCACHED__crap__
# сделать каталог кэша хэшированным, если файл кэша еще не существует (созданный предыдущей версией, которая не использовала хэшированные каталоги)
if ! [[ -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.exitstatus ]]; then
    RUNCACHED_CACHE_KEY=$RUNCACHED_CACHE_KEY[1,2]/$RUNCACHED_CACHE_KEY[3,4]/$RUNCACHED_CACHE_KEY[5,$]
    mkdir -p "$RUNCACHED_CACHE_DIR/${RUNCACHED_CACHE_KEY:h}" >/dev/null 2>/dev/null
fi

# Если мы не можем получить блокировку, мы хотим выполнить без кэша; в противном случае
# 'runcached' не был бы прозрачным, потому что это помешало бы
# параллельному выполнению нескольких экземпляров одной и той же команды.
# Блокировка необходима, чтобы избежать гонок между командой mv(1)
# ниже, заменяющей stderr новой версией и другим экземпляром
# runcached, использующим новый stdout с более старым stderr.
: >>$RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.lock 2>/dev/null
if zsystem flock -t 0 $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.lock 2>/dev/null; then
    if [[ -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout ]]; then
        if [[ $[EPOCHSECONDS-$(zstat +mtime $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout)] -le $RUNCACHED_MAX_AGE ]]; then
            cat $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout &
            cat $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stderr >&2 &
            wait
            exit $(<$RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.exitstatus)
        else
            rm -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.{stdout,stderr,exitstatus} 2>/dev/null
        fi
    fi

    # достижимо только если кэш не существовал или был слишком старым
    if [[ -d $RUNCACHED_CACHE_DIR/. ]]; then
        RUNCACHED_tempdir=$(mktemp -d 2>/dev/null)
        if [[ -d $RUNCACHED_tempdir/. ]]; then
            $@ >&1 >$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.stdout 2>&2 2>$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.stderr
            RUNCACHED_ret=$?
            echo $RUNCACHED_ret >$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.exitstatus 2>/dev/null
            mv $RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.{stdout,stderr,exitstatus} $RUNCACHED_CACHE_DIR/${RUNCACHED_CACHE_KEY:h} 2>/dev/null
            rmdir $RUNCACHED_tempdir 2>/dev/null
            exit $RUNCACHED_ret
        fi
    fi
fi

# достижимо только если кэш не был успешно создан или блокировку не удалось получить
exec $@

Реализация существует здесь: https://github.com/sivann/runcached. Кэширует исполняемый путь, вывод, код завершения, запоминает аргументы. Настраиваемое время истечения. Реализовано на bash, C, python, выбирайте, что вам подходит. Версия “bash” несколько ограничена.

Для пользователей Bash я создал bash-cache, который предоставляет аналогичный набор функций подходу Андраша Корна на zsh. Он поддерживает переменные окружения, избегает гонок, кэширует stderr и коды завершения, недействительные устаревшие данные, поддерживает асинхронное разогревание и мьютекс-блокировки и многое другое.

Например, я думаю, что вы можете реализовать то, что описали, так:

foo() {
  python foo.py <<<"$*" # это записывает все аргументы в stdin python
} && bc::cache foo

Которую вы затем вызовете как:

foo param1 param2 param3

И foo будет кэшировать результаты и повторно использовать их в последующих вызовах.

Смотрите мою предыдущую ответ для получения дополнительных сведений.

Простое решение на Python, основанное на существующих пакетах (joblib.Memory для мемоизации):

cmdcache.py:

import sys, os
import joblib
import subprocess

mem = joblib.Memory('.', verbose=False)
@mem.cache
def run_cmd(args, env):
    process = subprocess.run(args, capture_output=True, text=True)
    return process.stdout, process.stderr, process.returncode

stdout, stderr, returncode = run_cmd(sys.argv[1:], dict(os.environ))
sys.stdout.write(stdout)
sys.stdout.write(stderr)
sys.exit(returncode)

Пример использования:

python cmdcache.py ls

Ответ или решение

Мемоизация и кэширование вывода командной строки

Введение

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

Зачем это нужно?

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

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

Как это реализовать?

Для решения задачи кэширования вывода команды при запуске из Bash-скриптов можно воспользоваться несколькими подходами:

  1. Скрипты на языке shell:
    Вы можете создать Bash-скрипт, который будет принимать параметры и выполнять команду только в случае, если результаты уже не кэшированы или устарели. Ниже приведён пример простого Bash-скрипта:

    #!/bin/bash
    
    CACHE_FILE="cache_output.txt"
    CACHE_TTL=300 # Время жизни кеша в секундах
    
    if [[ -f $CACHE_FILE && $(($(date +%s) - $(stat -c %Y $CACHE_FILE))) -le $CACHE_TTL ]]; then
       cat $CACHE_FILE
    else
       echo "Running command..."
       OUTPUT=$(echo "$1 $2 $3" | python foo.py) # вызов Python скрипта
       echo "$OUTPUT" > $CACHE_FILE
       echo "$OUTPUT"  # вывод результата
    fi

    Этот скрипт сначала проверяет, существует ли кэш и актуален ли он. Если да, то просто выводит содержимое кэша, иначе запускает команду и сохраняет результат.

  2. Использование сторонних инструментов:
    Существуют уже готовые решения, которые выполняют кэширование. Например, популярные инструменты, которые упоминались в вашем вопросе:

    • runcached: Этот инструмент на Zsh обеспечивает кэширование вывода команд, учитывает переменные окружения и текущую директорию.
    • bash-cache: Реализация аналогичных функций в Bash с поддержкой кэширования stderr и кода завершения.

    Пример команды с использованием bash-cache будет следующий:

    foo() {
       python foo.py "$@"
    } && bc::cache foo

    Вызывая foo param1 param2 param3, скрипт будет кэшировать результаты.

  3. Python решения:
    Если вам удобнее работать на Python, вы можете использовать библиотеку joblib, чтобы кэшировать вывод команд:

    import sys
    import joblib
    import subprocess
    
    mem = joblib.Memory('.', verbose=0)
    
    @mem.cache
    def run_cmd(args):
       return subprocess.run(args, capture_output=True, text=True)
    
    stdout, stderr, returncode = run_cmd(sys.argv[1:])
    sys.stdout.write(stdout)
    sys.stderr.write(stderr)
    sys.exit(returncode)

    Этот код кэширует вызовы команд, что позволяет повторно использовать результаты выполнения.

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...