blob: 0de17b5d370ce767bbdd6894040cda0c98e0af8b [file] [log] [blame]
# Aggressively cache agendas.
#
# Most of the heavy lifting is done by the ASF::Board::Agenda class
# This class is mainly focused on caching the results.
#
# This code also maintains a "working copy" of agendas when updates are
# made that may not yet be reflected in the local svn checkout.
#
require 'digest'
class Agenda
CACHE = File.join(AGENDA_WORK, 'cache')
FileUtils.mkdir_p CACHE
@@cache = Hash.new {|hash, key| hash[key] = {mtime: 0}}
# for debug purposes
def self.cache
@@cache
end
# flush cache of files made with previous versions of the library
libmtime = ASF::library_mtime
Dir["#{CACHE}/*.yml"].each do |cache|
File.unlink(cache) if File.mtime(cache) < libmtime
end
# fetch parsed agenda from in memory cache if up to date, otherwise
# fall back to disk.
def self.[](file)
validate_board_file(file)
path = File.join(CACHE, file.sub(/\.txt$/, '.yml'))
data = @@cache[file]
if File.exist?(path) and File.mtime(path) != data[:mtime]
File.open(path) do |fh|
fh.flock(File::LOCK_SH)
data = YAML.safe_load(fh.read, permitted_classes: [Symbol, Time])
end
@@cache[file] = data
end
data
end
# update both in memory and disk caches with new parsed agenda
def self.[]=(file, data)
validate_board_file(file)
path = File.join(CACHE, file.sub(/\.txt$/, '.yml'))
File.open(path, File::RDWR|File::CREAT, 0644) do |fh|
fh.flock(File::LOCK_EX)
fh.write(YAML.dump(data))
fh.flush
fh.truncate(fh.pos)
if data[:mtime].instance_of? Time
File.utime data[:mtime], data[:mtime], path
end
end
@@cache[file] = data
data
end
def self.update_cache(file, path, contents, quick)
update = {
mtime: (quick ? -1 : File.mtime(path)),
digest: Digest::SHA256.base64digest(contents)
}
# update cache if there wasn't a previous entry, the digest changed,
# or the previous entry was the result of a 'quick' parse.
current = Agenda[file]
if not current or current[:digest] != update[:digest] or
current[:mtime].to_i < update[:mtime].to_i
then
if current and current[:digest] == update[:digest] and
current[:mtime].to_i > 0
then
update[:parsed] = current[:parsed]
else
update[:parsed] = ASF::Board::Agenda.parse(contents, quick)
end
Agenda[file] = update
end
end
def self.uptodate(file)
validate_board_file(file)
path = File.expand_path(file, FOUNDATION_BOARD)
return false unless File.exist? path
return Agenda[file][:mtime] == File.mtime(path)
end
def self.parse(file, mode)
validate_board_file(file)
# for quick mode, anything will do
mode = :quick if ENV['RACK_ENV'] == 'test'
return Agenda[file][:parsed] if mode == :quick and Agenda[file][:mtime] != 0
path = File.expand_path(file, FOUNDATION_BOARD)
return Agenda[file][:parsed] unless File.exist? path
# Does the working copy have more recent data?
working_copy = File.join(AGENDA_WORK, file)
if File.size?(working_copy) and File.mtime(working_copy) > File.mtime(path)
path = working_copy
end
if Agenda[file][:mtime] != File.mtime(path)
File.open(working_copy, File::RDWR|File::CREAT, 0644) do |work_file|
work_file.flock(File::LOCK_EX)
if Agenda[file][:mtime] != File.mtime(path)
self.update_cache(file, path, File.read(path), mode == :quick)
end
end
# do a full parse in the background if a quick parse was done
if Agenda[file][:mtime] == -1
Thread.new do
# self.update(file, nil) {} # Does not work, because update needs _
parse(file, :full)
end
end
end
Agenda[file][:parsed]
end
# update agenda file in SVN
def self.update(file, message, retries=20, auth: nil, &block)
return unless block
validate_board_file(file)
commit_rc = 0
# Create a temporary work directory
dir = Dir.mktmpdir
#extract context from block
_, env = eval('[_, env]', block.binding)
if (not auth) and env.password
auth = [['--username', env.user, '--password', env.password]]
end
working_copy = File.join(AGENDA_WORK, file)
File.open(working_copy, File::RDWR|File::CREAT, 0644) do |work_file|
work_file.flock(File::LOCK_EX)
# capture current version of the file
path = File.join(FOUNDATION_BOARD, file)
baseline = File.read(path) if Agenda[file][:mtime] == File.mtime(path)
# check out empty directory
board = ASF::SVN.getInfoItem(FOUNDATION_BOARD,'url')
ASF::SVN.svn_!('checkout', [board, dir], _, {depth: 'empty', auth: auth})
# update the file in question
path = File.join(dir, file)
ASF::SVN.svn_!('update', path, _, {auth: auth})
# invoke block, passing it the current contents of the file
if block and message
input = IO.read(path)
output = yield input.dup
# if the output differs, update and commit the file in question
if output != input
IO.write(path, output)
commit_rc = ASF::SVN.svn_('commit', path, _, {auth: auth, msg: message})
end
else
output = IO.read(path)
end
if commit_rc == 0
# update the work file, and optionally the cache, if successful
work_file.rewind
if output != baseline
# update the cache if the file has changed
self.update_cache(file, path, output, ENV['RACK_ENV'] == 'test')
work_file.write(output)
work_file.flush
end
work_file.truncate(work_file.pos)
else
# if not successful, retry
if retries > 0
work_file.close
sleep rand(41-retries*2)*0.1 if retries <= 20
update(file, message, retries-1, &block)
else
Wunderbar.error _.target! # show the transcript
raise Exception.new("svn commit failed")
end
end
# return the result in the response
_.method_missing(:_agenda, Agenda[file][:parsed])
_.method_missing(:_digest, Agenda[file][:digest])
end
ensure
FileUtils.rm_rf dir if dir
end
end