Skip to content

Commit

Permalink
Chdir: merge redundant fchdir() system calls
Browse files Browse the repository at this point in the history
This commit adds a class which manages fchdir() system calls and lets
multiple callers use the same directory.

This prepares for submitting "cull" commands to `/dev/cachefiles` with
io_uring.
  • Loading branch information
MaxKellermann committed Dec 5, 2024
1 parent f40746f commit b7ab4d9
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 22 deletions.
1 change: 1 addition & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ cm4all-cash (0.3) unstable; urgency=low
* ignore cachefilesd options "culltable", "resume_thresholds"
* disallow the "bind" command in the configuration file
* limit statx() concurrency to 16k (from 1 million)
* merge redundant fchdir() system calls

--

Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ executable('cm4all-cash',
'src/Cull.cxx',
'src/DevCachefiles.cxx',
'src/Walk.cxx',
'src/Chdir.cxx',
include_directories: inc,
dependencies: [
io_linux_dep,
Expand Down
110 changes: 110 additions & 0 deletions src/Chdir.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright CM4all GmbH
// author: Max Kellermann <[email protected]>

#include "Chdir.hxx"

#include <unistd.h>

Chdir::Chdir(EventLoop &event_loop) noexcept
:defer_next(event_loop, BIND_THIS_METHOD(Next))
{
}

Chdir::~Chdir() noexcept
{
/* revert to "/" so we don't occupy an arbitrary directory
(that would prevent unmounting, for example) */
chdir("/");
}

inline void
Chdir::Next() noexcept
{
/* if there is a valid "current" directory, we don't need to
fchdir() again */
const bool need_chdir = current == map.end();

if (current == map.end()) {
/* find the first non-empty list, skipping canceled
directories */
while (true) {
current = map.begin();
if (current == map.end())
/* no waiters left - stop here */
return;

if (!current->second.empty())
/* this is not empty - use it */
break;

/* all waiters have been canceled; remove this
directory; the FileDescriptor may have been closed
already, but this doesn't matter, we don't use it
anyway */
current = map.erase(current);
}
}

auto &list = current->second;

if (need_chdir && fchdir(current->first.Get()) < 0) {
// error

/* move the list to the stack and erase the failed
directory from the map */
auto tmp = std::move(list);

map.erase(current);
current = map.end();

/* schedule the next fchdir() after the callback loop
below has finished */
defer_next.Schedule();

list.clear_and_dispose([](auto *i){
i->OnChdirError();
});
} else {
/* success: create a lease first; this is kept alive
until all waiters have been invoked to avoid
OnAbandoned() to be called in the middle of this
loop */
SharedLease lease{*this};

while (true) {
auto &i = list.pop_front();
if (list.empty()) {
/* the last waiter inherits this
lease, so it won't be released at
the end of this scope */
i.OnChdir(std::move(lease));
break;
}

/* all other waiters get a copy of the
lease */
i.OnChdir(lease);
}
}
}

void
Chdir::Add(FileDescriptor directory, ChdirWaiter &w) noexcept
{
map[directory].push_back(w);

if (current == map.end() || current->first == directory)
defer_next.Schedule();
}

void
Chdir::OnAbandoned() noexcept
{
assert(current != map.end());

map.erase(current);
current = map.end();

defer_next.Schedule();
}
91 changes: 91 additions & 0 deletions src/Chdir.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright CM4all GmbH
// author: Max Kellermann <[email protected]>

#pragma once

#include "event/DeferEvent.hxx"
#include "io/FileDescriptor.hxx"
#include "util/IntrusiveList.hxx"
#include "util/SharedLease.hxx"

#include <map>

/**
* Handler class for #Chdir.
*/
class ChdirWaiter {
IntrusiveListHook<IntrusiveHookMode::AUTO_UNLINK> chdir_siblings;

public:
using List = IntrusiveList<ChdirWaiter, IntrusiveListMemberHookTraits<&ChdirWaiter::chdir_siblings>>;

/**
* The current working directory is now the one passed to
* Chdir::Add(). This method may now perform I/O, even after
* this method has returned. When all I/O in this directory
* is complete, release the #lease.
*/
virtual void OnChdir(SharedLease lease) noexcept = 0;

/**
* An (unspecified) error has occurred and the directory could
* not be changed.
*/
virtual void OnChdirError() noexcept = 0;
};

/**
* Provides an optimization for changing the current working directory
* using fchdir(): multiple callers can ask to change the working
* directory and this class groups all callers with the same
* directory. It calls them back using the #ChdirWaiter interface,
* waits for all leases to be released, and then goes on with the next
* directory.
*/
class Chdir final : SharedAnchor {
DeferEvent defer_next;

struct FileDescriptorCompare {
constexpr bool operator()(FileDescriptor a, FileDescriptor b) const noexcept {
return a.Get() < b.Get();
}
};

using WaiterMap = std::map<FileDescriptor, ChdirWaiter::List, FileDescriptorCompare>;

/**
* Maps directory file descriptors to a list of waiters. The
* file descriptors are owned by the waiters.
*/
WaiterMap map;

/**
* If this is not end(), then this points to the current
* working directory and there are unreleased leases.
*/
WaiterMap::iterator current = map.end();

public:
explicit Chdir(EventLoop &event_loop) noexcept;
~Chdir() noexcept;

Chdir(const Chdir &) = delete;
Chdir &operator=(const Chdir &) = delete;

/**
* Schedule a fchdir() call.
*
* @param directory where to change to; must remain valid
* until the #ChdirWaiter has been called
* @param w the waiter that will be called back; it may be
* canceled by simply destructing the #ChdirWaiter instance
*/
void Add(FileDescriptor directory, ChdirWaiter &w) noexcept;

private:
void Next() noexcept;

// virtual methods from SharedAnchor
void OnAbandoned() noexcept override;
};
95 changes: 76 additions & 19 deletions src/Cull.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,60 @@
// author: Max Kellermann <[email protected]>

#include "Cull.hxx"
#include "util/DeleteDisposer.hxx"

#include <cassert>
#include <memory>

#include <fmt/core.h> // TODO

class Cull::CullFileOperation final : public IntrusiveListHook<>, ChdirWaiter {
Cull &cull;

std::unique_ptr<WalkResult::File> file;

public:
CullFileOperation(Cull &_cull, WalkResult::File *_file) noexcept
:cull(_cull), file(_file) {}

void Start() noexcept {
cull.chdir.Add(file->parent->fd, *this);
}

// virtual methods from ChdirWaiter
void OnChdir(SharedLease lease) noexcept override;
void OnChdirError() noexcept override;
};

void
Cull::CullFileOperation::OnChdir(SharedLease lease) noexcept
{
switch (cull.dev_cachefiles.CullFile(file->name)) {
case DevCachefiles::CullResult::SUCCESS:
++cull.n_deleted_files;
cull.n_deleted_bytes += file->size;
break;

case DevCachefiles::CullResult::BUSY:
++cull.n_busy;
break;

case DevCachefiles::CullResult::ERROR:
++cull.n_errors;
break;
}

lease = {};
cull.OperationFinished(*this);
}

void
Cull::CullFileOperation::OnChdirError() noexcept
{
++cull.n_errors;
cull.OperationFinished(*this);
}

static DevCachefiles::CullResult
CullFile(DevCachefiles &dev_cachefiles,
FileDescriptor directory_fd, std::string_view filename)
Expand All @@ -19,13 +68,14 @@ CullFile(DevCachefiles &dev_cachefiles,
return dev_cachefiles.CullFile(filename);
}

Cull::Cull(Uring::Queue &_uring,
Cull::Cull(EventLoop &event_loop, Uring::Queue &_uring,
FileDescriptor _dev_cachefiles,
uint_least64_t _cull_files, std::size_t _cull_bytes,
Callback _callback)
:dev_cachefiles(_dev_cachefiles),
callback(_callback),
walk(_uring, _cull_files, _cull_bytes, *this)
walk(_uring, _cull_files, _cull_bytes, *this),
chdir(event_loop)
{
assert(callback);
}
Expand All @@ -51,34 +101,41 @@ Cull::OnWalkFinished(WalkResult &&result) noexcept
fmt::print(stderr, "Cull: delete {} files, {} bytes\n",
result.files.size(), result.total_bytes);

std::size_t n_deleted_files = 0, n_busy = 0;
uint_least64_t n_deleted_bytes = 0, n_errors = 0;

result.files.clear_and_dispose([&](WalkResult::File *file){
#ifndef NDEBUG
result.total_bytes -= file->size;
#endif

switch (CullFile(dev_cachefiles, file->parent->fd, file->name.c_str())) {
case DevCachefiles::CullResult::SUCCESS:
++n_deleted_files;
n_deleted_bytes += file->size;
break;
auto *op = new CullFileOperation(*this, file);
new_operations.push_back(*op);
});

case DevCachefiles::CullResult::BUSY:
++n_busy;
break;
assert(result.total_bytes == 0);

case DevCachefiles::CullResult::ERROR:
++n_errors;
}
if (operations.empty() && new_operations.empty()) {
Finish();
return;
}

delete file;
new_operations.clear_and_dispose([this](auto *op){
operations.push_back(*op);
op->Start();
});
}

fmt::print(stderr, "Cull: deleted {} files, {} bytes; {} in use; {} errors\n", n_deleted_files, n_deleted_bytes, n_busy, n_errors);
void
Cull::OperationFinished(CullFileOperation &op) noexcept
{
assert(!operations.empty());

assert(result.total_bytes == 0);
operations.erase_and_dispose(operations.iterator_to(op), DeleteDisposer{});
if (operations.empty() && new_operations.empty())
Finish();
}

void
Cull::Finish() noexcept
{
fmt::print(stderr, "Cull: deleted {} files, {} bytes; {} in use; {} errors\n", n_deleted_files, n_deleted_bytes, n_busy, n_errors);
callback();
}
15 changes: 14 additions & 1 deletion src/Cull.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
#include "DevCachefiles.hxx"
#include "Walk.hxx"
#include "WHandler.hxx"
#include "Chdir.hxx"
#include "io/UniqueFileDescriptor.hxx"
#include "util/BindMethod.hxx"
#include "util/IntrusiveList.hxx"

#include <cstdint>
#include <string>
Expand All @@ -28,9 +30,17 @@ class Cull final : WalkHandler {

Walk walk;

Chdir chdir;

class CullFileOperation;
IntrusiveList<CullFileOperation> operations, new_operations;

std::size_t n_deleted_files = 0, n_busy = 0;
uint_least64_t n_deleted_bytes = 0, n_errors = 0;

public:
[[nodiscard]]
Cull(Uring::Queue &_uring,
Cull(EventLoop &event_loop, Uring::Queue &_uring,
FileDescriptor _dev_cachefiles,
std::size_t _cull_files, uint_least64_t _cull_bytes,
Callback _callback);
Expand All @@ -39,6 +49,9 @@ public:
void Start(FileDescriptor root_fd);

private:
void OperationFinished(CullFileOperation &op) noexcept;
void Finish() noexcept;

// virtual methods from WalkHandler
void OnWalkAncient(FileDescriptor directory_fd,
std::string_view filename) noexcept override;
Expand Down
Loading

0 comments on commit b7ab4d9

Please sign in to comment.