diff --git a/docs/live-snapshot.md b/docs/live-snapshot.md index 12dbb195..4aa43dac 100644 --- a/docs/live-snapshot.md +++ b/docs/live-snapshot.md @@ -62,3 +62,21 @@ The new config file should include the current upper layer as the last lower lay ``` **Note**: The new upper layer must be different from the old upper layer. + +`lowers` is the old list plus one entry: the (N+1)-th lower is the old upper data file, sealed. Where that layer ends up is decided by its `file`: the current `upper.data` keeps it in place, while any other absolute path moves it there as part of the snapshot. A move requires the target to be absent and on the same file system as the data file; missing parent directories are created. + +The first N entries must match the old lowers exactly. The old upper's `upper.index` is not referenced after the snapshot and can be deleted by the caller. + +## Failure Semantics and Crash Recovery + +Updating the config is the commit point. A failure before it returns -1, removes the transaction files and leaves the layers untouched, so the request can be retried right away; after it, only in-memory operations remain. A request that describes the stack already committed returns success, and the caller's config file is left as is. + +Two transaction files live next to the config: `.snapshot` holds the new config bytes, so its presence means the snapshot is uncommitted; `.snapshot.undo` holds the move target and exists only for a move. If the snapshot is uncommitted, the sealed layer is moved back as `.undo` records and both files are cleaned up; if it is committed, only a stale `.undo` is removed. Recovery covers process crashes only; a host power loss is out of scope. + +A failed seal needs no rollback: it only appends, leaving an unreachable index and trailer at the end of the data file, and since the header is untouched the device still opens after a restart. Sparse writable layers are the exception: their mappings are rebuilt by scanning the data file, so the file is truncated to `virtual_size` when opened. + +## Limitations + +- Snapshots are not supported with a warpfile upper layer (`upper.target`). +- With a hybrid or sparse upper, writes do not take the snapshot's write lock, so the caller must pause writes while a snapshot runs. +- Snapshots on one device are serialized; a running snapshot holds the device's write path. diff --git a/docs/live-snapshot_zh.md b/docs/live-snapshot_zh.md index 8da287a3..f39340d1 100644 --- a/docs/live-snapshot_zh.md +++ b/docs/live-snapshot_zh.md @@ -61,4 +61,20 @@ curl -X POST "http://127.0.0.1:9862/snapshot?dev_id=123&config=/path/to/new_conf } ``` -**注意**:新的 upper 层必须与旧的 upper 层不同。 +lowers 由旧的 lowers 追加一层构成:第 N+1 个 lower 是旧 upper 的 data 文件经 seal 得到,其 `file` 等于当前 `upper.data` 时原地 seal;为其它绝对路径时,seal 后把 data 文件 rename 到该路径,要求目标不存在、与 data 文件同文件系统,父目录缺失时自动创建。 + +前 N 项与旧的 lowers 逐字段一致,upper 必须与旧的 upper 不同。旧 upper 的 `upper.index` 快照后不再被引用,可由调用方清理。 + +## 失败语义与崩溃恢复 + +提交点是更新 config:之前失败返回 -1、清理事务文件,层结构不变,可立即重试;之后只剩内存操作。描述已提交层结构的请求直接返回成功,调用方传入的 config 不会被消费。 + +config 旁有两个事务文件:`.snapshot`(待更新的 config 字节,存在即未提交)与 `.snapshot.undo`(改名目标,仅改名时存在)。未提交时先按 `.undo` 把 sealed 层移回原路径,再清理两个文件;已提交时只清理残留的 `.undo`。恢复只覆盖进程崩溃(kill、异常退出),主机掉电不在保证范围内。 + +seal 失败处理:seal 只追加写,data 尾部仅留下不可达的 index 与 trailer,header 未变,重启后的设备仍可打开,因此不需要回滚。稀疏可写层例外:其映射靠扫描 data 文件重建,因此打开时需要按 `virtual_size` 截断。 + +## 限制 + +- upper 为 warpfile(`upper.target`)时,不支持快照。 +- upper 为 hybrid 或 sparse 时,写路径不持快照使用的写锁,快照期间需调用方暂停写入。 +- 同一设备的快照串行执行;快照期间设备的写路径被占用。 diff --git a/src/image_file.cpp b/src/image_file.cpp index b1fe5c08..4b1f02ec 100644 --- a/src/image_file.cpp +++ b/src/image_file.cpp @@ -16,7 +16,12 @@ #include #include #include +#include +#include +#include #include +#include +#include #include #include #include @@ -27,6 +32,7 @@ #include #include #include +#include #include "overlaybd/lsmt/file.h" #include "overlaybd/lsmt/index.h" #include "overlaybd/zfile/zfile.h" @@ -46,6 +52,266 @@ using namespace photon::fs; #define SET_LOCAL_DIR 118 #define SET_SIZE 119 +static const char *SNAPSHOT_SUFFIX = ".snapshot"; +static const char *SNAPSHOT_UNDO_SUFFIX = ".snapshot.undo"; + +static int drop_snapshot_journals(const std::string &config_path) { + int ret = 0; + for (auto suffix : {SNAPSHOT_SUFFIX, SNAPSHOT_UNDO_SUFFIX}) { + auto path = config_path + suffix; + if (::unlink(path.c_str()) != 0 && errno != ENOENT) { + LOG_ERROR("unlink(`) failed: `", path, strerror(errno)); + ret = -1; + } + } + return ret; +} + +static int write_file(const std::string &path, const std::string &content) { + int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd < 0) + LOG_ERRNO_RETURN(0, -1, "open(`) failed", path); + DEFER(::close(fd)); + auto p = content.data(); + size_t left = content.size(); + while (left > 0) { + auto ret = ::write(fd, p, left); + if (ret <= 0) { + if (ret < 0 && errno == EINTR) + continue; + LOG_ERRNO_RETURN(0, -1, "write(`) failed", path); + } + p += ret; + left -= ret; + } + return 0; +} + +static int read_file_content(const std::string &path, std::string &content) { + int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) + LOG_ERRNO_RETURN(0, -1, "open(`) failed", path); + DEFER(::close(fd)); + char buf[4096]; + while (true) { + auto ret = ::read(fd, buf, sizeof(buf)); + if (ret < 0) { + if (errno == EINTR) + continue; + LOG_ERRNO_RETURN(0, -1, "read(`) failed", path); + } + if (ret == 0) + break; + content.append(buf, ret); + } + return 0; +} + +static std::string absolute_path(const std::string &path) { + if (!path.empty() && path[0] == '/') + return path; + char buf[PATH_MAX]; + if (getcwd(buf, sizeof(buf)) == nullptr) + return path; + return std::string(buf) + "/" + path; +} + +static int mkdir_p(const std::string &path) { + auto lfs = new_localfs_adaptor(); + if (lfs == nullptr) + LOG_ERRNO_RETURN(0, -1, "new localfs adaptor failed"); + DEFER(delete lfs); + // the trailing slash makes photon create the last component as well + return photon::fs::mkdir_recursive(path + "/", lfs); +} + +// the record of a move: where the layer is and where it was moved to +static std::string snapshot_move_record(const std::string &data_path, const std::string &move_to) { + return absolute_path(data_path) + "\n" + move_to + "\n"; +} + +static int parse_snapshot_move_record(const std::string &record, std::string &data_path, + std::string &move_to) { + auto path_end = record.find('\n'); + if (path_end == std::string::npos || path_end == 0) + return -1; + auto move_end = record.find('\n', path_end + 1); + if (move_end == std::string::npos) + return -1; + data_path = record.substr(0, path_end); + move_to = record.substr(path_end + 1, move_end - path_end - 1); + if (data_path[0] != '/' || move_to.empty() || move_to[0] != '/') + return -1; + return 0; +} + +#ifndef RENAME_NOREPLACE +#define RENAME_NOREPLACE (1 << 0) +#endif + +static int rename_noreplace(const std::string &from, const std::string &to) { +#if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 28)) + return ::renameat2(AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE); +#elif defined(SYS_renameat2) + return ::syscall(SYS_renameat2, AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), + RENAME_NOREPLACE); +#else + errno = ENOSYS; + return -1; +#endif +} + +// moves the layer without ever replacing an entry which is already there +static int move_layer(const std::string &from, const std::string &to) { + if (rename_noreplace(from, to) != 0) + LOG_ERRNO_RETURN(0, -1, "rename(`) to ` failed", from, to); + return 0; +} + +// puts the layer back where it was; the original name is gone only after a move +// that succeeded, so its presence means there is nothing to move back +static int move_layer_back(const std::string &data_path, const std::string &move_to) { + if (::access(data_path.c_str(), F_OK) == 0) + return 0; + if (::access(move_to.c_str(), F_OK) != 0) + LOG_ERROR_RETURN(ENOENT, -1, "` is gone", data_path); + return move_layer(move_to, data_path); +} + +static bool layer_config_equal(ImageConfigNS::LayerConfig &a, ImageConfigNS::LayerConfig &b) { + return a.file() == b.file() && a.gzipIndex() == b.gzipIndex() && + a.targetFile() == b.targetFile() && a.dir() == b.dir() && + a.digest() == b.digest() && a.targetDigest() == b.targetDigest() && + a.size() == b.size(); +} + +// a retry after a lost response describes exactly the committed stack +static bool snapshot_request_matches(ImageConfigNS::ImageConfig &committed, + ImageConfigNS::ImageConfig &request) { + auto committed_lowers = committed.lowers(); + auto request_lowers = request.lowers(); + if (committed_lowers.size() != request_lowers.size()) + return false; + for (size_t i = 0; i < committed_lowers.size(); i++) { + if (!layer_config_equal(committed_lowers[i], request_lowers[i])) + return false; + } + auto committed_upper = committed.upper(); + auto request_upper = request.upper(); + return committed_upper.index() == request_upper.index() && + committed_upper.data() == request_upper.data() && + committed_upper.target() == request_upper.target() && + committed_upper.gzipIndex() == request_upper.gzipIndex(); +} + +static bool same_dir_entry(const std::string &a, const std::string &b) { + auto a_parts = photon::fs::Path(absolute_path(a)).dir_base_name(); + auto b_parts = photon::fs::Path(absolute_path(b)).dir_base_name(); + if (a_parts.second != b_parts.second) + return false; + char a_real[PATH_MAX], b_real[PATH_MAX]; + if (::realpath(std::string(a_parts.first).c_str(), a_real) == nullptr || + ::realpath(std::string(b_parts.first).c_str(), b_real) == nullptr) + return false; + return std::string(a_real) == b_real; +} + +// a layer which occupies a transaction file name would be deleted by the recovery, +// so such a config is refused before anything is touched +static int reject_journal_collision(ImageConfigNS::ImageConfig &cfg, + const std::string &config_path) { + const std::string records[] = {config_path + SNAPSHOT_SUFFIX, + config_path + SNAPSHOT_UNDO_SUFFIX}; + auto upper = cfg.upper(); + auto lowers = cfg.lowers(); + std::vector layers = {upper.data(), upper.index()}; + for (size_t i = 0; i < lowers.size(); i++) + layers.push_back(lowers[i].file()); + for (size_t i = 0; i < layers.size(); i++) { + if (layers[i].empty()) + continue; + for (auto &record : records) { + if (same_dir_entry(layers[i], record)) + LOG_ERROR_RETURN(EINVAL, -1, + "the layer ` collides with a snapshot record of `, rename it.", + layers[i], config_path); + } + } + return 0; +} + +// the sealed layer is moved when the request names another path for it, so the +// target must be free and reachable by rename +static int snapshot_move_target(ImageConfigNS::ImageConfig &committed, + ImageConfigNS::LayerConfig &appended, std::string &move_to) { + move_to.clear(); + auto data_path = committed.upper().data(); + if (appended.file() == data_path) + return 0; + + struct stat target, data, parent; + if (::lstat(appended.file().c_str(), &target) == 0) + LOG_ERROR_RETURN(EEXIST, -1, "the snapshot target ` already exists.", appended.file()); + if (errno != ENOENT) + LOG_ERRNO_RETURN(0, -1, "stat(`) failed", appended.file()); + move_to = absolute_path(appended.file()); + // the undo record is line based, so a path holding a newline can not be stored + if (data_path.find('\n') != std::string::npos || move_to.find('\n') != std::string::npos) + LOG_ERROR_RETURN(EINVAL, -1, "` or ` holds a newline, which the move record can not store.", + data_path, move_to); + auto dir = move_to.substr(0, move_to.find_last_of('/')); + if (dir.empty()) + dir = "/"; + if (mkdir_p(dir) != 0) + LOG_ERRNO_RETURN(0, -1, "failed to create the directory of `", move_to); + if (::stat(dir.c_str(), &parent) != 0) + LOG_ERRNO_RETURN(0, -1, "stat(`) failed", dir); + if (!S_ISDIR(parent.st_mode)) + LOG_ERROR_RETURN(ENOTDIR, -1, "` is not a directory.", dir); + if (::lstat(data_path.c_str(), &data) != 0) + LOG_ERRNO_RETURN(0, -1, "lstat(`) failed", data_path); + // a rename moves the link itself, which would leave the new lower dangling + if (S_ISLNK(data.st_mode)) + LOG_ERROR_RETURN(EINVAL, -1, "the upper data ` is a symlink, refusing to move it.", + data_path); + if (parent.st_dev != data.st_dev) + LOG_ERROR_RETURN(EXDEV, -1, "` is not on the same file system as `.", move_to, data_path); + return 0; +} + +static int validate_snapshot_config(ImageConfigNS::ImageConfig &committed, + ImageConfigNS::ImageConfig &request, std::string &move_to) { + auto committed_upper = committed.upper(); + auto request_upper = request.upper(); + if (committed_upper.target() != "" || request_upper.target() != "") + LOG_ERROR_RETURN(ENOTSUP, -1, "snapshot is not supported with a warpfile upper layer"); + if (request_upper.index() == committed_upper.index() || + request_upper.data() == committed_upper.data()) + LOG_ERROR_RETURN(0, -1, + "The new upper layer(`, `) should be different from the old upper layer(`, `).", + request_upper.data(), request_upper.index(), committed_upper.data(), + committed_upper.index()); + + auto committed_lowers = committed.lowers(); + auto request_lowers = request.lowers(); + if (request_lowers.size() != committed_lowers.size() + 1) + LOG_ERROR_RETURN(0, -1, "new lowers count is `, expect ` (the old lowers plus the old upper).", + request_lowers.size(), committed_lowers.size() + 1); + for (size_t i = 0; i < committed_lowers.size(); i++) { + if (!layer_config_equal(committed_lowers[i], request_lowers[i])) + LOG_ERROR_RETURN(0, -1, "the `-th lower of the new config differs from the current one.", + i); + } + auto &appended = request_lowers.back(); + if (appended.gzipIndex() != "" || appended.targetFile() != "" || appended.dir() != "" || + appended.digest() != "" || appended.targetDigest() != "" || appended.size() != 0) + LOG_ERROR_RETURN(0, -1, + "the last lower(`) should only reference the old upper data(`), got dir(`) digest(`) size(`).", + appended.file(), committed_upper.data(), appended.dir(), + appended.digest(), appended.size()); + return snapshot_move_target(committed, appended, move_to); +} + IFile *ImageFile::__open_ro_file(const std::string &path) { int flags = O_RDONLY; @@ -461,6 +727,12 @@ LSMT::IFileRW *ImageFile::open_upper(ImageConfigNS::UpperConfig &upper) { } int ImageFile::init_image_file() { + // a pending snapshot must be rolled back before any layer is opened + if (recover_pending_snapshot() != 0) { + set_failed("failed to recover the pending snapshot"); + return -1; + } + LSMT::IFileRO *lower_file = nullptr; LSMT::IFileRW *upper_file = nullptr; LSMT::IFileRW *stack_ret = nullptr; @@ -579,6 +851,52 @@ void ImageFile::set_failed(const Ts &...xs) { } } +int ImageFile::recover_pending_snapshot() { + if (reject_journal_collision(conf, config_path) != 0) + return -1; + + auto pending_path = config_path + SNAPSHOT_SUFFIX; + auto undo_path = config_path + SNAPSHOT_UNDO_SUFFIX; + if (::access(pending_path.c_str(), F_OK) != 0) { + // the snapshot is either committed or never started, so nothing on disk + // needs to be undone + if (::access(undo_path.c_str(), F_OK) == 0 && drop_snapshot_journals(config_path) != 0) + LOG_ERROR_RETURN(0, -1, "failed to remove the snapshot records, refusing to start."); + return 0; + } + + // the config is not updated: put a moved layer back + if (::access(undo_path.c_str(), F_OK) == 0) { + std::string record, data_path, move_to; + if (read_file_content(undo_path, record) != 0 || + parse_snapshot_move_record(record, data_path, move_to) != 0 || + data_path != absolute_path(conf.upper().data())) + LOG_ERROR_RETURN(EINVAL, -1, "` is invalid, refusing to start.", undo_path); + if (move_layer_back(data_path, move_to) != 0) + LOG_ERROR_RETURN(0, -1, "move ` back to ` failed, refusing to start.", move_to, + data_path); + } + if (drop_snapshot_journals(config_path) != 0) + LOG_ERROR_RETURN(0, -1, "failed to remove the snapshot records, refusing to start."); + LOG_INFO("the uncommitted snapshot of ` is discarded.", config_path); + return 0; +} + +void ImageFile::swap_snapshot_layers(LSMT::IFileRW *upper_file) { + if(m_upper_file) { + // transfer the sealed layer from m_upper_file to m_lower_file before m_upper_file is destructed + auto sealed = ((LSMT::IFileRW *)m_upper_file)->get_file(0); + ((LSMT::IFileRO *)m_lower_file)->insert_file(sealed); + ((LSMT::IFileRW *)m_upper_file)->clear_files(); + safe_delete(m_upper_file); + } + // m_file already writes into upper_file, only the ownership moves here + auto combo_index = (LSMT::IComboIndex *)((LSMT::IFileRW *)m_file)->index(); // m_file->m_index + ((LSMT::IFileRO *)m_lower_file)->index(combo_index->backing_index()); + + m_upper_file = upper_file; +} + int ImageFile::create_snapshot(const char *new_config_path) { // load new config file to get the snapshot layer path // open new upper layer @@ -586,58 +904,92 @@ int ImageFile::create_snapshot(const char *new_config_path) { if(!m_lower_file || !m_upper_file) LOG_ERROR_RETURN(0, -1, "Lower or upper layer is NULL."); + typedef photon::scoped_lock Lock; + Lock lock(m_snapshot_mtx); + + auto pending_path = config_path + SNAPSHOT_SUFFIX; + auto undo_path = config_path + SNAPSHOT_UNDO_SUFFIX; + bool pending = ::access(pending_path.c_str(), F_OK) == 0; + bool undo = ::access(undo_path.c_str(), F_OK) == 0; + // only both records together mean that a move may still be in flight + if (pending && undo) + LOG_ERROR_RETURN(EBUSY, -1, + "a previous snapshot is not committed, restart the device to recover " + "before taking another one."); + if ((pending || undo) && drop_snapshot_journals(config_path) != 0) + LOG_ERROR_RETURN(0, -1, "failed to remove the stale snapshot records of `", config_path); + + std::string new_cfg_bytes; ImageConfigNS::ImageConfig new_cfg; - LSMT::IFileRW *upper_file = nullptr; - LOG_INFO("Load new config `.", new_config_path); - if (!new_cfg.ParseJSON(new_config_path)) { + if (read_file_content(new_config_path, new_cfg_bytes) != 0) + return -1; + if (!new_cfg.ParseJSONStream(new_cfg_bytes)) { LOG_ERROR_RETURN(0, -1, "Error parse new config json: `.", new_config_path); } + if (reject_journal_collision(new_cfg, config_path) != 0) + return -1; - auto upper = new_cfg.upper(); - // auto lowers = new_cfg.lowers(); - // if(lowers[lowers.size()-1].file() != conf.upper().data()) - // LOG_ERROR_RETURN(0, -1, "The last lower layer(`) should be the same as old upper layer(`) after restack.", lowers[lowers.size()-1].file(), conf.upper().data()); - if(upper.index() == conf.upper().index() || upper.data() == conf.upper().data()) - LOG_ERROR_RETURN(0, -1, "The new upper layer(`, `) should be different from the old upper layer(`, `).", upper.data(), upper.index(), conf.upper().data(), conf.upper().index()); + if (snapshot_request_matches(conf, new_cfg)) { + LOG_INFO("snapshot of ` is already committed, nothing to do.", new_config_path); + return 0; + } + std::string move_to; + if (validate_snapshot_config(conf, new_cfg, move_to) != 0) + return -1; - upper_file = open_upper(upper); + auto upper = new_cfg.upper(); + std::unique_ptr upper_guard(open_upper(upper)); + auto upper_file = upper_guard.get(); if (!upper_file) LOG_ERROR_RETURN(0, -1, "Open upper layer failed."); - if(((LSMT::IFileRW *)m_file)->restack(upper_file) != 0) { - delete upper_file; - LOG_ERRNO_RETURN(0, -1, "Restack new rwlayer failed."); - } + if (((LSMT::IFileRO *)m_lower_file)->reserve_files(1) != 0) + LOG_ERROR_RETURN(0, -1, "failed to reserve a slot for the sealed layer."); - if(m_upper_file) { - // transfer the sealed layer from m_upper_file to m_lower_file before m_upper_file is destructed - auto sealed = ((LSMT::IFileRW *)m_upper_file)->get_file(0); - ((LSMT::IFileRO *)m_lower_file)->insert_file(sealed); - ((LSMT::IFileRW *)m_upper_file)->clear_files(); - safe_delete(m_upper_file); + auto data_path = absolute_path(conf.upper().data()); + + // the move record comes first, so a pending `.snapshot` always has a complete one + if (!move_to.empty() && write_file(undo_path, snapshot_move_record(data_path, move_to)) != 0) { + drop_snapshot_journals(config_path); + return -1; + } + if (write_file(pending_path, new_cfg_bytes) != 0) { + drop_snapshot_journals(config_path); + return -1; } - // set m_lower_file->m_index = m_file->m_index->m_backing_index because m_file is not responsible for the destruction of m_backing_index - auto combo_index = (LSMT::IComboIndex *)((LSMT::IFileRW *)m_file)->index(); // m_file->m_index - ((LSMT::IFileRO *)m_lower_file)->index(combo_index->backing_index()); - // set m_file->m_index->m_index0 = upper_file->m_index - auto upper_file_index = (LSMT::IMemoryIndex0 *)((LSMT::IFileRW *)upper_file)->index(); // upper_file->m_index - combo_index->front_index(upper_file_index); - m_upper_file = upper_file; + auto commit = [&]() -> int { + if (!move_to.empty() && move_layer(data_path, move_to) != 0) + return -1; + if (::rename(pending_path.c_str(), config_path.c_str()) != 0) { + int rename_errno = errno; + if (!move_to.empty() && move_layer_back(data_path, move_to) != 0) + LOG_ERROR_RETURN(0, -1, "the layer stays at `", move_to); + errno = rename_errno; + LOG_ERRNO_RETURN(0, -1, "rename(`) to ` failed", pending_path, config_path); + } + return 0; + }; - // overwrite the config file in use in case the old files are used again after the process restarts - auto lfs = photon::fs::new_localfs_adaptor(); - if (lfs == nullptr) { - LOG_ERRNO_RETURN(0, -1, "new localfs_adaptor failed"); - } - DEFER(delete lfs); - int ret = lfs->rename(new_config_path, this->config_path.c_str()); - if (ret != 0) { - LOG_ERRNO_RETURN(0, -1, "rename(`,`) failed", new_config_path, this->config_path); + if (((LSMT::IFileRW *)m_file)->restack(upper_file, commit) != 0) { + int saved_errno = errno; + // the records are kept only when the layer can not be put back, so that + // the next start can finish it + if (!move_to.empty() && move_layer_back(data_path, move_to) != 0) + LOG_ERROR_RETURN(0, -1, "move ` back to ` failed, keep the records.", move_to, + data_path); + if (drop_snapshot_journals(config_path) != 0) + LOG_WARN("failed to remove the snapshot records of `", config_path); + errno = saved_errno; + LOG_ERRNO_RETURN(0, -1, "Restack new rwlayer failed."); } - LOG_INFO("rename(`,`) success", new_config_path, this->config_path); + swap_snapshot_layers(upper_file); + upper_guard.release(); + conf.CopyFrom(new_cfg, conf.GetAllocator()); + ::unlink(undo_path.c_str()); + LOG_INFO("snapshot committed, config ` is updated.", config_path); return 0; } diff --git a/src/image_file.h b/src/image_file.h index 06ff49d3..719211bb 100644 --- a/src/image_file.h +++ b/src/image_file.h @@ -160,6 +160,11 @@ class ImageFile : public photon::fs::ForwardFile { LSMT::IFileRO *open_lowers(std::vector &, bool &); LSMT::IFileRW *open_upper(ImageConfigNS::UpperConfig &); + // discards an uncommitted snapshot, must run before any layer is opened + int recover_pending_snapshot(); + // moves pointers only + void swap_snapshot_layers(LSMT::IFileRW *upper_file); + IFile *open_localfile(ImageConfigNS::LayerConfig &layer, std::string &opened); IFile *__open_ro_file(const std::string &); IFile *__open_ro_target_file(const std::string &); @@ -168,4 +173,7 @@ class ImageFile : public photon::fs::ForwardFile { // size_t seek_data(off_t begin, off_t end); void start_bk_dl_thread(); + + // serializes the snapshots of the same device + photon::mutex m_snapshot_mtx; }; diff --git a/src/overlaybd/lsmt/file.cpp b/src/overlaybd/lsmt/file.cpp index 78d65317..2e31d835 100644 --- a/src/overlaybd/lsmt/file.cpp +++ b/src/overlaybd/lsmt/file.cpp @@ -595,6 +595,11 @@ class LSMTReadOnlyFile : public IFileRW { return m_files[file_idx]; } + virtual int reserve_files(size_t n) override { + m_files.reserve(m_files.size() + n); + return 0; + } + virtual int insert_file(IFile * file) override { m_files.insert(m_files.begin(), file); return 0; @@ -1325,58 +1330,93 @@ class LSMTFile : public LSMTReadOnlyFile { return compact(opts, _no_use_var); } - int reserve_top_layer(LSMTFile *top_layer) - { - // restack also reshuffles m_files, so no reader may be in flight - photon::scoped_rwlock ilock(m_index_rwlock, photon::WLOCK); - std::vector pmappings; // temp index for reserved layer + // everything the swap needs is prepared up front, so that the swap itself can + // not fail + struct RestackPlan { + LSMTReadOnlyFile *gc_layer = nullptr; // view of the sealed layer + IFile *sealed = nullptr; // the sealed data file, not owned + IMemoryIndex *new_backing = nullptr; // new backing index of the combo index + }; + + // the caller holds m_rw_mtx and the WLOCK of m_index_rwlock + int prepare_restack(LSMTFile *upper, RestackPlan &plan) { /* ==== close_seal the top RW layer and reopen it. ==== */ - IFileRO* gc_layer = nullptr; auto fseal = (LSMTFile*)open_file_rw(m_files[m_rw_tag], m_log->file(), false); if (fseal==nullptr){ return -1; } + DEFER(safe_delete(fseal)); + IFileRO *gc_layer = nullptr; if (fseal->close_seal(&gc_layer)!=0){ LOG_ERROR_RETURN(0, -1, "close seal top RW layer failed."); } if (gc_layer == nullptr){ LOG_ERROR_RETURN(0, -1, "reopen sealed RW layer failed."); } - /* ==== reserve m_files[0] for new layer. ==== */ - auto u = top_layer; + plan.gc_layer = (LSMTReadOnlyFile *)gc_layer; + plan.sealed = plan.gc_layer->get_file(0); + + plan.new_backing = ((IComboIndex *)m_index)->prepare_commit(1); + if (plan.new_backing == nullptr) + LOG_ERROR_RETURN(0, -1, "prepare backing index failed."); + + /* ==== reserve m_files[0] for the sealed layer. ==== */ + LOG_INFO("m_files.insert new layer: file ptr: 0x`", upper->m_files[0]); + m_files.reserve(m_files.size() + 1); + return 0; + } + + static void discard_restack(RestackPlan &plan) { + safe_delete(plan.gc_layer); + plan.gc_layer = nullptr; + plan.sealed = nullptr; + safe_delete(plan.new_backing); + plan.new_backing = nullptr; + } - LOG_INFO("m_files.insert new layer: file ptr: 0x`", u->m_files[0]); - // m_files[m_rw_tag] = ((LSMTReadOnlyFile*)gc_layer)->m_files[0]; - m_files.insert(m_files.begin(), ((LSMTReadOnlyFile*)gc_layer)->m_files[0]); + // moves pointers only + void commit_restack(LSMTFile *upper, RestackPlan &plan) { + m_files.insert(m_files.begin(), plan.sealed); m_rw_tag++; - m_files[m_rw_tag] = u->m_files[0]; // fnew_layer; + m_files[m_rw_tag] = upper->m_files[0]; // fnew_layer; LOG_DEBUG("m_files.size(): `, rw_tag: `", m_files.size(), m_rw_tag); - m_rw_type = u->m_rw_type; - m_vsize = u->m_vsize; - m_data_offset = u->m_data_offset.load(); - m_data_file_direct_io = u->m_data_file_direct_io; - ((IComboIndex *)m_index)->commit_index0(); - - safe_delete(fseal); - safe_delete(gc_layer); + m_rw_type = upper->m_rw_type; + m_vsize = upper->m_vsize; + m_data_offset = upper->m_data_offset.load(); + m_data_file_direct_io = upper->m_data_file_direct_io; + ((IComboIndex *)m_index)->apply_commit(plan.new_backing); + plan.new_backing = nullptr; + // the new upper takes over writes here, under the same locks as the seal + ((IComboIndex *)m_index)->front_index((IMemoryIndex0 *)upper->m_index); + safe_delete(plan.gc_layer); + plan.gc_layer = nullptr; safe_delete(m_log); - m_log = u->m_log; - u->m_log = nullptr; - return 0; + m_log = upper->m_log; + upper->m_log = nullptr; } - int restack(IFileRW* upper_layer) override { + return restack(upper_layer, nullptr); + } + + int restack(IFileRW *upper_layer, const std::function &commit) override { + Lock _(m_rw_mtx); if (flush_index_log() != 0) LOG_ERRNO_RETURN(0, -1, "flush index log before restack failed"); - Lock rwlock(m_rw_mtx); + photon::scoped_rwlock ilock(m_index_rwlock, photon::WLOCK); LOG_INFO("restack new rwlayer, seal old."); - int ret = reserve_top_layer((LSMTFile*)upper_layer); - if (ret != 0) { - LOG_ERRNO_RETURN(0, -1, "restack new rwlayer failed."); + RestackPlan plan; + // a failed seal only appends unreachable bytes to the data file, which + // neither the running device nor a reopen of the layer looks at + if (prepare_restack((LSMTFile *)upper_layer, plan) != 0 || + (commit && commit() != 0)) { + discard_restack(plan); + LOG_ERROR("restack new rwlayer aborted."); + return -1; } + commit_restack((LSMTFile *)upper_layer, plan); LOG_INFO("current ro layers count: `", m_files.size() - 1); - return ret; + return 0; } }; @@ -1806,6 +1846,15 @@ IFileRW *open_file_rw(IFile *fdata, IFile *findex, bool ownership) { pht = &ht; } else { LOG_DEBUG("create index from sparse file."); + // the size of a sparse layer is fixed by its vsize, anything beyond it is + // left by a seal that was never committed + auto expected = pht->virtual_size + HeaderTrailer::SPACE; + if ((uint64_t)stat.st_size > expected) { + LOG_INFO("truncate sparse layer from ` to ` bytes", stat.st_size, expected); + if (fdata->ftruncate(expected) != 0) + LOG_ERRNO_RETURN(0, nullptr, "failed to truncate the sparse layer file."); + stat.st_size = expected; + } vector mappings; if (LSMTSparseFile::create_mappings(fdata, mappings) == -1) { LOG_ERROR_RETURN(0, nullptr, "failed to create segment mappings from sparse file!"); diff --git a/src/overlaybd/lsmt/file.h b/src/overlaybd/lsmt/file.h index fee4e57c..dc7da0ad 100644 --- a/src/overlaybd/lsmt/file.h +++ b/src/overlaybd/lsmt/file.h @@ -25,6 +25,8 @@ IMemoryIndex -> IMemoryIndex0 -> IComboIndex -> Index0 ( set ) -> Co #pragma once #include #include +#include +#include #include #include #include @@ -55,6 +57,11 @@ class IFileRO : public photon::fs::VirtualReadOnlyFile { virtual IFile *get_file(size_t file_idx) const = 0; + // make room for `n` more files, so that insert_file() allocates nothing later + virtual int reserve_files(size_t n) { + return 0; + } + virtual int insert_file(IFile * file) = 0; virtual int clear_files() = 0; @@ -115,6 +122,12 @@ class IFileRW : public IFileRO { // close_seal current RW layer (change it to RO layer) and re-stack with upper layer virtual int restack(IFileRW *upper) = 0; + + // restack() with `commit` called under the write lock between the seal and the swap + virtual int restack(IFileRW *upper, const std::function &commit) { + errno = ENOSYS; + return -1; + } }; enum class RWType : uint8_t { diff --git a/src/overlaybd/lsmt/index.cpp b/src/overlaybd/lsmt/index.cpp index 7b5e4c11..29e83c41 100644 --- a/src/overlaybd/lsmt/index.cpp +++ b/src/overlaybd/lsmt/index.cpp @@ -942,27 +942,54 @@ class ComboIndex : public Index0 { return new Index(std::move(mappings)); } - virtual int commit_index0() override { - // Merge index0 (mapping) and backing_index - m_backing_index->increase_tag(1); - auto merged_index = create_memory_index0(m_backing_index->buffer(), m_backing_index->size(), 0, UINT64_MAX); - vector dumped; - dumped.assign(mapping.begin(), mapping.end()); - auto idx_size = compress_raw_index(&dumped[0], dumped.size()); - // for (auto it : mapping) { - for (size_t i = 0; i < idx_size; i++) { - auto p = dumped[i]; - p.tag = 0; - merged_index->insert(p); + virtual IMemoryIndex *prepare_commit(int tag_shift) const override { + if (m_backing_index == nullptr) { + LOG_ERROR_RETURN(EINVAL, nullptr, "combo index without backing index can NOT be committed"); + } + vector shifted; + shifted.reserve(m_backing_index->size() + mapping.size()); + for (auto m : ptr_array((SegmentMapping *)m_backing_index->buffer(), + m_backing_index->size())) { + m.tag += tag_shift; + shifted.push_back(m); + } + unique_ptr merged( + create_memory_index0(shifted.data(), shifted.size(), 0, UINT64_MAX)); + if (!merged) { + LOG_ERROR_RETURN(0, nullptr, "create merged index failed."); } + if (!mapping.empty()) { // the writable mappings belong to the layer being sealed + vector dumped(mapping.begin(), mapping.end()); + auto idx_size = compress_raw_index(dumped.data(), dumped.size()); + for (size_t i = 0; i < idx_size; i++) { + auto p = dumped[i]; + p.tag = 0; + merged->insert(p); + } + } + auto rst = merged->make_read_only_index(); + if (!rst) { + LOG_ERROR_RETURN(0, nullptr, "make read only index failed."); + } + return rst; + } + + virtual void apply_commit(IMemoryIndex *prepared) override { + if (prepared == nullptr) + return; if(m_ownership) { // !!! safe_delete(m_backing_index); } - m_backing_index = (Index*)(merged_index->make_read_only_index()); // set ownership=false - safe_delete(merged_index); - LOG_INFO("rebuild backing index done. {count: `}", m_backing_index->size()); - // Clear original index0 + m_backing_index = (Index *)prepared; mapping.clear(); + LOG_INFO("rebuild backing index done. {count: `}", m_backing_index->size()); + } + + virtual int commit_index0() override { + auto prepared = prepare_commit(1); + if (prepared == nullptr) + LOG_ERROR_RETURN(0, -1, "prepare backing index failed."); + apply_commit(prepared); return 0; } diff --git a/src/overlaybd/lsmt/index.h b/src/overlaybd/lsmt/index.h index f1ac0ae9..f33fd717 100644 --- a/src/overlaybd/lsmt/index.h +++ b/src/overlaybd/lsmt/index.h @@ -26,6 +26,7 @@ IMemoryIndex -> IMemoryIndex0 -> IComboIndex -> Index0 ( set ) -> Co #include #include #include +#include #include namespace LSMT { @@ -164,6 +165,18 @@ class IComboIndex : public IMemoryIndex0 { // virtual IMemoryIndex0* gc_index() = 0; virtual IMemoryIndex *load_range_index(int, int) const = 0; + // build the backing index that commit_index0() would produce, shifting tags by + // `tag_shift`, without modifying this index; the caller owns the result + virtual IMemoryIndex *prepare_commit(int tag_shift) const { + errno = ENOSYS; + return nullptr; + } + + // install the index of prepare_commit(), drop the writable mappings + virtual void apply_commit(IMemoryIndex *prepared) { + delete prepared; + } + // commit index0 (upper index) to backing index (lower index) // rebuild backing index before async-compact. // merge the image's backing_index and gc_idx( dump from original index0 ) for pread() diff --git a/src/test/image_service_test.cpp b/src/test/image_service_test.cpp index b7dcb3ee..4eb67ec7 100644 --- a/src/test/image_service_test.cpp +++ b/src/test/image_service_test.cpp @@ -372,7 +372,7 @@ class CreateSnapshotTest : public DevIDRegisterTest { srand(154574045); } - void create_file_rw(char *data_name, char *index_name, bool sparse = false) { + void create_file_rw(const char *data_name, const char *index_name, bool sparse = false) { auto fdata = photon::fs::open_localfile_adaptor(data_name, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); auto findex = photon::fs::open_localfile_adaptor(index_name, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); LSMT::LayerInfo args(fdata, findex); @@ -381,6 +381,30 @@ class CreateSnapshotTest : public DevIDRegisterTest { auto file = LSMT::create_file_rw(args, true); delete file; } + + static off_t file_size(const std::string &path) { + struct stat st; + return ::stat(path.c_str(), &st) == 0 ? st.st_size : -1; + } + + static void write_file(const std::string &path, const std::string &content) { + int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + ASSERT_GE(fd, 0) << path; + EXPECT_EQ(::write(fd, content.data(), content.size()), (ssize_t)content.size()); + ::close(fd); + } + + static void append_bytes(const std::string &path, size_t count) { + int fd = ::open(path.c_str(), O_WRONLY | O_APPEND); + ASSERT_GE(fd, 0) << path; + std::string junk(count, 'x'); + EXPECT_EQ(::write(fd, junk.data(), count), (ssize_t)count); + ::close(fd); + } + + std::string move_record(const std::string &data_path, const std::string &move_to) { + return data_path + "\n" + move_to + "\n"; + } }; TEST_F(CreateSnapshotTest, create_snapshot) { @@ -793,6 +817,455 @@ TEST(UpperIOEngine, SparseIouringDirect) { check_upper_engine(ImageConfigNS::IOENGINE_IOURING_DIRECT, true); } +TEST_F(CreateSnapshotTest, create_snapshot_idempotent) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + + auto len = 1 << 20; + ALIGNED_MEM4K(buf, len); + for (auto i = 0; i < len; i++) + buf[i] = rand() % 256; + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf, len, 0), len); + + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), 0); + auto sealed_size = file_size(data0); + EXPECT_GT(sealed_size, (off_t)len); + + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), 0); + EXPECT_EQ(file_size(data0), sealed_size); + + delete imgfile; +} + +TEST_F(CreateSnapshotTest, create_snapshot_invalid_lowers) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + + auto len = 1 << 20; + ALIGNED_MEM4K(buf, len); + for (auto i = 0; i < len; i++) + buf[i] = rand() % 256; + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf, len, 0), len); + auto size0 = file_size(data0); + + write_file(new_image_config_path, R"delimiter({ + "lowers" : [ { "file" : "/opt/overlaybd/baselayers/ext4_64" } ], + "upper": { "index": "/tmp/overlaybd/index1.lsmt", "data": "/tmp/overlaybd/data1.lsmt" } +})delimiter"); + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), -1); + + write_file(new_image_config_path, R"delimiter({ + "lowers" : [ + { "file" : "/opt/overlaybd/baselayers/ext4_64" }, + { "file" : "/tmp/overlaybd/data1.lsmt" } + ], + "upper": { "index": "/tmp/overlaybd/index1.lsmt", "data": "/tmp/overlaybd/data1.lsmt" } +})delimiter"); + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), -1); + + write_file(new_image_config_path, R"delimiter({ + "lowers" : [ + { "file" : "/opt/overlaybd/baselayers/ext4_64", "digest": "sha256:0" }, + { "file" : "/tmp/overlaybd/data0.lsmt" } + ], + "upper": { "index": "/tmp/overlaybd/index1.lsmt", "data": "/tmp/overlaybd/data1.lsmt" } +})delimiter"); + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), -1); + + EXPECT_EQ(file_size(data0), size0); + ALIGNED_MEM4K(buf0, len); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf0, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf0[i], buf[i]); + + delete imgfile; +} + +TEST_F(CreateSnapshotTest, create_snapshot_commit_failure) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + + auto len = 1 << 20; + ALIGNED_MEM4K(buf, len); + for (auto i = 0; i < len; i++) + buf[i] = rand() % 256; + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf, len, 0), len); + auto size0 = file_size(data0); + + // updating the config fails when its path is a directory + ASSERT_EQ(::unlink(image_config_path.c_str()), 0); + ASSERT_EQ(::mkdir(image_config_path.c_str(), 0755), 0); + + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), -1); + // the seal is not rolled back, its tail is unreachable for reads and writes + EXPECT_GT(file_size(data0), size0); + EXPECT_NE(::access((image_config_path + ".snapshot").c_str(), F_OK), 0); + EXPECT_NE(::access((image_config_path + ".snapshot.undo").c_str(), F_OK), 0); + + ALIGNED_MEM4K(buf0, len); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf0, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf0[i], buf[i]); + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf0, 4096, len), 4096); + + // the same request succeeds once the config path is writable again + ASSERT_EQ(::rmdir(image_config_path.c_str()), 0); + write_file(image_config_path, image_config_content); + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), 0); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf0, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf0[i], buf[i]); + + delete imgfile; +} + +TEST_F(CreateSnapshotTest, create_snapshot_twice) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + create_file_rw("/tmp/overlaybd/data2.lsmt", "/tmp/overlaybd/index2.lsmt"); + + auto len = 1 << 20; + auto offset = 4 << 20; + ALIGNED_MEM4K(buf0, len); + ALIGNED_MEM4K(buf1, len); + for (auto i = 0; i < len; i++) { + buf0[i] = rand() % 256; + buf1[i] = rand() % 256; + } + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf0, len, 0), len); + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), 0); + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf1, len, offset), len); + + auto config2 = test_dir + "/image_config2.json"; + write_file(config2, R"delimiter({ + "lowers" : [ + { "file" : "/opt/overlaybd/baselayers/ext4_64" }, + { "file" : "/tmp/overlaybd/data0.lsmt" }, + { "file" : "/tmp/overlaybd/data1.lsmt" } + ], + "upper": { "index": "/tmp/overlaybd/index2.lsmt", "data": "/tmp/overlaybd/data2.lsmt" } +})delimiter"); + EXPECT_EQ(imgfile->create_snapshot(config2.c_str()), 0); + + ALIGNED_MEM4K(buf, len); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf[i], buf0[i]); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf, len, offset), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf[i], buf1[i]); + delete imgfile; + + imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf[i], buf0[i]); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf, len, offset), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf[i], buf1[i]); + delete imgfile; +} + +TEST_F(CreateSnapshotTest, recover_uncommitted_snapshot) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + + auto len = 1 << 20; + ALIGNED_MEM4K(buf, len); + for (auto i = 0; i < len; i++) + buf[i] = rand() % 256; + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf, len, 0), len); + auto orig_size = file_size(data0); + delete imgfile; + + // a seal appended to the data file, then the process died + append_bytes(data0, 4096); + write_file(image_config_path + ".snapshot", "{}"); + + imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + // the tail stays in place, a reopened layer never looks at it + EXPECT_EQ(file_size(data0), orig_size + 4096); + EXPECT_NE(::access((image_config_path + ".snapshot").c_str(), F_OK), 0); + + ALIGNED_MEM4K(buf0, len); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf0, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf0[i], buf[i]); + delete imgfile; +} + +TEST_F(CreateSnapshotTest, recover_committed_snapshot) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + auto orig_size = file_size(data0); + delete imgfile; + + // the config was already updated, only the cleanup of the move record was lost + append_bytes(data0, 4096); + write_file(image_config_path + ".snapshot.undo", move_record(data0, data0 + ".moved")); + + imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + EXPECT_EQ(file_size(data0), orig_size + 4096); + EXPECT_NE(::access((image_config_path + ".snapshot.undo").c_str(), F_OK), 0); + delete imgfile; +} + +TEST_F(CreateSnapshotTest, create_snapshot_move) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + const std::string moved = "/tmp/overlaybd/archive/2026/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + ::unlink(moved.c_str()); + + write_file(new_image_config_path, R"delimiter({ + "lowers" : [ + { "file" : "/opt/overlaybd/baselayers/ext4_64" }, + { "file" : "/tmp/overlaybd/archive/2026/data0.lsmt" } + ], + "upper": { "index": "/tmp/overlaybd/index1.lsmt", "data": "/tmp/overlaybd/data1.lsmt" } +})delimiter"); + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + + auto len = 1 << 20; + ALIGNED_MEM4K(buf, len); + for (auto i = 0; i < len; i++) + buf[i] = rand() % 256; + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf, len, 0), len); + + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), 0); + EXPECT_NE(::access(data0.c_str(), F_OK), 0); + EXPECT_EQ(::access(moved.c_str(), F_OK), 0); + + // the same request describes the committed stack, so it is a no-op + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), 0); + delete imgfile; + + imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + ALIGNED_MEM4K(buf0, len); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf0, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf0[i], buf[i]); + delete imgfile; +} + +TEST_F(CreateSnapshotTest, create_snapshot_move_target_exists) { + create_file_rw("/tmp/overlaybd/data0.lsmt", "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + create_file_rw("/tmp/overlaybd/data2.lsmt", "/tmp/overlaybd/index2.lsmt"); + + // the last lower names a file which is not the old upper, so it is a move + // onto an occupied path + write_file(new_image_config_path, R"delimiter({ + "lowers" : [ + { "file" : "/opt/overlaybd/baselayers/ext4_64" }, + { "file" : "/tmp/overlaybd/data2.lsmt" } + ], + "upper": { "index": "/tmp/overlaybd/index1.lsmt", "data": "/tmp/overlaybd/data1.lsmt" } +})delimiter"); + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + + auto len = 1 << 20; + ALIGNED_MEM4K(buf, len); + for (auto i = 0; i < len; i++) + buf[i] = rand() % 256; + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf, len, 0), len); + + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), -1); + EXPECT_NE(::access((image_config_path + ".snapshot").c_str(), F_OK), 0); + EXPECT_NE(::access((image_config_path + ".snapshot.undo").c_str(), F_OK), 0); + + ALIGNED_MEM4K(buf0, len); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf0, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf0[i], buf[i]); + delete imgfile; +} + +TEST_F(CreateSnapshotTest, create_snapshot_rejects_symlink_upper) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + const std::string link = "/tmp/overlaybd/data0_link.lsmt"; + const std::string moved = "/tmp/overlaybd/archive/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + ::unlink(link.c_str()); + ::unlink(moved.c_str()); + ASSERT_EQ(::symlink("data0.lsmt", link.c_str()), 0); + + // the device runs on a symlinked upper data file + write_file(image_config_path, R"delimiter({ + "lowers" : [ { "file" : "/opt/overlaybd/baselayers/ext4_64" } ], + "upper": { "index": "/tmp/overlaybd/index0.lsmt", "data": "/tmp/overlaybd/data0_link.lsmt" } +})delimiter"); + write_file(new_image_config_path, R"delimiter({ + "lowers" : [ + { "file" : "/opt/overlaybd/baselayers/ext4_64" }, + { "file" : "/tmp/overlaybd/archive/data0.lsmt" } + ], + "upper": { "index": "/tmp/overlaybd/index1.lsmt", "data": "/tmp/overlaybd/data1.lsmt" } +})delimiter"); + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + ASSERT_NE(imgfile, nullptr); + + // a move would relocate the link itself and leave the new lower dangling + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), -1); + EXPECT_EQ(::access(link.c_str(), F_OK), 0); + EXPECT_EQ(::access(data0.c_str(), F_OK), 0); + EXPECT_NE(::access(moved.c_str(), F_OK), 0); + EXPECT_NE(::access((image_config_path + ".snapshot").c_str(), F_OK), 0); + delete imgfile; +} + +TEST_F(CreateSnapshotTest, create_snapshot_rejects_newline_target) { + create_file_rw("/tmp/overlaybd/data0.lsmt", "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + + // the last lower names a path holding a newline, which the undo record cannot store + write_file(new_image_config_path, R"delimiter({ + "lowers" : [ + { "file" : "/opt/overlaybd/baselayers/ext4_64" }, + { "file" : "/tmp/overlaybd/archive/data\n0.lsmt" } + ], + "upper": { "index": "/tmp/overlaybd/index1.lsmt", "data": "/tmp/overlaybd/data1.lsmt" } +})delimiter"); + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + ASSERT_NE(imgfile, nullptr); + + EXPECT_EQ(imgfile->create_snapshot(new_image_config_path.c_str()), -1); + EXPECT_NE(::access((image_config_path + ".snapshot").c_str(), F_OK), 0); + EXPECT_NE(::access((image_config_path + ".snapshot.undo").c_str(), F_OK), 0); + delete imgfile; +} + +TEST_F(CreateSnapshotTest, reject_layer_named_as_journal) { + create_file_rw("/tmp/overlaybd/data0.lsmt", "/tmp/overlaybd/index0.lsmt"); + // the upper data is named like a snapshot record, which the recovery would delete + const std::string clash = image_config_path + ".snapshot"; + write_file(clash, "layer"); + + std::string cfg = R"delimiter({ + "lowers" : [ { "file" : "/opt/overlaybd/baselayers/ext4_64" } ], + "upper": { "index": "/tmp/overlaybd/index0.lsmt", "data" : ")"delimiter"; + cfg += clash + R"delimiter(" } +})delimiter"; + write_file(image_config_path, cfg); + + EXPECT_EQ(imgservice->create_image_file(image_config_path.c_str(), ""), nullptr); + // the refused config must leave the file carrying that name alone + EXPECT_EQ(::access(clash.c_str(), F_OK), 0); + + // an aliased spelling of the same directory entry is recognised as well + std::string alias_cfg = R"delimiter({ + "lowers" : [ { "file" : "/opt/overlaybd/baselayers/ext4_64" } ], + "upper": { "index": "/tmp/overlaybd/index0.lsmt", "data" : ")"delimiter"; + alias_cfg += test_dir + "/./image_config.json.snapshot" + R"delimiter(" } +})delimiter"; + write_file(image_config_path, alias_cfg); + EXPECT_EQ(imgservice->create_image_file(image_config_path.c_str(), ""), nullptr); + EXPECT_EQ(::access(clash.c_str(), F_OK), 0); + + ::unlink(clash.c_str()); +} + +TEST_F(CreateSnapshotTest, recover_uncommitted_move) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + const std::string moved = data0 + ".moved"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt"); + + auto len = 1 << 20; + ALIGNED_MEM4K(buf, len); + for (auto i = 0; i < len; i++) + buf[i] = rand() % 256; + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf, len, 0), len); + delete imgfile; + + // the layer was moved and sealed, then the process died before updating the config + ::unlink(moved.c_str()); + ASSERT_EQ(::rename(data0.c_str(), moved.c_str()), 0); + append_bytes(moved, 4096); + write_file(image_config_path + ".snapshot", "{}"); + write_file(image_config_path + ".snapshot.undo", move_record(data0, moved)); + + imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + EXPECT_EQ(::access(data0.c_str(), F_OK), 0); + EXPECT_NE(::access(moved.c_str(), F_OK), 0); + EXPECT_NE(::access((image_config_path + ".snapshot").c_str(), F_OK), 0); + EXPECT_NE(::access((image_config_path + ".snapshot.undo").c_str(), F_OK), 0); + + ALIGNED_MEM4K(buf0, len); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf0, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf0[i], buf[i]); + delete imgfile; +} + +TEST_F(CreateSnapshotTest, recover_uncommitted_seal_of_sparse_upper) { + const std::string data0 = "/tmp/overlaybd/data0.lsmt"; + create_file_rw(data0.c_str(), "/tmp/overlaybd/index0.lsmt", true); + + auto len = 1 << 20; + ALIGNED_MEM4K(buf, len); + for (auto i = 0; i < len; i++) + buf[i] = rand() % 256; + + ImageFile *imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + EXPECT_EQ(PWRITEV_SINGLE(imgfile, buf, len, 0), len); + auto sparse_size = file_size(data0); + delete imgfile; + + // a seal which was never committed appended an index and a trailer + append_bytes(data0, 4096); + write_file(image_config_path + ".snapshot", "{}"); + ASSERT_GT(file_size(data0), sparse_size); + + imgfile = imgservice->create_image_file(image_config_path.c_str(), ""); + EXPECT_NE(imgfile, nullptr); + // a sparse layer is cut back to its vsize when it is opened + EXPECT_EQ(file_size(data0), sparse_size); + + ALIGNED_MEM4K(buf0, len); + EXPECT_EQ(PREADV_SINGLE(imgfile, buf0, len, 0), len); + for (auto i = 0; i < len; i++) + EXPECT_EQ(buf0[i], buf[i]); + delete imgfile; +} + int main(int argc, char** argv) { photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_DEFAULT); DEFER(photon::fini(););