/* Copyright 2024 Andrew Noyes Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #pragma once #include #include #ifdef __cplusplus namespace weaselab { /** A data structure to facilitate implementing multi-version concurrency * control reads on bitwise-lexicographically-ordered keys. Indexes mutations by * key and version, and provides an iterator api which can be used to merge * versioned results with an underlying unversioned data structure. @warning you * must not apply mutations to your data structure through a version that * overtakes a concurrent versioned reader. * Thread safety: * - It's safe to operate on two different VersionedMaps in two different * threads concurrently * - It's safe to have multiple threads operating on the same VersionedMap * concurrently if and only if all threads only call const methods. */ struct VersionedMap { /** Indicates how `Mutation::param1` and `Mutation::param2` are to be * interpreted. */ enum MutationType { /** `param1` is the key, `param2` is the value we set the key to. */ Set, /** `param1` is the beginning of the range to clear, and `param2` is the end of the range - i.e. the range to clear is [param1, param2). If `param2Length` == 0 then this clears the single key `param1`. */ Clear, }; /** bytes ordered bitwise-lexicographically. */ struct Key { const uint8_t *p; int len; }; /** Mutations are bitwise-lexicographically ordered by param1. */ struct Mutation { const uint8_t *param1; const uint8_t *param2; int param1Length; int param2Length; MutationType type; }; /** Mutations must be sorted and non-overlapping. `version` * must be strictly increasing. Postcondition: `getVersion()` == `version` */ void addMutations(const Mutation *mutations, int numMutations, int64_t version); /** Reclaim mutations older than `version`. Must be <= `getVersion()`. * Postcondition: `getOldestVersion()` == `version`. @warning performs work * proportional to the mutation rate. Call frequently to favor * memory usage, and infrequently to favor speed. @warning Invalidates any * iterator from a version less than `version`. There shouldn't be any anyway * because you should have already applied all mutations through `version` to * your unversioned data structure. */ void setOldestVersion(int64_t version); /** The version of the most recent call to `addMutations`. */ int64_t getVersion() const; /** The version of the most recent call to `setOldestVersion`. */ int64_t getOldestVersion() const; /** Fixed to a specific version. It's thread-safe to operate on an iterator * concurrently with any method of `VersionedMap`, as long as it's not * invalidated by `setOldestVersion`. @warning must not outlive its * `VersionedMap`. */ struct Iterator { Iterator() = default; ~Iterator(); Iterator(const Iterator &); Iterator &operator=(const Iterator &); Iterator(Iterator &&) noexcept; Iterator &operator=(Iterator &&) noexcept; /** iter must not be `end()`. Memory pointed-to by return value is valid as * long as the iterator is valid. */ Mutation operator*() const; /** iter must not be `end()` */ Iterator &operator++(); /** iter must not be `end()` */ Iterator operator++(int); /** iter must not be `begin()` */ Iterator &operator--(); /** iter must not be `begin()` */ Iterator operator--(int); using difference_type = ptrdiff_t; using value_type = Mutation; bool operator==(const Iterator &) const; bool operator!=(const Iterator &) const; /** 0 if this iterator's param1 is equal to the queried key, < 0 if this * iterator's param1 is less than the queried key, and > 0 if this * iterator's param1 is greater than the queried key. Iterating forward is * treated as a query for the first param1 greater than this iterator's key, * so will always result in a `cmp` > 0, and the converse for iterating * backward (`cmp` < 0). */ int cmp() const; /** @private */ struct Impl; private: Impl *impl = nullptr; }; /** Perform `count` "first greater than or equal to" queries. The result of * querying `key[i]` at `version[i]` is `iterator[i]`. `version[i]` must be >= * `getOldestVersion()` and <= `getVersion()`. Thread-safe as long as a * version is not concurrently invalidated by `setOldestVersion`. */ void firstGeq(const Key *key, const int64_t *version, Iterator *iterator, int count) const; /** Returns an iterator to the first mutation visible at `version`, or `end()` * if none exists. Thread-safe as long as `version` is not concurrently * invalidated by `setOldestVersion`. */ Iterator begin(int64_t version) const; /** The "past-the-end" iterator. */ Iterator end() const; /** Returns the memory usage in bytes. */ int64_t getBytes() const; /** Map starts with no mutations, with `getOldestVersion()` == `getVersion()` * == `version`. */ explicit VersionedMap(int64_t version); ~VersionedMap(); #if __cplusplus > 199711L VersionedMap(VersionedMap &&other) noexcept; VersionedMap &operator=(VersionedMap &&other) noexcept; VersionedMap(const VersionedMap &) = delete; VersionedMap &operator=(const VersionedMap &) = delete; #endif /// @private struct Impl; private: Impl *impl; }; } /* namespace weaselab */ #else #endif