Compare commits

..
Author SHA1 Message Date
Adrien Beraud be210e27da update version to 4.4.0 2026-09-04 16:46:41 -04:00
Adrien Beraud f7243fca37 value_cache: only emit an edition when the content actually changed
ValueCache re-emitted a known value as an edition on a higher seq alone,
while OpValueCache requires the content to differ as well. Since
Value::operator== ignores seq, identical content re-announced with a
bumped seq was re-emitted as an add: a source-counting OpValueCache
then counted the same node twice and kept the value after the node
expired or left the search. Apply the same rule at both levels.
2026-09-04 15:44:35 -04:00
Adrien Beraud 1f6efd5736 tests: cover same-content higher-seq re-announce in ValueCache
Value::operator== ignores seq, so a node re-announcing identical content
with a bumped seq is the same value refreshed, not an edition. ValueCache
must not re-emit it: a source-counting OpValueCache above would count the
node twice and keep the value after the node is gone.
2026-09-04 15:44:35 -04:00
Adrien Beraud 54fbad3a7e op_cache: ignore expiry of versions that were never counted
onValuesAdded ignores a different payload that does not carry a higher
seq (e.g. the same signed content re-signed with a randomized
signature), but onValuesExpired only skipped strictly older versions.
The uncounted source could therefore decrement the refcount and expire
a value still held by others. Apply the same rule on both sides.
2026-09-04 15:18:35 -04:00
Adrien Beraud 1315489a7b tests: cover expiry of an uncounted same-seq version in OpValueCache
A source reporting a different payload with the same seq is ignored on
add and not counted; its later expiration must not release the value
still held by the counted sources.
2026-09-04 15:18:35 -04:00
Adrien Beraud c189fa0e82 proxy_server: detach pending push requests before destruction
~DhtProxyServer destroyed requests_ whose ~http::Request notifies DONE;
the handler registered by sendPushNotification then erased from the map
being destroyed, corrupting the heap (double free under ASan) whenever a
push request was still pending, e.g. an unreachable push server. Detach
the state-change callbacks and release the requests before the members
are destroyed.
2026-09-04 15:18:24 -04:00
Adrien Beraud 3eae03289e storage: re-storing identical content is a refresh, not an edition
Storage::store reported edited_values=1 whenever the stored Sp<Value>
differed from the incoming one, even with equal content. A local put()
of an already-stored value was therefore announced to local listeners
as a new value a second time, unbalancing listen caches that count how
many sources hold a value. Only report an edition when the content
actually changed; the expiration is still refreshed.
2026-09-04 15:18:24 -04:00
Adrien Béraud e5e5a45b91 op_cache, proxy_client: guard SearchCache::get and push refresh expiry
SearchCache::get called the GetCallback unconditionally; Dht::query
passes none, which threw bad_function_call as soon as a listen cache
existed for the key. Return false without a callback so the network
path runs. A callback returning false means the consumer has enough
values, not a failure: report done(true) as the network path does.

DhtProxyClient push refresh expired every previously cached value once
the refresh get completed, including values that were just re-delivered
or edited, producing phantom expire callbacks. Track the ids received by
the get and only expire cached values missing from it, and skip the
reconciliation when the get failed so a transient error does not expire
values still present on the DHT.
2026-09-04 15:18:24 -04:00
Adrien Beraud 7a71beb892 value_cache: detect editions by increasing seq, like OpValueCache
ValueCache emitted any content change of a known value as an edition
while OpValueCache only accepts a higher seq. A node reporting an older
version made ValueCache replace its copy and re-emit versions that were
then dropped or, with source counting, double counted downstream. Use
the same rule at both levels.
2026-09-04 15:18:24 -04:00
Adrien Béraud 74a894229f value_cache, op_cache: fix value accounting after edition and re-delivery
OpValueCache counted every re-delivery of an identical value as a new
source, but the proxy client is a single logical source that re-sends
values on push refresh or listen restart: the refcount grew unbounded
and expirations were never propagated. Add a countSources flag to
OpValueCache, OpCache and SearchCache; the proxy client disables it.

When a value is edited (same id, higher seq), only the reporting source
is known to hold the new version: replace it in place, reset the
refcount to 1 and emit a single add. Ignore older versions from lagging
sources on add, and ignore their expiration so they cannot release a
newer version still held by others.

ValueCache emits an edited value as an add instead of silently
refreshing its old copy, and applies a grace period of one type
expiration while the listen is synced so that a single lost refresh
notification does not drop a value the remote node still holds.

SearchCache::get only answers from the cache once the listen is synced,
so a get never returns a partial view that the network would complete.

Add the single-source OpCacheTester cases enabled by countSources.
2026-09-04 15:18:13 -04:00
Adrien Beraud c81c1f70c8 tests: add listen cache tests for OpValueCache, ValueCache, DhtRunner and proxy
OpCacheTester and ValueCacheTester exercise the listen caches in
isolation: add/edit/expire flows, per-source refcounting, stale
versions, sync status, SearchCache::get and node drop.

DhtRunnerTester and DhtProxyTester gain end-to-end listen tests for
value edition, duplicate puts, multi-source and partial expiration,
get after a synced listen, and the push refresh/expire paths.

The proxy fixture joins the proxy DhtRunner before destroying the
DhtProxyServer so no DHT callback can reach a destroyed server, and
push notifications are never awaited while holding the mutex taken by
listen callbacks: the result is produced on the DHT thread.

Register the new executables in CMake and Meson.
2026-09-04 15:17:34 -04:00
10 changed files with 183 additions and 53 deletions

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ if(POLICY CMP0167)
endif()
set (opendht_VERSION_MAJOR 4)
set (opendht_VERSION_MINOR 3.0)
set (opendht_VERSION_MINOR 4.0)
set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
set (PACKAGE_VERSION ${opendht_VERSION})
set (VERSION "${opendht_VERSION}")
+19 -1
View File
@@ -5,7 +5,7 @@ project(
'cython',
license: 'MIT',
license_files: 'LICENSE',
version: '4.3.0',
version: '4.4.0',
meson_version: '>=1.1.0',
default_options: ['cpp_std=c++20', 'warning_level=3'],
)
@@ -292,6 +292,24 @@ if get_option('tests').enabled()
)
test('NetworkEngine', test_networkengine)
test_opcache = executable(
'test_opcache',
'tests/test_opcache.cpp',
'tests/tests_runner.cpp',
include_directories: opendht_inc,
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
)
test('OpCache', test_opcache)
test_valuecache = executable(
'test_valuecache',
'tests/test_valuecache.cpp',
'tests/tests_runner.cpp',
include_directories: opendht_inc,
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
)
test('ValueCache', test_valuecache)
test_crypto = executable(
'test_crypto',
'tests/test_crypto.cpp',
+4 -2
View File
@@ -39,8 +39,10 @@ OpValueCache::onValuesExpired(const std::vector<Sp<Value>>& vals, const system_c
if (vit != values.end()) {
if (vit->second.updated > t)
continue;
if (v->seq < vit->second.data->seq)
continue; // source held an older version, not counted
// Mirror onValuesAdded: a version that was never counted (different
// content, not a newer seq) must not decrement the refcount
if (*v != *vit->second.data and v->seq <= vit->second.data->seq)
continue;
vit->second.updated = t;
vit->second.refCount--;
+1 -1
View File
@@ -189,7 +189,7 @@ private:
v->second.created = now;
v->second.expiration = now + te;
v->second.typeExpiration = te;
if (value->seq > v->second.data->seq) {
if (*value != *v->second.data and value->seq > v->second.data->seq) {
// edited value (same rule as OpValueCache): emit as add
v->second.data = value;
nvals.emplace_back(value);
+61 -47
View File
@@ -16,6 +16,21 @@ using namespace std::chrono_literals;
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(DhtProxyTester);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
namespace {
// The result is produced on the DHT thread, which may be blocked in a listen
// callback waiting for the same mutex: never hold it while waiting.
dht::PushNotificationResult
sendPush(dht::DhtRunner& node, const std::map<std::string, std::string>& data, std::unique_lock<std::mutex>& lk)
{
lk.unlock();
auto ret = node.pushNotificationReceived(data).get();
lk.lock();
return ret;
}
} // namespace
#endif
void
DhtProxyTester::setUp()
{
@@ -332,7 +347,7 @@ DhtProxyTester::testPushListenValueEdit()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
nodeClient.pushNotificationReceived(push_data).get();
sendPush(nodeClient, push_data, lk);
// Wait for first add callback
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
@@ -357,7 +372,7 @@ DhtProxyTester::testPushListenValueEdit()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
nodeClient.pushNotificationReceived(push_data).get();
sendPush(nodeClient, push_data, lk);
// Wait for the second add
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
@@ -389,7 +404,7 @@ DhtProxyTester::testPushListenValueEdit()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::ValuesExpired);
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::ValuesExpired);
// Wait for the expire callback
CPPUNIT_ASSERT_MESSAGE("Should receive expire callback via push", cv.wait_for(lk, 10s, [&] {
@@ -541,7 +556,7 @@ DhtProxyTester::testPushNotification()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::Values);
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::Values);
// Wait for client to receive the value
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return gotValues; }));
@@ -570,7 +585,7 @@ DhtProxyTester::testPushNotification()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::Values);
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::Values);
// Wait for client to receive the edited value
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return gotValues; }));
@@ -587,7 +602,7 @@ DhtProxyTester::testPushNotification()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::ValuesExpired);
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::ValuesExpired);
cv.wait_for(lk, 500ms, [&] { return gotExpiredValues; });
@@ -1118,7 +1133,8 @@ DhtProxyTester::testPushMultiValueEditExpire()
auto key = dht::InfoHash::get("pushMultiEditExpire");
auto identity = dht::crypto::generateIdentity("MultiEditAuthor");
struct Event {
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
@@ -1176,7 +1192,7 @@ DhtProxyTester::testPushMultiValueEditExpire()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
nodeClient.pushNotificationReceived(push_data).get();
sendPush(nodeClient, push_data, lk);
// Wait for both values to arrive
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
@@ -1209,7 +1225,7 @@ DhtProxyTester::testPushMultiValueEditExpire()
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
push_data.erase("exp");
nodeClient.pushNotificationReceived(push_data).get();
sendPush(nodeClient, push_data, lk);
// Wait for the edited value (id=100, seq=2) to arrive
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
@@ -1232,13 +1248,11 @@ DhtProxyTester::testPushMultiValueEditExpire()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
CPPUNIT_ASSERT(nodeClient.pushNotificationReceived(push_data).get() == dht::PushNotificationResult::ValuesExpired);
CPPUNIT_ASSERT(sendPush(nodeClient, push_data, lk) == dht::PushNotificationResult::ValuesExpired);
// Wait for expire callback for value B
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) {
return e.id == 200 && e.expired;
});
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.id == 200 && e.expired; });
}));
// Verify: value A (edited) should NOT have been expired
@@ -1288,7 +1302,8 @@ DhtProxyTester::testPushRefreshNoSpuriousExpire()
auto key = dht::InfoHash::get("pushRefreshNoExpire");
auto identity = dht::crypto::generateIdentity("RefreshAuthor");
struct Event {
struct Event
{
dht::Value::Id id;
bool expired;
};
@@ -1328,7 +1343,7 @@ DhtProxyTester::testPushRefreshNoSpuriousExpire()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
nodeClient.pushNotificationReceived(push_data).get();
sendPush(nodeClient, push_data, lk);
// Wait for all 3 values to arrive
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
@@ -1354,7 +1369,7 @@ DhtProxyTester::testPushRefreshNoSpuriousExpire()
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
push_data.erase("exp");
nodeClient.pushNotificationReceived(push_data).get();
sendPush(nodeClient, push_data, lk);
// Wait a bit for any callbacks
cv.wait_for(lk, 1s);
@@ -1375,13 +1390,11 @@ DhtProxyTester::testPushRefreshNoSpuriousExpire()
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
nodeClient.pushNotificationReceived(push_data).get();
sendPush(nodeClient, push_data, lk);
// Wait for expire callback
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) {
return e.id == 20 && e.expired;
});
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.id == 20 && e.expired; });
}));
// Verify only value 20 expired, not 10 or 30
@@ -1426,7 +1439,12 @@ DhtProxyTester::testProxyListenEditChain()
auto key = dht::InfoHash::get("proxyEditChain");
auto identity = dht::crypto::generateIdentity("ProxyChainEditTester");
struct Event { dht::Value::Id id; uint16_t seq; bool expired; };
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::vector<Event> events;
auto ftoken = nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
@@ -1493,8 +1511,7 @@ DhtProxyTester::testProxyListenEditChain()
for (const auto& e : events)
if (!e.expired && e.seq > maxSeq)
maxSeq = e.seq;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Latest add should be the final version",
(uint16_t) NUM_EDITS, maxSeq);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Latest add should be the final version", (uint16_t) NUM_EDITS, maxSeq);
}
// Wait for natural expiration
@@ -1518,8 +1535,7 @@ DhtProxyTester::testProxyListenEditChain()
}
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should have exactly 1 expire via proxy", 1, expireCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the latest",
(uint16_t) NUM_EDITS, expiredSeq);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the latest", (uint16_t) NUM_EDITS, expiredSeq);
}
nodeClient.cancelListen(key, ftoken.get());
@@ -1557,7 +1573,12 @@ DhtProxyTester::testPushRapidEditsNoPhantom()
auto key = dht::InfoHash::get("pushRapidEdits");
auto identity = dht::crypto::generateIdentity("RapidEditAuthor");
struct Event { dht::Value::Id id; uint16_t seq; bool expired; };
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::vector<Event> events;
auto ftoken = nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
@@ -1593,12 +1614,9 @@ DhtProxyTester::testPushRapidEditsNoPhantom()
std::map<std::string, std::string> push_data;
push_data["key"] = key.toString();
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
lk.unlock();
nodeClient.pushNotificationReceived(push_data).get();
lk.lock();
sendPush(nodeClient, push_data, lk);
}
// Wait for edits to arrive (at least 2: initial + latest)
@@ -1669,7 +1687,12 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
auto key = dht::InfoHash::get("pushRefreshAfterEdit");
auto identity = dht::crypto::generateIdentity("RefreshEditAuthor");
struct Event { dht::Value::Id id; uint16_t seq; bool expired; };
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::vector<Event> events;
auto ftoken = nodeClient.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
@@ -1705,12 +1728,9 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
std::map<std::string, std::string> push_data;
push_data["key"] = key.toString();
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
lk.unlock();
nodeClient.pushNotificationReceived(push_data).get();
lk.lock();
sendPush(nodeClient, push_data, lk);
}
// Wait for initial add
@@ -1742,12 +1762,9 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
std::map<std::string, std::string> push_data;
push_data["key"] = key.toString();
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
lk.unlock();
nodeClient.pushNotificationReceived(push_data).get();
lk.lock();
sendPush(nodeClient, push_data, lk);
}
// Wait for the edit add (seq=3)
@@ -1765,12 +1782,9 @@ DhtProxyTester::testPushRefreshAfterEditKeepsLatest()
std::map<std::string, std::string> push_data;
push_data["key"] = key.toString();
push_data["t"] = std::to_string(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
.count());
lk.unlock();
nodeClient.pushNotificationReceived(push_data).get();
lk.lock();
sendPush(nodeClient, push_data, lk);
}
// Wait a bit for any callbacks
+49
View File
@@ -210,6 +210,32 @@ OpCacheTester::testStaleExpireIgnored()
CPPUNIT_ASSERT_EQUAL((uint16_t) 2, events.back().seq);
}
void
OpCacheTester::testSameSeqConflictExpireIgnored()
{
// A different payload with the same seq (e.g. the same signed content
// re-signed with a randomized signature) is ignored on add and not
// counted: its expiration must be ignored too.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto x = makeValue(60, 0, "x");
auto y = makeValue(60, 0, "y");
cache.onValuesAdded({x}); // A
cache.onValuesAdded({y}); // B: same seq, different content
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
CPPUNIT_ASSERT(cache.get(60)->data == x->data);
cache.onValuesExpired({y}); // B
CPPUNIT_ASSERT_EQUAL_MESSAGE("An uncounted version must not expire the value", (size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
cache.onValuesExpired({x}); // A
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testCallbacks()
{
@@ -543,6 +569,29 @@ OpCacheTester::testSourceDropKeepsValue()
CPPUNIT_ASSERT_EQUAL((size_t) 0, searchCache.get(dht::Value::Filter {}).size());
}
void
OpCacheTester::testSeqOnlyRefreshNotCounted()
{
// A node re-announcing the same content with a bumped seq must not be
// counted as an additional source by the OpValueCache above it, or the
// value would survive the node's departure.
std::vector<Event> events;
dht::OpValueCache parent(recordEvents(events));
dht::TypeStore types;
auto now = dht::clock::now();
auto v = makeValue(42, 0, "same");
auto vBumped = makeValue(42, 1, "same");
{
dht::ValueCache node([&](const Values& vals, bool expired) { parent.onValue(vals, expired); });
node.onValues({v}, {}, {}, types, now);
node.onValues({vBumped}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Node gone: its value must be expired once", 1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, parent.size());
}
void
OpCacheTester::testHighChurnRefCountConsistency()
{
+4
View File
@@ -16,6 +16,7 @@ class OpCacheTester : public CppUnit::TestFixture
CPPUNIT_TEST(testEditChainMultiSource);
CPPUNIT_TEST(testStaleAddIgnored);
CPPUNIT_TEST(testStaleExpireIgnored);
CPPUNIT_TEST(testSameSeqConflictExpireIgnored);
CPPUNIT_TEST(testCallbacks);
CPPUNIT_TEST(testFilters);
CPPUNIT_TEST(testSyncStatus);
@@ -26,6 +27,7 @@ class OpCacheTester : public CppUnit::TestFixture
CPPUNIT_TEST(testGetCallbackReturnsFalse);
CPPUNIT_TEST(testTimestampOrdering);
CPPUNIT_TEST(testSourceDropKeepsValue);
CPPUNIT_TEST(testSeqOnlyRefreshNotCounted);
CPPUNIT_TEST(testHighChurnRefCountConsistency);
CPPUNIT_TEST(testValueUpdateSingleSourcePhantom);
CPPUNIT_TEST(testExpireAfterUpdate);
@@ -43,6 +45,7 @@ public:
void testEditChainMultiSource();
void testStaleAddIgnored();
void testStaleExpireIgnored();
void testSameSeqConflictExpireIgnored();
void testCallbacks();
void testFilters();
void testSyncStatus();
@@ -53,6 +56,7 @@ public:
void testGetCallbackReturnsFalse();
void testTimestampOrdering();
void testSourceDropKeepsValue();
void testSeqOnlyRefreshNotCounted();
void testHighChurnRefCountConsistency();
void testValueUpdateSingleSourcePhantom();
void testExpireAfterUpdate();
+41
View File
@@ -353,6 +353,47 @@ ValueCacheTester::testSameSeqEditIgnored()
CPPUNIT_ASSERT_EQUAL(2, addCount);
}
void
ValueCacheTester::testSameContentHigherSeqIsRefresh()
{
// Value::operator== ignores seq: identical content with a bumped seq is
// the same value re-announced, not an edition. Re-emitting it would make
// a source-counting OpValueCache count this node twice.
int addCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (!expired)
addCount += vals.size();
});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 0;
v1->data = {'A'};
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto sameContent = std::make_shared<dht::Value>();
sameContent->id = 1;
sameContent->seq = 1;
sameContent->data = {'A'};
auto later = now + std::chrono::minutes(5);
cache.onValues({sameContent}, {}, {}, types, later);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Same content with a higher seq is a refresh, not an edit", 1, addCount);
// The refresh still extends the expiration (default type: 10 minutes)
cache.expireValues(now + std::chrono::minutes(11));
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
auto edited = std::make_shared<dht::Value>();
edited->id = 1;
edited->seq = 2;
edited->data = {'B'};
cache.onValues({edited}, {}, {}, types, later);
CPPUNIT_ASSERT_EQUAL(2, addCount);
}
void
ValueCacheTester::testShortExpirationRefreshLost()
{
+2
View File
@@ -20,6 +20,7 @@ class ValueCacheTester : public CppUnit::TestFixture
CPPUNIT_TEST(testMaxValues);
CPPUNIT_TEST(testUpdateTypeExpiration);
CPPUNIT_TEST(testSameSeqEditIgnored);
CPPUNIT_TEST(testSameContentHigherSeqIsRefresh);
CPPUNIT_TEST(testShortExpirationRefreshLost);
CPPUNIT_TEST(testShortExpirationHighChurn);
CPPUNIT_TEST(testGracePeriodWhileSynced);
@@ -40,6 +41,7 @@ public:
void testMaxValues();
void testUpdateTypeExpiration();
void testSameSeqEditIgnored();
void testSameContentHigherSeqIsRefresh();
void testShortExpirationRefreshLost();
void testShortExpirationHighChurn();
void testGracePeriodWhileSynced();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "opendht",
"version-string": "4.3.0",
"version-string": "4.4.0",
"port-version": 1,
"description": "OpenDHT: A lightweight C++17 Distributed Hash Table library.",
"homepage": "https://opendht.net",