コマンドラインプログラムをメモ(キャッシュ)しますか?

コマンドラインプログラムをメモ(キャッシュ)しますか?

時には、同じ出力を得るために同じで高価なコマンドを繰り返し実行します。たとえば、ffprobeメディアファイルに関する情報を取得します。同じ入力が与えられると、常に同じ出力を生成する必要があるため、キャッシュが可能でなければなりません。

私は見たコマンドライン出力の記憶/キャッシュしかし、私はより徹底的な実装を探しています。特に実装は単にコマンドラインを比較するようです。渡されたファイルの1つが変更されたかどうかは不明です。 (固定長バッファもたくさんあって疑わしくもあり、デーモンというのもおかしいですね。)

私の記事を書き始める前に記事が存在するかどうか疑問に思いました。主な要件:

  • コマンドラインの入力ファイルが変更された場合は、コマンドを再実行する必要があります。
  • コマンドラインオプションが変更された場合は、コマンドを再実行する必要があります。
  • 私はコマンドが「非対話型」で実行されることに同意します。たとえば、/dev/nullstdinを使用し、stdoutとstderrのような2つの異なるファイルを使用します。
  • コマンドが正しくない場合は、終了コードでキャッシュするか、まったくキャッシュしないかを選択できます。
  • 上記を考慮すると、キャッシュされたコンテンツはできるだけ頻繁に返される必要があります。しかし、正確さが優先です。
  • たとえば、NFSを介してキャッシュを複数のシステム(すべて共通の制御下にある)間で共有できる場合は、より良いでしょう。

基本的に私が自分で書いたいのは(簡潔にするためにいくつかのロックとエラーチェックをスキップすることです)、コマンドライン+コマンドラインの各項目の統計を取得することです(エラーまたはdev、inode、サイズ、mtime)。 SHA-512またはSHA-256を介して全体の混乱を伝えます。これにより、固定サイズのキーが提供されますが、コマンドまたはファイルが変更されるとキーも変更されます(誰かがサイズとランタイムを維持する変更を行わない限り、その資格がある場合)。キーがキャッシュディレクトリにあることを確認してください。すでに存在する場合は、その内容を stdout と stderr にコピーします。それ以外の場合は、stdin /dev/null と stdout と stderr という 2 つのファイルを使用して、子プロセスでコマンドを実行します。成功したら、ファイルをキャッシュディレクトリに配置します。その後、その内容はstdoutとstderrにコピーされます。結果を直接作成した場合は、デザインフィードバックを歓迎します。その結果はフリーソフトウェアになります。

答え1

あなたが望むものがうまくいかなかったので、本当に良い結果を与える一般的なツールが見つからないことがよくあります。

  • コマンドラインにないファイルにアクセスするコマンド。 ( locate myfile)
  • ネットワークにアクセスするコマンドです。 ( wget http://news.example.com/headlines)
  • 時間依存コマンド。 ( date)
  • ランダム出力でコマンドを発行します。 ( pwgen)

ツールを適用するコマンドを決定する必要がある場合は、必要なのはビルドツールです。出力が最新でなくてもコマンドを実行するツールです。高貴なもの作る良くないでしょう。依存関係を手動で定義し、特に他のコマンドのキャッシュを慎重に分離し、コマンドを変更するときにキャッシュを手動でキャンセルし、各キャッシュを別々のファイルに保存する必要があります。つまり不便だ。の一つ多くの選択おそらく作業に近いです。SConsチェックサムベースおよびタイムスタンプベースの依存関係分析をサポートし、その上にキャッシュメカニズムがあり、Pythonコードを書いて調整できます。

答え2

これは実際の答えよりも脳ダンプに近いですが、説明するには長すぎます。そうでない場合は削除します。教えてください。肩をすくめる

まず、最大の問題は、「コマンド - >結果」という観点から考えていることです。 「ファイル - >結果」の場合を使用できますmake。ファイルから結果につながる固定数のコマンドしか存在しない場合でも、以下を使用できます。make各コマンドのターゲットを作成します。make

「すべてのコマンド - >結果」でなければならないと主張する場合、最初に浮かぶのは一種のREPLまたはShell-in-Language-Xです。最近は不足がなく、2週間程度に一度ずつ新しいものが現れるようです。要点は、これにより次のことができることです。構造化単純な文字列(コマンド)と複数のファイルではないデータ。

dev+++のチェックサムを取得するのが合理的なようですinode。間違った肯定が気になる場合は、いつでも完全な比較を実行できます(注:完全な比較は、各ファイルに対してSHA-*を実行して結果を比較するよりも常に高速です)。バックエンドの場合はSQLiteを使用できますが、古いレコードを期限切れにするにはいくつかのメカニズムが必要です。sizemtime

コマンドおよび/またはファイルのより多くの制限を指摘できれば、より簡単になります。 「コマンド-->結果」の完全な汎用キャッシュを達成することを目指していますが、まだ入力ファイルの変更を追跡することはやや野心的なようです。

答え3

私はほぼ同じ目的で私自身のスクリプトを書いている間にこれを見つけました、そして、それはdev + inode + size + mtimeを使ってファイルをキャッシュすることについてのあなたのアイデアが非常に役に立ったので、それを追加しました。私はこのページを非常に遅く見つけ、すべてを書き直さないことを決めたので、あなたのアイデアは私の実装とは異なります。

  1. 簡単にするために、スクリプトはキャッシュエントリを単一のYAMLファイルに保存します。このファイルは複数のシステムで共有できますが、RCEリスクがあり、YAMLファイルのTOCTOUのためにロックラッパーも作成する必要があります。

  2. Linuxでのみ実行できますが、運が良ければ他のUnixでも実行できます。

  3. 自分の責任で使用してください。キャッシュされたコンテンツは保護されません。

まず実行してくださいgem install chronic_duration

#!/usr/bin/env ruby
# Usage: memoize [-D DATABASE] [-T TIMEOUT] [-F] [--] COMMAND [ARG]...
#     or memoize [-D DATABASE] --cleanup
#
# OPTIONS
#   -D DATABASE      Store entries in YAML format in DATABASE file.
#   -T TIMEOUT       Invalidate memoized entries older than TIMEOUT.
#   -F               Track file changes (dev+inode+size+mtime).
#   --cleanup        Remove all stale entries.

require 'date'
require 'optparse'
require 'digest'
require 'yaml'
require 'chronic_duration'
require 'open3'

MYSELF          = File.basename(__FILE__)
DEFAULT_DBFILE  = "#{Dir.home}/.config/memoize.yml"
DEFAULT_TIMEOUT = '1 week'

def fc(fpath) # File characteristic
  return [:dev, :ino, :size, :mtime].map do |s|
    Digest::SHA1.digest(Integer(File.stat(fpath).send(s)).to_s.b)
  end.join
end

def cmdline_checksum(cmdline, fchanges)
  pre_cksum_bytes = "".b

  cmdline.each do |c|
    characteristic   = (File.exists?(c) and fchanges) ? fc(c) : c
    pre_cksum_bytes += Digest::SHA1.digest(characteristic)
  end

  return Digest::SHA1.digest(pre_cksum_bytes)
end

def timed_out?(entry)
  return (entry[:timestamp] + Integer(entry[:timeout])) < Time.now
end

def pluralize(n, singular, plural)
  return (n % 100 == 11 || n % 10 != 1) ? plural : singular
end

fail "memoize: FATAL: this is a script, not a library" unless __FILE__ == $0

$dbfile   = DEFAULT_DBFILE
$timeout  = DEFAULT_TIMEOUT
$fchanges = false
$cleanup  = false
$retcode  = 0
$replay   = false

ARGV.options do |o|
  o.version = '2018.06.23'
  o.banner  = "Usage: memoize [OPTION]... [--] COMMAND [ARG]...\n"+
              "Cache results of COMMAND and replay its output"

  o.separator ""
  o.separator "OPTIONS"

  o.summary_indent = "  "
  o.summary_width  = 17

  o.on('-D=DATABASE', "Default: #{DEFAULT_DBFILE}")       { |d| $dbfile   = d    }
  o.on('-T=TIMEOUT',  "Default: #{DEFAULT_TIMEOUT}")      { |t| $timeout  = t    }
  o.on('-F', "Track file changes (dev+inode+size+mtime)") {     $fchanges = true }
  o.on('--cleanup', "Remove all stale entries")           {     $cleanup  = true }
end.parse!

begin
  File.open($dbfile, 'a') {}
  File.chmod(0600, $dbfile)
end unless File.exists?($dbfile)

db      = (YAML.load(File.read($dbfile)) or {})
cmdline = ARGV
cksum   = cmdline_checksum(cmdline, $fchanges)
entry   = {
  cmdline:   cmdline,
  timestamp: Time.now,
  timeout:   '1 week',
  stdout:    "",
  stderr:    "",
  retcode:   0,
}

if $cleanup
  entries = db.keys.select{|k| timed_out?(db[k]) }
  c = entries.count

  entries.each do |k|
    db.delete(k)
  end

  STDERR.puts "memoize: NOTE: #{c} stale #{pluralize(c, "entry", "entries")} removed"

  File.open($dbfile, 'w') { |f| f << YAML.dump(db) }

  exit
end

$replay = db.key?(cksum) && (not timed_out?(db[cksum]))

if $replay
  entry = db[cksum]
else
  Open3.popen3(*cmdline) do |i, o, e, t|
    i.close
    entry[:stdout]    = o.read
    entry[:stderr]    = e.read
    entry[:retcode]   = t.value.exitstatus
  end

  entry[:timestamp] = Time.now
  entry[:timeout]   = Integer(ChronicDuration.parse($timeout))
  db[cksum] = entry
end

$retcode = entry[:retcode]
STDOUT.write(entry[:stdout]) # NOTE: we don't record or replay stream timing
STDERR.write(entry[:stderr])
STDOUT.flush
STDERR.flush

File.open($dbfile, 'w') { |f| f << YAML.dump(db) }

exit! $retcode

関連情報