프로세스 출력을 캐싱하기 위한 쉘 명령이나 유틸리티가 있습니까?

프로세스 출력을 캐싱하기 위한 쉘 명령이나 유틸리티가 있습니까?

이곳이 올바른 질문이기를 바랍니다.

이와 유사한 UNIX 도구가 있습니까?

# invokes echo, saves to cache (using command with arguments as key), returns "hi"
cache 10 echo hi 

# wait 2 seconds
sleep 2

# doesn't invoke echo, from cache returns "hi"
cache 10 echo hi 

# wait 10 seconds
sleep 10

# with cache expired, invokes echo, returns "hi"
cache 10 echo hi 

분명히 echo는 실제 사용 사례가 아닙니다.

기본적으로 주어진 명령 + 매개변수에 대한 STDOUT, STDERR 및 상태를 캐시하므로 다음에 동일한 프로세스가 호출될 때 다시 실행할 필요가 없습니다.

이를 수행하기 위한 스크립트를 작성할 수 있지만 Unix 도구 세트에 내가 모르는 스크립트가 있는지 궁금합니다.

답변1

나는 이것에 대해 상당히 완전한 스크립트를 작성했습니다. 최신 버전은 다음과 같습니다.https://gist.github.com/akorn/51ee2fe7d36fa139723c851d87e56096.

#!/bin/zsh
#
# Purpose: run speficied command with specified arguments and cache result. If cache is fresh enough, don't run command again but return cached output.
# Also cache exit status and stderr.
# License: GPLv3

# Use silly long variable names to avoid clashing with whatever the invoked program might use
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 "Usage: runcached [--ttl <max cache age>] [--cache-dir <cache directory>]"
    echo "       [--ignore-env] [--ignore-pwd] [--help] [--prune-cache]"
    echo "       [--] command [arg1 [arg2 ...]]"
    echo
    echo "Run 'command' with the specified args and cache stdout, stderr and exit"
    echo "status. If you run the same command again and the cache is fresh, cached"
    echo "data is returned and the command is not actually run."
    echo
    echo "Normally, all exported environment variables as well as the current working"
    echo "directory are included in the cache key. The --ignore options disable this."
    echo "The OLDPWD variable is always ignored."
    echo
    echo "--prune-cache deletes all cache entries older than the maximum age. There is"
    echo "no other mechanism to prevent the cache growing without bounds."
    echo
    echo "The default cache directory is ${RUNCACHED_CACHE_DIR}."
    echo "Maximum cache age defaults to ${RUNCACHED_MAX_AGE}."
    echo
    echo "CAVEATS:"
    echo
    echo "Side effects of 'command' are obviously not cached."
    echo
    echo "There is no cache invalidation logic except cache age (specified in seconds)."
    echo
    echo "If the cache can't be created, the command is run uncached."
    echo
    echo "This script is always silent; any output comes from the invoked command. You"
    echo "may thus not notice errors creating the cache and such."
    echo
    echo "stdout and stderr streams are saved separately. When both are written to a"
    echo "terminal from cache, they will almost certainly be interleaved differently"
    echo "than originally. Ordering of messages within the two streams is preserved."
    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

# This is racy, but the race is harmless; at worst, the program is run uncached 
# because the cache is unusable. Testing for directory existence saves an
# mkdir(1) execution in the common case, improving performance infinitesimally;
# it could matter if runcached is run from inside a tight loop.
# Hide errors so that runcached itself is transparent (doesn't mix new messages 
# into whatever the called program outputs).
[[ -d "$RUNCACHED_CACHE_DIR/." ]] || 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 # if no command specified, exit silently

(
    # Almost(?) nothing uses OLDPWD, but taking it into account potentially reduces cache efficency.
    # Thus, we ignore it for the purpose of coming up with a cache key.
    unset OLDPWD
    ((RUNCACHED_IGNORE_PWD)) && unset PWD
    ((RUNCACHED_IGNORE_ENV)) || env
    echo -E "$@"
) | md5sum | read RUNCACHED_CACHE_KEY RUNCACHED__crap__

# Unfortunately, I couldn't find a less convoluted way of getting rid of an error message when trying to open a logfile in a nonexistent cache directory.
exec {RUNCACHED_temp_stderr} >&2
exec 2>/dev/null
exec {RUNCACHED_LOCK_FD}>>$RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.lock
exec 2>&$RUNCACHED_temp_stderr
exec {RUNCACHED_temp_stderr}>&-

# If we can't obtain a lock, we want to run uncached; otherwise
# 'runcached' wouldn't be transparent because it would prevent
# parallel execution of several instances of the same command.
# Locking is necessary to avoid races between the mv(1) command
# below replacing stderr with a newer version and another instance
# of runcached using a newer stdout with the older stderr.
if flock -n $RUNCACHED_LOCK_FD 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

    # only reached if cache didn't exist or was too old
    if [[ -d $RUNCACHED_CACHE_DIR/. ]]; then
        RUNCACHED_tempdir=$(mktemp -d 2>/dev/null)
        if [[ -d $RUNCACHED_tempdir/. ]]; then
            $@ >&1 >$RUNCACHED_tempdir/$RUNCACHED_CACHE_KEY.stdout 2>&2 2>$RUNCACHED_tempdir/$RUNCACHED_CACHE_KEY.stderr
            RUNCACHED_ret=$?
            echo $RUNCACHED_ret >$RUNCACHED_tempdir/$RUNCACHED_CACHE_KEY.exitstatus 2>/dev/null
            mv $RUNCACHED_tempdir/$RUNCACHED_CACHE_KEY.{stdout,stderr,exitstatus} $RUNCACHED_CACHE_DIR/ 2>/dev/null
            rmdir $RUNCACHED_tempdir 2>/dev/null
            exit $RUNCACHED_ret
        fi
    fi
fi

# only reached if cache not created successfully or lock couldn't be obtained
exec $@

답변2

편집하다:내 것도 참조하세요다른 답변~에 대한bkt, 독립형 하위 프로세스 캐싱 유틸리티

내가 만든다배쉬 캐시, Bash의 메모리 뱅크는 설명대로 정확하게 작동합니다. Bash 함수를 캐싱하기 위해 특별히 설계되었지만 함수에서 다른 명령에 대한 호출을 래핑할 수도 있습니다.

단순한 캐싱 메커니즘이 무시하는 많은 극단적인 경우 동작을 처리합니다. stdout 및 stderr만 남겨두고 출력의 후행 공백을 유지하면서 원래 호출의 종료 코드를 보고합니다( $()명령 대체는 후행 공백을 자릅니다).

데모:

# Define function normally, then decorate it with bc::cache
$ maybe_sleep() {
  sleep "$@"
  echo "Did I sleep?"
} && bc::cache maybe_sleep

# Initial call invokes the function
$ time maybe_sleep 1
Did I sleep?

real    0m1.047s
user    0m0.000s
sys     0m0.020s

# Subsequent call uses the cache
$ time maybe_sleep 1
Did I sleep?

real    0m0.044s
user    0m0.000s
sys     0m0.010s

# Invocations with different arguments are cached separately
$ time maybe_sleep 2
Did I sleep?

real    0m2.049s
user    0m0.000s
sys     0m0.020s

캐싱 오버헤드를 보여주는 벤치마크 기능도 있습니다.

$ bc::benchmark maybe_sleep 1
Original:       1.007
Cold Cache:     1.052
Warm Cache:     0.044

따라서 읽기/쓰기 오버헤드를 볼 수 있습니다(내 컴퓨터에서는 이를 사용합니다).임시 파일 시스템)은 약 1/20초입니다. 이 벤치마크 유틸리티는 특정 호출을 캐싱할 가치가 있는지 결정하는 데 도움이 될 수 있습니다.

답변3

결과를 파일에 저장하고 해당 파일에서 다시 읽을 수 있습니다.

tmpDir=/tmp/$$
rm -rf "$tmpDir"
mkdir "$tmpDir"

echo cmd1 > "$tmpDir"/cmd1_stdout 2> "$tmpDir"/cmd1_stderr
echo $? > "$tmpDir"/cmd1_exitcode

# Retrieving output of cmd1:
( cat "$tmpDir"/cmd1_stdout ; cat "$tmpDir"/cmd1_stderr 1>&2; exit $(cat "$tmpDir"/cmd1_exitcode) )

이것으로부터 우리는 "캐시" 기능을 정의할 수 있습니다. 이 버전에는 인수로 사용하지 않을 문자가 필요합니다. 예를 들어 쉼표 ","입니다. "IFS=" 줄에서 변경할 수 있습니다.

tmpDir=/tmp/$$
rm -rf "$tmpDir"
mkdir "$tmpDir"

cache() {

 IFS=, cmd="$*"
 if [ -f "$tmpDir/$cmd"_exitcode ]; then 
   cat "$tmpDir/$cmd"_stdout
   cat "$tmpDir/$cmd"_stderr 1>&2
   return $(cat "$tmpDir"/cmd1_exitcode)
 fi

   # This line is bash-only:
 "$@" 2> >(tee "$tmpDir/$cmd"_stderr 1>&2) > >(tee "$tmpDir/$cmd"_stdout)
 local e=$?
 echo $e > "$tmpDir/$cmd"_exitcode

 return $e
}

"date +%s" 및 "stat -c %Y"를 사용하여 시간 초과를 달성할 수 있습니다.

tmpDir=/tmp/$$
rm -rf "$tmpDir"
mkdir "$tmpDir"

cache() {

 local timeout=$1
 shift

 IFS=, cmd="$*"
 if [ -f "$tmpDir/$cmd"_exitcode ]; then 

   local now=$(date +%s)
   local fdate=$(stat -c %Y "$tmpDir/$cmd"_exitcode)

   if [ $((now-fdate)) -le $timeout ]; then 
     cat "$tmpDir/$cmd"_stdout
     cat "$tmpDir/$cmd"_stderr 1>&2
     return $(cat "$tmpDir/$cmd"_exitcode)
   fi

 fi

   # This line is bash-only:
 "$@" 2> >(tee "$tmpDir/$cmd"_stderr 1>&2) > >(tee "$tmpDir/$cmd"_stdout)
 local e=$?
 echo $e > "$tmpDir/$cmd"_exitcode

 return $e
}

"bash only" 줄은 다음으로 대체될 수 있습니다:

  "$@" 2> "$tmpDir/$cmd"_stderr > "$tmpDir/$cmd"_stdout
  local e=$?
  cat "$tmpDir/$cmd"_stdout
  cat "$tmpDir/$cmd"_stderr 1>&2

답변4

쉘 스크립트에서 명령 출력을 캐싱하는 가장 일반적인 메커니즘은 이를 변수에 할당하는 것입니다. 이는 서브쉘을 통해 쉽게 수행할 수 있습니다. 기존 캐시처럼 만료되지는 않지만 쉘 스크립트를 작성하는 사람들은 종종 이를 허용한다고 생각합니다. 다음은 서브쉘과 변수를 사용하는 위의 스크립트입니다.

HI=$(echo hi)
echo $HI
sleep 2
echo $HI
sleep 10
echo $HI

또 다른 옵션은 쉘 스크립트에서 캐시 기능을 생성하는 것입니다. 그것은 마치 ...

cache() {
expiry=$1
cmd=$2-
cache=/tmp/{$2-}_cache

if test "`find -not -newermt '-30 seconds' -delete ${cache}`"; then
$cmd |tee "$cache"
else
cat "$cache"
fi
}

관련 정보