Memoriza (cache) para programas de linha de comando?

7

Às vezes, acabo executando o mesmo comando, bastante caro, repetidamente, para obter a mesma saída. Por exemplo, ffprobe para obter informações sobre um arquivo de mídia. Dada a mesma entrada, a mesma saída deve ser produzida sempre - portanto, o armazenamento em cache deve ser possível.

Eu vejo Saída de linha de comando em memo / cache , mas eu ' Estou à procura de uma implementação mais minuciosa: em particular, parece apenas comparar a linha de comando - se um dos arquivos passados for modificado, ele não notará. (Ele também tem um monte de buffers de comprimento fixo que me fazem suspeitar e é estranhamente um daemon.)

Antes de eu sair e escrever o meu, estou curioso para saber se já existe um. Os principais requisitos:

  • É necessário executar novamente o comando se qualquer um dos arquivos de entrada (na linha de comandos) mudar
  • É necessário executar novamente o comando se qualquer uma das opções de linha de comando mudar
  • Estou bem (e honestamente espero) que os comandos sejam executados como "não interativos": por exemplo, com /dev/null como stdin e dois arquivos diferentes como stdout e stderr.
  • Se o comando der errado, eu estou bem com o que está sendo armazenado em cache junto com o código de saída ou, alternativamente, apenas não sendo armazenado em cache.
  • Deve devolver o conteúdo em cache o mais frequentemente possível, tendo em conta o que precede. Mas a correção vem em primeiro lugar.
  • Preferível se o cache puder ser compartilhado entre várias máquinas (todas sob controle comum), por exemplo, via NFS.

Basicamente, o que eu estou pensando em fazer, se eu escrevo, é (pular alguns trancos e checar erros por brevidade): pegue a linha de comando + os resultados stat de cada item na linha de comando (erro ou dev , inode, tamanho, mtime) e passar essa bagunça inteira através do SHA-512 ou SHA-256. Isso dará uma chave que é um tamanho fixo, mas mudará se o comando ou os arquivos mudarem (a menos que alguém faça uma alteração de tamanho e preservação de mtime, caso em que eles merecem o que recebem). Verifique se essa chave está no diretório de cache. Se já existir, copie o conteúdo para stdout & stderr. Caso contrário, execute o comando em um subprocesso com stdin / dev / null e dois arquivos como stdout e stderr. Se bem sucedido, coloque os arquivos no diretório de cache. Em seguida, copie o conteúdo para stdout e stderr. Se acontecer de eu acabar escrevendo meu próprio bem-vindo design feedback. E o resultado será software livre.

    
por derobert 05.05.2017 / 01:42

3 respostas

4

Existem muitos casos em que o que você deseja não funciona, e você não encontrará uma ferramenta genérica que forneça resultados realmente bons:

  • Comandos que acessam arquivos que não estão na linha de comando. ( locate myfile )
  • Comandos que acessam a rede. ( wget http://news.example.com/headlines )
  • Comandos que dependem do tempo. ( date )
  • Comandos que possuem saída aleatória. ( pwgen )

Se você resolver a tarefa de decidir em quais comandos aplicar a ferramenta, o que você deseja é uma ferramenta de compilação: uma ferramenta que executa comandos se a saída não estiver atualizada. O venerável make não será muito bom: você tem que definir as dependências manualmente, em particular você precisa armazene cuidadosamente caches separados para comandos diferentes e revogue manualmente caches se você alterar o comando e precisar armazenar cada cache em um arquivo separado, o que é inconveniente. Uma das muitas alternativas pode estar mais à altura da tarefa, talvez SCons que suporta análise de dependência baseada em checksum e timestamp, tem um mecanismo de cache em cima disso, e pode ser ajustado escrevendo código Python. / p>     

por 06.05.2017 / 01:01
2

Isso é mais um despejo cerebral do que uma resposta real, mas é muito longo para um comentário. Se isso for inapropriado, vou apagá-lo. Apenas me avise. encolher de ombros

Primeiro, acho que o principal problema é que você está pensando em "comando - > resultado". Se fosse "arquivo (s) - > resultado" você poderia usar apenas make . Se houver apenas um pequeno número fixo de comandos que leva de arquivo (s) para resultados, você ainda pode usar make : escrever um make target para cada comando.

Se você insiste que deve ser "comando arbitrário - > resultado", a primeira coisa que vem à mente é algum tipo de REPL, ou um shell-in-language-X. Não há escassez dessas coisas nos dias de hoje, um novo parece surgir a cada duas semanas ou mais. O ponto é que isso permitiria que você trabalhasse com dados estruturados , em vez de apenas uma string (o comando) e vários arquivos.

A verificação da soma de dev + inode + size + mtime parece sensata. Você sempre pode fazer uma comparação completa se você se preocupa com falsos positivos (em uma nota lateral: uma comparação completa é sempre mais rápida do que tomar SHA- * para cada arquivo e comparar os resultados). Para o backend, você poderia usar o SQLite, mas precisaria de algum mecanismo para expirar os registros antigos.

As coisas podem ser mais fáceis se você puder apontar mais restrições sobre o que os comandos e / ou arquivos podem ser. Visando um cache completamente geral de "comando - > resultado", mas ainda mantendo o controle de alterações nos arquivos de entrada parece um pouco ambicioso demais.

    
por 05.05.2017 / 08:28
1

Eu encontrei esta questão enquanto escrevia meu próprio script para mais ou menos o mesmo propósito e sua idéia sobre o armazenamento em cache de arquivos com seu dev + inode + size + mtime pareceu muito útil, então eu o adicionei. A sua ideia e a minha implementação diferem, uma vez que tropecei tarde nesta página e decidi não reescrever tudo:

  1. O script armazena entradas de cache em um único arquivo YAML para simplificar. Você ainda pode compartilhar esse arquivo em várias máquinas, mas há um risco de um RCE, além de precisar gravar um wrapper de bloqueio por causa do TOCTOU no arquivo YAML.

  2. Ele provavelmente funcionará apenas no Linux e, com sorte, em outros Unixes.

  3. Use por sua conta e risco. Seu conteúdo de cache não será protegido.

Execute gem install chronic_duration primeiro.

#!/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
    
por 23.06.2018 / 01:11