Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
ringfilecache.cc
Go to the documentation of this file.
1
2/*
3 Aleph_w
4
5 Data structures & Algorithms
6 version 2.0.0b
7 https://github.com/lrleon/Aleph-w
8
9 This file is part of Aleph-w library
10
11 Copyright (c) 2002-2026 Leandro Rabindranath Leon
12
13 Permission is hereby granted, free of charge, to any person obtaining a copy
14 of this software and associated documentation files (the "Software"), to deal
15 in the Software without restriction, including without limitation the rights
16 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 copies of the Software, and to permit persons to whom the Software is
18 furnished to do so, subject to the following conditions:
19
20 The above copyright notice and this permission notice shall be included in all
21 copies or substantial portions of the Software.
22
23 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 SOFTWARE.
30*/
31
32
38#include <gtest/gtest.h>
39
40#include <ringfilecache.H>
41
42#include <atomic>
43#include <chrono>
44#include <cstring>
45#include <filesystem>
46#include <random>
47#include <string>
48#include <vector>
49
50using namespace Aleph;
51using namespace std;
52namespace fs = std::filesystem;
53
54namespace {
55
56struct TempPaths
57{
58 fs::path pars;
59 fs::path cache;
60};
61
62TempPaths make_temp_paths()
63{
64 static std::atomic<unsigned long long> counter{0};
65 const auto now = std::chrono::steady_clock::now().time_since_epoch().count();
66 const auto id = std::to_string(now) + "_" + std::to_string(counter++);
67
68 const fs::path dir = fs::temp_directory_path() / "aleph_ringcache_tests";
69 fs::create_directories(dir);
70 return {dir / (id + ".pars"), dir / (id + ".cache")};
71}
72
73struct TempFiles
74{
75 TempPaths paths;
76 explicit TempFiles(TempPaths p) : paths(std::move(p)) {}
78 {
79 std::error_code ec;
80 fs::remove(paths.pars, ec);
81 fs::remove(paths.cache, ec);
82 }
83};
84
85template <typename T>
86std::vector<T> to_vector(const Array<T> &arr)
87{
88 std::vector<T> out;
89 for (size_t i = 0; i < arr.size(); ++i)
90 out.push_back(arr[i]);
91 return out;
92}
93
95{
96 TempFiles tmp(make_temp_paths());
97 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 4);
98 ASSERT_TRUE(RingFileCache<int>::test(tmp.paths.pars.string()));
99
100 RingFileCache<int> cache(tmp.paths.pars.string());
101 EXPECT_TRUE(cache.is_initialized());
102 EXPECT_EQ(cache.capacity(), 4u);
103 EXPECT_EQ(cache.size(), 0u);
104 EXPECT_TRUE(cache.read_all().is_empty());
105
106 EXPECT_TRUE(cache.put(1));
107 EXPECT_TRUE(cache.put(2));
108 EXPECT_TRUE(cache.put(3));
109 EXPECT_TRUE(cache.put(4));
110 EXPECT_FALSE(cache.put(5)); // full
111 EXPECT_EQ(cache.size(), 4u);
112
113 int buf[4] = {0, 0, 0, 0};
114 EXPECT_TRUE(cache.read(buf, 4));
115 EXPECT_EQ(std::vector<int>(buf, buf + 4), std::vector<int>({1, 2, 3, 4}));
116 EXPECT_EQ(cache.read_first(), 1);
117 EXPECT_EQ(cache.read_last(), 4);
118 EXPECT_EQ(cache.oldest(), 1);
119 EXPECT_EQ(cache.oldest(2), 3);
120
121 EXPECT_TRUE(cache.get(2));
122 EXPECT_EQ(cache.size(), 2u);
123 EXPECT_EQ(cache.read_first(), 3);
124 EXPECT_EQ(cache.read_last(), 4);
125 EXPECT_TRUE(cache.get(2));
126 EXPECT_TRUE(cache.is_empty());
127
128 EXPECT_THROW(cache.read_first(), std::underflow_error);
129 EXPECT_THROW(cache.read_last(), std::underflow_error);
130 EXPECT_FALSE(cache.get(1)); // cannot extract if empty
131 EXPECT_FALSE(cache.read(buf, 1)); // cannot read if empty
132}
133
135{
136 TempFiles tmp(make_temp_paths());
137 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 5);
138 RingFileCache<int> cache(tmp.paths.pars.string());
139
140 for (int v : {10, 20, 30, 40})
141 ASSERT_TRUE(cache.put(v));
142
143 auto sub = cache.read_from(1, 2);
144 EXPECT_EQ(to_vector(sub), std::vector<int>({20, 30}));
145
146 RingFileCache<int>::Pointer ptr(cache); // at head (10)
147 ptr += 2; // points to 30
148 auto sub_ptr = cache.read_from(ptr, 3); // should stop at available items
149 EXPECT_EQ(to_vector(sub_ptr), std::vector<int>({30, 40}));
150
151 std::vector<int> iterated;
152 for (auto it = cache.get_it(); it.has_curr(); it.next_ne())
153 iterated.push_back(it.get_curr_ne());
154 EXPECT_EQ(iterated, std::vector<int>({10, 20, 30, 40}));
155}
156
158{
159 TempPaths paths = make_temp_paths();
160 TempFiles tmp(paths);
161 {
162 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 6);
163 RingFileCache<int> cache(tmp.paths.pars.string());
164 for (int v : {1, 2, 3})
165 ASSERT_TRUE(cache.put(v));
166 cache.flush();
167 }
168
169 RingFileCache<int> reopened(tmp.paths.pars.string());
170 EXPECT_EQ(reopened.size(), 3u);
171 EXPECT_EQ(reopened.read_first(), 1);
172 EXPECT_EQ(reopened.read_last(), 3);
173 auto all = reopened.read_all();
174 EXPECT_EQ(to_vector(all), std::vector<int>({1, 2, 3}));
175 reopened.get(2);
176 EXPECT_EQ(reopened.size(), 1u);
177 EXPECT_EQ(reopened.read_first(), 3);
178}
179
181{
182 TempFiles tmp(make_temp_paths());
183 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 2);
184 RingFileCache<int> cache(tmp.paths.pars.string());
185
186 cache.put(7);
187 cache.put(8);
188 EXPECT_EQ(cache.capacity(), 2u);
189 cache.resize(5);
190 EXPECT_EQ(cache.capacity(), 5u);
191 EXPECT_TRUE(cache.put(9));
192 EXPECT_TRUE(cache.put(10));
193 EXPECT_TRUE(cache.put(11));
194
195 auto all = cache.read_all();
196 EXPECT_EQ(to_vector(all), std::vector<int>({7, 8, 9, 10, 11}));
197}
198
200{
201 TempFiles tmp(make_temp_paths());
202 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 3);
203 RingFileCache<int> cache(tmp.paths.pars.string());
204
205 auto all = cache.read_all();
206 EXPECT_TRUE(all.is_empty());
207}
208
210{
211 TempFiles tmp(make_temp_paths());
212 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 3);
213 RingFileCache<int> cache(tmp.paths.pars.string());
214
215 cache.put(1);
216 cache.put(2);
217 cache.put(3);
218
219 RingFileCache<int>::Pointer ptr(cache); // at head
220 EXPECT_EQ(ptr.get_pos_respect_to_head(), 0u);
221
222 auto first_two = cache.read_from(ptr, 2);
223 EXPECT_EQ(to_vector(first_two), std::vector<int>({1, 2}));
224
225 ptr += 1;
226 EXPECT_EQ(ptr.get_pos_respect_to_head(), 1u);
227
228 ptr += 5; // wraps around dimension 3
229 EXPECT_EQ(ptr.get_pos_respect_to_head(), 0u);
230
231 cache.get(2); // remove 1 and 2
232 cache.put(4);
233 cache.put(5);
234
235 RingFileCache<int>::Pointer head_ptr(cache);
236 auto wrapped = cache.read_from(head_ptr, 3);
237 EXPECT_EQ(to_vector(wrapped), std::vector<int>({3, 4, 5}));
238
239 std::vector<int> iterated;
240 for (auto it = cache.get_it(); it.has_curr(); it.next_ne())
241 iterated.push_back(it.get_curr_ne());
242 EXPECT_EQ(iterated, std::vector<int>({3, 4, 5}));
243}
244
246{
247 TempPaths paths = make_temp_paths();
248 TempFiles tmp(paths);
249
250 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 4);
251 RingFileCache<int> cache;
252 cache.init(tmp.paths.pars.string());
254 cache.put(42);
255 cache.close();
256 cache.close(); // should be a no-op
257
258 RingFileCache<int> reopened(tmp.paths.pars.string());
259 EXPECT_EQ(reopened.size(), 1u);
260 EXPECT_EQ(reopened.read_first(), 42);
261}
262
264{
265 TempFiles tmp(make_temp_paths());
266 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 3);
267 RingFileCache<int> cache(tmp.paths.pars.string());
268
269 EXPECT_FALSE(cache.is_full());
270 EXPECT_TRUE(cache.is_empty());
271
272 cache.put(1);
273 EXPECT_FALSE(cache.is_full());
274 EXPECT_FALSE(cache.is_empty());
275
276 cache.put(2);
277 cache.put(3);
278 EXPECT_TRUE(cache.is_full());
279 EXPECT_FALSE(cache.is_empty());
280
281 cache.get(1);
282 EXPECT_FALSE(cache.is_full());
283}
284
286{
287 TempFiles tmp(make_temp_paths());
288 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 4);
289 RingFileCache<int> cache(tmp.paths.pars.string());
290
291 // Fill, empty, refill multiple cycles to stress wraparound
292 for (int cycle = 0; cycle < 5; ++cycle)
293 {
294 // Fill completely
295 for (int i = 0; i < 4; ++i)
296 ASSERT_TRUE(cache.put(cycle * 10 + i));
297 EXPECT_TRUE(cache.is_full());
298
299 // Verify contents
300 for (int i = 0; i < 4; ++i)
301 EXPECT_EQ(cache.oldest(i), cycle * 10 + i);
302
303 // Empty completely
304 EXPECT_TRUE(cache.get(4));
305 EXPECT_TRUE(cache.is_empty());
306 }
307
308 // Partial fill/empty cycles
309 for (int cycle = 0; cycle < 10; ++cycle)
310 {
311 cache.put(cycle);
312 cache.put(cycle + 100);
313 EXPECT_EQ(cache.size(), 2u);
314 EXPECT_EQ(cache.read_first(), cycle);
315 EXPECT_EQ(cache.read_last(), cycle + 100);
316 cache.get(2);
317 }
318}
319
321{
322 TempFiles tmp(make_temp_paths());
323 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 4);
324 RingFileCache<int> cache(tmp.paths.pars.string());
325
326 // Create wraparound: fill, remove some, add more
327 cache.put(1);
328 cache.put(2);
329 cache.put(3);
330 cache.put(4);
331 cache.get(2); // remove 1, 2 -> head moves forward
332 cache.put(5);
333 cache.put(6); // now data wraps: [5, 6, 3, 4] with head at index 2
334
335 EXPECT_EQ(cache.size(), 4u);
336 auto before = cache.read_all();
337 EXPECT_EQ(to_vector(before), std::vector<int>({3, 4, 5, 6}));
338
339 // Resize while wrapped
340 cache.resize(8);
341 EXPECT_EQ(cache.capacity(), 8u);
342 EXPECT_EQ(cache.size(), 4u);
343
344 // Data should still be correct after resize
345 auto after = cache.read_all();
346 EXPECT_EQ(to_vector(after), std::vector<int>({3, 4, 5, 6}));
347
348 // Can add more now
349 EXPECT_TRUE(cache.put(7));
350 EXPECT_TRUE(cache.put(8));
351 EXPECT_TRUE(cache.put(9));
352 EXPECT_TRUE(cache.put(10));
353 EXPECT_EQ(cache.size(), 8u);
354 EXPECT_TRUE(cache.is_full());
355}
356
358{
359 TempFiles tmp(make_temp_paths());
360 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 4);
361 RingFileCache<int> cache(tmp.paths.pars.string());
362
363 // Resize empty cache
364 cache.resize(6);
365 EXPECT_EQ(cache.capacity(), 6u);
366 EXPECT_EQ(cache.size(), 0u);
367
368 // Resize to same size should be no-op (but allowed)
369 cache.put(1);
370 cache.resize(6);
371 EXPECT_EQ(cache.capacity(), 6u);
372 EXPECT_EQ(cache.size(), 1u);
373 EXPECT_EQ(cache.read_first(), 1);
374}
375
377{
378 TempFiles tmp(make_temp_paths());
379 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 3);
380 RingFileCache<int> cache(tmp.paths.pars.string());
381
382 cache.put(1);
383 cache.put(2);
384
385 auto it = cache.get_it();
386 EXPECT_TRUE(it.has_curr());
387 EXPECT_EQ(it.get_curr(), 1);
388 it.next();
389 EXPECT_EQ(it.get_curr(), 2);
390 it.next();
391 EXPECT_FALSE(it.has_curr());
392
393 // get_curr() should throw when exhausted
394 EXPECT_THROW(it.get_curr(), std::overflow_error);
395 // next() should throw when exhausted
396 EXPECT_THROW(it.next(), std::overflow_error);
397}
398
400{
401 TempFiles tmp(make_temp_paths());
402 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 5);
403 RingFileCache<int> cache(tmp.paths.pars.string());
404
405 cache.put(10);
406 cache.put(20);
407 cache.put(30);
408
409 EXPECT_EQ(cache.oldest(0), 10);
410 EXPECT_EQ(cache.oldest(1), 20);
411 EXPECT_EQ(cache.oldest(2), 30);
412
413 // Out of bounds should throw
414 EXPECT_THROW(cache.oldest(3), std::overflow_error);
415 EXPECT_THROW(cache.oldest(100), std::overflow_error);
416}
417
419{
420 TempFiles tmp(make_temp_paths());
421 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 4);
422
423 RingFileCache<int> cache;
424 cache.init(tmp.paths.pars.string());
426
427 // Second init should throw
428 EXPECT_THROW(cache.init(tmp.paths.pars.string()), std::domain_error);
429}
430
432{
433 TempFiles tmp(make_temp_paths());
434 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 5);
435 RingFileCache<int> cache(tmp.paths.pars.string());
436
437 for (int i = 0; i < 5; ++i)
438 cache.put(i * 10);
439
440 RingFileCache<int>::Pointer ptr(cache, 2); // points to entry with value 20
441 EXPECT_EQ(cache.read(ptr), 20);
442
443 ptr -= 1;
444 EXPECT_EQ(cache.read(ptr), 10);
445
446 ptr -= 1;
447 EXPECT_EQ(cache.read(ptr), 0);
448
449 // Wraparound backward
450 ptr -= 1;
451 EXPECT_EQ(cache.read(ptr), 40); // wraps to last entry
452
453 ptr -= 2;
454 EXPECT_EQ(cache.read(ptr), 20);
455
456 // Large negative offset
457 ptr -= 7; // 7 % 5 = 2 steps back from 20 -> 0
458 EXPECT_EQ(cache.read(ptr), 0);
459}
460
462{
463 TempFiles tmp(make_temp_paths());
464 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 4);
465 RingFileCache<int> cache(tmp.paths.pars.string());
466
467 for (int i = 0; i < 4; ++i)
468 cache.put(i);
469
470 RingFileCache<int>::Pointer ptr(cache, 2); // at value 2
471 EXPECT_EQ(cache.read(ptr), 2);
472
473 auto ptr2 = ptr + (-1); // should go back 1
474 EXPECT_EQ(cache.read(ptr2), 1);
475
476 ptr2 = ptr + (-3); // should wrap to value 3
477 EXPECT_EQ(cache.read(ptr2), 3);
478}
479
480// Test with a non-int trivially copyable type
481struct TestRecord
482{
483 int id;
484 double value;
485 char tag[8];
486
487 bool operator==(const TestRecord &other) const
488 {
489 return id == other.id && value == other.value &&
490 std::strncmp(tag, other.tag, 8) == 0;
491 }
492};
493
495{
496 TempFiles tmp(make_temp_paths());
497 RingFileCache<TestRecord>::create(tmp.paths.pars.string(),
498 tmp.paths.cache.string(), 3);
499 RingFileCache<TestRecord> cache(tmp.paths.pars.string());
500
501 TestRecord r1{1, 3.14, "alpha"};
502 TestRecord r2{2, 2.71, "beta"};
503 TestRecord r3{3, 1.41, "gamma"};
504
505 EXPECT_TRUE(cache.put(r1));
506 EXPECT_TRUE(cache.put(r2));
507 EXPECT_TRUE(cache.put(r3));
508 EXPECT_TRUE(cache.is_full());
509
510 EXPECT_EQ(cache.read_first(), r1);
511 EXPECT_EQ(cache.read_last(), r3);
512 EXPECT_EQ(cache.oldest(1), r2);
513
514 cache.get(1);
515 EXPECT_EQ(cache.read_first(), r2);
516}
517
519{
520 TempFiles tmp(make_temp_paths());
521 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 5);
522 RingFileCache<int> cache(tmp.paths.pars.string());
523
524 EXPECT_TRUE(cache.is_empty());
525 EXPECT_FALSE(cache.is_full());
526 EXPECT_EQ(cache.size(), 0u);
527 EXPECT_EQ(cache.avail(), 5u);
528
529 // These should return false, not throw
530 EXPECT_FALSE(cache.get(1));
531 int buf[1];
532 EXPECT_FALSE(cache.read(buf, 1));
533
534 // read_all on empty returns empty array
535 auto all = cache.read_all();
536 EXPECT_TRUE(all.is_empty());
537
538 // Iterator on empty cache
539 auto it = cache.get_it();
540 EXPECT_FALSE(it.has_curr());
541}
542
544{
545 TempFiles tmp(make_temp_paths());
546 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 10);
547 RingFileCache<int> cache(tmp.paths.pars.string());
548
549 cache.put(1);
550 cache.put(2);
551 cache.put(3);
552
553 // read_from with pos beyond size should return partial or empty
554 auto result = cache.read_from(5, 3); // pos=5 but only 3 items exist
555 EXPECT_TRUE(result.is_empty());
556
557 // read_from with large m should stop at available items
558 auto result2 = cache.read_from(1, 100); // start at 1, request 100, only 2 available
559 EXPECT_EQ(to_vector(result2), std::vector<int>({2, 3}));
560}
561
563{
564 TempPaths paths = make_temp_paths();
565 TempFiles tmp(paths);
566
567 {
568 RingFileCache<int>::create(tmp.paths.pars.string(), tmp.paths.cache.string(), 4);
569 RingFileCache<int> cache(tmp.paths.pars.string());
570
571 // Create wraparound state
572 cache.put(1);
573 cache.put(2);
574 cache.put(3);
575 cache.put(4);
576 cache.get(2); // remove 1, 2
577 cache.put(5);
578 cache.put(6);
579 // State: [5, 6, 3, 4] with head at 2
580
581 cache.flush();
582 }
583
584 // Reopen and verify wraparound state persisted correctly
585 RingFileCache<int> reopened(tmp.paths.pars.string());
586 EXPECT_EQ(reopened.size(), 4u);
587 auto all = reopened.read_all();
588 EXPECT_EQ(to_vector(all), std::vector<int>({3, 4, 5, 6}));
589}
590
591} // namespace
bool operator==(const Time &l, const Time &r)
Definition ah-time.H:133
Simple dynamic array with automatic resizing and functional operations.
Definition tpl_array.H:138
constexpr size_t size() const noexcept
Return the number of elements stored in the stack.
Definition tpl_array.H:333
size_t size() const noexcept
Count the number of elements of the list.
Definition htlist.H:1319
Defines a pointer to a specific location in the cache.
Persistent ring buffer cache with file-backed storage.
T read(const Pointer &ptr)
Read the entry pointed to by ptr
T read_first()
Read the oldest entry in the cache.
Array< T > read_all()
Read all entries in insertion order.
bool is_empty() const noexcept
Returns true if the cache is empty.
Iterator get_it()
Returns a forward iterator starting at the oldest entry.
bool put(const T &item)
Insert an item into the cache.
void init(const std::string &pars_fname)
Initialize a cache constructed with the default constructor.
size_t size() const noexcept
Returns the number of entries stored in the cache.
size_t capacity() const noexcept
Returns the maximum capacity.
bool is_full() const noexcept
Returns true if the cache is full (no more entries can be inserted)
bool is_initialized() const
True if the cache has been initialized with a valid parameters file.
Array< T > read_from(const size_t pos, const size_t m)
Read up to m entries starting from the pos-th oldest position.
bool get(const size_t m=1) noexcept
Extracts (deletes) from the cache the m oldest inserted items.
void flush()
Flushes the current cache state to disk.
void close()
Flushes state and closes the underlying streams.
static void create(const std::string &pars_file_name, const std::string &cache_file_name, const size_t num_entries)
Create a brand new on-disk ring cache.
void resize(const size_t sz)
Resize the maximum capacity of the cache.
size_t avail() const noexcept
Returns the number of available entries.
T oldest()
Alias for read_first().
T read_last()
Read the youngest (most recently inserted) entry in the cache.
#define TEST(name)
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
bool all(Container &container, Operation &operation)
Return true if all elements satisfy a predicate.
std::vector< typename C::Item_Type > to_vector(const C &c)
Convert a container to a std::vector.
Definition ah-convert.H:228
DynList< T > maps(const C &c, Op op)
Classic map operation.
double sub(double a, double b)
STL namespace.
File-backed ring buffer cache for persistent storage.