Skip to content

Commit c008a47

Browse files
committed
refactor: publish parser routing snapshots
1 parent 812bef7 commit c008a47

6 files changed

Lines changed: 329 additions & 130 deletions

File tree

CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ endif()
7979
find_package(lexbor REQUIRED)
8080
find_package(fmt REQUIRED)
8181
find_package(Boost REQUIRED COMPONENTS json regex)
82+
find_package(tl-expected CONFIG REQUIRED)
8283

8384
# ---- ADD LIBRARY
8485

@@ -88,6 +89,7 @@ add_library(aniparse ${aniparse_library_type}
8889
"include/aniparse/detail/BitsetLite.hpp"
8990
"include/aniparse/detail/StrongBitset.hpp"
9091
"include/aniparse/detail/DomainScanner.hpp"
92+
"include/aniparse/detail/DomainStore.hpp"
9193
# src/net/Client.cpp (the curl backend) is added conditionally below.
9294
"include/aniparse/net/Client.hpp"
9395
"include/aniparse/net/CancellingTask.hpp"
@@ -209,14 +211,15 @@ target_include_directories(aniparse PUBLIC
209211
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
210212
)
211213

212-
# fmt / Boost are find_package'd imported targets a consumer recreates via
214+
# fmt / Boost / tl-expected are find_package'd imported targets a consumer recreates via
213215
# find_dependency (see aniparse-config.cmake.in), so they belong on the install
214216
# interface too — not only BUILD_INTERFACE. lexbor is a vendored static lib the
215217
# install does not ship, so it stays build-only.
216218
target_link_libraries(aniparse PUBLIC
217219
fmt::fmt
218220
Boost::json
219221
Boost::regex
222+
tl::expected
220223
$<BUILD_INTERFACE:${LEXBOR_LIBRARIES}>
221224
)
222225

@@ -322,4 +325,4 @@ install(FILES
322325
"${CMAKE_CURRENT_BINARY_DIR}/aniparse-config.cmake"
323326
"${CMAKE_CURRENT_BINARY_DIR}/aniparse-config-version.cmake"
324327
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/aniparse"
325-
)
328+
)

aniparse-config.cmake.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ include(CMakeFindDependencyMacro)
1111
# consumer's project.
1212
find_dependency(fmt CONFIG)
1313
find_dependency(Boost CONFIG COMPONENTS json regex)
14+
find_dependency(tl-expected CONFIG)
1415

1516
include("${CMAKE_CURRENT_LIST_DIR}/aniparse-targets.cmake")
1617

include/aniparse/ParserStore.hpp

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44
* Author: Toilettrauma <macosinternal@gmail.com>
55
*/
66
#pragma once
7-
#include "aniparse/detail/DomainScanner.hpp"
7+
#include "aniparse/detail/DomainStore.hpp"
88
#include "aniparse/Parser.hpp"
99
#include "aniparse/types/ParsedUrl.hpp"
1010

11-
#include "aniparse/utility/AtomicSharedPtr.hpp"
1211
#include <string>
1312
#include <map>
14-
#include <numeric>
1513
#include <optional>
1614
#include <vector>
1715

@@ -32,7 +30,36 @@ struct UrlRoute {
3230
*/
3331
class ParserStore {
3432
public:
35-
ParserStore();
33+
struct ParserDomainEmitter {
34+
void operator()(const Parser& parser, EmplaceDomainsContext& context) const {
35+
parser.emplace_domains(context);
36+
}
37+
};
38+
using Domains = detail::DomainStore<Parser, EmplaceDomainsContext, ParserDomainEmitter>;
39+
using DomainEdit = Domains::Edit;
40+
41+
class Edit {
42+
public:
43+
Edit(const Edit&) = delete;
44+
Edit& operator=(const Edit&) = delete;
45+
Edit(Edit&&) noexcept = default;
46+
Edit& operator=(Edit&&) noexcept = default;
47+
48+
std::shared_ptr<Parser> add_parser(std::shared_ptr<Parser> parser);
49+
bool remove_parser(std::string_view identifier);
50+
void refresh_domains(std::map<std::string, std::vector<std::string>, std::less<>> volatile_domains);
51+
void commit();
52+
53+
private:
54+
friend class ParserStore;
55+
explicit Edit(DomainEdit edit) : edit_(std::move(edit)) {}
56+
DomainEdit edit_;
57+
};
58+
59+
ParserStore() = default;
60+
61+
/// Start a local mutable draft; commit() publishes one new routing snapshot.
62+
[[nodiscard]] Edit begin_edit() { return Edit(domains_.begin_edit()); }
3663

3764
/**
3865
* @brief Add parser to the store
@@ -100,28 +127,6 @@ class ParserStore {
100127
void refresh_domains(std::map<std::string, std::vector<std::string>, std::less<>> volatile_domains);
101128

102129
private:
103-
using Scanner = DomainScanner<std::shared_ptr<Parser>>;
104-
105-
/**
106-
* Check if the parser identifier doesn't conflicting with existing parsers
107-
* @see Parser
108-
* @param parser The parser to check identifier uniqueness
109-
* @return true if the identifier is unique, false otherwise
110-
*/
111-
bool check_is_conflicting(const std::shared_ptr<Parser>& parser) const;
112-
113-
/**
114-
* Build a fresh routing index from the current parsers and volatile domains.
115-
* Every parser contributes its static domains plus any volatile domains keyed
116-
* by its identifier. Pure: produces a new scanner and mutates nothing.
117-
*/
118-
std::shared_ptr<Scanner> build_scanner() const;
119-
120-
/// Rebuild the routing index and swap it in atomically.
121-
void rebuild_index();
122-
123-
std::map<std::string, std::shared_ptr<Parser>, std::less<>> parsers_;
124-
std::map<std::string, std::vector<std::string>, std::less<>> volatile_domains_;
125-
AtomicSharedPtr<Scanner> scanner_;
130+
Domains domains_;
126131
};
127-
} // namespace aniparse
132+
} // namespace aniparse
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Copyright (C) 2025-2026 Toilettrauma
3+
*
4+
* Author: Toilettrauma <macosinternal@gmail.com>
5+
*/
6+
#pragma once
7+
#include "aniparse/detail/DomainScanner.hpp"
8+
#include "aniparse/utility/AtomicSharedPtr.hpp"
9+
10+
#include <map>
11+
#include <memory>
12+
#include <mutex>
13+
#include <ranges>
14+
#include <stdexcept>
15+
#include <string>
16+
#include <string_view>
17+
#include <utility>
18+
#include <vector>
19+
20+
namespace aniparse::detail {
21+
22+
/**
23+
* @brief Immutable-snapshot domain registry for parser-like source stores.
24+
*
25+
* Readers load one complete snapshot. Writers make an Edit, mutate only its
26+
* local maps, then commit one newly built DomainScanner with an atomic swap.
27+
* A stale concurrent edit is rejected rather than silently overwriting a newer
28+
* registry revision.
29+
*/
30+
template <class T, class DomainContext, class DomainEmitter>
31+
class DomainStore {
32+
public:
33+
using ValuePtr = std::shared_ptr<T>;
34+
using DomainOverrides = std::map<std::string, std::vector<std::string>, std::less<>>;
35+
36+
class Edit;
37+
38+
DomainStore()
39+
: snapshot_(make_snapshot({}, {})) {}
40+
41+
[[nodiscard]] Edit begin_edit();
42+
43+
[[nodiscard]] bool contains(std::string_view identifier) const {
44+
return snapshot_.load()->entries.contains(identifier);
45+
}
46+
47+
[[nodiscard]] ValuePtr find_by_key(std::string_view identifier) const {
48+
auto snapshot = snapshot_.load();
49+
auto it = snapshot->entries.find(identifier);
50+
return it == snapshot->entries.end() ? nullptr : it->second.value;
51+
}
52+
53+
[[nodiscard]] std::vector<ValuePtr> values() const {
54+
auto snapshot = snapshot_.load();
55+
std::vector<ValuePtr> result;
56+
result.reserve(snapshot->entries.size());
57+
for (const auto& [identifier, entry] : snapshot->entries) {
58+
result.push_back(entry.value);
59+
}
60+
return result;
61+
}
62+
63+
template <class Predicate>
64+
[[nodiscard]] ValuePtr find_by_host(std::string_view host, Predicate&& accepts) const {
65+
auto snapshot = snapshot_.load();
66+
auto labels = split_domains(host);
67+
auto found = snapshot->scanner->search_all(labels, [&accepts](ValuePtr& value) {
68+
return accepts(*value);
69+
});
70+
return found ? *found : nullptr;
71+
}
72+
73+
private:
74+
using Scanner = DomainScanner<ValuePtr>;
75+
76+
struct Entry {
77+
ValuePtr value;
78+
};
79+
80+
class DomainAdder final : public DomainContext {
81+
public:
82+
DomainAdder(Scanner& scanner, ValuePtr value)
83+
: scanner_(scanner), value_(std::move(value)) {}
84+
85+
void add_domain(std::string_view domain) override {
86+
scanner_.add_domain_parser(split_domains(domain), value_);
87+
}
88+
89+
private:
90+
Scanner& scanner_;
91+
ValuePtr value_;
92+
};
93+
94+
struct Snapshot {
95+
std::map<std::string, Entry, std::less<>> entries;
96+
DomainOverrides volatile_domains;
97+
std::shared_ptr<Scanner> scanner;
98+
};
99+
100+
template <std::ranges::viewable_range Range>
101+
static constexpr auto split_domains(Range&& range) {
102+
// clang-format off
103+
return std::forward<Range>(range)
104+
| std::views::reverse
105+
| std::views::split('.')
106+
| std::views::transform(std::views::reverse);
107+
// clang-format on
108+
}
109+
110+
std::shared_ptr<const Snapshot> make_snapshot(
111+
std::map<std::string, Entry, std::less<>> entries,
112+
DomainOverrides volatile_domains) {
113+
auto scanner = std::make_shared<Scanner>();
114+
for (const auto& [identifier, entry] : entries) {
115+
DomainAdder adder(*scanner, entry.value);
116+
emitter_(*entry.value, adder);
117+
auto add_domains = [&scanner, &entry](const std::vector<std::string>& domains) {
118+
for (const std::string& domain : domains) {
119+
scanner->add_domain_parser(split_domains(domain), entry.value);
120+
}
121+
};
122+
if (auto it = volatile_domains.find(identifier); it != volatile_domains.end()) {
123+
add_domains(it->second);
124+
}
125+
}
126+
return std::make_shared<const Snapshot>(Snapshot{
127+
.entries = std::move(entries),
128+
.volatile_domains = std::move(volatile_domains),
129+
.scanner = std::move(scanner),
130+
});
131+
}
132+
133+
void commit(std::shared_ptr<const Snapshot> base,
134+
std::map<std::string, Entry, std::less<>> entries,
135+
DomainOverrides volatile_domains) {
136+
std::lock_guard lock(writer_mutex_);
137+
auto live = snapshot_.load();
138+
if (live != base) {
139+
throw std::logic_error("DomainStore edit is stale");
140+
}
141+
snapshot_.store(make_snapshot(std::move(entries), std::move(volatile_domains)));
142+
}
143+
144+
mutable std::mutex writer_mutex_;
145+
DomainEmitter emitter_;
146+
AtomicSharedPtr<const Snapshot> snapshot_;
147+
148+
public:
149+
/** A local mutable draft of one DomainStore snapshot. */
150+
class Edit {
151+
public:
152+
Edit(const Edit&) = delete;
153+
Edit& operator=(const Edit&) = delete;
154+
Edit(Edit&&) noexcept = default;
155+
Edit& operator=(Edit&&) noexcept = default;
156+
157+
ValuePtr add(std::string identifier, ValuePtr value) {
158+
auto [it, inserted] = entries_.emplace(
159+
std::move(identifier), Entry{ .value = std::move(value) });
160+
return inserted ? it->second.value : nullptr;
161+
}
162+
163+
[[nodiscard]] bool contains(std::string_view identifier) const {
164+
return entries_.contains(identifier);
165+
}
166+
167+
bool remove(std::string_view identifier) {
168+
auto it = entries_.find(identifier);
169+
if (it == entries_.end()) {
170+
return false;
171+
}
172+
entries_.erase(it);
173+
return true;
174+
}
175+
176+
void set_volatile_domains(DomainOverrides volatile_domains) {
177+
volatile_domains_ = std::move(volatile_domains);
178+
}
179+
180+
void commit() {
181+
if (!store_) {
182+
throw std::logic_error("DomainStore edit was already committed");
183+
}
184+
store_->commit(base_, std::move(entries_), std::move(volatile_domains_));
185+
store_ = nullptr;
186+
}
187+
188+
private:
189+
friend class DomainStore;
190+
191+
explicit Edit(DomainStore& store)
192+
: store_(&store), base_(store.snapshot_.load())
193+
, entries_(base_->entries), volatile_domains_(base_->volatile_domains) {}
194+
195+
DomainStore* store_;
196+
std::shared_ptr<const Snapshot> base_;
197+
std::map<std::string, Entry, std::less<>> entries_;
198+
DomainOverrides volatile_domains_;
199+
};
200+
201+
};
202+
203+
template <class T, class DomainContext, class DomainEmitter>
204+
typename DomainStore<T, DomainContext, DomainEmitter>::Edit
205+
DomainStore<T, DomainContext, DomainEmitter>::begin_edit() {
206+
return Edit(*this);
207+
}
208+
209+
} // namespace aniparse::detail

0 commit comments

Comments
 (0)