wsd: support parallel systemplate setup

When tests are run in parallel, they will all
compete to update and set up the systemplate
directory, which has a handful of files that
need to be up-to-date. This is a source of errors.

Normally, these files are linked (hard- or soft-
link, whichever succeeds). With linking, we
only need to worry about the initial setup,
as the files will never be out-of-date from
then on. However, when linking fails, we need
to copy the files, and update them (by copying
over fresh versions of the files, if necessary)
every time a new kit is forked. Copying over
is tricky, as it's not atomic. To make it
atomic, we copy the files to the destination
directory under a temporary (random) name,
and then rename to the final name (which is
atomic, including replacing the target file,
if it exists).

No such race exists in production, where there
is (or should be) but one instance of loolwsd
(which does the initial setup) and forkit
(which updates systemplate before forking
new kit instances).
This is an issue with parallel tests only.

Change-Id: I6ba1514d00a84da7397d28efeb6378619711d52f
Reviewed-on: https://gerrit.libreoffice.org/c/online/+/97785
Tested-by: Jenkins
Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com>
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
private/tml/ios-gen2
Ashod Nakashian 2020-07-02 17:54:28 -04:00 committed by Ashod Nakashian
parent e5188272c7
commit d6259d6a54
4 changed files with 195 additions and 71 deletions

View File

@ -13,6 +13,7 @@
#include <dirent.h>
#include <ftw.h>
#include <sys/time.h>
#ifdef __linux
#include <sys/vfs.h>
#elif defined IOS
@ -85,7 +86,7 @@ namespace FileUtil
return name;
}
void copyFileTo(const std::string &fromPath, const std::string &toPath)
bool copy(const std::string& fromPath, const std::string& toPath, bool log, bool throw_on_error)
{
int from = -1, to = -1;
try {
@ -110,7 +111,10 @@ namespace FileUtil
throw;
}
LOG_INF("Copying " << st.st_size << " bytes from " << anonymizeUrl(fromPath) << " to " << anonymizeUrl(toPath));
// Logging may be redundant and/or noisy.
if (log)
LOG_INF("Copying " << st.st_size << " bytes from " << anonymizeUrl(fromPath)
<< " to " << anonymizeUrl(toPath));
char buffer[64 * 1024];
@ -150,15 +154,24 @@ namespace FileUtil
}
close(from);
close(to);
return true;
}
catch (...)
{
LOG_SYS("Failed to copy from " << anonymizeUrl(fromPath) << " to " << anonymizeUrl(toPath));
std::ostringstream oss;
oss << "Failed to copy from " << anonymizeUrl(fromPath) << " to "
<< anonymizeUrl(toPath);
const std::string err = oss.str();
LOG_SYS(err);
close(from);
close(to);
unlink(toPath.c_str());
throw Poco::Exception("failed to copy");
if (throw_on_error)
throw Poco::Exception(err);
}
return false;
}
std::string getTempFilePath(const std::string& srcDir, const std::string& srcFilename, const std::string& dstFilenamePrefix)
@ -252,6 +265,69 @@ namespace FileUtil
return path;
}
bool isEmptyDirectory(const char* path)
{
DIR* dir = opendir(path);
if (dir == nullptr)
return errno != EACCES; // Assume it's not empty when EACCES.
int count = 0;
while (readdir(dir) && ++count < 3)
;
closedir(dir);
return count <= 2; // Discounting . and ..
}
bool updateTimestamps(const std::string& filename, timespec tsAccess, timespec tsModified)
{
// The timestamp is in seconds and microseconds.
timeval timestamps[2]{ { tsAccess.tv_sec, tsAccess.tv_nsec / 1000 },
{ tsModified.tv_sec, tsModified.tv_nsec / 1000 } };
if (utimes(filename.c_str(), timestamps) != 0)
{
LOG_SYS("Failed to update the timestamp of [" << filename << "]");
return false;
}
return true;
}
bool copyAtomic(const std::string& fromPath, const std::string& toPath, bool preserveTimestamps)
{
const std::string randFilename = toPath + Util::rng::getFilename(12);
if (copy(fromPath, randFilename, /*log=*/false, /*throw_on_error=*/false))
{
if (preserveTimestamps)
{
const Stat st(fromPath);
updateTimestamps(randFilename, st.sb().st_atim, st.sb().st_mtim);
}
// Now rename atomically, replacing any existing files with the same name.
if (rename(randFilename.c_str(), toPath.c_str()) == 0)
return true;
LOG_SYS("Failed to copy [" << fromPath << "] -> [" << toPath
<< "] while atomically renaming:");
removeFile(randFilename, false); // Cleanup.
}
return false;
}
bool linkOrCopyFile(const char* source, const char* target)
{
if (link(source, target) == -1)
{
LOG_INF("link(\"" << source << "\", \"" << target << "\") failed: " << strerror(errno)
<< ". Will copy.");
return copy(source, target, /*log=*/false, /*throw_on_error=*/false);
}
return true;
}
} // namespace FileUtil
namespace
@ -406,41 +482,6 @@ namespace FileUtil
return AnonymizeUserData ? Util::anonymize(username, AnonymizationSalt) : username;
}
bool isEmptyDirectory(const char* path)
{
DIR* dir = opendir(path);
if (dir == nullptr)
return errno != EACCES; // Assume it's not empty when EACCES.
int count = 0;
while (readdir(dir) && ++count < 3)
;
closedir(dir);
return count <= 2; // Discounting . and ..
}
bool linkOrCopyFile(const char* source, const char* target)
{
if (link(source, target) == -1)
{
LOG_INF("link(\"" << source << "\", \"" << target << "\") failed: " << strerror(errno)
<< ". Will copy.");
try
{
Poco::File(source).copyTo(target);
}
catch (const std::exception& exc)
{
LOG_ERR("Copying of [" << source << "] to [" << target
<< "] failed: " << exc.what());
return false;
}
}
return true;
}
} // namespace FileUtil
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

View File

@ -70,8 +70,25 @@ namespace FileUtil
bool isEmptyDirectory(const char* path);
inline bool isEmptyDirectory(const std::string& path) { return isEmptyDirectory(path.c_str()); }
/// Update the access-time and modified-time metadata for the given file.
bool updateTimestamps(const std::string& filename, timespec tsAccess, timespec tsModified);
/// Copy the source file to the target.
bool copy(const std::string& fromPath, const std::string& toPath, bool log,
bool throw_on_error);
/// Atomically copy a file and optionally preserve its timestamps.
/// The file is copied with a temporary name, and then atomically renamed.
/// NOTE: toPath must be a valid filename, not a directory.
/// Does not log (except errors), does not throw. Returns true on success.
bool copyAtomic(const std::string& fromPath, const std::string& toPath,
bool preserveTimestamps);
/// Copy a file from @fromPath to @toPath, throws on failure.
void copyFileTo(const std::string &fromPath, const std::string &toPath);
inline void copyFileTo(const std::string& fromPath, const std::string& toPath)
{
copy(fromPath, toPath, /*log=*/true, /*throw_on_error=*/true);
}
/// Make a temp copy of a file, and prepend it with a prefix.
std::string getTempFilePath(const std::string& srcDir, const std::string& srcFilename,
@ -100,7 +117,7 @@ namespace FileUtil
class Stat
{
public:
/// Stat the given path. Symbolic links are stats when @link is true.
/// Stat the given path. Symbolic links are stat'ed when @link is true.
Stat(const std::string& file, bool link = false)
: _path(file)
, _res(link ? lstat(file.c_str(), &_sb) : stat(file.c_str(), &_sb))
@ -119,9 +136,33 @@ namespace FileUtil
bool isFile() const { return S_ISREG(_sb.st_mode); }
bool isLink() const { return S_ISLNK(_sb.st_mode); }
/// Returns true iff the path exists, regarlesss of access permission.
/// Returns the filesize in bytes.
size_t size() const { return _sb.st_size; }
/// Returns the modified time.
timespec modifiedTime() const { return _sb.st_mtim; }
/// Returns true iff the path exists, regardless of access permission.
bool exists() const { return good() || (_errno != ENOENT && _errno != ENOTDIR); }
/// Returns true if both files exist and have
/// the same size and modified timestamp.
bool isUpToDate(const Stat& other) const
{
if (exists() && other.exists() && !isDirectory() && !other.isDirectory())
{
// No need to check whether they are linked or not,
// since if they are, the following check will match,
// and if they aren't, we still need to rely on the following.
return (size() == other.size()
&& modifiedTime().tv_sec == other.modifiedTime().tv_sec
&& (modifiedTime().tv_nsec / 1000000) // Millisecond precision.
== (other.modifiedTime().tv_nsec / 1000000));
}
return false;
}
private:
const std::string _path;
struct ::stat _sb;

View File

@ -245,63 +245,109 @@ namespace SysTemplate
/// These must be up-to-date, as they can change during
/// the long lifetime of our process. Also, it's unlikely
/// that systemplate will get re-generated after installation.
static const auto DynamicFilePaths = { "/etc/passwd", "/etc/group", "/etc/host.conf",
"/etc/hosts", "/etc/nsswitch.conf", "/etc/resolv.conf" };
static const auto DynamicFilePaths
= { "/etc/passwd", "/etc/group", "/etc/host.conf", "/etc/hosts",
"/etc/nsswitch.conf", "/etc/resolv.conf", "etc/timezone", "etc/localtime" };
/// Copy by default for KIT_IN_PROCESS.
/// Copy (false) by default for KIT_IN_PROCESS.
static bool LinkDynamicFiles = false;
void setupDynamicFiles(const std::string& sysTemplate)
{
LOG_INF("Setting up dynamic files in sysTemplate.");
LOG_INF("Setting up systemplate dynamic files in [" << sysTemplate << "].");
const std::string etcSysTemplatePath = Poco::Path(sysTemplate, "etc").toString();
LinkDynamicFiles = true;
LinkDynamicFiles = true; // Prefer linking, unless it fails.
for (const auto& srcFilename : DynamicFilePaths)
{
const Poco::File srcFilePath(srcFilename);
if (!srcFilePath.exists())
FileUtil::Stat srcStat(srcFilename);
if (!srcStat.exists())
continue;
// Remove the file to create a symlink.
const Poco::Path dstFilePath(sysTemplate, srcFilename);
const std::string dstFilename = Poco::Path(sysTemplate, srcFilename).toString();
FileUtil::Stat dstStat(dstFilename);
// Is it outdated?
if (dstStat.isUpToDate(srcStat))
{
LOG_INF("File [" << dstFilename << "] is already up-to-date.");
continue;
}
if (LinkDynamicFiles)
{
LOG_INF("Linking [" << srcFilename << "] -> [" << dstFilePath.toString() << "].");
FileUtil::removeFile(dstFilePath);
LOG_INF("Linking [" << srcFilename << "] -> [" << dstFilename << "].");
// Link or copy.
if (link(srcFilename, dstFilePath.toString().c_str()) != -1)
if (link(srcFilename, dstFilename.c_str()) == 0)
continue;
// Hard-linking failed, try symbolic linking.
if (symlink(srcFilename, dstFilename.c_str()) == 0)
continue;
const int linkerr = errno;
// With parallel tests, another test might have linked already.
if (Poco::File(dstFilename).exists()) // stat again.
continue;
// Failed to link a file. Disable linking and copy instead.
LOG_WRN("Failed to link [" << srcFilename << "] -> [" << dstFilePath.toString() << "] ("
<< strerror(errno) << "). Will copy.");
LOG_WRN("Failed to link ["
<< srcFilename << "] -> [" << dstFilename << "] (" << strerror(linkerr)
<< "). Will copy and disable linking dynamic system files in this run.");
LinkDynamicFiles = false;
}
// Linking fails, just copy.
LOG_INF("Copying [" << srcFilename << "] -> [" << dstFilePath.toString() << "].");
srcFilePath.copyTo(etcSysTemplatePath);
// Linking failed, just copy.
LOG_INF("Copying [" << srcFilename << "] -> [" << dstFilename << "].");
if (!FileUtil::copyAtomic(srcFilename, dstFilename, true))
{
if (!Poco::File(dstFilename).exists()) // stat again.
LOG_ERR("Failed to copy [" << srcFilename << "] -> [" << dstFilename
<< "], some functionality may be missing.");
}
}
if (LinkDynamicFiles)
LOG_INF("Successfully linked the systemplate dynamic files in ["
<< sysTemplate << "] and will not need to update them again.");
}
void updateDynamicFiles(const std::string& sysTemplate)
{
// If the files are linked, they are always up-to-date.
if (!LinkDynamicFiles)
{
LOG_INF("Updating dynamic files in sysTemplate.");
LOG_INF("Updating systemplate dynamic files in [" << sysTemplate << "].");
const std::string etcSysTemplatePath = Poco::Path(sysTemplate, "etc").toString();
for (const auto& srcFilename : DynamicFilePaths)
{
const Poco::File srcFilePath(srcFilename);
if (!srcFilePath.exists())
FileUtil::Stat srcStat(srcFilename);
if (!srcStat.exists())
continue;
const Poco::Path dstFilePath(sysTemplate, srcFilename);
LOG_DBG("Copying [" << srcFilename << "] -> [" << dstFilePath.toString() << "].");
srcFilePath.copyTo(etcSysTemplatePath);
const std::string dstFilename = Poco::Path(sysTemplate, srcFilename).toString();
FileUtil::Stat dstStat(dstFilename);
// Is it outdated?
if (dstStat.isUpToDate(srcStat))
{
LOG_INF("File [" << dstFilename << "] is already up-to-date.");
}
else
{
LOG_INF("Copying [" << srcFilename << "] -> [" << dstFilename << "].");
if (!FileUtil::copyAtomic(srcFilename, dstFilename, true))
{
if (!Poco::File(dstFilename).exists()) // stat again.
LOG_ERR("Failed to copy [" << srcFilename << "] -> [" << dstFilename
<< "], some functionality may be missing.");
}
}
}
}
}

View File

@ -21,16 +21,12 @@ cd / || exit 1
# into the template tree of system files for the chroot jails.
# First essential files and shared objects
find etc/hosts etc/nsswitch.conf etc/resolv.conf \
etc/passwd etc/group etc/host.conf \
etc/ld.so.* \
find etc/ld.so.* \
lib/ld-* lib64/ld-* \
lib/libnss_* lib64/libnss_* lib/*/libnss_* \
lib/libresolv* lib64/libresolv* lib/*/libresolv* \
var/cache/fontconfig \
etc/fonts \
etc/timezone \
etc/localtime \
usr/lib/locale/en_US.utf8 \
usr/lib/locale/C.UTF-8 \
usr/lib/locale/locale_archive \
@ -42,9 +38,6 @@ find etc/hosts etc/nsswitch.conf etc/resolv.conf \
-type f 2>/dev/null
find etc/fonts \
etc/timezone \
etc/localtime \
etc/resolv.conf \
lib/ld-* lib64/ld-* \
lib/libnss_* lib64/libnss_* lib/*/libnss_* \
lib/libresolv* lib64/libresolv* lib/*/libresolv* \
@ -65,6 +58,9 @@ grep -v dynamic | cut -d " " -f 3 | grep -E '^(/lib|/usr)' | sort -u | sed -e 's
# This will now copy the file a symlink points to, but whatever.
cpio -p -d -L $CHROOT
# Remove the dynamic files, which are linked by loolwsd.
rm -f $CHROOT/etc/{hosts,nsswitch.conf,resolv.conf,passwd,group,host.conf,timezone,localtime}
mkdir -p $CHROOT/lo
mkdir -p $CHROOT/dev
mkdir -p $CHROOT/tmp/dev