Compare commits

...
63 Commits
Author SHA1 Message Date
Adrien Beraud 53eb8682f5 tools/dhtnode: print listen sync status 2026-10-08 09:39:49 -04:00
Adrien Beraud 6f1ff2e10b tests: retain legacy listeners through late values and renewal 2026-10-08 09:39:49 -04:00
Adrien Beraud 497d3d2543 tests: cover legacy LISTEN acknowledgments without tokens 2026-10-08 09:39:49 -04:00
Adrien Beraud 43937eb454 tests: cover deferred cached readiness lifecycle 2026-10-08 09:39:49 -04:00
Adrien Beraud e216d1c8a0 tests: emit revisioned proxy integration pushes 2026-10-08 09:39:49 -04:00
Adrien Beraud 88773525bb tests: reject unrevisioned pushes from updated proxies 2026-10-08 09:39:49 -04:00
Adrien Beraud 5b1fa36920 tests: cover legacy and mixed-version LISTEN readiness 2026-10-08 09:39:49 -04:00
Adrien Beraud f7aeab7713 doc: specify single-pass native listener synchronization 2026-10-08 09:39:49 -04:00
Adrien Beraud a950e06d10 build: bump ABI and cover proxy configuration tests in Meson 2026-10-08 09:39:42 -04:00
Adrien Beraud 743a82af6f proxy_client: dispatch readiness outside locks 2026-10-08 09:39:42 -04:00
Adrien Beraud 1890726251 dht: synchronize listeners through sequenced initial UPDATE batches 2026-10-08 09:39:42 -04:00
Adrien Beraud e413af7eb5 tests: cover ordered LISTEN delivery and safe proxy recovery 2026-10-08 08:41:02 -04:00
Adrien Beraud 524d9acc27 tests: require single-pass initial LISTEN synchronization 2026-10-08 08:41:02 -04:00
Adrien Beraud 81cbd90609 dht: report registration failures and roll back partial listeners
Fail pending readiness when LISTEN retries are exhausted, reject search capacity exhaustion with a zero token rather than an exception, guard obsolete expiration callbacks, and cancel local and family listeners after partial registration fails.
2026-10-08 08:41:02 -04:00
Adrien Beraud 732d82c31b proxy_server: drain initializing snapshots when subscriptions end
Capture snapshot contexts weakly and synchronously fail queued responses on cancellation, expiry, replacement, or destruction. Make readiness completion idempotent to avoid stale callbacks changing settled contexts.
2026-10-07 22:25:19 -04:00
Adrien Beraud 727fabb5a1 proxy_client: propagate readiness failure and authoritative expiry
Let contiguous revision deletions override legacy timestamps. Propagate false completion and initial request failures through the operation cache, retain source synchronization reporting for recovery, and fail pending readiness outside the search lock during shutdown.
2026-10-07 22:25:19 -04:00
Adrien Beraud 194e9b8b15 tests: cover readiness failure and subscription cleanup
Reproduce timestamped legacy values surviving revision expiration, proxy false completion and HTTP failures, shutdown and recovery readiness, retained pending subscription contexts, and native listen exhaustion and partial dual-stack capacity rejection.
2026-10-07 22:25:19 -04:00
Adrien Beraud 223eb6a7a9 tests: cover native listener snapshot synchronization 2026-10-07 22:25:19 -04:00
Adrien Beraud bd8c72bb07 dht: fail pending readiness on shutdown and teardown 2026-10-07 22:25:19 -04:00
Adrien Beraud a853279de7 op_cache: honor reentrant readiness cancellation 2026-10-07 22:25:19 -04:00
Adrien Beraud cded92e9f5 tests: strengthen initial readiness lifecycle regressions 2026-10-07 22:25:19 -04:00
Adrien Beraud ad05b85798 tests: cover initial listener readiness edge cases
Review b1d0435c with cache, native transport, and runner coverage. Add 13 tests for source synchronization and discovery ordering, cancellation, failure and recovery, cached filters, operation reuse, address families, stopped and invalid requests, and signed values.

Three regressions intentionally fail against the current implementation: a reordered listen confirmation reports readiness before initial UPDATE values; shutdown abandons pending readiness callbacks; cancellation from another readiness callback does not suppress the canceled notification. Production behavior is unchanged.
2026-10-07 22:25:19 -04:00
Adrien Beraud 57a3b658ae proxy: reconcile revisioned listener snapshots
Serve atomic cached snapshots after listener readiness instead of independent GETs. Validate unchanged tokens without downloads, apply contiguous expirations locally, reject malformed revisions and stale generations, bound subscription requests and recovery backoff, and keep wrong-session pushes isolated. Preserve legacy payload handling, document transport and protocol behavior, and include implementation-dependent regressions.
2026-10-07 22:25:19 -04:00
Adrien Beraud ab0e922e02 http: reject incomplete and failed responses
Clear successful status after transport failure or cancellation and require complete HTTP framing at EOF. Revision recovery transport regressions are included with the proxy implementation that provides their fixtures.
2026-10-07 22:25:19 -04:00
Adrien Beraud 3478255fd8 dht: notify initial listener readiness
Preserve existing listen entry points and add one-shot initial readiness through the runner, security layer, and native searches. Wait for target discovery and source synchronization, handle isolated families and failed searches, and initialize reused operations. Include API-dependent native/cache regressions.
2026-10-07 22:25:19 -04:00
Adrien Beraud 621b44c8e8 Revert "disable windows wheel for now"
This reverts commit fc6edb4d20.
2026-09-23 14:42:57 -04:00
Adrien Beraud e1c088f2e4 build: bump version to 4.4.1 2026-09-23 14:38:36 -04:00
Adrien Beraud a6e0e441ac crypto: use standard unsigned int* for gnutls_ocsp_resp_get_single 2026-09-22 11:31:45 -04:00
Adrien Beraud dcc2ba3492 crypto: use gnutls_ocsp_cert_status_t* in gnutls_ocsp_resp_get_single 2026-09-22 11:31:45 -04:00
Adrien Beraud ccf00d6b09 ports: update shiftmedia-libgnutls to f76d780a6 2026-09-22 11:31:45 -04:00
Adrien Beraud d8250574fc ports: use ForcedIncludeFiles for gnutls_msvc_fixes.h in shiftmedia-libgnutls 2026-09-22 11:31:45 -04:00
Adrien Beraud 0d698c76e8 ports: update shiftmedia-libgnutls to 9ebeb826d 2026-09-22 11:31:45 -04:00
Adrien Beraud e4e3b4e73b ports: set LanguageStandard_C to stdc11 in shiftmedia-libgnutls 2026-09-22 11:31:45 -04:00
Adrien Beraud b26d622f5d ports: update shiftmedia-libgnutls to 37840cfa5 2026-09-22 11:31:45 -04:00
Adrien Beraud 78694751bd ports: update shiftmedia-libgnutls to 4ab78d57e 2026-09-22 11:31:45 -04:00
Adrien Beraud f9b90c5c1c ports: update shiftmedia-libgnutls to 5a5a9dd95 2026-09-22 11:31:45 -04:00
Adrien Beraud 56b848bf47 ports: fix savoirfairelinux/gnutls commit hash and SHA512 in shiftmedia-libgnutls 2026-09-22 11:31:45 -04:00
Adrien Beraud dd875230c3 ports: update shiftmedia-libgnutls to 90eb686e8 2026-09-22 11:31:45 -04:00
Adrien Beraud de1d428e63 ports: use gitlab gnulib mirror archive in shiftmedia-libgnutls 2026-09-22 11:31:45 -04:00
Adrien Beraud 55e3a6514f ports: fix gnulib snapshot SHA512 hash in shiftmedia-libgnutls 2026-09-22 11:31:45 -04:00
Adrien Beraud f4bbef210c ports: fix savoirfairelinux/gnutls archive SHA512 hash in shiftmedia-libgnutls 2026-09-22 11:31:45 -04:00
Adrien Beraud 2c1470cef6 vcpkg: add overlay port for savoirfairelinux/gnutls on Windows MSVC 2026-09-22 11:31:45 -04:00
Adrien Beraud adb6aa57e6 update vcpkg 2026-09-22 11:31:45 -04:00
Adrien Beraud acd0abdb70 ci: fix vcpkg cache keys across workflows and remove manual version string 2026-09-22 11:31:45 -04:00
Cyrille BéraudandCopilot b9c8f5e0fd runner: complete pushes received while stopped
Resolve idle-runner push futures immediately under the operation queue lock. A stopped runner has no loop left to execute the queued promise, otherwise consumers can wait indefinitely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-21 16:02:47 -04:00
Cyrille BéraudandCopilot 18fedf431f tests: cover pushes received by stopped runners
Require immediate completion before run and after join, including a burst of notifications. Keep active-runner coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-21 16:02:47 -04:00
Adrien Beraud fc6edb4d20 disable windows wheel for now 2026-09-19 22:09:44 -04:00
Adrien Beraud d1093c77a3 ci: use libmsgpack-cxx-dev on Debian 13 2026-09-19 21:56:06 -04:00
Adrien Beraud b543f1e3cb update vcpkg to 2026.06.01 2026-09-19 21:10:44 -04:00
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
Adrien BeraudandCopilot 3a383ac873 http: close connections on the io_context thread
asio sockets are not thread-safe. http::Connection::close() used to close
the socket synchronously on the calling thread. Request::cancel() and
Request::~Request() (via terminate()) are called from arbitrary threads
(e.g. DhtProxyClient::stop(), cancelAllListeners(), cancelListen()),
so the close raced with in-flight operations on the io_context thread.

In particular, asio::async_connect's range_connect_op closes and
re-opens the socket when trying the next endpoint (common on mobile
where the IPv6 endpoint fails first). A concurrent close() from another
thread corrupts the epoll reactor's per-descriptor state, leading to
crashes in epoll_reactor::deregister_descriptor / register_descriptor
called from range_connect_op::process.

Dispatch the close to the io_context thread when not already on it.
Also keep the connection alive during the TLS handshake, like every
other asynchronous operation, so that the destructor can never run
while an operation is pending.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-04 14:13:20 -04:00
Adrien Beraud 3149fe60c4 proxy: increase OP_MARGIN to 8 hours 2026-09-04 11:04:42 -04:00
Adrien Beraud d5956e0778 proxy: use system_clock for subscriber expiration timeouts
Track subscribe/permanent-put expiration with std::chrono::system_clock
(wall clock) instead of steady_clock in both the proxy client and server,
so persisted expirations survive restarts and map directly to epoch time.

- DhtProxyServer::clock is now system_clock; expireTimer/expireNotifyTimer
  use asio::system_timer.
- DhtProxyClient::refreshSubscriberTimer uses asio::system_timer and
  system_clock::now() for subscriber refresh.
2026-09-04 11:04:42 -04:00
54 changed files with 8559 additions and 537 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ env:
UBUNTU_FULL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
UBUNTU_MESON_DEPS: "ninja-build libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-dev python3-setuptools python3-pip libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
UBUNTU_MINIMAL_DEPS: "libncurses5-dev libreadline-dev nettle-dev libfmt-dev libgnutls28-dev libcppunit-dev libmsgpack-dev libargon2-dev"
DEBIAN13_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-dev libargon2-dev"
DEBIAN13_DEPS: "git libtool pkg-config wget cmake libncurses5-dev libreadline-dev nettle-dev libasio-dev libgnutls28-dev libuv1-dev python3-pip python3-dev python3-setuptools libcppunit-dev libjsoncpp-dev libfmt-dev libmsgpack-cxx-dev libargon2-dev"
on: [push, pull_request]
+4 -2
View File
@@ -36,7 +36,6 @@ jobs:
name: Build wheels on Windows
runs-on: windows-latest
env:
VCPKG_VERSION: "2026.06.01"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows-release
@@ -53,7 +52,10 @@ jobs:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/python-wheels.yml') }}
restore-keys: |
${{ runner.os }}-vcpkg-${{ env.VCPKG_DEFAULT_TRIPLET }}-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ env.VCPKG_DEFAULT_TRIPLET }}-
- name: Setup vcpkg and install dependencies
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
+7 -6
View File
@@ -309,7 +309,6 @@ jobs:
name: Build Windows MSI Installer
runs-on: windows-latest
env:
VCPKG_VERSION: "2026.06.01"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows
@@ -339,7 +338,10 @@ jobs:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
key: ${{ runner.os }}-vcpkg-x64-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/release-package.yml') }}
restore-keys: |
${{ runner.os }}-vcpkg-x64-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-x64-shared-
- name: Setup vcpkg and install dependencies
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
@@ -413,7 +415,6 @@ jobs:
name: Build Windows dhtnode
runs-on: windows-latest
env:
VCPKG_VERSION: "2026.06.01"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_DEFAULT_TRIPLET: x64-windows-static-release
@@ -437,10 +438,10 @@ jobs:
path: |
${{ env.VCPKG_INSTALLED_DIR }}
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}
key: ${{ runner.os }}-vcpkg-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/release-package.yml') }}
restore-keys: |
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-x64-static-
${{ runner.os }}-vcpkg-x64-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-x64-static-
- name: Setup vcpkg and install static dependencies
if: steps.cache-vcpkg-static.outputs.cache-hit != 'true'
+10 -11
View File
@@ -9,7 +9,6 @@ on:
branches: [ main, master ]
env:
VCPKG_VERSION: "2026.06.01"
VCPKG_INSTALLATION_ROOT: "C:\\vcpkg"
VCPKG_DISABLE_METRICS: "1"
VCPKG_INSTALLED_DIR: ${{ github.workspace }}\vcpkg_installed
@@ -36,7 +35,7 @@ jobs:
id: generate_cache_key
shell: pwsh
run: |
$key_value = "${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
$key_value = "${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
echo "key=$key_value" >> $env:GITHUB_OUTPUT
- name: Cache vcpkg installed packages
@@ -48,8 +47,8 @@ jobs:
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ steps.generate_cache_key.outputs.key }}
restore-keys: |
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-
${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-
- name: Setup vcpkg and install dependencies
if: steps.cache-vcpkg.outputs.cache-hit != 'true'
@@ -84,7 +83,7 @@ jobs:
id: generate_static_cache_key
shell: pwsh
run: |
$key_value = "${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
$key_value = "${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}-${{ hashFiles('.github/workflows/windows-build.yml') }}"
echo "key=$key_value" >> $env:GITHUB_OUTPUT
- name: Cache vcpkg static installed packages
@@ -96,8 +95,8 @@ jobs:
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ steps.generate_static_cache_key.outputs.key }}
restore-keys: |
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-
${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-
- name: Setup vcpkg and install static dependencies
if: steps.cache-vcpkg-static.outputs.cache-hit != 'true'
@@ -139,8 +138,8 @@ jobs:
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ needs.prepare-dependencies.outputs.vcpkg_cache_key }}
restore-keys: |
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-shared-
${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ matrix.arch }}-shared-
- name: Configure CMake
shell: pwsh
@@ -216,8 +215,8 @@ jobs:
${{ env.VCPKG_DEFAULT_BINARY_CACHE }}
key: ${{ needs.prepare-static-dependencies.outputs.vcpkg_static_cache_key }}
restore-keys: |
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ env.VCPKG_VERSION }}-${{ matrix.arch }}-static-
${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-${{ hashFiles('**/vcpkg.json', '**/vcpkg-configuration.json') }}
${{ runner.os }}-vcpkg-${{ matrix.arch }}-static-
- name: Configure CMake for Static Build
shell: pwsh
+6 -2
View File
@@ -9,8 +9,8 @@ if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()
set (opendht_VERSION_MAJOR 4)
set (opendht_VERSION_MINOR 3.0)
set (opendht_VERSION_MAJOR 5)
set (opendht_VERSION_MINOR 0.0)
set (opendht_VERSION ${opendht_VERSION_MAJOR}.${opendht_VERSION_MINOR})
set (PACKAGE_VERSION ${opendht_VERSION})
set (VERSION "${opendht_VERSION}")
@@ -870,6 +870,10 @@ if (BUILD_TESTING)
tests/test_parsedmessage.cpp
tests/test_networkengine.h
tests/test_networkengine.cpp
tests/test_opcache.h
tests/test_opcache.cpp
tests/test_valuecache.h
tests/test_valuecache.cpp
)
if (OPENDHT_PROXY_CLIENT)
list (APPEND test_FILES
+2
View File
@@ -23,6 +23,8 @@ Values are distributed over the network, with redundancy.
## Documentation
See the wiki: <https://github.com/savoirfairelinux/opendht/wiki>
Push subscription recovery protocol: [doc/proxy-sync.md](doc/proxy-sync.md).
#### How to run a node
You can help contribute to the public network by running a stable node with a public IP address.
+178
View File
@@ -0,0 +1,178 @@
# Push Subscription Synchronization
Revision-aware clients require an updated proxy. Old clients remain supported:
requests without `sync` retain the original subscription response format.
## Validation and Snapshots
A `SUBSCRIBE /key/<hash>` request contains the existing push registration fields
and a `sync` object. An empty object requests a full snapshot. A synchronized
client instead supplies its last reconciled token:
```json
{"sync":{"epoch":"<listener-epoch>","revision":7}}
```
The epoch is unique to the server-side listener. New and restored listeners get
fresh epochs. Each addition, edit, or expiration reported by the DHT listener
increments its revision. Epoch/revision state is not restored from disk.
When the token matches, the response contains only a completion record. Otherwise,
the response contains newline-delimited value objects followed by this record:
```json
{"sync":{"epoch":"<listener-epoch>","revision":8,"snapshot":true,"complete":true}}
```
The listener retains its current values, including edits and expirations, under
the same mutex as its revision. The server copies both atomically; it does not
run a GET for a synchronization request. New listeners wait for initial readiness
before responding. `snapshot:false` means validation found the client's cache
unchanged. Successful responses always have `complete:true`; initial search
failure returns HTTP 503. The deprecated `retry` field is not used. Every
subscription request renews the lease, independently of whether values are sent.
Clients buffer snapshot values and reconcile only after a valid completion
record and successful HTTP framing. Reconciliation delivers new values and
higher-sequence edits, suppresses duplicates and older versions, and expires
cached IDs absent from the snapshot. Failed or invalidated responses never
remove cached values. Snapshot deletion is authoritative and does not depend
on wall-clock timestamps.
## Push Ordering and Recovery
Value pushes include string fields `epoch` and `revision`. A push no newer than
the reconciled revision is ignored. Revisions must be unsigned decimal integers
without signs, whitespace, trailing characters, or overflow. A contiguous
expiration revision removes the listed IDs locally without a download. The
`expired:"true"` field identifies batched expirations using `ids`; the original
single-value `exp` field remains supported. Other same-epoch changes and revision
gaps require a cached snapshot. Unknown epochs first validate against the proxy,
so delayed pushes from old epochs do not force a value download. Revisions cover
edits as well as additions and deletions.
Clients allow one sync request per listener and coalesce overlapping triggers.
Request generations reject callbacks from superseded requests. Completion state
changes run under the listener's owning lock after checking the generation.
Same-epoch push observations can invalidate an older snapshot. Confirmed retired
epochs are remembered in a bounded, push-token-scoped set; other unknown epochs
are validated without changing cached values. Returning to an earlier push token
remains valid.
Token rotation during a pending request is applied after that request completes.
Clients require an updated proxy server. Value pushes without both epoch and
revision are malformed and return `Error`; they do not mutate the listener cache
or trigger synchronization requests. The proxy adds these fields independently
of whether its upstream DHT peers support version-2 native LISTEN.
Pushes for another client session return `IgnoredWrongSession` without changing
subscriptions, cached values, or connectivity state. Their application-level
purpose is to wake the mobile app; session creation and desired subscriptions
are independent actions. A new session requests its own full initial snapshot
and does not inherit synchronization state from the rejected push.
Connectivity changes and existing lease refreshes validate the last reconciled
token; unchanged data is not downloaded again. Successful lease renewal alone
does not clear pending recovery. Failed recovery retries use the existing lease
timer with exponential delays from one to sixty seconds; successful recovery
restores the normal lease schedule. Stale and malformed responses also use this
backoff, rather than retrying immediately. Each subscription request has a
30-second steady-clock deadline covering DNS, connection, and response body
receipt. Expired requests release the in-flight slot and enter recovery. No
healthy-state polling timer is added.
Without another event or polling, a silently dropped final push cannot be
detected immediately. The next connectivity validation or existing lease refresh
detects a revision mismatch. Guarantees concern reconciliation with the proxy's
observed DHT state, not linearizable global DHT storage.
## Listener Readiness
The existing four-argument `listen` API remains available. The new overload adds
a one-shot `DoneCallbackSimple` as its fifth argument:
```cpp
runner.listen(key, valuesCallback, {}, {}, readyCallback);
```
`readyCallback(true)` follows completed search/listener synchronization for each
enabled address family. Version-2 native sources complete after initial delivery
and contiguous updates through their observed high-water mark. Legacy sources
complete when LISTEN registration is acknowledged; initial values can arrive
after readiness. Isolated families with no peers are ready after delivering local
values. Initial search or registration failure reports `false`; cancellation
does not promise a callback.
This is an initial completion notification, not a global consistency guarantee.
It does not start a GET or add polling. Attaching to a synchronized cache delivers
cached values before notifying readiness.
Mixed-version and entirely legacy networks can initialize successfully without
GET fallback or polling. A legacy acknowledgment confirms registration, not
complete baseline delivery or absence of values. Late values remain ordinary
listener updates: the proxy advances its revision and push clients reconcile
through the existing subscription path. A client snapshot is authoritative for
the proxy's observed cache, not proof of a complete upstream DHT baseline. A
legacy update never received by the proxy cannot be detected by proxy revision
validation. Proxy readiness callbacks run outside the client's search lock;
cancellation suppresses queued notifications.
Streaming proxy clients request `/key/<hash>/listen?sync=1`. Updated proxies send
`{"sync":{"complete":true}}` after initial values, or `complete:false` on initial
failure. Requests without this opt-in retain the original stream format.
## Native Initial Delivery
Native clients request LISTEN version `ve:2` with an `ls` tuple:
```text
[registration, epoch, sequence, snapshotEnd]
```
The registration is a fresh nonzero client nonce; a LISTEN request sends zeros
for the other fields. The server explicitly acknowledges version 2, assigns a
nonzero stream epoch, and declares the number of initial UPDATE batches in
`snapshotEnd`. An empty baseline declares zero batches. Each UPDATE carries the
same registration, epoch, and snapshotEnd with a consecutive sequence starting
at one. The initial batches occupy sequences 1 through snapshotEnd; live changes
follow them. The acknowledgment's sequence is the server's current high-water
mark. All tuple fields are unsigned 64-bit integers.
The server registers the listener and serializes the initial batches on its DHT
thread. Existing UPDATE fragmentation and acknowledgments deliver each initial
value once per source registration on a loss-free connection. There is no
additional GET or second value download. Retries resend the same immutable
request bytes. Repeated registrations with the same nonce reuse the baseline;
healthy renewals return metadata only. Recreated sockets use a new nonce and
cannot accept the previous registration's updates.
Live updates, including edits, expirations, and refreshes, are sent immediately
while the baseline is in flight. The receiver buffers them until every declared
initial batch has been fully reassembled and validated. It then applies the
baseline followed by contiguous live changes in sequence order. Duplicates are
acknowledged without being applied again. Known sequence gaps prevent the cache
from being considered synchronized; retransmission can fill them. Initialization
or an unresolved live gap fails after 30 seconds and allows a fresh registration.
Batches contain at most 32 values and target the existing 56 KiB serialized-value
budget; individual larger values retain the existing fragment transport. The
receiver bounds buffered batches to 4096 and payload bytes to 64 MiB, and rejects
baselines exceeding its 4096-value cache capacity. Invalid version-2 values fail
the batch instead of being silently skipped. This design adds neither pacing
nor send windows. Per-peer baselines are coherent with their subsequent updates,
but do not provide an atomic snapshot of the entire distributed DHT.
Peers that do not explicitly acknowledge version 2 remain legacy listeners.
Their values are delivered normally and successful LISTEN acknowledgment
completes their best-effort initialization. No GET fallback or polling is added.
Legacy LISTEN acknowledgments may omit the token; clients retain the token
previously obtained during discovery and keep the listener socket open. A
version-2 acknowledgment must include its token and synchronization metadata.
The source records whether it uses legacy completion or a verified version-2
stream. An established version-2 stream cannot downgrade on a metadata-free
acknowledgment; invalid initialization and registration failures still report
failure.
## HTTP Transport Note
HTTP requests now report status code zero for transport errors and cancellation,
even if a 200 status was received before the failure. Cancellation also sets
`aborted`. EOF succeeds only when the HTTP parser confirms complete framing;
truncated content-length and chunked responses fail. Callers must not treat an
earlier status callback as proof that a response body completed successfully.
+21 -4
View File
@@ -236,6 +236,7 @@ public:
* @return a token to cancel the listener later.
*/
size_t listen(const InfoHash&, ValueCallback, Value::Filter = {}, Where = {}) override;
size_t listen(const InfoHash&, ValueCallback, Value::Filter, Where, DoneCallbackSimple ready) override;
size_t listen(const InfoHash& key, GetCallback cb, Value::Filter f = {}, Where w = {}) override
{
@@ -460,7 +461,12 @@ private:
void reportedAddr(const SockAddr&);
// Storage
void storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t tid, Query&& = {}, int version = 0);
std::optional<net::ListenSync> storageAddListener(const InfoHash& id,
const Sp<Node>& node,
size_t tid,
Query&& = {},
int version = 0,
const std::optional<net::ListenSync>& sync = {});
bool storageStore(const InfoHash& id,
const Sp<Value>& value,
time_point created,
@@ -535,7 +541,12 @@ private:
DoneCallback callback,
time_point created = time_point::max(),
bool permanent = false);
size_t listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f = {}, const Sp<Query>& q = {});
size_t listenTo(const InfoHash& id,
sa_family_t af,
ValueCallback cb,
Value::Filter f = {},
const Sp<Query>& q = {},
DoneCallbackSimple ready = {});
/**
* Refill the search with good nodes if possible.
@@ -609,6 +620,7 @@ private:
void searchStep(std::weak_ptr<Search> ws);
void searchSynchedNodeListen(const Sp<Search>&, SearchNode&);
void searchNodeListenDeadline(const Sp<Search>&, SearchNode&, const Sp<Query>&);
void dumpSearch(const Search& sr, std::ostream& out) const;
@@ -626,8 +638,13 @@ private:
net::RequestAnswer onGetValues(Sp<Node> node, const InfoHash& hash, want_t want, const Query& q);
void onGetValuesDone(const Sp<Node>& status, net::RequestAnswer& a, Sp<Search>& sr, const Sp<Query>& orig_query);
/* when we receive a listen request */
net::RequestAnswer onListen(
Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version = 0);
net::RequestAnswer onListen(Sp<Node> node,
const InfoHash& hash,
const Blob& token,
size_t socket_id,
const Query& query,
int version = 0,
const std::optional<net::ListenSync>& sync = {});
void onListenDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr);
/* when we receive an announce request */
net::RequestAnswer onAnnounce(Sp<Node> node,
+40 -21
View File
@@ -79,8 +79,7 @@ public:
virtual time_point periodic(const uint8_t* buf, size_t buflen, SockAddr, const time_point& now) = 0;
virtual time_point periodic(
const uint8_t* buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now)
= 0;
const uint8_t* buf, size_t buflen, const sockaddr* from, socklen_t fromlen, const time_point& now) = 0;
/**
* Get a value by searching on all available protocols (IPv4, IPv6),
@@ -93,17 +92,22 @@ public:
* @param f a filter function used to prefilter values.
*/
virtual void get(
const InfoHash& key, GetCallback cb, DoneCallback donecb = {}, Value::Filter&& f = {}, Where&& w = {})
= 0;
virtual void get(
const InfoHash& key, GetCallback cb, DoneCallbackSimple donecb = {}, Value::Filter&& f = {}, Where&& w = {})
= 0;
virtual void get(
const InfoHash& key, GetCallbackSimple cb, DoneCallback donecb = {}, Value::Filter&& f = {}, Where&& w = {})
= 0;
virtual void get(
const InfoHash& key, GetCallbackSimple cb, DoneCallbackSimple donecb, Value::Filter&& f = {}, Where&& w = {})
= 0;
const InfoHash& key, GetCallback cb, DoneCallback donecb = {}, Value::Filter&& f = {}, Where&& w = {}) = 0;
virtual void get(const InfoHash& key,
GetCallback cb,
DoneCallbackSimple donecb = {},
Value::Filter&& f = {},
Where&& w = {}) = 0;
virtual void get(const InfoHash& key,
GetCallbackSimple cb,
DoneCallback donecb = {},
Value::Filter&& f = {},
Where&& w = {}) = 0;
virtual void get(const InfoHash& key,
GetCallbackSimple cb,
DoneCallbackSimple donecb,
Value::Filter&& f = {},
Where&& w = {}) = 0;
/**
* Similar to Dht::get, but sends a Query to filter data remotely.
@@ -138,26 +142,22 @@ public:
Sp<Value>,
DoneCallback cb = nullptr,
time_point created = time_point::max(),
bool permanent = false)
= 0;
bool permanent = false) = 0;
virtual void put(const InfoHash& key,
const Sp<Value>& v,
DoneCallbackSimple cb,
time_point created = time_point::max(),
bool permanent = false)
= 0;
bool permanent = false) = 0;
virtual void put(const InfoHash& key,
Value&& v,
DoneCallback cb = nullptr,
time_point created = time_point::max(),
bool permanent = false)
= 0;
bool permanent = false) = 0;
virtual void put(const InfoHash& key,
Value&& v,
DoneCallbackSimple cb,
time_point created = time_point::max(),
bool permanent = false)
= 0;
bool permanent = false) = 0;
/**
* Get data currently being put at the given hash.
@@ -261,6 +261,25 @@ public:
*/
virtual PushNotificationResult pushNotificationReceived(const std::map<std::string, std::string>& data) = 0;
/**
* Listen with a one-shot notification after initial synchronization completes
* on all enabled address families. Version-2 native peers complete after
* initial delivery; legacy peers complete on LISTEN acknowledgment and may
* deliver initial values after the notification.
* @param ready called with false on initialization failure or shutdown.
* Canceling the listener suppresses a pending readiness notification.
* The callback may run before listen returns. Implementations that do
* not support readiness report false while preserving the listener.
*/
virtual size_t listen(
const InfoHash& key, ValueCallback cb, Value::Filter filter, Where where, DoneCallbackSimple ready)
{
auto token = listen(key, std::move(cb), std::move(filter), std::move(where));
if (ready)
ready(false);
return token;
}
protected:
std::shared_ptr<Logger> logger_ {};
std::queue<std::function<void()>> onConnectCallbacks_ {};
+12 -4
View File
@@ -189,6 +189,7 @@ public:
* @return a token to cancel the listener later.
*/
virtual size_t listen(const InfoHash&, ValueCallback, Value::Filter = {}, Where = {}) override;
size_t listen(const InfoHash&, ValueCallback, Value::Filter, Where, DoneCallbackSimple ready) override;
virtual size_t listen(const InfoHash& key, GetCallback cb, Value::Filter f = {}, Where w = {}) override
{
@@ -291,9 +292,10 @@ public:
virtual size_t getStorageLimit() const override { return 0; }
void setLocalStorageLimit(size_t) override {}
virtual size_t getLocalStorageLimit() const override { return STORAGE_LIMIT_UNLIMITED; }
void connectivityChanged(sa_family_t) override { getProxyInfos(); }
void connectivityChanged(sa_family_t) override { connectivityChanged(); }
void connectivityChanged() override
{
launchConnectedCbs_ = true;
getProxyInfos();
loopSignal_();
}
@@ -329,7 +331,6 @@ private:
enum class ListenMethod {
LISTEN,
SUBSCRIBE,
RESUBSCRIBE,
};
using CacheValueCallback
= std::function<bool(const std::vector<std::shared_ptr<Value>>& values, bool expired, system_clock::time_point)>;
@@ -341,11 +342,16 @@ private:
const CacheValueCallback& cb,
const Sp<OperationState>& opstate,
Listener& listener,
ListenMethod method = ListenMethod::LISTEN);
ListenMethod method,
const InfoHash& key,
size_t token);
void handleResubscribe(const asio::error_code& ec,
const InfoHash& key,
const size_t token,
std::shared_ptr<OperationState> opstate);
void scheduleListenRetry(const InfoHash& key, size_t token, Listener& listener);
bool reconcileListenerSnapshot(Listener& listener, const std::vector<Sp<Value>>& values, const Json::Value& marker);
std::chrono::seconds subscriptionRequestTimeout_ {30};
void doPut(const InfoHash&, Sp<Value>, DoneCallbackSimple, time_point created, bool permanent);
void handleRefreshPut(const asio::error_code& ec, InfoHash key, Value::Id id);
@@ -419,6 +425,7 @@ private:
*/
std::mutex lockCallbacks_;
std::vector<std::function<void()>> callbacks_;
std::vector<std::function<void()>> readinessCallbacks_;
Sp<InfoState> infoState_;
@@ -437,6 +444,8 @@ private:
/**
* Refresh a listen via a token
* @param token
* @param fetchValues ask the server to send the values it stores, for a listener
* that may have missed some (its previous subscription failed).
*/
void resubscribe(const InfoHash& key, const size_t token, Listener& listener);
@@ -468,7 +477,6 @@ private:
const std::function<void()> loopSignal_;
#ifdef OPENDHT_PUSH_NOTIFICATIONS
std::string fillBody(bool resubscribe);
void getPushRequest(Json::Value&) const;
#endif // OPENDHT_PUSH_NOTIFICATIONS
+21 -8
View File
@@ -76,8 +76,14 @@ public:
asio::io_context& io_context() const;
// Monotonic clock for elapsed-time statistics (request rate, uptime).
// Must stay on steady_clock so NTP/wall-clock corrections don't skew durations.
using clock = std::chrono::steady_clock;
using time_point = clock::time_point;
// Wall clock for persisted subscribe/permanent-put lease expirations,
// so they map directly to epoch time and survive restarts.
using sys_clock = std::chrono::system_clock;
using sys_time_point = sys_clock::time_point;
struct PushStats
{
@@ -266,12 +272,19 @@ private:
{
std::mutex lock;
std::string sessionId;
const std::string epoch {InfoHash::getRandom().toString()};
uint64_t revision {0};
std::map<Value::Id, Json::Value> values;
bool initialized {false};
bool initialOk {false};
std::vector<std::function<void()>> pendingSnapshots;
PushSessionContext(const std::string& id)
: sessionId(id)
{}
};
#ifdef OPENDHT_PUSH_NOTIFICATIONS
void onPushListenReady(const std::shared_ptr<PushSessionContext>& sessionCtx, bool ok);
PushType getTypeFromString(const std::string& type);
std::string getDefaultTopic(PushType type);
@@ -401,12 +414,12 @@ private:
std::shared_ptr<ConnectionListener> connListener_;
struct PermanentPut
{
time_point expiration;
sys_time_point expiration;
std::string pushToken;
std::string clientId;
std::shared_ptr<PushSessionContext> sessionCtx;
std::unique_ptr<asio::steady_timer> expireTimer;
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
std::unique_ptr<asio::system_timer> expireTimer;
std::unique_ptr<asio::system_timer> expireNotifyTimer;
Sp<Value> value;
PushType type;
std::string topic;
@@ -419,7 +432,7 @@ private:
p.pack("value");
p.pack(value);
p.pack("exp");
p.pack(to_time_t(expiration));
p.pack(sys_clock::to_time_t(expiration));
if (not clientId.empty()) {
p.pack("cid");
p.pack(clientId);
@@ -460,12 +473,12 @@ private:
#ifdef OPENDHT_PUSH_NOTIFICATIONS
struct Listener
{
time_point expiration;
sys_time_point expiration;
std::string clientId;
std::shared_ptr<PushSessionContext> sessionCtx;
std::future<size_t> internalToken;
std::unique_ptr<asio::steady_timer> expireTimer;
std::unique_ptr<asio::steady_timer> expireNotifyTimer;
std::unique_ptr<asio::system_timer> expireTimer;
std::unique_ptr<asio::system_timer> expireNotifyTimer;
PushType type;
std::string topic;
@@ -476,7 +489,7 @@ private:
p.pack("cid");
p.pack(clientId);
p.pack("exp");
p.pack(to_time_t(expiration));
p.pack(sys_clock::to_time_t(expiration));
if (sessionCtx) {
std::lock_guard l(sessionCtx->lock);
p.pack("sid");
+3
View File
@@ -156,6 +156,7 @@ public:
}
std::future<size_t> listen(InfoHash key, ValueCallback vcb, Value::Filter f = {}, Where w = {});
std::future<size_t> listen(InfoHash key, ValueCallback vcb, Value::Filter f, Where w, DoneCallbackSimple ready);
std::future<size_t> listen(InfoHash key, GetCallback cb, Value::Filter f = {}, Where w = {})
{
@@ -532,6 +533,8 @@ public:
/**
* Insert a push notification to process for OpenDHT
* Returns an immediately ready future with IgnoredStopped if the runner is idle,
* or IgnoredDisabled if push notification support is disabled at build time.
*/
std::future<PushNotificationResult> pushNotificationReceived(const std::map<std::string, std::string>& data);
+7
View File
@@ -106,9 +106,16 @@ public:
void timeout(const std::chrono::seconds& timeout, HandlerCb cb = {});
/**
* Close the underlying socket.
* Safe to call from any thread: the actual close is dispatched to
* the thread running the io_context.
*/
void close();
private:
void doClose();
template<typename T>
T wrapCallback(T cb) const
{
+45 -22
View File
@@ -19,6 +19,7 @@
#include <algorithm>
#include <memory>
#include <queue>
#include <optional>
namespace dht {
namespace net {
@@ -78,6 +79,15 @@ private:
struct ParsedMessage;
struct ListenSync
{
uint64_t registration {0};
uint64_t epoch {0};
uint64_t sequence {0};
uint64_t snapshotEnd {0};
MSGPACK_DEFINE_ARRAY(registration, epoch, sequence, snapshotEnd)
};
/**
* Answer for a request.
*/
@@ -91,6 +101,7 @@ struct RequestAnswer
std::vector<Sp<FieldValueIndex>> fields {};
std::vector<Sp<Node>> nodes4 {};
std::vector<Sp<Node>> nodes6 {};
std::optional<ListenSync> listenSync {};
RequestAnswer() {}
RequestAnswer(ParsedMessage&& msg);
};
@@ -167,7 +178,9 @@ private:
* @param token (type: Blob) security token.
* @param rid (type: uint16_t) request id.
*/
std::function<RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int)> onListen {};
std::function<
RequestAnswer(Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int, const std::optional<ListenSync>&)>
onListen {};
/**
* Called on announce request.
*
@@ -231,29 +244,32 @@ public:
* @param values The values to send.
* @param version If version = 1, a request will be used to answer to the listener
*/
void tellListener(const Sp<Node>& n,
Tid socket_id,
const InfoHash& hash,
want_t want,
const Blob& ntoken,
std::vector<Sp<Node>>&& nodes,
std::vector<Sp<Node>>&& nodes6,
std::vector<Sp<Value>>&& values,
const Query& q,
int version);
std::vector<Sp<Request>> tellListener(const Sp<Node>& n,
Tid socket_id,
const InfoHash& hash,
want_t want,
const Blob& ntoken,
std::vector<Sp<Node>>&& nodes,
std::vector<Sp<Node>>&& nodes6,
std::vector<Sp<Value>>&& values,
const Query& q,
int version,
ListenSync* sync = nullptr);
void tellListenerRefreshed(const Sp<Node>& n,
Tid socket_id,
const InfoHash& hash,
const Blob& ntoken,
const std::vector<Value::Id>& values,
int version);
int version,
ListenSync* sync = nullptr);
void tellListenerExpired(const Sp<Node>& n,
Tid socket_id,
const InfoHash& hash,
const Blob& ntoken,
const std::vector<Value::Id>& values,
int version);
int version,
ListenSync* sync = nullptr);
bool isRunning(sa_family_t af) const;
inline want_t want() const { return dht_socket->hasIPv4() and dht_socket->hasIPv6() ? (WANT4 | WANT6) : -1; }
@@ -358,7 +374,8 @@ public:
const Blob& token,
Tid socketId,
RequestCb&& on_done,
RequestExpiredCb&& on_expired);
RequestExpiredCb&& on_expired,
const std::optional<ListenSync>& sync = {});
/**
* Send a "announce" request to a given node.
*
@@ -411,19 +428,22 @@ public:
*
* @return the request with information concerning its success.
*/
void sendUpdateValues(const Sp<Node>& n,
const InfoHash& infohash,
std::vector<Sp<Value>>&& values,
time_point created,
const Blob& token,
size_t sid);
static size_t getUpdateBatchCount(const std::vector<Sp<Value>>& values);
std::vector<Sp<Request>> sendUpdateValues(const Sp<Node>& n,
const InfoHash& infohash,
std::vector<Sp<Value>>&& values,
time_point created,
const Blob& token,
size_t sid,
ListenSync* sync = nullptr);
Sp<Request> sendUpdateValues(const Sp<Node>& n,
const InfoHash& infohash,
std::vector<Sp<Value>>::iterator begin,
std::vector<Sp<Value>>::iterator end,
time_point created,
const Blob& token,
size_t sid);
size_t sid,
ListenSync* sync = nullptr);
/**
* Parses a message and calls appropriate callbacks.
@@ -552,7 +572,10 @@ private:
std::pair<Blob, Blob> bufferNodes(
sa_family_t af, const InfoHash& id, want_t want, std::vector<Sp<Node>>& nodes, std::vector<Sp<Node>>& nodes6);
/* answer to a listen request */
void sendListenConfirmation(const SockAddr& addr, Tid tid, const Blob& token = {});
void sendListenConfirmation(const SockAddr& addr,
Tid tid,
const Blob& token = {},
const std::optional<ListenSync>& sync = {});
/* answer to put request */
void sendValueAnnounced(const SockAddr& addr, Tid, Value::Id);
/* answer in case of error */
+1 -1
View File
@@ -8,7 +8,7 @@ namespace dht {
namespace proxy {
constexpr const std::chrono::hours OP_TIMEOUT {24}; // one day
constexpr const std::chrono::hours OP_MARGIN {2}; // two hours
constexpr const std::chrono::hours OP_MARGIN {8}; // eight hours
using ListenToken = uint64_t;
+1
View File
@@ -272,6 +272,7 @@ public:
bool cancelPut(const InfoHash& h, const Value::Id& vid) override { return dht_->cancelPut(h, vid); }
size_t listen(const InfoHash& key, ValueCallback, Value::Filter = {}, Where = {}) override;
size_t listen(const InfoHash& key, ValueCallback, Value::Filter, Where, DoneCallbackSimple ready) override;
size_t listen(const InfoHash& key, GetCallback cb, Value::Filter = {}, Where w = {}) override;
size_t listen(const InfoHash& key, GetCallbackSimple cb, Value::Filter f = {}, Where w = {}) override
{
+35 -2
View File
@@ -5,7 +5,7 @@ project(
'cython',
license: 'MIT',
license_files: 'LICENSE',
version: '4.3.0',
version: '5.0.0',
meson_version: '>=1.1.0',
default_options: ['cpp_std=c++20', 'warning_level=3'],
)
@@ -129,6 +129,8 @@ endif
opendht = library(
'opendht',
opendht_src,
version: meson.project_version(),
soversion: '5',
include_directories: opendht_inc,
dependencies: deps,
cpp_args: ['-DOPENDHT_BUILD', '-Dopendht_EXPORTS'],
@@ -178,6 +180,8 @@ if get_option('c').enabled()
opendht_c = library(
'opendht-c',
['c/opendht.cpp', 'c/opendht_c.h'],
version: meson.project_version(),
soversion: '5',
dependencies: [opendht_dep, msgpack, jsoncpp, fmt],
cpp_args: ['-DOPENDHT_C_BUILD', '-Dopendht_c_EXPORTS'],
install: true,
@@ -292,6 +296,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',
@@ -326,6 +348,17 @@ if get_option('tests').enabled()
test('Http', test_http)
endif
if get_option('proxy_client').enabled()
test_proxy_client = executable(
'test_dhtproxy_client',
'tests/test_dhtproxy_client.cpp',
'tests/tests_runner.cpp',
include_directories: opendht_inc,
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack, llhttp],
)
test('DhtProxyClient', test_proxy_client)
endif
if get_option('peer_discovery').enabled()
test_peerdiscovery = executable(
'test_peerdiscovery',
@@ -343,7 +376,7 @@ if get_option('tests').enabled()
'tests/tests_runner.cpp',
dependencies: [opendht_dep, cppunit, jsoncpp, fmt, openssl, msgpack],
)
test('DhtProxy', test_proxy)
test('DhtProxy', test_proxy, timeout: 120)
if get_option('long_tests').enabled()
test_proxystress = executable(
@@ -0,0 +1,118 @@
diff --git a/SMP/libgnutls.vcxproj.filters b/SMP/libgnutls.vcxproj.filters
index b182189..91ee160 100644
--- a/SMP/libgnutls.vcxproj.filters
+++ b/SMP/libgnutls.vcxproj.filters
@@ -103,9 +103,6 @@
<Filter Include="Header Files\lib\inih">
<UniqueIdentifier>{ae0c3eeb-53df-4c72-a85a-6b46de35e7ba}</UniqueIdentifier>
</Filter>
- <Filter Include="Source Files\libtasn1">
- <UniqueIdentifier>{32be60b7-8c65-486e-9df5-7e529443cf07}</UniqueIdentifier>
- </Filter>
<Filter Include="Source Files\lib\nettle\gost">
<UniqueIdentifier>{1f3549a8-3c3d-475f-8fd6-20451d336464}</UniqueIdentifier>
</Filter>
@@ -1553,30 +1550,6 @@
<ClCompile Include="..\lib\nettle\sysrng-bcrypt.c">
<Filter>Source Files\lib\nettle</Filter>
</ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\coding.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\element.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\structure.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\version.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
<ClCompile Include="..\lib\nettle\gost\acpkm.c">
<Filter>Source Files\lib\nettle\gost</Filter>
</ClCompile>
diff --git a/SMP/libgnutls_files.props b/SMP/libgnutls_files.props
index adcb074..2317c2a 100644
--- a/SMP/libgnutls_files.props
+++ b/SMP/libgnutls_files.props
@@ -179,13 +179,6 @@
<ItemGroup>
<ClCompile Include="lib\gnutls_asn1_tab.c" />
<ClCompile Include="lib\pkix_asn1_tab.c" />
- <ClCompile Include="..\devel\libtasn1\lib\coding.c" />
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c" />
- <ClCompile Include="..\devel\libtasn1\lib\element.c" />
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c" />
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c" />
- <ClCompile Include="..\devel\libtasn1\lib\structure.c" />
- <ClCompile Include="..\devel\libtasn1\lib\version.c" />
<ClCompile Include="..\gnulib\lib\c-strcasecmp.c" />
<ClCompile Include="..\gnulib\lib\c-strncasecmp.c" />
<ClCompile Include="..\gnulib\lib\cloexec.c" />
@@ -572,9 +565,6 @@
<ClCompile Include="..\gnulib\lib\readdir.c">
<ForcedIncludeFiles>dirent-private.h;%(ForcedIncludeFiles)</ForcedIncludeFiles>
</ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
- <ObjectFileName>$(IntDir)\tasn1_$(filename).obj</ObjectFileName>
- </ClCompile>
<ClCompile Include="..\lib\algorithms\ecc.c">
<ObjectFileName>$(IntDir)\alg_%(Filename).obj</ObjectFileName>
</ClCompile>
diff --git a/SMP/libgnutls_winrt.vcxproj.filters b/SMP/libgnutls_winrt.vcxproj.filters
index 014a408..ae5ecff 100644
--- a/SMP/libgnutls_winrt.vcxproj.filters
+++ b/SMP/libgnutls_winrt.vcxproj.filters
@@ -103,9 +103,6 @@
<Filter Include="Header Files\lib\inih">
<UniqueIdentifier>{ae0c3eeb-53df-4c72-a85a-6b46de35e7ba}</UniqueIdentifier>
</Filter>
- <Filter Include="Source Files\libtasn1">
- <UniqueIdentifier>{32be60b7-8c65-486e-9df5-7e529443cf07}</UniqueIdentifier>
- </Filter>
<Filter Include="Header Files\lib\nettle\gost">
<UniqueIdentifier>{4fada990-3138-4089-a6c7-ae722a0e7fe9}</UniqueIdentifier>
</Filter>
@@ -1553,30 +1550,6 @@
<ClCompile Include="..\lib\nettle\sysrng-bcrypt.c">
<Filter>Source Files\lib\nettle</Filter>
</ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\coding.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\decoding.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\element.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\errors.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\gstr.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\parser_aux.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\structure.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
- <ClCompile Include="..\devel\libtasn1\lib\version.c">
- <Filter>Source Files\libtasn1</Filter>
- </ClCompile>
<ClCompile Include="..\lib\accelerated\afalg.c">
<Filter>Source Files\lib\accelerated</Filter>
</ClCompile>
@@ -0,0 +1,12 @@
--- a/lib/gnutls.pc.in
+++ b/lib/gnutls.pc.in
@@ -18,7 +18,7 @@
Name: GnuTLS
Description: Transport Security Layer implementation for the GNU system
URL: https://www.gnutls.org/
Version: @VERSION@
-Libs: -L${libdir} -lgnutls
+Libs: -L${libdir} @GNUTLS_LIBS@
Libs.private: @LIBINTL@ @LIBSOCKET@ @INET_PTON_LIB@ @LIBPTHREAD@ @LIB_SELECT@ @TSS_LIBS@ @GMP_LIBS@ @LIBUNISTRING@ @LIBATOMIC_LIBS@ @GNUTLS_LIBS_PRIVATE@
@GNUTLS_REQUIRES_PRIVATE@
Cflags: -I${includedir}
+187
View File
@@ -0,0 +1,187 @@
set(GNULIB_REF "1cc0125a28cba4e8503d853bf99b854fe4dd454c")
vcpkg_from_github(
OUT_SOURCE_PATH SOURCE_PATH
REPO savoirfairelinux/gnutls
REF 300b0c721fc1809f3d191b7f9e289c0267eabd01
SHA512 8a74fbf0343e658f6321da0f28bcc05c580fb7b950f6edfb6a8fdaad6d814456ed2dbf94e8dd330c7740c836f01e04552e649d529ee29424c3641861c41344bf
HEAD_REF fix/ci-msvc
PATCHES
external-libtasn1.patch
pkgconfig.patch
)
file(REMOVE_RECURSE "${SOURCE_PATH}/devel/perlasm")
vcpkg_download_distfile(
GNULIB_SNAPSHOT
URLS "https://gitlab.com/libidn/gnulib-mirror/-/archive/${GNULIB_REF}/gnulib-mirror-${GNULIB_REF}.tar.gz"
FILENAME "gnulib-mirror-${GNULIB_REF}.tar.gz"
SHA512 340539bd7b8e30b1e0f81a15fb207960e9ae3c4ef515f39f1d1bdb43ed52ad94a29c6a6ce8bbca3484ba5392e1dae7447c2f6b58124209614e884345e13914f5
)
vcpkg_extract_source_archive(
GNULIB_SOURCE_PATH
ARCHIVE "${GNULIB_SNAPSHOT}"
SOURCE_BASE ${GNULIB_REF}
)
file(REMOVE_RECURSE "${SOURCE_PATH}/gnulib")
file(RENAME "${GNULIB_SOURCE_PATH}" "${SOURCE_PATH}/gnulib")
include("${CURRENT_HOST_INSTALLED_DIR}/share/yasm-tool-helper/yasm-tool-helper.cmake")
yasm_tool_helper(OUT_VAR YASM)
file(TO_NATIVE_PATH "${YASM}" YASM)
if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic")
set(CONFIGURATION_RELEASE ReleaseDLL)
set(CONFIGURATION_DEBUG DebugDLL)
else()
set(CONFIGURATION_RELEASE Release)
set(CONFIGURATION_DEBUG Debug)
endif()
if(VCPKG_TARGET_IS_UWP)
string(APPEND CONFIGURATION_RELEASE WinRT)
string(APPEND CONFIGURATION_DEBUG WinRT)
endif()
set(_gnutlsproject "${SOURCE_PATH}/SMP/libgnutls.vcxproj")
file(READ "${_gnutlsproject}" _contents)
string(REPLACE [[<Import Project="$(VCTargetsPath)\BuildCustomizations\yasm.props" />]]
"<Import Project=\"${CURRENT_HOST_INSTALLED_DIR}/share/vs-yasm/yasm.props\" />"
_contents "${_contents}")
string(REPLACE [[<Import Project="$(VCTargetsPath)\BuildCustomizations\yasm.targets" />]]
"<Import Project=\"${CURRENT_HOST_INSTALLED_DIR}/share/vs-yasm/yasm.targets\" />"
_contents "${_contents}")
string(REGEX REPLACE "${VCPKG_ROOT_DIR}/installed/[^/]+/share" "${CURRENT_HOST_INSTALLED_DIR}/share" _contents "${_contents}") # Above already
file(WRITE "${_gnutlsproject}" "${_contents}")
if(VCPKG_CRT_LINKAGE STREQUAL "static")
set(RuntimeLibraryExt "")
else()
set(RuntimeLibraryExt "DLL")
endif()
# patch output library file path and name
foreach(PROPS IN ITEMS
"${SOURCE_PATH}/SMP/smp_deps.props"
"${SOURCE_PATH}/SMP/smp_winrt_deps.props")
vcpkg_replace_string(
"${PROPS}"
[=[_winrt</TargetName>]=]
[=[</TargetName>]=]
IGNORE_UNCHANGED
)
vcpkg_replace_string(
"${PROPS}"
[=[<TargetName>lib$(RootNamespace)]=]
[=[<TargetName>$(RootNamespace)]=]
)
endforeach()
# patch hogweed, gmp, nettle, zlib libraries file names; inject RuntimeLibrary property to control CRT linkage
foreach(VCXPROJ IN ITEMS
"${SOURCE_PATH}/SMP/libgnutls.vcxproj"
"${SOURCE_PATH}/SMP/libgnutls_winrt.vcxproj")
vcpkg_replace_string(
"${VCXPROJ}"
"_winrt.lib"
".lib"
IGNORE_UNCHANGED
)
vcpkg_replace_string(
"${VCXPROJ}"
"libhogweed"
"hogweed"
)
vcpkg_replace_string(
"${VCXPROJ}"
"hogweedd"
"hogweed"
)
vcpkg_replace_string(
"${VCXPROJ}"
"libgmp"
"gmp"
)
vcpkg_replace_string(
"${VCXPROJ}"
"gmpd"
"gmp"
)
vcpkg_replace_string(
"${VCXPROJ}"
"libnettle"
"nettle"
)
vcpkg_replace_string(
"${VCXPROJ}"
"nettled"
"nettle"
)
set(zlib_basename "zlib")
if(EXISTS "${CURRENT_INSTALLED_DIR}/lib/zs.lib")
set(zlib_basename "zs")
elseif(EXISTS "${CURRENT_INSTALLED_DIR}/lib/z.lib")
set(zlib_basename "z")
endif()
vcpkg_replace_string(
"${VCXPROJ}"
"libzlib"
"${zlib_basename}"
)
vcpkg_replace_string(
"${VCXPROJ}"
"zlib"
"${zlib_basename}"
)
vcpkg_replace_string(
"${VCXPROJ}"
[=[</DisableSpecificWarnings>]=]
[=[</DisableSpecificWarnings><ForcedIncludeFiles>gnutls_msvc_fixes.h;%(ForcedIncludeFiles)</ForcedIncludeFiles><RuntimeLibrary>$(RuntimeLibrary)</RuntimeLibrary>]=]
)
endforeach()
vcpkg_install_msbuild(
USE_VCPKG_INTEGRATION
SOURCE_PATH "${SOURCE_PATH}"
PROJECT_SUBPATH SMP/libgnutls.sln
PLATFORM ${TRIPLET_SYSTEM_ARCH}
LICENSE_SUBPATH LICENSE
RELEASE_CONFIGURATION ${CONFIGURATION_RELEASE}
DEBUG_CONFIGURATION ${CONFIGURATION_DEBUG}
SKIP_CLEAN
OPTIONS /p:YasmPath="${YASM}" /p:OutDir=..\\msvc
OPTIONS_DEBUG /p:RuntimeLibrary=MultiThreadedDebug${RuntimeLibraryExt}
OPTIONS_RELEASE /p:RuntimeLibrary=MultiThreaded${RuntimeLibraryExt}
)
get_filename_component(SOURCE_PATH_SUFFIX "${SOURCE_PATH}" NAME)
if(VCPKG_TARGET_IS_UWP)
set(WINRT_SUBFOLDER libgnutls_winrt)
endif()
file(INSTALL "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/${SOURCE_PATH_SUFFIX}/msvc/${WINRT_SUBFOLDER}/include" DESTINATION "${CURRENT_PACKAGES_DIR}")
set(GNUTLS_REQUIRES_PRIVATE "Requires.private: gmp, nettle, hogweed, libtasn1, zlib")
set(GNUTLS_LIBS_PRIVATE "-lcrypt32 -lws2_32 -lkernel32 -lncrypt")
set(prefix "${CURRENT_INSTALLED_DIR}")
set(exec_prefix "\${prefix}")
set(libdir "\${prefix}/lib")
set(includedir "\${prefix}/include")
set(GNUTLS_LIBS "-lgnutls")
configure_file("${SOURCE_PATH}/lib/gnutls.pc.in" "${CURRENT_PACKAGES_DIR}/lib/pkgconfig/gnutls.pc" @ONLY)
if(NOT VCPKG_BUILD_TYPE)
set(prefix "${CURRENT_INSTALLED_DIR}/debug")
set(exec_prefix "\${prefix}")
set(libdir "\${prefix}/lib")
set(includedir "\${prefix}/../include")
set(GNUTLS_LIBS "-lgnutlsd")
configure_file("${SOURCE_PATH}/lib/gnutls.pc.in" "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/gnutls.pc" @ONLY)
endif()
vcpkg_fixup_pkgconfig()
vcpkg_copy_pdbs()
file(COPY "${CURRENT_PORT_DIR}/vcpkg-cmake-wrapper.cmake" DESTINATION "${CURRENT_PACKAGES_DIR}/share/gnutls")
@@ -0,0 +1,2 @@
find_library(GNUTLS_LIBRARY NAMES gnutls gnutlsd NAMES_PER_DIR)
_find_package(${ARGS})
+23
View File
@@ -0,0 +1,23 @@
{
"name": "shiftmedia-libgnutls",
"version": "3.8.13",
"port-version": 0,
"description": "GnuTLS fork maintained by Savoir-faire Linux with native Visual Studio project build tools.",
"homepage": "https://github.com/savoirfairelinux/gnutls",
"license": "LGPL-2.1-only",
"supports": "windows & !arm & !mingw & !xbox",
"dependencies": [
"gmp",
"libtasn1",
"nettle",
{
"name": "vs-yasm",
"host": true
},
{
"name": "yasm-tool-helper",
"host": true
},
"zlib"
]
}
+1 -32
View File
@@ -1659,24 +1659,8 @@ OcspResponse::toString(const bool compact) const
gnutls_ocsp_cert_status_t
OcspResponse::getCertificateStatus() const
{
int ret;
unsigned int status;
// MSVC/vcpkg GnuTLS cast workaround
#if _WIN32 && (GNUTLS_VERSION_NUMBER == 0x030807)
ret = gnutls_ocsp_resp_get_single(response,
0,
NULL,
NULL,
NULL,
NULL,
reinterpret_cast<gnutls_ocsp_cert_status_t*>(&status),
NULL,
NULL,
NULL,
NULL);
#else
ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status, NULL, NULL, NULL, NULL);
#endif
int ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status, NULL, NULL, NULL, NULL);
if (ret < 0)
throw CryptoException(gnutls_strerror(ret));
return (gnutls_ocsp_cert_status_t) status;
@@ -1735,22 +1719,7 @@ OcspResponse::verifyDirect(const Certificate& crt, const Blob& nonce)
// Check certificate revocation status
unsigned int status_ocsp;
// MSVC/vcpkg GnuTLS cast workaround
#if _WIN32 && (GNUTLS_VERSION_NUMBER == 0x030807)
ret = gnutls_ocsp_resp_get_single(response,
0,
NULL,
NULL,
NULL,
NULL,
reinterpret_cast<gnutls_ocsp_cert_status_t*>(&status_ocsp),
NULL,
NULL,
NULL,
NULL);
#else
ret = gnutls_ocsp_resp_get_single(response, 0, NULL, NULL, NULL, NULL, &status_ocsp, NULL, NULL, NULL, NULL);
#endif
if (ret < 0)
throw CryptoException(gnutls_strerror(ret));
return (gnutls_ocsp_cert_status_t) status_ocsp;
+203 -45
View File
@@ -83,6 +83,7 @@ Dht::shutdown(ShutdownCallback cb, bool stop)
cb(false, {});
}
sr.second->announce.clear();
sr.second->cache.onReady(false);
sr.second->listeners.clear();
}
}
@@ -648,6 +649,30 @@ Dht::searchSendAnnounceValue(const Sp<Search>& sr, unsigned syncLevel)
}
}
void
Dht::searchNodeListenDeadline(const Sp<Search>& search, SearchNode& source, const Sp<Query>& query)
{
auto found = source.listenStatus.find(query);
if (found == source.listenStatus.end() || found->second.initialized || found->second.syncDeadline)
return;
std::weak_ptr<Search> pending = search;
found->second.syncDeadline
= scheduler.add(scheduler.time() + std::chrono::seconds(30),
[this, pending, query, node = source.node, registration = found->second.registration] {
if (auto search = pending.lock()) {
if (auto source = search->getNode(node)) {
auto status = source->listenStatus.find(query);
if (status != source->listenStatus.end()
&& status->second.registration == registration && !status->second.initialized) {
search->cache.onReady(query, false);
source->listenStatus.erase(query);
scheduler.edit(search->nextSearchStep, scheduler.time());
}
}
}
});
}
void
Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
{
@@ -664,7 +689,11 @@ Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
if (auto sn = sr->getNode(node)) {
if (not answer.ntoken.empty())
sn->refreshToken(answer.ntoken);
sn->onValues(query, std::move(answer), types, scheduler);
if (!sn->onValues(query, std::move(answer), types, scheduler)) {
sr->cache.onReady(query, false);
sn->listenStatus.erase(query);
} else
searchNodeListenDeadline(sr, *sn, query);
}
}
});
@@ -677,6 +706,10 @@ Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
// logger_->d(sr->id, n.node->id, "[search %s] [node %s] Sending 'listen'",
// sr->id.toString().c_str(), n.node->toString().c_str());
if (r != n.listenStatus.end() && !n.node->getSocket(r->second.socketId)) {
n.listenStatus.erase(r);
r = n.listenStatus.end();
}
if (r == n.listenStatus.end()) {
r = n.listenStatus
.emplace(std::piecewise_construct,
@@ -690,36 +723,57 @@ Dht::searchSynchedNodeListen(const Sp<Search>& sr, SearchNode& n)
}
}
});
} else if (not n.node->getSocket(r->second.socketId)) {
r->second.socketId = openListenSocket();
}
if (!r->second.registration)
r->second.registration = std::uniform_int_distribution<uint64_t>(1)(rd);
searchNodeListenDeadline(sr, n, query);
auto new_req = network_engine.sendListen(
n.node,
sr->id,
*query,
n.token,
r->second.socketId,
[this, ws, query](const net::Request& req, net::RequestAnswer&& answer) mutable {
[this, ws, query, socketId = r->second.socketId](const net::Request& req,
net::RequestAnswer&& answer) mutable {
/* on done */
if (auto sr = ws.lock()) {
scheduler.edit(sr->nextSearchStep, scheduler.time());
if (auto sn = sr->getNode(req.node)) {
auto status = sn->listenStatus.find(query);
if (status == sn->listenStatus.end() || status->second.socketId != socketId)
return;
scheduler.edit(sr->nextSearchStep, scheduler.time());
onListenDone(req.node, answer, sr);
auto job = scheduler.add(sn->getListenTime(query, getListenExpiration()),
std::bind(&Dht::searchStep, this, ws));
sn->onListenSynced(query, true, std::move(job));
sn->onListenSynced(query, status->second.initialized, std::move(job));
if ((answer.listenSync && answer.ntoken.empty()) || !answer.fields.empty()
|| !answer.values.empty()
|| !sn->confirmListen(query, answer.listenSync, types, scheduler)) {
sr->cache.onReady(query, false);
status = sn->listenStatus.find(query);
if (status != sn->listenStatus.end())
sn->listenStatus.erase(status);
} else
searchNodeListenDeadline(sr, *sn, query);
}
onListenDone(req.node, answer, sr);
}
},
[this, ws, query](const net::Request& req, bool over) mutable {
[this, ws, query, socketId = r->second.socketId](const net::Request& req, bool over) mutable {
/* on request expired */
if (auto sr = ws.lock()) {
scheduler.edit(sr->nextSearchStep, scheduler.time());
if (over)
if (auto sn = sr->getNode(req.node))
if (auto sn = sr->getNode(req.node)) {
auto status = sn->listenStatus.find(query);
if (status == sn->listenStatus.end() || status->second.socketId != socketId)
return;
scheduler.edit(sr->nextSearchStep, scheduler.time());
if (over) {
sr->cache.onReady(query, false);
sn->listenStatus.erase(query);
}
}
}
});
},
net::ListenSync {r->second.registration, 0, 0, 0});
// Here the request may have failed and the CachedListenStatus removed
r = n.listenStatus.find(query);
if (r != n.listenStatus.end()) {
@@ -751,6 +805,9 @@ Dht::searchStep(std::weak_ptr<Search> ws)
if (sr->refill_time + Node::NODE_EXPIRE_TIME < now and sr->nodes.size() - sr->getNumberOfBadNodes() < SEARCH_NODES)
refill(*sr);
if (sr->nodes.empty())
sr->cache.onReady();
/* Check if the first TARGET_NODES (8) live nodes have replied. */
if (sr->isSynced(now)) {
if (not(sr->callbacks.empty() and sr->announce.empty())) {
@@ -798,6 +855,7 @@ Dht::searchStep(std::weak_ptr<Search> ws)
// Announce requests
searchSendAnnounceValue(sr);
sr->cache.onSearchReady();
if (sr->callbacks.empty() && sr->announce.empty() && sr->listeners.empty())
sr->setDone();
@@ -954,30 +1012,47 @@ Dht::announce(
}
size_t
Dht::listenTo(const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f, const Sp<Query>& q)
Dht::listenTo(
const InfoHash& id, sa_family_t af, ValueCallback cb, Value::Filter f, const Sp<Query>& q, DoneCallbackSimple ready)
{
if (!isRunning(af))
if (!isRunning(af)) {
if (ready)
ready(true);
return 0;
}
// logger__ERR("[search %s IPv%c] Search_time is now in %lfs", sr->id.toString().c_str(), (sr->af == AF_INET) ? '4'
// : '6', print_duration(tm-clock::now()));
// logger__WARN("listenTo %s", id.toString().c_str());
auto& srs = searches(af);
auto srp = srs.find(id);
Sp<Search> sr = (srp == srs.end()) ? search(id, af) : srp->second;
if (!sr)
throw DhtException("Unable to create search");
Sp<Search> sr = (srp == srs.end() || srp->second->expired) ? search(id, af) : srp->second;
if (!sr) {
if (ready)
ready(false);
return 0;
}
if (logger_)
logger_->warn("[search {} IPv{}] Listen", id.to_view(), (af == AF_INET) ? '4' : '6');
return sr->listen(cb, std::move(f), q, scheduler);
sr->done = false;
scheduler.edit(sr->nextSearchStep, scheduler.time());
return sr->listen(cb, std::move(f), q, scheduler, std::move(ready));
}
size_t
Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where)
{
return listen(id, std::move(cb), std::move(f), std::move(where), {});
}
size_t
Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where, DoneCallbackSimple ready)
{
if (not id) {
if (logger_)
logger_->warn("Listen called with invalid key");
if (ready)
ready(false);
return 0;
}
scheduler.syncTime();
@@ -994,18 +1069,60 @@ Dht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where where)
size_t tokenlocal = 0;
if (st != store.end()) {
tokenlocal = st->second.listen(gcb, filter, query);
if (tokenlocal == 0)
if (tokenlocal == 0) {
if (ready)
ready(false);
return 0;
}
}
auto token4 = Dht::listenTo(id, AF_INET, gcb, filter, query);
auto token6 = token4 == 0 ? 0 : Dht::listenTo(id, AF_INET6, gcb, filter, query);
if (token6 == 0 && st != store.end()) {
st->second.cancelListen(tokenlocal);
struct InitialReadiness
{
DoneCallbackSimple callback;
unsigned remaining {2};
bool registered {false};
bool failed {false};
void notify(bool ok)
{
failed = failed || !ok;
if (remaining)
--remaining;
finish();
}
void finish()
{
if (registered && (failed || remaining == 0) && callback) {
auto ready = std::move(callback);
ready(!failed);
}
}
};
auto initial = std::make_shared<InitialReadiness>();
initial->callback = std::move(ready);
auto onReady = [initial](bool ok) {
initial->notify(ok);
};
auto token4 = Dht::listenTo(id, AF_INET, gcb, filter, query, onReady);
auto token6 = token4 == 0 && isRunning(AF_INET) ? 0 : Dht::listenTo(id, AF_INET6, gcb, filter, query, onReady);
initial->registered = true;
if ((isRunning(AF_INET) && token4 == 0) || (isRunning(AF_INET6) && token6 == 0)) {
if (st != store.end())
st->second.cancelListen(tokenlocal);
for (auto family : {AF_INET, AF_INET6}) {
auto listenerToken = family == AF_INET ? token4 : token6;
if (!listenerToken)
continue;
auto& familySearches = searches(family);
auto search = familySearches.find(id);
if (search != familySearches.end())
search->second->cancelListen(listenerToken, scheduler);
}
initial->notify(false);
return 0;
}
listeners.emplace(token, std::make_tuple(tokenlocal, token4, token6));
initial->finish();
return token;
}
@@ -1414,7 +1531,8 @@ Dht::storageChanged(const InfoHash& id, Storage& st, const Sp<Value>& v, bool ne
{},
std::move(vals),
l.second.query,
l.second.version);
l.second.version,
l.second.stream ? &l.second.stream->sync : nullptr);
}
}
}
@@ -1487,35 +1605,56 @@ Dht::storageStore(const InfoHash& id,
return std::get<0>(store);
}
void
Dht::storageAddListener(const InfoHash& id, const Sp<Node>& node, size_t socket_id, Query&& query, int version)
std::optional<net::ListenSync>
Dht::storageAddListener(const InfoHash& id,
const Sp<Node>& node,
size_t socket_id,
Query&& query,
int version,
const std::optional<net::ListenSync>& sync)
{
const auto& now = scheduler.time();
auto st = store.find(id);
if (st == store.end()) {
if (store.size() >= max_store_keys)
return;
throw net::DhtProtocolException(net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION);
st = store.emplace(id, now).first;
}
auto& node_listeners = st->second.listeners[node];
auto l = node_listeners.find(socket_id);
if (l != node_listeners.end() && sync
&& (!l->second.stream || l->second.stream->sync.registration != sync->registration)) {
node_listeners.erase(l);
l = node_listeners.end();
}
if (l == node_listeners.end()) {
auto vals = st->second.get(query.where.getFilter());
l = node_listeners.emplace(socket_id, Listener {now, std::move(query), version}).first;
auto vals = st->second.get(l->second.query.where.getFilter());
if (version == 2 && sync) {
l->second.stream = std::make_shared<Listener::Stream>();
l->second.stream->sync = {sync->registration,
std::uniform_int_distribution<uint64_t>(1)(rd),
0,
network_engine.getUpdateBatchCount(vals)};
}
if (not vals.empty()) {
network_engine.tellListener(node,
socket_id,
id,
WANT4 | WANT6,
makeToken(node->getAddr(), false),
dht4.buckets.findClosestNodes(id, now, TARGET_NODES),
dht6.buckets.findClosestNodes(id, now, TARGET_NODES),
std::move(vals),
query,
version);
auto requests = network_engine.tellListener(node,
socket_id,
id,
WANT4 | WANT6,
makeToken(node->getAddr(), false),
dht4.buckets.findClosestNodes(id, now, TARGET_NODES),
dht6.buckets.findClosestNodes(id, now, TARGET_NODES),
std::move(vals),
l->second.query,
version,
l->second.stream ? &l->second.stream->sync : nullptr);
if (l->second.stream)
l->second.stream->initialRequests = std::move(requests);
}
node_listeners.emplace(socket_id, Listener {now, std::forward<Query>(query), version});
} else
l->second.refresh(now, std::forward<Query>(query));
return l->second.stream ? std::optional<net::ListenSync>(l->second.stream->sync) : std::nullopt;
}
void
@@ -1562,7 +1701,13 @@ Dht::storageRemoved(const InfoHash& id, Storage& st, const std::vector<Sp<Value>
id.to_view(),
node_listeners.first->toString());
Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
network_engine.tellListenerExpired(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
network_engine.tellListenerExpired(node_listeners.first,
l.first,
id,
ntoken,
ids,
l.second.version,
l.second.stream ? &l.second.stream->sync : nullptr);
}
}
}
@@ -2015,7 +2160,7 @@ Dht::Dht(std::unique_ptr<net::DatagramSocket>&& sock,
std::bind(&Dht::onPing, this, _1),
std::bind(&Dht::onFindNode, this, _1, _2, _3),
std::bind(&Dht::onGetValues, this, _1, _2, _3, _4),
std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5, _6),
std::bind(&Dht::onListen, this, _1, _2, _3, _4, _5, _6, _7),
std::bind(&Dht::onAnnounce, this, _1, _2, _3, _4, _5),
std::bind(&Dht::onRefresh, this, _1, _2, _3, _4))
, persistPath(config.persist_path)
@@ -2714,7 +2859,13 @@ Dht::onGetValuesDone(const Sp<Node>& node, net::RequestAnswer& a, Sp<Search>& sr
}
net::RequestAnswer
Dht::onListen(Sp<Node> node, const InfoHash& hash, const Blob& token, size_t socket_id, const Query& query, int version)
Dht::onListen(Sp<Node> node,
const InfoHash& hash,
const Blob& token,
size_t socket_id,
const Query& query,
int version,
const std::optional<net::ListenSync>& sync)
{
if (not hash) {
if (logger_)
@@ -2728,9 +2879,11 @@ Dht::onListen(Sp<Node> node, const InfoHash& hash, const Blob& token, size_t soc
throw net::DhtProtocolException {net::DhtProtocolException::UNAUTHORIZED,
net::DhtProtocolException::LISTEN_WRONG_TOKEN};
}
if (version == 2 && (!sync || sync->epoch || sync->sequence || sync->snapshotEnd))
throw net::DhtProtocolException(net::DhtProtocolException::NON_AUTHORITATIVE_INFORMATION);
Query q = query;
storageAddListener(hash, node, socket_id, std::move(q), version);
net::RequestAnswer answer {};
answer.listenSync = storageAddListener(hash, node, socket_id, std::move(q), version, sync);
answer.ntoken = makeToken(node->getAddr(), false);
return answer;
}
@@ -2872,8 +3025,13 @@ Dht::storageRefresh(const InfoHash& id, Value::Id vid)
id.toString(),
node_listeners.first->toString());
Blob ntoken = makeToken(node_listeners.first->getAddr(), false);
network_engine
.tellListenerRefreshed(node_listeners.first, l.first, id, ntoken, ids, l.second.version);
network_engine.tellListenerRefreshed(node_listeners.first,
l.first,
id,
ntoken,
ids,
l.second.version,
l.second.stream ? &l.second.stream->sync : nullptr);
}
}
}
+520 -160
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
#include "dht_proxy_client.h"
#include <charconv>
#include "dhtrunner.h"
#include "op_cache.h"
#include "utils.h"
@@ -21,41 +22,64 @@ struct DhtProxyClient::OperationState
{
std::atomic_bool ok {true};
std::atomic_bool stop {false};
std::atomic_uint64_t generation {0};
std::atomic_bool inFlight {false};
};
struct DhtProxyClient::Listener
{
Listener(OpValueCache&& c)
: cache(std::move(c))
// The proxy server is a single logical source that re-delivers values
// (push refresh, listen restart): don't count duplicates.
Listener(ValueCallback&& cb)
: cache(std::move(cb), false)
{}
OpValueCache cache;
CacheValueCallback cb;
Sp<OperationState> opstate;
std::shared_ptr<http::Request> request;
std::shared_ptr<asio::steady_timer> requestDeadline;
bool needsRecovery {true};
std::string syncEpoch;
uint64_t syncRevision {0};
std::string observedEpoch;
uint64_t observedRevision {0};
std::set<std::string> retiredEpochs;
std::string requestToken;
uint64_t pushGeneration {0};
std::chrono::seconds retryDelay {1};
DoneCallbackSimple ready;
#ifdef OPENDHT_PUSH_NOTIFICATIONS
std::unique_ptr<asio::steady_timer> refreshSubscriberTimer;
std::unique_ptr<asio::system_timer> refreshSubscriberTimer;
#endif
};
struct PermanentPut
{
PermanentPut(const Sp<Value>& v, std::unique_ptr<asio::steady_timer>&& j, const Sp<std::atomic_bool>& o)
PermanentPut(const Sp<Value>& v, std::unique_ptr<asio::system_timer>&& j, const Sp<std::atomic_bool>& o)
: value(v)
, refreshPutTimer(std::move(j))
, ok(o)
{}
Sp<Value> value;
std::unique_ptr<asio::steady_timer> refreshPutTimer;
std::unique_ptr<asio::system_timer> refreshPutTimer;
Sp<std::atomic_bool> ok;
};
struct DhtProxyClient::ProxySearch
{
SearchCache ops {};
struct Readiness
{
std::atomic_bool pending {true};
DoneCallbackSimple callback;
size_t token {0};
};
SearchCache ops {false};
std::unique_ptr<asio::steady_timer> opExpirationTimer;
std::map<size_t, Listener> listeners {};
std::map<size_t, std::shared_ptr<Readiness>> readiness;
std::map<Value::Id, PermanentPut> puts {};
std::set<Sp<Value>> pendingPuts {};
};
@@ -77,6 +101,7 @@ struct LineSplit
return false;
}
const std::string& line() const { return line_; }
bool empty() const { return buf_.empty(); }
private:
std::deque<char> buf_ {};
@@ -200,18 +225,17 @@ DhtProxyClient::stop()
}
if (resolver)
resolver->cancel();
cancelAllListeners();
if (infoState_)
infoState_->cancel = true;
{
std::lock_guard lock(requestLock_);
for (auto& request : requests_)
request.second->cancel();
}
if (not httpContext_.stopped())
httpContext_.stop();
if (httpClientThread_.joinable())
httpClientThread_.join();
cancelAllListeners();
for (auto& request : requests_) {
request.second->cancel();
request.second->terminate(asio::error::operation_aborted);
}
requests_.clear();
}
}
@@ -239,19 +263,24 @@ DhtProxyClient::getLocalById(const InfoHash& k, Value::Id id) const
void
DhtProxyClient::cancelAllListeners()
{
std::lock_guard lock(searchLock_);
std::map<InfoHash, ProxySearch> canceled;
{
std::lock_guard lock(searchLock_);
canceled.swap(searches_);
}
if (logger_)
logger_->debug("[proxy:client] [listeners] [{} searches] cancel all", searches_.size());
for (auto& s : searches_) {
s.second.ops.cancelAll([&](size_t token) {
auto l = s.second.listeners.find(token);
if (l == s.second.listeners.end())
return;
l->second.opstate->stop.store(true);
l->second.request->cancel();
// implicit request.reset()
s.second.listeners.erase(token);
});
logger_->debug("[proxy:client] [listeners] [{} searches] cancel all", canceled.size());
for (auto& s : canceled) {
for (auto& listener : s.second.listeners) {
listener.second.opstate->stop.store(true);
if (listener.second.request)
listener.second.request->cancel();
}
s.second.ops.onReady(false);
s.second.ops.cancelAll([&](size_t token) { s.second.listeners.erase(token); });
for (const auto& readiness : s.second.readiness)
if (readiness.second->pending.exchange(false))
readiness.second->callback(false);
}
}
@@ -303,6 +332,12 @@ DhtProxyClient::periodic(const uint8_t*, size_t, SockAddr, const time_point& /*n
for (auto& callback : callbacks)
callback();
callbacks.clear();
{
std::lock_guard lock(lockCallbacks_);
callbacks = std::move(readinessCallbacks_);
}
for (auto& callback : callbacks)
callback();
return time_point::max();
}
@@ -424,7 +459,7 @@ DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_po
auto& search = searches_[key];
if (val->id) {
auto id = val->id;
auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
auto refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
proxy::OP_TIMEOUT - proxy::OP_MARGIN);
refreshPutTimer->async_wait(
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, id));
@@ -540,7 +575,7 @@ DhtProxyClient::doPut(const InfoHash& key, Sp<Value> val, DoneCallbackSimple cb,
auto it = search.pendingPuts.find(val);
if (it != search.pendingPuts.end()) {
auto sok = std::make_shared<std::atomic_bool>(ok);
auto refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
auto refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
proxy::OP_TIMEOUT
- proxy::OP_MARGIN);
refreshPutTimer->async_wait(
@@ -831,17 +866,52 @@ DhtProxyClient::getPublicAddress(sa_family_t family)
size_t
DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filter, Where where)
{
return listen(key, std::move(cb), std::move(filter), std::move(where), {});
}
size_t
DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filter, Where where, DoneCallbackSimple ready)
{
if (logger_)
logger_->debug("[proxy:client] [listen] [search {}]", key.to_view());
if (isDestroying_)
if (isDestroying_) {
if (ready)
ready(false);
return 0;
}
std::lock_guard lock(searchLock_);
auto& search = searches_[key];
auto query = std::make_shared<Query>(Select {}, std::move(where));
return search.ops
.listen(cb, query, std::move(filter), [this, key](Sp<Query>, ValueCallback cb, SyncCallback) -> size_t {
std::shared_ptr<ProxySearch::Readiness> readiness;
DoneCallbackSimple notify;
if (ready) {
readiness = std::make_shared<ProxySearch::Readiness>();
readiness->callback = std::move(ready);
notify = [this, key, readiness](bool ok) {
{
std::lock_guard callbackLock(lockCallbacks_);
readinessCallbacks_.emplace_back([this, key, readiness, ok] {
{
std::lock_guard searchLock(searchLock_);
if (!readiness->pending.exchange(false))
return;
auto search = searches_.find(key);
if (search != searches_.end())
search->second.readiness.erase(readiness->token);
}
readiness->callback(ok);
});
}
loopSignal_();
};
}
auto token = search.ops.listen(
cb,
query,
std::move(filter),
[this, key](Sp<Query> query, ValueCallback cb, SyncCallback synced) -> size_t {
// Find search
auto search = searches_.find(key);
if (search == searches_.end()) {
@@ -869,6 +939,16 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt
// Add cache callback
auto opstate = std::make_shared<OperationState>();
l->second.opstate = opstate;
synced(ListenSyncStatus::ADDED);
l->second.ready = [this, key, query, synced, synchronized = std::make_shared<bool>(false)](bool ok) {
if (*synchronized != ok) {
*synchronized = ok;
synced(ok ? ListenSyncStatus::SYNCED : ListenSyncStatus::UNSYNCED);
}
auto search = searches_.find(key);
if (search != searches_.end())
search->second.ops.onReady(query, ok);
};
l->second.cb = [this, key, token, opstate](const std::vector<Sp<Value>>& values,
bool expired,
system_clock::time_point t) {
@@ -891,8 +971,8 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt
* (if the proxy crash for any reason)
*/
if (!l->second.refreshSubscriberTimer)
l->second.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
l->second.refreshSubscriberTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT
l->second.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
l->second.refreshSubscriberTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT
- proxy::OP_MARGIN);
l->second.refreshSubscriberTimer->async_wait(
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, opstate));
@@ -909,9 +989,17 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt
header.method(restinio::http_method_subscribe());
header.request_target(fmt::format("/key/{}", key.to_view()));
}
sendListen(header, l->second.cb, opstate, l->second, method);
sendListen(header, l->second.cb, opstate, l->second, method, key, token);
return token;
});
},
notify);
if (readiness) {
readiness->token = token;
search.readiness.emplace(token, readiness);
if (!token)
notify(false);
}
return token;
}
void
@@ -955,6 +1043,11 @@ DhtProxyClient::cancelListen(const InfoHash& key, size_t gtoken)
return false;
auto& ops = it->second.ops;
bool canceled = ops.cancelListen(gtoken, std::chrono::steady_clock::now());
auto readiness = it->second.readiness.find(gtoken);
if (readiness != it->second.readiness.end()) {
readiness->second->pending = false;
it->second.readiness.erase(readiness);
}
// define real cancel listen only once
if (not it->second.opExpirationTimer)
@@ -1051,77 +1144,313 @@ DhtProxyClient::handleExpireListener(const asio::error_code& ec, const InfoHash&
}
}
bool
DhtProxyClient::reconcileListenerSnapshot(Listener& listener,
const std::vector<Sp<Value>>& values,
const Json::Value& marker)
{
if (!marker.isObject() || !marker["epoch"].isString() || marker["epoch"].asString().empty()
|| !marker["revision"].isUInt64() || !marker["snapshot"].isBool() || !marker["complete"].isBool()
|| !marker["complete"].asBool())
return false;
auto epoch = marker["epoch"].asString();
auto revision = marker["revision"].asUInt64();
if (listener.retiredEpochs.count(epoch) || (listener.syncEpoch == epoch && listener.syncRevision > revision)
|| (listener.observedEpoch == epoch && listener.observedRevision > revision))
return false;
bool full = marker["snapshot"].asBool();
if (!full
&& (listener.needsRecovery || epoch != listener.syncEpoch || revision != listener.syncRevision
|| !values.empty()))
return false;
if (full) {
std::set<Value::Id> ids;
for (const auto& value : values)
ids.insert(value->id);
std::vector<Sp<Value>> expired;
for (const auto& value : listener.cache.getValues())
if (!ids.count(value->id))
expired.emplace_back(value);
if (!listener.cache.onValue(values, false, system_clock::time_point::min())
|| (!expired.empty() && !listener.cache.onValue(expired, true, system_clock::time_point::max()))) {
listener.opstate->stop = true;
return false;
}
}
if (!listener.syncEpoch.empty() && listener.syncEpoch != epoch)
listener.retiredEpochs.insert(listener.syncEpoch);
while (listener.retiredEpochs.size() > 8)
listener.retiredEpochs.erase(listener.retiredEpochs.begin());
listener.syncEpoch = epoch;
listener.syncRevision = revision;
listener.observedEpoch = epoch;
listener.observedRevision = revision;
listener.needsRecovery = false;
listener.retryDelay = std::chrono::seconds(1);
return true;
}
void
DhtProxyClient::scheduleListenRetry([[maybe_unused]] const InfoHash& key,
[[maybe_unused]] size_t token,
[[maybe_unused]] Listener& listener)
{
#ifdef OPENDHT_PUSH_NOTIFICATIONS
if (!listener.refreshSubscriberTimer)
listener.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
listener.refreshSubscriberTimer->expires_after(listener.retryDelay);
listener.retryDelay = std::min(listener.retryDelay * 2, std::chrono::seconds(60));
listener.refreshSubscriberTimer->async_wait(
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, listener.opstate));
#endif
}
void
DhtProxyClient::sendListen(const restinio::http_request_header_t& header,
const CacheValueCallback& cb,
const Sp<OperationState>& opstate,
Listener& listener,
ListenMethod method)
ListenMethod method,
const InfoHash& key,
size_t token)
{
if (logger_)
logger_->error("[proxy:client] [listen] sendListen: {}", (int) method);
try {
auto generation = ++opstate->generation;
bool syncRequest = method != ListenMethod::LISTEN;
opstate->inFlight = syncRequest;
if (syncRequest)
opstate->ok = false;
auto request = buildRequest();
listener.request = request;
auto deadline = std::make_shared<asio::steady_timer>(httpContext_);
listener.requestDeadline = deadline;
if (syncRequest) {
deadline->expires_after(subscriptionRequestTimeout_);
deadline->async_wait([weak = std::weak_ptr<http::Request>(request)](const asio::error_code& ec) {
if (!ec)
if (auto request = weak.lock()) {
request->cancel();
request->terminate(asio::error::timed_out);
}
});
}
auto reqid = request->id();
request->set_header(header);
setHeaderFields(*request);
auto requestHeader = header;
if (method == ListenMethod::LISTEN)
requestHeader.request_target(std::string(header.request_target()) + "?sync=1");
request->set_header(requestHeader);
setHeaderFields(*request);
if (method == ListenMethod::LISTEN) {
request->set_connection_type(restinio::http_connection_header_t::keep_alive);
}
#ifdef OPENDHT_PUSH_NOTIFICATIONS
std::string body;
if (method != ListenMethod::LISTEN)
body = fillBody(method == ListenMethod::RESUBSCRIBE);
request->set_body(body);
#endif
auto rxBuf = std::make_shared<LineSplit>();
request->add_on_body_callback([this, reqid, opstate, rxBuf, cb](const char* at, size_t length) {
try {
auto& b = *rxBuf;
b.append(at, length);
// one value per body line
while (b.getLine('\n') and !opstate->stop) {
std::string err;
Json::Value json;
const auto& line = b.line();
if (!jsonReader_->parse(line.data(), line.data() + line.size(), &json, &err)) {
opstate->ok.store(false);
return;
}
if (json.size() == 0) { // it's the end
break;
}
auto value = std::make_shared<Value>(json);
if (cb) {
auto expired = json.get("expired", Json::Value(false)).asBool();
{
std::lock_guard lock(lockCallbacks_);
callbacks_.emplace_back([cb, value, opstate, expired]() {
if (not opstate->stop.load()
and not cb({value}, expired, system_clock::time_point::min()))
opstate->stop.store(true);
});
}
loopSignal_();
}
}
} catch (const std::exception& e) {
if (logger_)
logger_->error("[proxy:client] [listen] request #{} error in parsing: {}", reqid, e.what());
opstate->ok.store(false);
std::string requestToken;
if (syncRequest) {
Json::Value root;
getPushRequest(root);
requestToken = root["key"].asString();
if (listener.requestToken != requestToken) {
listener.requestToken = requestToken;
listener.observedEpoch.clear();
listener.observedRevision = 0;
listener.retiredEpochs.clear();
}
});
request->add_on_done_callback([this, opstate, reqid](const http::Response& response) {
if (response.status_code != 200) {
if (logger_)
logger_->error("[proxy:client] [listen] send request #{} failed with code={}",
reqid,
response.status_code);
opstate->ok.store(false);
if (not response.aborted and response.status_code == 0)
opFailed();
root["sync"] = Json::Value(Json::objectValue);
if (!listener.needsRecovery && !listener.syncEpoch.empty()) {
root["sync"]["epoch"] = listener.syncEpoch;
root["sync"]["revision"] = Json::UInt64(listener.syncRevision);
}
body = Json::writeString(jsonBuilder_, root);
}
request->set_body(body);
#else
std::string requestToken;
#endif
auto pushGeneration = listener.pushGeneration;
auto rxBuf = std::make_shared<LineSplit>();
auto snapshot = std::make_shared<std::vector<Sp<Value>>>();
auto marker = std::make_shared<Json::Value>();
auto valid = std::make_shared<bool>(true);
request->add_on_body_callback(
[this, reqid, key, token, opstate, generation, rxBuf, cb, syncRequest, snapshot, marker, valid](
const char* at, size_t length) {
if (opstate->generation != generation || opstate->stop)
return;
try {
auto& b = *rxBuf;
b.append(at, length);
// one value per body line
while (b.getLine('\n') and !opstate->stop) {
std::string err;
Json::Value json;
const auto& line = b.line();
if (!jsonReader_->parse(line.data(), line.data() + line.size(), &json, &err)) {
*valid = false;
return;
}
if (syncRequest) {
if (!marker->isNull()) {
*valid = false;
continue;
}
if (json.isMember("sync")) {
*marker = json["sync"];
} else if (json.size() != 0) {
snapshot->emplace_back(std::make_shared<Value>(json));
} else {
*valid = false;
}
continue;
}
if (json.isMember("sync")) {
if (!json["sync"].isObject() || !json["sync"]["complete"].isBool()) {
*valid = false;
return;
}
std::lock_guard callbackLock(lockCallbacks_);
callbacks_.emplace_back(
[this, key, token, opstate, generation, ok = json["sync"]["complete"].asBool()] {
std::lock_guard searchLock(searchLock_);
if (opstate->generation != generation || opstate->stop)
return;
auto search = searches_.find(key);
if (search == searches_.end())
return;
auto found = search->second.listeners.find(token);
if (found != search->second.listeners.end() && found->second.ready) {
auto ready = found->second.ready;
ready(ok);
}
});
loopSignal_();
continue;
}
if (json.size() == 0) { // it's the end
break;
}
auto value = std::make_shared<Value>(json);
if (cb) {
auto expired = json.get("expired", Json::Value(false)).asBool();
{
std::lock_guard lock(lockCallbacks_);
callbacks_.emplace_back([cb, value, opstate, generation, expired]() {
if (opstate->generation == generation && not opstate->stop.load()
and not cb({value}, expired, system_clock::time_point::min()))
opstate->stop.store(true);
});
}
loopSignal_();
}
}
} catch (const std::exception& e) {
if (logger_)
logger_->error("[proxy:client] [listen] request #{} error in parsing: {}", reqid, e.what());
*valid = false;
}
});
request->add_on_done_callback([this,
opstate,
generation,
reqid,
key,
token,
syncRequest,
rxBuf,
snapshot,
marker,
valid,
deadline,
requestToken,
pushGeneration](const http::Response& response) {
deadline->cancel();
bool current = opstate->generation == generation && !opstate->stop;
bool successful = response.status_code == 200 && !response.aborted && *valid
&& (!syncRequest || rxBuf->empty());
if (current && !isDestroying_) {
{
std::lock_guard callbackLock(lockCallbacks_);
callbacks_.emplace_back([this,
key,
token,
opstate,
generation,
successful,
syncRequest,
transportFailed = !response.aborted && response.status_code == 0,
snapshot,
marker,
requestToken,
pushGeneration]() {
std::lock_guard searchLock(searchLock_);
if (opstate->generation != generation || opstate->stop)
return;
auto search = searches_.find(key);
if (search == searches_.end())
return;
auto found = search->second.listeners.find(token);
if (found == search->second.listeners.end() || found->second.opstate != opstate)
return;
auto& listener = found->second;
if (!successful) {
opstate->ok = false;
if (transportFailed)
opFailed();
}
opstate->inFlight = false;
if (!syncRequest) {
if (!successful && listener.ready) {
auto ready = listener.ready;
ready(false);
}
return;
}
auto observedEpoch = listener.observedEpoch;
if (successful && reconcileListenerSnapshot(listener, *snapshot, *marker)) {
bool validateEpoch = listener.pushGeneration != pushGeneration && !observedEpoch.empty()
&& observedEpoch != listener.syncEpoch;
opstate->ok = true;
#ifdef OPENDHT_PUSH_NOTIFICATIONS
if (listener.refreshSubscriberTimer) {
listener.refreshSubscriberTimer->expires_at(system_clock::now() + proxy::OP_TIMEOUT
- proxy::OP_MARGIN);
listener.refreshSubscriberTimer->async_wait(std::bind(&DhtProxyClient::handleResubscribe,
this,
std::placeholders::_1,
key,
token,
opstate));
}
#endif
if (listener.ready) {
auto ready = listener.ready;
ready(true);
}
bool changedToken;
{
std::lock_guard infoLock(lockCurrentProxyInfos_);
changedToken = requestToken != deviceKey_;
}
if (changedToken || validateEpoch)
resubscribe(key, token, listener);
return;
}
if (opstate->stop)
return;
listener.needsRecovery = true;
opstate->ok = false;
if (listener.ready) {
auto ready = listener.ready;
ready(false);
}
scheduleListenRetry(key, token, listener);
});
}
loopSignal_();
}
if (not isDestroying_) {
std::lock_guard l(requestLock_);
@@ -1132,14 +1461,21 @@ DhtProxyClient::sendListen(const restinio::http_request_header_t& header,
std::lock_guard l(requestLock_);
requests_[reqid] = request;
}
request->add_on_status_callback([request, seconds = this->listenKeepIdle()](unsigned status_code) {
if (status_code == 200) {
// increase TCP_KEEPIDLE to save power
request->get_connection()->set_keepalive(seconds);
}
});
request->add_on_status_callback(
[weak = std::weak_ptr<http::Request>(request), seconds = this->listenKeepIdle()](unsigned status_code) {
if (status_code == 200) {
// increase TCP_KEEPIDLE to save power
if (auto request = weak.lock())
if (auto connection = request->get_connection())
connection->set_keepalive(seconds);
}
});
request->send();
} catch (const std::exception& e) {
opstate->ok = false;
opstate->inFlight = false;
listener.needsRecovery = true;
scheduleListenRetry(key, token, listener);
if (logger_)
logger_->error("[proxy:client] [listen] request failed: {}", e.what());
}
@@ -1192,9 +1528,9 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
for (auto& put : search.second.puts) {
doPut(key, put.second.value, [ok = put.second.ok](bool result) { *ok = result; }, time_point::max(), true);
if (!put.second.refreshPutTimer) {
put.second.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_);
put.second.refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_);
}
put.second.refreshPutTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT
put.second.refreshPutTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT
- proxy::OP_MARGIN);
put.second.refreshPutTimer->async_wait(
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, put.first));
@@ -1210,8 +1546,7 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
// Connectivity changed, refresh all subscribe
for (auto& search : searches_)
for (auto& listener : search.second.listeners)
if (!listener.second.opstate->ok)
resubscribe(search.first, listener.first, listener.second);
resubscribe(search.first, listener.first, listener.second);
return;
}
if (logger_)
@@ -1221,7 +1556,8 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
auto& listener = l.second;
if (auto opstate = listener.opstate)
opstate->stop = true;
listener.request->cancel();
if (listener.request)
listener.request->cancel();
listener.request.reset();
}
}
@@ -1237,16 +1573,34 @@ DhtProxyClient::restartListeners(const asio::error_code& ec)
restinio::http_request_header_t header;
header.method(restinio::http_method_get());
header.request_target("/key/" + search.first.toString() + "/listen");
sendListen(header, cb, opstate, listener, ListenMethod::LISTEN);
sendListen(header, cb, opstate, listener, ListenMethod::LISTEN, search.first, l.first);
}
}
}
#ifdef OPENDHT_PUSH_NOTIFICATIONS
static uint64_t
parsePushNumber(const std::string& text)
{
uint64_t number = 0;
auto result = std::from_chars(text.data(), text.data() + text.size(), number);
if (text.empty() || result.ec != std::errc {} || result.ptr != text.data() + text.size())
throw std::invalid_argument("Invalid push number");
return number;
}
#endif
PushNotificationResult
DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::string, std::string>& notification)
{
auto ret = PushNotificationResult::IgnoredNoOp;
#ifdef OPENDHT_PUSH_NOTIFICATIONS
auto sessionId = notification.find("s");
if (sessionId != notification.end() and sessionId->second != pushSessionId_) {
if (logger_)
logger_->debug("[proxy:client] [push] ignoring push for other session");
return PushNotificationResult::IgnoredWrongSession;
}
{
// If a push notification is received, the proxy is up and running
std::lock_guard l(lockCurrentProxyInfos_);
@@ -1258,12 +1612,6 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
}
auto launchLoop = false;
try {
auto sessionId = notification.find("s");
if (sessionId != notification.end() and sessionId->second != pushSessionId_) {
if (logger_)
logger_->debug("[proxy:client] [push] ignoring push for other session");
return PushNotificationResult::IgnoredWrongSession;
}
std::lock_guard lock(searchLock_);
auto timeout = notification.find("timeout");
if (timeout != notification.cend()) {
@@ -1275,10 +1623,10 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
auto vid = std::stoull(vidIt->second);
auto& put = search.puts.at(vid);
if (!put.refreshPutTimer)
put.refreshPutTimer = std::make_unique<asio::steady_timer>(httpContext_,
std::chrono::steady_clock::now());
put.refreshPutTimer = std::make_unique<asio::system_timer>(httpContext_,
std::chrono::system_clock::now());
else
put.refreshPutTimer->expires_at(std::chrono::steady_clock::now());
put.refreshPutTimer->expires_at(std::chrono::system_clock::now());
put.refreshPutTimer->async_wait(
std::bind(&DhtProxyClient::handleRefreshPut, this, std::placeholders::_1, key, vid));
ret = PushNotificationResult::PutRefresh;
@@ -1291,13 +1639,24 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
}
} else {
auto key = InfoHash(notification.at("key"));
system_clock::time_point sendTime = system_clock::time_point::min();
try {
sendTime = system_clock::time_point(std::chrono::milliseconds(std::stoull(notification.at("t"))));
} catch (...) {
}
auto& search = searches_.at(key);
auto expired = notification.find("exp");
auto expirationFlag = notification.find("expired");
bool isExpired = expired != notification.end()
|| (expirationFlag != notification.end() && expirationFlag->second == "true");
auto epoch = notification.find("epoch");
auto revision = notification.find("revision");
if (epoch == notification.end() || revision == notification.end() || epoch->second.empty())
throw std::invalid_argument("Incomplete push revision");
auto number = parsePushNumber(revision->second);
std::vector<Value::Id> ids;
if (isExpired) {
std::stringstream stream(expired != notification.end() ? expired->second : notification.at("ids"));
for (std::string id; std::getline(stream, id, ',');)
ids.emplace_back(parsePushNumber(id));
if (ids.empty())
throw std::invalid_argument("Empty expiration list");
}
for (auto& list : search.listeners) {
if (list.second.opstate->stop)
continue;
@@ -1305,29 +1664,19 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
logger_->debug("[proxy:client] [push] [search {}] received", key);
auto token = list.first;
auto opstate = list.second.opstate;
if (expired == notification.end()) {
auto cb = list.second.cb;
auto oldValues = list.second.cache.getValues();
get(
key,
[cb, sendTime](const std::vector<Sp<Value>>& vals) { return cb(vals, false, sendTime); },
[cb, oldValues, sendTime](bool /*ok*/) {
// Decrement old values refcount to expire values not
// present in the new list
cb(oldValues, true, sendTime);
});
ret = PushNotificationResult::Values;
} else {
std::stringstream ss(expired->second);
std::vector<Value::Id> ids;
while (ss.good()) {
std::string substr;
getline(ss, substr, ',');
ids.emplace_back(std::stoull(substr));
}
auto& listener = list.second;
if (listener.retiredEpochs.count(epoch->second)
|| (epoch->second == listener.syncEpoch && number <= listener.syncRevision)
|| (epoch->second == listener.observedEpoch && number <= listener.observedRevision))
continue;
++listener.pushGeneration;
listener.observedEpoch = epoch->second;
listener.observedRevision = number;
if (isExpired && !listener.needsRecovery && epoch->second == listener.syncEpoch
&& number > listener.syncRevision && number - listener.syncRevision == 1) {
{
std::lock_guard lockCb(lockCallbacks_);
callbacks_.emplace_back([this, key, token, opstate, ids, sendTime]() {
callbacks_.emplace_back([this, key, token, opstate, ids, epoch = epoch->second, number]() {
if (opstate->stop)
return;
std::lock_guard lock(searchLock_);
@@ -1335,15 +1684,27 @@ DhtProxyClient::pushNotificationReceived([[maybe_unused]] const std::map<std::st
if (s == searches_.end())
return;
auto l = s->second.listeners.find(token);
if (l == s->second.listeners.end())
if (l == s->second.listeners.end() || l->second.opstate != opstate)
return;
if (not opstate->stop and not l->second.cache.onValuesExpired(ids, sendTime))
opstate->stop = true;
auto& listener = l->second;
if (!listener.needsRecovery && listener.syncEpoch == epoch && number > listener.syncRevision
&& number - listener.syncRevision == 1) {
if (!listener.cache.onValuesExpired(ids, system_clock::time_point::max()))
opstate->stop = true;
listener.syncRevision = number;
} else if (listener.syncEpoch != epoch || number > listener.syncRevision) {
listener.needsRecovery = true;
resubscribe(key, token, listener);
}
});
}
launchLoop = true;
ret = PushNotificationResult::ValuesExpired;
} else {
if (epoch->second == listener.syncEpoch)
listener.needsRecovery = true;
resubscribe(key, token, listener);
}
ret = isExpired ? PushNotificationResult::ValuesExpired : PushNotificationResult::Values;
}
}
} catch (const std::out_of_range& e) {
@@ -1371,20 +1732,31 @@ DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
logger_->debug("[proxy:client] [resubscribe] [search {}]", key);
auto opstate = listener.opstate;
listener.request.reset(); // This will update ok to false
opstate->ok = true;
if (opstate->stop)
return;
listener.needsRecovery = listener.needsRecovery || (!opstate->ok && !opstate->inFlight);
if (opstate->inFlight && listener.request) {
if (listener.refreshSubscriberTimer && listener.refreshSubscriberTimer->expiry() <= system_clock::now())
scheduleListenRetry(key, token, listener);
return;
}
if (listener.request) {
++opstate->generation;
listener.request->cancel();
listener.request.reset();
}
restinio::http_request_header_t header;
header.method(restinio::http_method_subscribe());
header.request_target("/key/" + key.toString());
if (!listener.refreshSubscriberTimer) {
listener.refreshSubscriberTimer = std::make_unique<asio::steady_timer>(httpContext_);
listener.refreshSubscriberTimer = std::make_unique<asio::system_timer>(httpContext_);
}
listener.refreshSubscriberTimer->expires_at(std::chrono::steady_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN);
listener.refreshSubscriberTimer->expires_at(std::chrono::system_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN);
listener.refreshSubscriberTimer->async_wait(
std::bind(&DhtProxyClient::handleResubscribe, this, std::placeholders::_1, key, token, opstate));
auto vcb = listener.cb;
sendListen(header, vcb, opstate, listener, ListenMethod::RESUBSCRIBE);
sendListen(header, vcb, opstate, listener, ListenMethod::SUBSCRIBE, key, token);
#endif
}
@@ -1392,6 +1764,7 @@ DhtProxyClient::resubscribe([[maybe_unused]] const InfoHash& key,
void
DhtProxyClient::getPushRequest(Json::Value& body) const
{
std::lock_guard infoLock(lockCurrentProxyInfos_);
body["key"] = deviceKey_;
body["client_id"] = pushClientId_;
body["session_id"] = pushSessionId_;
@@ -1399,23 +1772,6 @@ DhtProxyClient::getPushRequest(Json::Value& body) const
body["platform"] = platform_;
}
std::string
DhtProxyClient::fillBody(bool resubscribe)
{
// Fill body with
// {
// "key":"device_key",
// }
Json::Value body;
getPushRequest(body);
if (resubscribe) {
// This is the first listen, we want to retrieve previous values.
body["refresh"] = true;
}
auto content = Json::writeString(jsonBuilder_, body) + "\n";
std::replace(content.begin(), content.end(), '\n', ' ');
return content;
}
#endif // OPENDHT_PUSH_NOTIFICATIONS
void
@@ -1432,6 +1788,10 @@ DhtProxyClient::setPushNotificationToken([[maybe_unused]] const std::string& tok
}
if (logger_)
logger_->debug("[proxy:client] [push] token changed, resubscribing");
if (token.empty()) {
restartListeners({});
return;
}
std::lock_guard lock(searchLock_);
for (auto& search : searches_) {
for (auto& listener : search.second.listeners) {
+194 -91
View File
@@ -210,7 +210,7 @@ DhtProxyServer::PermanentPut::msgpack_unpack(const msgpack::object& o)
clientId = cid->as<std::string>();
}
if (auto exp = findMapValue(o, "exp"sv)) {
expiration = from_time_t(exp->as<time_t>());
expiration = sys_clock::from_time_t(exp->as<time_t>());
}
if (auto token = findMapValue(o, "token"sv)) {
pushToken = token->as<std::string>();
@@ -240,7 +240,7 @@ DhtProxyServer::Listener::msgpack_unpack(const msgpack::object& o)
clientId = cid->as<std::string>();
}
if (auto exp = findMapValue(o, "exp"sv)) {
expiration = from_time_t(exp->as<time_t>());
expiration = sys_clock::from_time_t(exp->as<time_t>());
}
if (auto sid = findMapValue(o, "sid"sv)) {
if (not sessionCtx)
@@ -414,7 +414,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
logger_->debug("[proxy:server] Loading {} persistent puts", puts_.size());
for (auto& put : puts_) {
for (auto& pput : put.second.puts) {
pput.second.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
pput.second.expireTimer = std::make_unique<asio::system_timer>(io_context(),
pput.second.expiration);
pput.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut,
this,
@@ -437,7 +437,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
}
return json;
};
pput.second.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
pput.second.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
pput.second.expiration
- proxy::OP_MARGIN);
pput.second.expireNotifyTimer->async_wait(
@@ -450,7 +450,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
pput.second.topic));
}
#endif
dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, time_point::max(), true);
dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, dht::time_point::max(), true);
}
}
} else {
@@ -467,27 +467,31 @@ DhtProxyServer::loadState(Is& is, size_t size)
for (auto& listeners : pushListener.second.listeners) {
for (auto& listener : listeners.second) {
// start listening
listener.internalToken
= dht_->listen(listeners.first,
[this,
infoHash = listeners.first,
pushToken = pushListener.first,
type = listener.type,
clientId = listener.clientId,
sessionCtx = listener.sessionCtx,
topic = listener.topic](const std::vector<Sp<Value>>& values,
bool expired) {
return this->handlePushListen(infoHash,
pushToken,
type,
clientId,
sessionCtx,
topic,
values,
expired);
});
listener.internalToken = dht_->listen(
listeners.first,
[this,
infoHash = listeners.first,
pushToken = pushListener.first,
type = listener.type,
clientId = listener.clientId,
sessionCtx = listener.sessionCtx,
topic = listener.topic](const std::vector<Sp<Value>>& values, bool expired) {
return this->handlePushListen(infoHash,
pushToken,
type,
clientId,
sessionCtx,
topic,
values,
expired);
},
{},
{},
[this, sessionCtx = listener.sessionCtx](bool ok) {
onPushListenReady(sessionCtx, ok);
});
// expire notify
listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
listener.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
listener.expiration
- proxy::OP_MARGIN);
auto jsonProvider = [infoHash = listeners.first.toString(),
@@ -509,7 +513,7 @@ DhtProxyServer::loadState(Is& is, size_t size)
listener.type,
listener.topic));
// cancel push listen
listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(),
listener.expireTimer = std::make_unique<asio::system_timer>(io_context(),
listener.expiration);
listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
this,
@@ -560,6 +564,7 @@ DhtProxyServer::~DhtProxyServer()
l.expireNotifyTimer->cancel();
if (l.expireTimer)
l.expireTimer->cancel();
onPushListenReady(l.sessionCtx, false);
dht_->cancelListen(ls.first, std::move(l.internalToken));
}
}
@@ -571,6 +576,18 @@ DhtProxyServer::~DhtProxyServer()
ioContext_->stop();
if (serverThread_.joinable())
serverThread_.join();
{
// ~Request notifies DONE and its handler erases from requests_:
// detach pending push requests so nothing calls back into this object.
decltype(requests_) pending;
{
std::lock_guard l(requestLock_);
pending = std::move(requests_);
}
for (auto& r : pending)
r.second->add_on_state_change_callback({});
pending.clear();
}
if (logger_)
logger_->debug("[proxy:server] http server closed");
}
@@ -822,16 +839,29 @@ DhtProxyServer::listen(restinio::request_handle_t request, restinio::router::rou
auto& session = listeners_[request->connection_id()];
session.hash = infoHash;
session.response = response;
session.token = dht_->listen(infoHash, [this, response](const std::vector<Sp<Value>>& values, bool expired) {
for (const auto& value : values) {
auto jsonVal = value->toJson();
if (expired)
jsonVal["expired"] = true;
response->append_chunk(Json::writeString(jsonBuilder_, jsonVal) + "\n");
}
response->flush();
return true;
});
bool notifyReady = request->header().query() == "sync=1";
session.token = dht_->listen(
infoHash,
[this, response](const std::vector<Sp<Value>>& values, bool expired) {
for (const auto& value : values) {
auto jsonVal = value->toJson();
if (expired)
jsonVal["expired"] = true;
response->append_chunk(Json::writeString(jsonBuilder_, jsonVal) + "\n");
}
response->flush();
return true;
},
{},
{},
[this, response, notifyReady](bool ok) {
if (notifyReady) {
Json::Value marker;
marker["sync"]["complete"] = ok;
response->append_chunk(Json::writeString(jsonBuilder_, marker) + "\n");
response->flush();
}
});
return restinio::request_handling_status_t::accepted;
} catch (const std::exception& e) {
return serverError(*request);
@@ -907,6 +937,7 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
Json::Value r;
auto* char_data = reinterpret_cast<const char*>(request->body().data());
auto reader = std::unique_ptr<Json::CharReader>(jsonReaderBuilder_.newCharReader());
if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)) {
auto response = initHttpResponse(request->create_response(restinio::status_bad_request()));
response.set_body(RESP_MSG_JSON_INCORRECT);
@@ -945,20 +976,30 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
listIt->clientId = clientId;
listIt->sessionCtx = std::make_shared<PushSessionContext>(sessionId);
} else {
std::lock_guard l(listIt->sessionCtx->lock);
listIt->sessionCtx->sessionId = sessionId;
bool failed;
{
std::lock_guard l(listIt->sessionCtx->lock);
listIt->sessionCtx->sessionId = sessionId;
failed = listIt->sessionCtx->initialized && !listIt->sessionCtx->initialOk;
}
if (failed) {
onPushListenReady(listIt->sessionCtx, false);
dht_->cancelListen(infoHash, std::move(listIt->internalToken));
listIt->sessionCtx = std::make_shared<PushSessionContext>(sessionId);
newListener = true;
}
}
auto& listener = *listIt;
// Expiration
auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
// Expiration (wall-clock: persisted as epoch time, survives restarts)
auto timeout = sys_clock::now() + proxy::OP_TIMEOUT;
listener.expiration = timeout;
listener.type = type;
listener.topic = topic;
if (listener.expireNotifyTimer)
listener.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
else
listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(), timeout - proxy::OP_MARGIN);
listener.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(), timeout - proxy::OP_MARGIN);
auto jsonProvider = [h = infoHash.toString(), clientId, sessionCtx = listener.sessionCtx]() {
Json::Value json;
json["timeout"] = h;
@@ -975,7 +1016,7 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
listener.type,
listener.topic));
if (!listener.expireTimer)
listener.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
listener.expireTimer = std::make_unique<asio::system_timer>(io_context(), timeout);
else
listener.expireTimer->expires_at(timeout);
listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen,
@@ -985,56 +1026,84 @@ DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::
infoHash,
clientId));
// Send response
if (not newListener) {
if (logger_)
logger_->debug("[proxy:server] [subscribe {}] found [client {}]", infoHash, listener.clientId);
// Send response header
auto response = std::make_shared<ResponseByPartsBuilder>(
initHttpResponse(request->create_response<ResponseByParts>()));
response->flush();
if (!root["refresh"].asBool()) {
// No Refresh
dht_->get(
infoHash,
[this, response](const Sp<Value>& value) {
auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n";
response->append_chunk(output);
response->flush();
return true;
},
[response](bool) { response->done(); });
} else {
// Refresh
response->append_chunk("{}\n");
return response->done();
}
} else {
// =========== No existing listener for an infoHash ============
// Add listen on dht
if (newListener) {
if (logger_)
logger_->debug("[proxy:server] [subscribe {}] new", infoHash);
listener.internalToken = dht_->listen(infoHash,
[this,
infoHash,
pushToken,
type,
clientId,
sessionCtx = listener.sessionCtx,
topic](const std::vector<Sp<Value>>& values, bool expired) {
return this->handlePushListen(infoHash,
pushToken,
type,
clientId,
sessionCtx,
topic,
values,
expired);
});
// Send response header
listener.internalToken = dht_->listen(
infoHash,
[this, infoHash, pushToken, type, clientId, sessionCtx = listener.sessionCtx, topic](
const std::vector<Sp<Value>>& values, bool expired) {
return this
->handlePushListen(infoHash, pushToken, type, clientId, sessionCtx, topic, values, expired);
},
{},
{},
[this, sessionCtx = listener.sessionCtx](bool ok) { onPushListenReady(sessionCtx, ok); });
}
if (root["sync"].isObject()) {
auto sessionCtx = listener.sessionCtx;
auto finish = [this,
request,
context = std::weak_ptr<PushSessionContext>(sessionCtx),
sync = root["sync"]]() {
auto sessionCtx = context.lock();
if (!sessionCtx) {
auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable()));
response.done();
return;
}
std::vector<Json::Value> snapshotValues;
Json::Value marker;
bool ok;
{
std::lock_guard sessionLock(sessionCtx->lock);
ok = sessionCtx->initialOk;
bool snapshot = !sync["epoch"].isString() || sync["epoch"].asString() != sessionCtx->epoch
|| !sync["revision"].isUInt64()
|| sync["revision"].asUInt64() != sessionCtx->revision;
marker["sync"]["epoch"] = sessionCtx->epoch;
marker["sync"]["revision"] = Json::UInt64(sessionCtx->revision);
marker["sync"]["snapshot"] = snapshot;
marker["sync"]["complete"] = true;
if (snapshot)
for (const auto& value : sessionCtx->values)
snapshotValues.emplace_back(value.second);
}
if (!ok) {
auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable()));
response.done();
return;
}
auto response = std::make_shared<ResponseByPartsBuilder>(
initHttpResponse(request->create_response<ResponseByParts>()));
for (const auto& value : snapshotValues)
response->append_chunk(Json::writeString(jsonBuilder_, value) + "\n");
response->append_chunk(Json::writeString(jsonBuilder_, marker) + "\n");
response->done();
};
{
std::lock_guard sessionLock(sessionCtx->lock);
if (sessionCtx->initialized)
asio::post(io_context(), std::move(finish));
else
sessionCtx->pendingSnapshots.emplace_back(std::move(finish));
}
} else if (newListener || root["refresh"].asBool()) {
auto response = initHttpResponse(request->create_response());
response.set_body("{}\n");
return response.done();
} else {
auto response = std::make_shared<ResponseByPartsBuilder>(
initHttpResponse(request->create_response<ResponseByParts>()));
response->flush();
dht_->get(
infoHash,
[this, response](const Sp<Value>& value) {
response->append_chunk(Json::writeString(jsonBuilder_, value->toJson()) + "\n");
response->flush();
return true;
},
[response](bool) { response->done(); });
}
} catch (...) {
return serverError(*request);
@@ -1121,6 +1190,7 @@ DhtProxyServer::handleCancelPushListen(const asio::error_code& ec,
for (auto listener = listeners->second.begin(); listener != listeners->second.end();) {
if (listener->clientId == clientId) {
onPushListenReady(listener->sessionCtx, false);
if (dht_)
dht_->cancelListen(key, std::move(listener->internalToken));
listener = listeners->second.erase(listener);
@@ -1134,6 +1204,29 @@ DhtProxyServer::handleCancelPushListen(const asio::error_code& ec,
pushListeners_.erase(pushListener);
}
void
DhtProxyServer::onPushListenReady(const std::shared_ptr<PushSessionContext>& sessionCtx, bool ok)
{
if (!sessionCtx)
return;
std::vector<std::function<void()>> pending;
{
std::lock_guard sessionLock(sessionCtx->lock);
if (sessionCtx->initialized)
return;
sessionCtx->initialized = true;
sessionCtx->initialOk = ok;
pending = std::move(sessionCtx->pendingSnapshots);
sessionCtx->pendingSnapshots.clear();
}
for (auto& response : pending) {
if (ok)
asio::post(io_context(), std::move(response));
else
response();
}
}
bool
DhtProxyServer::handlePushListen(const InfoHash& infoHash,
const std::string& pushToken,
@@ -1152,7 +1245,15 @@ DhtProxyServer::handlePushListen(const InfoHash& infoHash,
{
std::lock_guard l(sessionCtx->lock);
for (const auto& value : values) {
if (expired)
sessionCtx->values.erase(value->id);
else
sessionCtx->values[value->id] = value->toJson();
}
json["s"] = sessionCtx->sessionId;
json["epoch"] = sessionCtx->epoch;
json["revision"] = std::to_string(++sessionCtx->revision);
}
// Build a comma-separated list of ids from the values. This is intended to be used when
@@ -1175,6 +1276,8 @@ DhtProxyServer::handlePushListen(const InfoHash& infoHash,
if (expired && values.size() < 2) {
json["exp"] = json["ids"];
}
if (expired)
json["expired"] = "true";
auto minPriority = 1000u;
for (const auto& v : values)
@@ -1405,7 +1508,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
topic = pVal["topic"].asString();
}
std::lock_guard lock(lockSearchPuts_);
auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT;
auto timeout = sys_clock::now() + proxy::OP_TIMEOUT;
auto& sPuts = puts_[infoHash];
if (value->id == Value::INVALID_ID) {
for (auto& pp : sPuts.puts) {
@@ -1439,7 +1542,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
pput.expiration = timeout;
if (not pput.expireTimer) {
// cancel permanent put
pput.expireTimer = std::make_unique<asio::steady_timer>(io_context(), timeout);
pput.expireTimer = std::make_unique<asio::system_timer>(io_context(), timeout);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
if (not pushToken.empty()) {
pput.pushToken = pushToken;
@@ -1478,7 +1581,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
return json;
};
if (!pput.expireNotifyTimer)
pput.expireNotifyTimer = std::make_unique<asio::steady_timer>(io_context(),
pput.expireNotifyTimer = std::make_unique<asio::system_timer>(io_context(),
timeout - proxy::OP_MARGIN);
else
pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN);
@@ -1507,7 +1610,7 @@ DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_
response.done();
}
},
time_point::max(),
dht::time_point::max(),
permanent);
return restinio::request_handling_status_t::accepted;
} else {
+13 -1
View File
@@ -819,16 +819,24 @@ DhtRunner::query(const InfoHash& hash, QueryCallback cb, DoneCallback done_cb, Q
std::future<size_t>
DhtRunner::listen(InfoHash hash, ValueCallback vcb, Value::Filter f, Where w)
{
return listen(hash, std::move(vcb), std::move(f), std::move(w), {});
}
std::future<size_t>
DhtRunner::listen(InfoHash hash, ValueCallback vcb, Value::Filter f, Where w, DoneCallbackSimple ready)
{
auto ret_token = std::make_shared<std::promise<size_t>>();
std::unique_lock lck(storage_mtx);
if (running != State::Running) {
lck.unlock();
ret_token->set_value(0);
if (ready)
ready(false);
return ret_token->get_future();
}
pending_ops.emplace([=](SecureDht& dht) mutable {
ret_token->set_value(dht.listen(hash, std::move(vcb), std::move(f), std::move(w)));
ret_token->set_value(dht.listen(hash, std::move(vcb), std::move(f), std::move(w), std::move(ready)));
});
cv.notify_all();
return ret_token->get_future();
@@ -1239,6 +1247,10 @@ DhtRunner::pushNotificationReceived([[maybe_unused]] const std::map<std::string,
auto ret_token = std::make_shared<std::promise<PushNotificationResult>>();
auto future = ret_token->get_future();
std::lock_guard lck(storage_mtx);
if (running == State::Idle) {
ret_token->set_value(PushNotificationResult::IgnoredStopped);
return future;
}
pending_ops_prio.emplace([ret_token, this, data](SecureDht&) {
if (dht_)
ret_token->set_value(dht_->pushNotificationReceived(data));
+34 -8
View File
@@ -233,11 +233,32 @@ Connection::Connection(asio::io_context& ctx,
Connection::~Connection()
{
close();
// No asynchronous operation can be pending here: their handlers keep
// the connection alive (see wrapCallback), so closing synchronously
// is safe regardless of the calling thread.
doClose();
}
void
Connection::close()
{
// asio sockets are not thread-safe. Closing from a thread other than the
// one running the io_context races with in-flight operations on the same
// socket (e.g. async_connect closing and reopening it to try the next
// endpoint), corrupting the reactor's descriptor state.
// Always perform the close on the io_context thread.
if (ctx_.get_executor().running_in_this_thread()) {
doClose();
return;
}
asio::post(ctx_, [w = weak_from_this()] {
if (auto sthis = w.lock())
sthis->doClose();
});
}
void
Connection::doClose()
{
std::lock_guard lock(mutex_);
asio::error_code ec;
@@ -631,12 +652,13 @@ Connection::async_handshake(HandlerCb cb)
{
std::lock_guard lock(mutex_);
if (ssl_socket_) {
std::weak_ptr<Connection> wthis = shared_from_this();
// Keep the connection alive while the handshake is in flight, like
// every other asynchronous operation (see wrapCallback).
ssl_socket_
->async_handshake(asio::ssl::stream<asio::ip::tcp::socket>::client, [wthis, cb](const asio::error_code& ec) {
->async_handshake(asio::ssl::stream<asio::ip::tcp::socket>::client, [sthis = shared_from_this(), cb](const asio::error_code& ec) {
if (ec == asio::error::operation_aborted)
return;
if (auto sthis = wthis.lock()) {
{
auto& this_ = *sthis;
auto verify_ec = SSL_get_verify_result(this_.ssl_socket_->asio_ssl_stream().native_handle());
if (this_.logger_) {
@@ -1505,9 +1527,8 @@ Request::terminate(const asio::error_code& ec)
return;
response_.aborted = ec == asio::error::operation_aborted;
if (ec == asio::error::basic_errors::broken_pipe)
response_.status_code
= 0U; // Avoid to give a successful answer (happen with a broken pipe, takes the last status)
if (ec and ec != asio::error::eof)
response_.status_code = 0U;
if (logger_) {
if (ec and ec != asio::error::eof and !response_.aborted)
@@ -1554,7 +1575,12 @@ Request::handle_response(const asio::error_code& ec, size_t /* n_bytes */)
terminate(ec);
return;
}
auto request = (ec == asio::error::eof) ? std::string {} : conn_->read_bytes();
if (ec == asio::error::eof) {
if (!parser_ || llhttp_finish(parser_.get()) != HPE_OK || state_ != State::DONE)
terminate(asio::error::broken_pipe);
return;
}
auto request = conn_->read_bytes();
enum llhttp_errno ret = llhttp_execute(parser_.get(), request.c_str(), request.size());
if (ret != HPE_OK && ret != HPE_PAUSED) {
if (logger_)
+14
View File
@@ -5,6 +5,8 @@
#include "value.h"
#include "utils.h"
#include "callbacks.h"
#include "network_engine.h"
#include "request.h"
namespace dht {
@@ -13,9 +15,21 @@ namespace dht {
*/
struct Listener
{
struct Stream
{
net::ListenSync sync;
std::vector<Sp<net::Request>> initialRequests;
~Stream()
{
for (const auto& request : initialRequests)
request->cancel();
}
};
time_point time;
Query query;
int version;
std::shared_ptr<Stream> stream;
Listener(time_point t, Query&& q, int version = 0)
: time(t)
+101 -34
View File
@@ -103,6 +103,7 @@ RequestAnswer::RequestAnswer(ParsedMessage&& msg)
, fields(std::move(msg.fields))
, nodes4(std::move(msg.nodes4))
, nodes6(std::move(msg.nodes6))
, listenSync(std::move(msg.listenSync))
{}
NetworkEngine::NetworkEngine(InfoHash& myid,
@@ -144,7 +145,7 @@ NetworkEngine::~NetworkEngine()
clear();
}
void
std::vector<Sp<Request>>
NetworkEngine::tellListener(const Sp<Node>& node,
Tid socket_id,
const InfoHash& hash,
@@ -154,12 +155,13 @@ NetworkEngine::tellListener(const Sp<Node>& node,
std::vector<Sp<Node>>&& nodes6,
std::vector<Sp<Value>>&& values,
const Query& query,
int version)
int version,
ListenSync* sync)
{
auto nnodes = bufferNodes(node->getFamily(), hash, want, nodes, nodes6);
try {
if (version >= 1) {
sendUpdateValues(node, hash, std::move(values), scheduler.time(), ntoken, socket_id);
return sendUpdateValues(node, hash, std::move(values), scheduler.time(), ntoken, socket_id, sync);
} else {
sendNodesValues(node->getAddr(), socket_id, nnodes.first, nnodes.second, std::move(values), query, ntoken);
}
@@ -167,6 +169,7 @@ NetworkEngine::tellListener(const Sp<Node>& node,
if (logger_)
logger_->error("Unable to send value: buffer not large enough.");
}
return {};
}
void
@@ -175,7 +178,8 @@ NetworkEngine::tellListenerRefreshed(const Sp<Node>& n,
const InfoHash&,
const Blob& token,
const std::vector<Value::Id>& values,
int version)
int version,
ListenSync* sync)
{
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
@@ -183,7 +187,15 @@ NetworkEngine::tellListenerRefreshed(const Sp<Node>& n,
pk.pack_map(4 + (version >= 1 ? 1 : 0) + (config.network ? 1 : 0) + (config.is_client ? 1 : 0));
pk.pack(version >= 1 ? KEY_A : KEY_U);
pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty() ? 1 : 0) + (not token.empty() ? 1 : 0));
pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty() ? 1 : 0) + (not token.empty() ? 1 : 0)
+ (sync ? 2 : 0));
if (sync) {
++sync->sequence;
pk.pack(KEY_VERSION);
pk.pack(2);
pk.pack(KEY_LISTEN_SYNC);
pk.pack(*sync);
}
pk.pack(KEY_REQ_ID);
pk.pack(myid);
if (version >= 1) {
@@ -246,7 +258,8 @@ NetworkEngine::tellListenerExpired(const Sp<Node>& n,
const InfoHash&,
const Blob& token,
const std::vector<Value::Id>& values,
int version)
int version,
ListenSync* sync)
{
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
@@ -254,7 +267,15 @@ NetworkEngine::tellListenerExpired(const Sp<Node>& n,
pk.pack_map(4 + (version >= 1 ? 1 : 0) + (config.network ? 1 : 0) + (config.is_client ? 1 : 0));
pk.pack(version >= 1 ? KEY_A : KEY_U);
pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty() ? 1 : 0) + (not token.empty() ? 1 : 0));
pk.pack_map(1 + (version >= 1 ? 1 : 0) + (not values.empty() ? 1 : 0) + (not token.empty() ? 1 : 0)
+ (sync ? 2 : 0));
if (sync) {
++sync->sequence;
pk.pack(KEY_VERSION);
pk.pack(2);
pk.pack(KEY_LISTEN_SYNC);
pk.pack(*sync);
}
pk.pack(KEY_REQ_ID);
pk.pack(myid);
if (version >= 1) {
@@ -784,10 +805,15 @@ NetworkEngine::process(std::unique_ptr<ParsedMessage>&& msg, const SockAddr& fro
if (logIncoming_ and logger_)
logger_->debug("[node {}] got 'listen' request for {}", node->toString(), msg->info_hash.toString());
++in_stats.listen;
RequestAnswer answer
= onListen(node, msg->info_hash, msg->token, msg->socket_id, std::move(msg->query), msg->version);
RequestAnswer answer = onListen(node,
msg->info_hash,
msg->token,
msg->socket_id,
std::move(msg->query),
msg->version,
msg->listenSync);
auto nnodes = bufferNodes(from.getFamily(), msg->info_hash, msg->want, answer.nodes4, answer.nodes6);
sendListenConfirmation(from, msg->tid, answer.ntoken);
sendListenConfirmation(from, msg->tid, answer.ntoken, answer.listenSync);
break;
}
case MessageType::UpdateValue: {
@@ -1307,7 +1333,8 @@ NetworkEngine::sendListen(const Sp<Node>& n,
const Blob& token,
Tid socketId,
RequestCb&& on_done,
RequestExpiredCb&& on_expired)
RequestExpiredCb&& on_expired,
const std::optional<ListenSync>& sync)
{
Tid tid(n->getNewTid());
msgpack::sbuffer buffer;
@@ -1316,11 +1343,15 @@ NetworkEngine::sendListen(const Sp<Node>& n,
auto has_query = not query.where.empty() or not query.select.empty();
pk.pack(KEY_A);
pk.pack_map(5 + has_query);
pk.pack_map(5 + has_query + (sync ? 1 : 0));
pk.pack(KEY_REQ_ID);
pk.pack(myid);
pk.pack(KEY_VERSION);
pk.pack(1);
pk.pack(sync ? 2 : 1);
if (sync) {
pk.pack(KEY_LISTEN_SYNC);
pk.pack(*sync);
}
pk.pack(KEY_REQ_H);
pk.pack(hash);
pk.pack(KEY_REQ_TOKEN);
@@ -1366,14 +1397,23 @@ NetworkEngine::sendListen(const Sp<Node>& n,
}
void
NetworkEngine::sendListenConfirmation(const SockAddr& addr, Tid tid, const Blob& token)
NetworkEngine::sendListenConfirmation(const SockAddr& addr,
Tid tid,
const Blob& token,
const std::optional<ListenSync>& sync)
{
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> pk(&buffer);
pk.pack_map(4 + (config.network ? 1 : 0) + (config.is_client ? 1 : 0));
pk.pack(KEY_R);
pk.pack_map(2 + (not token.empty() ? 1 : 0));
pk.pack_map(2 + (not token.empty() ? 1 : 0) + (sync ? 2 : 0));
if (sync) {
pk.pack(KEY_VERSION);
pk.pack(2);
pk.pack(KEY_LISTEN_SYNC);
pk.pack(*sync);
}
pk.pack(KEY_REQ_ID);
pk.pack(myid);
insertAddr(pk, addr);
@@ -1471,29 +1511,50 @@ NetworkEngine::sendAnnounceValue(const Sp<Node>& n,
return req;
}
void
static std::vector<size_t>
updateBatchEnds(const std::vector<Sp<Value>>& values)
{
std::vector<size_t> ends;
size_t totalSize = 0;
size_t count = 0;
for (size_t index = 0; index < values.size(); ++index) {
auto size = packMsg(*values[index]).size();
if (count && (count == 32 || totalSize + size > 56 * 1024)) {
ends.emplace_back(index);
totalSize = 0;
count = 0;
}
totalSize += size;
++count;
}
if (count)
ends.emplace_back(values.size());
return ends;
}
size_t
NetworkEngine::getUpdateBatchCount(const std::vector<Sp<Value>>& values)
{
return updateBatchEnds(values).size();
}
std::vector<Sp<Request>>
NetworkEngine::sendUpdateValues(const Sp<Node>& n,
const InfoHash& infohash,
std::vector<Sp<Value>>&& values,
time_point created,
const Blob& token,
size_t socket_id)
size_t socket_id,
ListenSync* sync)
{
size_t total_size = 0;
auto b = values.begin(), e = values.begin();
while (e != values.end()) {
if (total_size >= MAX_MESSAGE_VALUE_SIZE) {
sendUpdateValues(n, infohash, b, e, created, token, socket_id);
b = e;
total_size = 0;
}
total_size += (*e)->size();
++e;
}
if (b != e) {
sendUpdateValues(n, infohash, b, e, created, token, socket_id);
std::vector<Sp<Request>> requests;
size_t begin = 0;
for (auto end : updateBatchEnds(values)) {
requests.emplace_back(
sendUpdateValues(n, infohash, values.begin() + begin, values.begin() + end, created, token, socket_id, sync));
begin = end;
}
return requests;
}
Sp<Request>
@@ -1503,7 +1564,8 @@ NetworkEngine::sendUpdateValues(const Sp<Node>& n,
std::vector<Sp<Value>>::iterator end,
time_point created,
const Blob& token,
size_t socket_id)
size_t socket_id,
ListenSync* sync)
{
Tid tid(n->getNewTid());
Tid sid(socket_id);
@@ -1513,11 +1575,16 @@ NetworkEngine::sendUpdateValues(const Sp<Node>& n,
pk.pack_map(5 + (config.network ? 1 : 0) + (config.is_client ? 1 : 0));
pk.pack(KEY_A);
pk.pack_map((created < scheduler.time() ? 7 : 6));
pk.pack_map((created < scheduler.time() ? 7 : 6) + (sync ? 1 : 0));
pk.pack(KEY_REQ_ID);
pk.pack(myid);
pk.pack(KEY_VERSION);
pk.pack(1);
pk.pack(sync ? 2 : 1);
if (sync) {
++sync->sequence;
pk.pack(KEY_LISTEN_SYNC);
pk.pack(*sync);
}
pk.pack(KEY_REQ_H);
pk.pack(infohash);
pk.pack(KEY_REQ_SID);
+32 -20
View File
@@ -12,18 +12,20 @@ OpValueCache::onValuesAdded(const std::vector<Sp<Value>>& vals, const system_clo
std::vector<Sp<Value>> newValues;
for (const auto& v : vals) {
auto viop = values.emplace(v->id, v);
auto& stored = viop.first->second;
if (viop.second) {
newValues.emplace_back(v);
} else if (*viop.first->second.data != *v) {
// Special case for edition
if (v->seq > viop.first->second.data->seq) {
viop.first->second.data = v;
newValues.emplace_back(v);
}
} else {
viop.first->second.refCount++;
} else if (*stored.data != *v) {
if (v->seq <= stored.data->seq)
continue; // older version from a lagging source
// Edited: only the reporting source is known to hold this version
stored.data = v;
stored.refCount = 1;
newValues.emplace_back(v);
} else if (countSources) {
stored.refCount++;
}
viop.first->second.updated = t;
stored.updated = t;
}
return newValues.empty() ? true : callback(newValues, false);
}
@@ -37,6 +39,10 @@ OpValueCache::onValuesExpired(const std::vector<Sp<Value>>& vals, const system_c
if (vit != values.end()) {
if (vit->second.updated > t)
continue;
// 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--;
@@ -168,13 +174,17 @@ SearchCache::getOp(const Sp<Query>& q) const
}
size_t
SearchCache::listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filter&& filter, const OnListen& onListen)
SearchCache::listen(const ValueCallback& get_cb,
const Sp<Query>& q,
Value::Filter&& filter,
const OnListen& onListen,
DoneCallbackSimple ready)
{
// find exact match
auto op = getOp(q);
if (op == ops.end()) {
// New query
op = ops.emplace(q, std::make_unique<OpCache>()).first;
op = ops.emplace(q, std::make_unique<OpCache>(countSources_, waitForSearch_)).first;
auto& cache = *op->second;
cache.searchToken = onListen(
q,
@@ -184,7 +194,7 @@ SearchCache::listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filt
auto token = nextToken_++;
if (nextToken_ == 0)
nextToken_++;
return op->second->addListener(token, get_cb, q, std::move(filter)) ? token : 0;
return op->second->addListener(token, get_cb, q, std::move(filter), std::move(ready)) ? token : 0;
}
bool
@@ -240,15 +250,17 @@ SearchCache::expire(const time_point& now, const std::function<void(size_t)>& on
bool
SearchCache::get(const Value::Filter& f, const Sp<Query>& q, const GetCallback& gcb, const DoneCallback& dcb) const
{
if (not gcb)
return false;
auto op = getOp(q);
if (op != ops.end()) {
auto vals = op->second->get(f);
if ((not vals.empty() and not gcb(vals)) or op->second->isSynced()) {
dcb(true, {});
return true;
}
}
return false;
if (op == ops.end() or not op->second->isSynced())
return false;
auto vals = op->second->get(f);
if (not vals.empty())
gcb(vals); // a consumer stopping early is not a failure
if (dcb)
dcb(true, {});
return true;
}
std::vector<Sp<Value>>
+108 -16
View File
@@ -21,19 +21,29 @@ struct OpCacheValueStorage
class OpValueCache
{
public:
OpValueCache(ValueCallback&& cb) noexcept
/**
* @param countSources When true, a value is kept until every source
* that reported it has expired it (per-node caches,
* IPv4/IPv6 searches, local storage). When false,
* re-deliveries of the same value are not counted
* and a single expiration removes it (single
* logical source, e.g. a proxy server).
*/
OpValueCache(ValueCallback&& cb, bool countSources = true) noexcept
: callback(std::forward<ValueCallback>(cb))
, countSources(countSources)
{}
explicit OpValueCache(OpValueCache&& o) noexcept
: values(std::move(o.values))
, callback(std::move(o.callback))
, countSources(o.countSources)
{
o.callback = {};
}
static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel)
static ValueCallback cacheCallback(ValueCallback&& cb, std::function<void()>&& onCancel, bool countSources = true)
{
return [cache = std::make_shared<OpValueCache>(std::forward<ValueCallback>(cb)),
return [cache = std::make_shared<OpValueCache>(std::forward<ValueCallback>(cb), countSources),
onCancel = std::move(onCancel)](const std::vector<Sp<Value>>& vals, bool expired) {
auto ret = cache->onValue(vals, expired);
if (not ret)
@@ -89,44 +99,95 @@ private:
size_t syncedNodes {0};
std::map<Value::Id, OpCacheValueStorage> values {};
ValueCallback callback;
bool countSources {true};
};
class OpCache
{
public:
OpCache()
: cache([this](const std::vector<Sp<Value>>& vals, bool expired) {
if (expired)
onValuesExpired(vals);
else
onValuesAdded(vals);
return true;
})
explicit OpCache(bool countSources = true, bool waitForSearch = false)
: cache(
[this](const std::vector<Sp<Value>>& vals, bool expired) {
if (expired)
onValuesExpired(vals);
else
onValuesAdded(vals);
return true;
},
countSources)
, searchReady(!waitForSearch)
{}
bool onValue(const std::vector<Sp<Value>>& vals, bool expired) { return cache.onValue(vals, expired); }
void onNodeChanged(ListenSyncStatus status) { cache.onNodeChanged(status); }
void onNodeChanged(ListenSyncStatus status)
{
cache.onNodeChanged(status);
if (searchReady && cache.isSynced())
onReady();
else
initialReady = false;
}
void onReady(bool ok = true)
{
initialReady = ok;
searchReady = ok;
std::vector<size_t> pending;
pending.reserve(readyCallbacks.size());
for (const auto& callback : readyCallbacks)
pending.emplace_back(callback.first);
for (auto token : pending) {
auto found = readyCallbacks.find(token);
if (found == readyCallbacks.end())
continue;
auto callback = std::move(found->second);
readyCallbacks.erase(found);
callback(ok);
}
}
void onSearchReady()
{
searchReady = true;
if (cache.isSynced())
onReady();
}
void onValuesAdded(const std::vector<Sp<Value>>& vals);
void onValuesExpired(const std::vector<Sp<Value>>& vals);
bool addListener(size_t token, const ValueCallback& cb, const Sp<Query>& q, Value::Filter&& filter)
bool addListener(size_t token,
const ValueCallback& cb,
const Sp<Query>& q,
Value::Filter&& filter,
DoneCallbackSimple ready = {})
{
auto cached = cache.get(filter);
if (not cached.empty() and not cb(cached, false)) {
return false;
}
listeners.emplace(token, LocalListener {q, std::move(filter), cb});
if (ready) {
if (initialReady || (searchReady && cache.isSynced()))
ready(true);
else
readyCallbacks.emplace(token, std::move(ready));
}
return true;
}
bool removeListener(size_t token, const time_point& now)
{
lastRemoved = now;
readyCallbacks.erase(token);
return listeners.erase(token) > 0;
}
void removeAll() { listeners.clear(); }
void removeAll()
{
listeners.clear();
readyCallbacks.clear();
}
bool isDone() { return listeners.empty(); }
@@ -150,17 +211,46 @@ private:
OpValueCache cache;
std::map<size_t, LocalListener> listeners;
std::map<size_t, DoneCallbackSimple> readyCallbacks;
bool initialReady {false};
bool searchReady {true};
time_point lastRemoved {clock::now()};
};
class SearchCache
{
public:
SearchCache() {}
explicit SearchCache(bool countSources = true, bool waitForSearch = false)
: countSources_(countSources)
, waitForSearch_(waitForSearch)
{}
SearchCache(SearchCache&&) = default;
using OnListen = std::function<size_t(Sp<Query>, ValueCallback, SyncCallback)>;
size_t listen(const ValueCallback& get_cb, const Sp<Query>& q, Value::Filter&& filter, const OnListen& onListen);
size_t listen(const ValueCallback& get_cb,
const Sp<Query>& q,
Value::Filter&& filter,
const OnListen& onListen,
DoneCallbackSimple ready = {});
void onReady(bool ok = true)
{
for (auto& operation : ops)
operation.second->onReady(ok);
}
void onReady(const Sp<Query>& query, bool ok)
{
auto operation = getOp(query);
if (operation != ops.end())
operation->second->onReady(ok);
}
void onSearchReady()
{
for (auto& operation : ops)
operation.second->onSearchReady();
}
bool cancelListen(size_t gtoken, const time_point& now);
void cancelAll(const std::function<void(size_t)>& onCancel);
@@ -192,6 +282,8 @@ private:
size_t nextToken_ {1};
time_point nextExpiration_ {time_point::max()};
bool countSources_ {true};
bool waitForSearch_ {false};
};
} // namespace dht
+33 -3
View File
@@ -5,6 +5,7 @@
#include "infohash.h"
#include "sockaddr.h"
#include "net.h"
#include "network_engine.h"
#include <algorithm>
#include <map>
@@ -44,6 +45,7 @@ static constexpr auto KEY_REQ_REFRESHED = "re"sv;
static constexpr auto KEY_REQ_FIELDS = "fileds"sv;
static constexpr auto KEY_REQ_WANT = "w"sv;
static constexpr auto KEY_VERSION = "ve"sv;
static constexpr auto KEY_LISTEN_SYNC = "ls"sv;
static constexpr auto QUERY_PING = "ping"sv;
static constexpr auto QUERY_FIND = "find"sv;
@@ -128,6 +130,7 @@ struct ParsedMessage
/* reported address by the distant node */
std::string ua;
int version {0};
std::optional<ListenSync> listenSync {};
SockAddr addr;
void msgpack_unpack(const msgpack::object& o);
@@ -180,7 +183,10 @@ ParsedMessage::complete()
auto assembled = pv.reassemble();
msgpack::unpacked msg;
msgpack::unpack(msg, (const char*) assembled.data(), assembled.size());
values.emplace_back(std::make_shared<Value>(msg.get()));
auto value = std::make_shared<Value>(msg.get());
if (listenSync && value->id == Value::INVALID_ID)
throw msgpack::type_error();
values.emplace_back(std::move(value));
}
return true;
}
@@ -337,8 +343,22 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
parsedReq.want = &o.val;
else if (key == KEY_VERSION)
version = o.val.as<int>();
else if (key == KEY_LISTEN_SYNC) {
if (o.val.type != msgpack::type::ARRAY || o.val.via.array.size != 4)
throw msgpack::type_error();
for (const auto& number :
{o.val.via.array.ptr[0], o.val.via.array.ptr[1], o.val.via.array.ptr[2], o.val.via.array.ptr[3]})
if (number.type != msgpack::type::POSITIVE_INTEGER)
throw msgpack::type_error();
listenSync = o.val.as<ListenSync>();
if (!listenSync->registration)
throw msgpack::type_error();
}
}
if (listenSync && version != 2)
throw msgpack::type_error();
if (parsedReq.sa) {
if (parsedReq.sa->type != msgpack::type::BIN)
throw msgpack::type_error();
@@ -360,18 +380,28 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
if (parsedReq.values) {
if (parsedReq.values->type != msgpack::type::ARRAY)
throw msgpack::type_error();
if (listenSync && parsedReq.values->via.array.size > 32)
throw msgpack::type_error();
for (size_t i = 0; i < parsedReq.values->via.array.size; i++) {
auto& packed_v = parsedReq.values->via.array.ptr[i];
if (packed_v.type == msgpack::type::POSITIVE_INTEGER) {
// Skip oversize values with a small margin for header overhead
if (packed_v.via.u64 > MAX_VALUE_SIZE + 32)
if (packed_v.via.u64 > MAX_VALUE_SIZE + 32 || (listenSync && !packed_v.via.u64)) {
if (listenSync)
throw msgpack::type_error();
continue;
}
value_parts[i].total_size = packed_v.via.u64;
} else {
try {
values.emplace_back(std::make_shared<Value>(parsedReq.values->via.array.ptr[i]));
auto value = std::make_shared<Value>(parsedReq.values->via.array.ptr[i]);
if (listenSync && value->id == Value::INVALID_ID)
throw msgpack::type_error();
values.emplace_back(std::move(value));
} catch (const std::exception& e) {
// DHT_LOG_WARN("Error reading value: %s", e.what());
if (listenSync)
throw msgpack::type_error();
}
}
}
+158 -18
View File
@@ -65,6 +65,16 @@ struct Dht::SearchNode
Sp<Scheduler::Job> refresh {};
Sp<Scheduler::Job> cacheExpirationJob {};
Sp<net::Request> req {};
Sp<Scheduler::Job> syncDeadline {};
uint64_t registration {0};
std::optional<net::ListenSync> manifest;
std::map<std::pair<uint64_t, uint64_t>, net::RequestAnswer> pendingUpdates;
uint64_t nextSequence {1};
uint64_t watermark {0};
size_t pendingBytes {0};
bool baselineComplete {false};
bool initialized {false};
bool legacy {false};
Tid socketId {0};
CachedListenStatus(ValueStateCallback&& cb, SyncCallback scb, Tid sid)
: cache(std::forward<ValueStateCallback>(cb), std::forward<SyncCallback>(scb))
@@ -74,6 +84,12 @@ struct Dht::SearchNode
CachedListenStatus(const CachedListenStatus&) = delete;
~CachedListenStatus()
{
if (syncDeadline)
syncDeadline->cancel();
if (refresh)
refresh->cancel();
if (cacheExpirationJob)
cacheExpirationJob->cancel();
if (socketId and req and req->node) {
req->node->closeSocket(socketId);
}
@@ -253,17 +269,130 @@ struct Dht::SearchNode
getStatus.clear();
}
void onValues(const Sp<Query>& q, net::RequestAnswer&& answer, const TypeStore& types, Scheduler& scheduler)
bool confirmListen(const Sp<Query>& query,
const std::optional<net::ListenSync>& manifest,
const TypeStore& types,
Scheduler& scheduler)
{
auto l = listenStatus.find(q);
if (l != listenStatus.end()) {
auto next = l->second.cache.onValues(answer.values,
answer.refreshed_values,
answer.expired_values,
types,
scheduler.time());
scheduler.edit(l->second.cacheExpirationJob, next);
auto found = listenStatus.find(query);
if (found == listenStatus.end())
return false;
auto& status = found->second;
if (!manifest) {
if (status.manifest)
return false;
status.legacy = true;
status.initialized = true;
status.pendingUpdates.clear();
status.pendingBytes = 0;
scheduler.cancel(status.syncDeadline);
status.cache.onSynced(true);
return true;
}
if (manifest->registration != status.registration || !manifest->epoch || manifest->snapshotEnd > 4096
|| manifest->sequence < manifest->snapshotEnd
|| (status.manifest
&& (status.manifest->epoch != manifest->epoch || status.manifest->snapshotEnd != manifest->snapshotEnd)))
return false;
status.legacy = false;
status.manifest = manifest;
status.watermark = std::max(status.watermark, manifest->sequence);
return onValues(query, {}, types, scheduler);
}
bool onValues(const Sp<Query>& query, net::RequestAnswer&& answer, const TypeStore& types, Scheduler& scheduler)
{
auto found = listenStatus.find(query);
if (found == listenStatus.end())
return true;
auto& status = found->second;
auto apply = [&](net::RequestAnswer&& update) {
auto next = status.cache.onValues(update.values,
update.refreshed_values,
update.expired_values,
types,
scheduler.time());
scheduler.edit(status.cacheExpirationJob, next);
};
if (!answer.listenSync
&& (!answer.values.empty() || !answer.refreshed_values.empty() || !answer.expired_values.empty())) {
if (status.manifest)
return true;
status.legacy = true;
apply(std::move(answer));
return true;
}
if (answer.listenSync) {
auto sync = *answer.listenSync;
if (sync.registration != status.registration || (status.manifest && sync.epoch != status.manifest->epoch))
return true;
if (!sync.epoch || !sync.sequence || sync.snapshotEnd > 4096
|| (status.manifest && sync.snapshotEnd != status.manifest->snapshotEnd))
return false;
if (!answer.fields.empty()
|| (sync.sequence <= sync.snapshotEnd
&& (answer.values.empty() || !answer.expired_values.empty() || !answer.refreshed_values.empty())))
return false;
if (sync.sequence < status.nextSequence)
return true;
auto key = std::make_pair(sync.epoch, sync.sequence);
if (!status.pendingUpdates.count(key)) {
size_t bytes = 0;
for (const auto& value : answer.values)
bytes += value->size();
bytes += (answer.refreshed_values.size() + answer.expired_values.size()) * sizeof(Value::Id);
if (status.pendingUpdates.size() >= 4096 || bytes > 64 * 1024 * 1024 - status.pendingBytes)
return false;
status.pendingBytes += bytes;
status.pendingUpdates.emplace(key, std::move(answer));
}
}
if (!status.manifest)
return true;
auto epoch = status.manifest->epoch;
for (auto pending = status.pendingUpdates.begin(); pending != status.pendingUpdates.end();) {
if (pending->first.first != epoch)
pending = status.pendingUpdates.erase(pending);
else {
if (pending->second.listenSync->snapshotEnd != status.manifest->snapshotEnd)
return false;
status.watermark = std::max(status.watermark, pending->first.second);
++pending;
}
}
if (!status.baselineComplete) {
size_t values = 0;
for (uint64_t sequence = 1; sequence <= status.manifest->snapshotEnd; ++sequence) {
auto batch = status.pendingUpdates.find({epoch, sequence});
if (batch == status.pendingUpdates.end())
return true;
values += batch->second.values.size();
}
if (values > 4096)
return false;
status.baselineComplete = true;
}
while (true) {
auto pending = status.pendingUpdates.find({epoch, status.nextSequence});
if (pending == status.pendingUpdates.end())
break;
auto update = std::move(pending->second);
status.pendingUpdates.erase(pending);
++status.nextSequence;
apply(std::move(update));
}
status.pendingBytes = 0;
for (const auto& pending : status.pendingUpdates) {
for (const auto& value : pending.second.values)
status.pendingBytes += value->size();
status.pendingBytes += (pending.second.refreshed_values.size() + pending.second.expired_values.size())
* sizeof(Value::Id);
}
status.initialized = status.nextSequence > status.watermark;
if (status.initialized)
scheduler.cancel(status.syncDeadline);
status.cache.onSynced(status.initialized);
return true;
}
void onListenSynced(const Sp<Query>& q, bool synced = true, Sp<Scheduler::Job> refreshJob = {})
@@ -467,7 +596,7 @@ struct Dht::Search
size_t listener_token = 1;
/* Cache */
SearchCache cache;
SearchCache cache {true, true};
Sp<Scheduler::Job> opExpirationJob;
~Search()
@@ -596,16 +725,25 @@ struct Dht::Search
}
}
size_t listen(const ValueCallback& cb, Value::Filter&& f, const Sp<Query>& q, Scheduler& scheduler)
size_t listen(const ValueCallback& cb,
Value::Filter&& f,
const Sp<Query>& q,
Scheduler& scheduler,
DoneCallbackSimple ready = {})
{
// DHT_LOG.e(id, "[search %s IPv%c] listen", id.toString().c_str(), (af == AF_INET) ? '4' : '6');
return cache.listen(cb, q, std::move(f), [&](const Sp<Query>& q, ValueCallback vcb, SyncCallback scb) {
done = false;
auto token = ++listener_token;
listeners.emplace(token, SearchListener {q, vcb, scb});
scheduler.edit(nextSearchStep, scheduler.time());
return token;
});
return cache.listen(
cb,
q,
std::move(f),
[&](const Sp<Query>& q, ValueCallback vcb, SyncCallback scb) {
done = false;
auto token = ++listener_token;
listeners.emplace(token, SearchListener {q, vcb, scb});
scheduler.edit(nextSearchStep, scheduler.time());
return token;
},
std::move(ready));
}
void cancelListen(size_t token, Scheduler& scheduler)
@@ -769,6 +907,7 @@ struct Dht::Search
{
// no nodes or all expired nodes. This is most likely a connectivity change event.
expired = true;
cache.onReady(false);
nodes.clear();
if (announce.empty() && listeners.empty())
@@ -841,6 +980,7 @@ struct Dht::Search
void clear()
{
cache.onReady(false);
announce.clear();
callbacks.clear();
listeners.clear();
+11 -1
View File
@@ -333,7 +333,17 @@ SecureDht::get(const InfoHash& id, GetCallback cb, DoneCallback donecb, Value::F
size_t
SecureDht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where w)
{
return dht_->listen(id, getCallbackFilter(cb, std::forward<Value::Filter>(f)), {}, std::forward<Where>(w));
return listen(id, std::move(cb), std::move(f), std::move(w), {});
}
size_t
SecureDht::listen(const InfoHash& id, ValueCallback cb, Value::Filter f, Where w, DoneCallbackSimple ready)
{
return dht_->listen(id,
getCallbackFilter(cb, std::forward<Value::Filter>(f)),
{},
std::forward<Where>(w),
std::move(ready));
}
size_t
+3 -1
View File
@@ -232,6 +232,8 @@ Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, t
if (it->data != value) {
size_t size_old = it->data->size();
ssize_t size_diff = size_new - (ssize_t) size_old;
// Re-storing identical content is a refresh, not an edition
size_t edited = (*it->data != *value) ? 1 : 0;
// DHT_LOG.DEBUG("Updating %s -> %s", id.toString().c_str(), value->toString().c_str());
// clear quota for previous value
if (it->store_bucket)
@@ -243,7 +245,7 @@ Storage::store(const InfoHash& id, const Sp<Value>& value, time_point created, t
sb->insert(id, *value, expiration);
it->data = value;
total_size += size_diff;
return std::make_pair(&(*it), StoreDiff {size_diff, 0, 0, 1});
return std::make_pair(&(*it), StoreDiff {size_diff, 0, 0, edited});
}
} else {
// DHT_LOG.DEBUG("Storing %s -> %s", id.toString().c_str(), value->toString().c_str());
+27 -6
View File
@@ -71,11 +71,21 @@ public:
{
std::vector<Sp<Value>> expired_values;
for (auto it = values.begin(); it != values.end();) {
if (it->second.expiration <= now) {
auto effectiveExpiration = it->second.expiration;
if (status == ListenSyncStatus::SYNCED)
// While the listen is synced, the remote node pushes
// explicit expirations and periodic refreshes. Add a
// grace period proportional to the value's type
// expiration so that a single lost UDP refresh
// notification does not cause a premature local drop.
// A grace of one type-expiration covers exactly one
// missed re-announcement cycle.
effectiveExpiration += it->second.typeExpiration;
if (effectiveExpiration <= now) {
expired_values.emplace_back(std::move(it->second.data));
it = values.erase(it);
} else {
next = std::min(next, it->second.expiration);
next = std::min(next, effectiveExpiration);
++it;
}
}
@@ -148,12 +158,14 @@ private:
Sp<Value> data {};
time_point created {};
time_point expiration {};
duration typeExpiration {}; /* value type's expiration period, used as grace when synced */
CacheValueStorage() {}
CacheValueStorage(const Sp<Value>& v, time_point t, time_point e)
CacheValueStorage(const Sp<Value>& v, time_point t, time_point e, duration te)
: data(v)
, created(t)
, expiration(e)
, typeExpiration(te)
{}
};
@@ -166,15 +178,22 @@ private:
{
std::vector<Sp<Value>> nvals;
for (const auto& value : new_values) {
auto te = types.getType(value->type).expiration;
auto v = values.find(value->id);
if (v == values.end()) {
// new value
nvals.emplace_back(value);
values.emplace(value->id, CacheValueStorage(value, now, now + types.getType(value->type).expiration));
values.emplace(value->id, CacheValueStorage(value, now, now + te, te));
} else {
// refreshed value
v->second.created = now;
v->second.expiration = now + types.getType(v->second.data->type).expiration;
v->second.expiration = now + te;
v->second.typeExpiration = te;
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);
}
}
}
CallbackQueue ret;
@@ -201,8 +220,10 @@ private:
auto v = values.find(vid);
if (v == values.end())
return;
auto te = types.getType(v->second.data->type).expiration;
v->second.created = now;
v->second.expiration = now + types.getType(v->second.data->type).expiration;
v->second.expiration = now + te;
v->second.typeExpiration = te;
}
};
+1814 -1
View File
File diff suppressed because it is too large. Load diff
+64 -1
View File
@@ -7,8 +7,26 @@
#include <cppunit/extensions/HelperMacros.h>
#include <opendht/dhtrunner.h>
#include <opendht/http.h>
#include <restinio/all.hpp>
#include <restinio/tls.hpp>
#include <json/json.h>
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wkeyword-macro"
#elif defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#endif
#define private public
#include <opendht/dht_proxy_server.h>
#include <opendht/log.h>
#undef private
#if defined(__clang__)
#pragma clang diagnostic pop
#elif defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
namespace test {
@@ -17,10 +35,28 @@ class DhtProxyTester : public CppUnit::TestFixture
CPPUNIT_TEST_SUITE(DhtProxyTester);
CPPUNIT_TEST(testGetPut);
CPPUNIT_TEST(testListen);
CPPUNIT_TEST(testListenReadiness);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
CPPUNIT_TEST(testInitializingSubscriptionCancellation);
#endif
CPPUNIT_TEST(testListenValueEdit);
CPPUNIT_TEST(testPushListenValueEdit);
CPPUNIT_TEST(testResubscribeGetValues);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
CPPUNIT_TEST(testFullSubscribeReturnsValuesWithoutExistingListener);
#endif
CPPUNIT_TEST(testPushNotification);
CPPUNIT_TEST(testProxyServerClientFullChain);
CPPUNIT_TEST(testPutGet40KChars);
CPPUNIT_TEST(testFuzzy);
CPPUNIT_TEST(testShutdownStop);
CPPUNIT_TEST(testGetAfterListen);
CPPUNIT_TEST(testListenDuplicatePut);
CPPUNIT_TEST(testPushMultiValueEditExpire);
CPPUNIT_TEST(testPushRefreshNoSpuriousExpire);
CPPUNIT_TEST(testProxyListenEditChain);
CPPUNIT_TEST(testPushRapidEditsNoPhantom);
CPPUNIT_TEST(testPushRefreshAfterEditKeepsLatest);
CPPUNIT_TEST_SUITE_END();
public:
@@ -41,11 +77,31 @@ public:
* Test listen
*/
void testListen();
void testListenReadiness();
void testInitializingSubscriptionCancellation();
/**
* Test that editing a value only triggers an add callback via proxy, no expire
*/
void testListenValueEdit();
/**
* Test that editing a value only triggers an add callback via push,
* no phantom expire, and that real expiration is received.
*/
void testPushListenValueEdit();
/**
* When a proxy redo a subscribe on the proxy
* it should retrieve existant values
*/
void testResubscribeGetValues();
void testFullSubscribeReturnsValuesWithoutExistingListener();
/**
* Test push notification mechanism and OpValueCache
*/
void testPushNotification();
/**
* Test end-to-end chain through proxy server and proxy client
*/
void testProxyServerClientFullChain();
/**
* Test MTU put/get on dht
*/
@@ -54,6 +110,13 @@ public:
void testFuzzy();
void testShutdownStop();
void testGetAfterListen();
void testListenDuplicatePut();
void testPushMultiValueEditExpire();
void testPushRefreshNoSpuriousExpire();
void testProxyListenEditChain();
void testPushRapidEditsNoPhantom();
void testPushRefreshAfterEditKeepsLatest();
private:
dht::DhtRunner::Config clientConfig {};
+945 -6
View File
@@ -31,14 +31,318 @@ CPPUNIT_TEST_SUITE_REGISTRATION(DhtProxyClientTester);
using namespace dht;
#ifdef OPENDHT_PUSH_NOTIFICATIONS
namespace {
void
finishSync(DhtProxyClient& client,
const std::shared_ptr<dht::http::Request>& request,
const std::vector<Sp<Value>>& values,
uint64_t revision = 1,
bool complete = true,
bool snapshot = true,
const std::string& epoch = "test-epoch")
{
Json::Value marker;
marker["sync"]["epoch"] = epoch;
marker["sync"]["revision"] = Json::UInt64(revision);
marker["sync"]["complete"] = complete;
marker["sync"]["snapshot"] = snapshot;
auto body = std::string {};
for (const auto& value : values)
body += Json::writeString(client.jsonBuilder_, value->toJson()) + "\n";
body += Json::writeString(client.jsonBuilder_, marker) + "\n";
request->cbs_.on_body(body.data(), body.size());
dht::http::Response response;
response.status_code = 200;
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
client.periodic(nullptr, 0, {}, clock::now());
}
} // namespace
#endif
void
DhtProxyClientTester::testReadinessShutdown()
{
for (bool push : {false, true}) {
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", push ? "push-token" : "");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("readiness-shutdown");
std::vector<bool> results;
auto values = [](const std::vector<Sp<Value>>&, bool) {
return true;
};
CPPUNIT_ASSERT(client.listen(key, values, {}, {}, [&](bool ok) {
results.push_back(ok);
CPPUNIT_ASSERT(client.getLocal(key, {}).empty());
}) != 0);
CPPUNIT_ASSERT(results.empty());
client.shutdown({}, true);
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
client.shutdown({}, true);
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
CPPUNIT_ASSERT_EQUAL(size_t(0), client.listen(key, values, {}, {}, [&](bool ok) { results.push_back(ok); }));
CPPUNIT_ASSERT(results == std::vector<bool>({false, false}));
}
}
void
DhtProxyClientTester::testReadinessReentrancy()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("ready-reentrancy");
std::vector<bool> results;
client.listen(
key,
[](const std::vector<Sp<Value>>&, bool) { return true; },
{},
{},
[&](bool ok) {
auto unlocked = client.searchLock_.try_lock();
if (unlocked)
client.searchLock_.unlock();
CPPUNIT_ASSERT(unlocked);
CPPUNIT_ASSERT(client.getLocal(key, {}).empty());
results.push_back(ok);
});
auto request = client.searches_.at(key).listeners.begin()->second.request;
std::string marker = "{\"sync\":{\"complete\":true}}\n";
request->cbs_.on_body(marker.data(), marker.size());
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(results == std::vector<bool>({true}));
}
void
DhtProxyClientTester::testCachedReadinessDispatch()
{
for (unsigned action = 0; action < 3; ++action) {
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("cached-readiness-dispatch");
auto values = [](const std::vector<Sp<Value>>&, bool) {
return true;
};
auto first = client.listen(key, values, {}, {});
CPPUNIT_ASSERT(first != 0);
auto value = std::make_shared<Value>("cached");
value->id = 42;
auto request = client.searches_.at(key).listeners.begin()->second.request;
auto body = Json::writeString(client.jsonBuilder_, value->toJson()) + "\n{\"sync\":{\"complete\":true}}\n";
request->cbs_.on_body(body.data(), body.size());
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(client.getLocalById(key, 42));
std::vector<bool> results;
size_t delivered = 0;
auto attached = client.listen(
key,
[&](const std::vector<Sp<Value>>& cached, bool expired) {
if (!expired)
delivered += cached.size();
return true;
},
{},
{},
[&](bool ok) {
results.emplace_back(ok);
if (ok) {
CPPUNIT_ASSERT(client.getLocalById(key, 42));
auto nested = client.listen(key, values, {}, {});
CPPUNIT_ASSERT(nested != 0);
CPPUNIT_ASSERT(client.cancelListen(key, nested));
} else
CPPUNIT_ASSERT(client.getLocal(key, {}).empty());
});
CPPUNIT_ASSERT(attached != 0);
CPPUNIT_ASSERT_EQUAL(size_t(1), delivered);
CPPUNIT_ASSERT(results.empty());
CPPUNIT_ASSERT_EQUAL(size_t(1), client.readinessCallbacks_.size());
if (action == 1)
CPPUNIT_ASSERT(client.cancelListen(key, attached));
else if (action == 2) {
client.shutdown({}, true);
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
}
client.periodic(nullptr, 0, {}, clock::now());
if (action == 1)
CPPUNIT_ASSERT(results.empty());
else
CPPUNIT_ASSERT(results == std::vector<bool>({action == 0}));
client.shutdown({}, true);
CPPUNIT_ASSERT_EQUAL(action == 1 ? size_t(0) : size_t(1), results.size());
}
}
void
DhtProxyClientTester::testReadinessCancelsAnotherListener()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("ready-cancel-neighbor");
size_t second = 0;
unsigned firstCalls = 0;
unsigned secondCalls = 0;
auto values = [](const std::vector<Sp<Value>>&, bool) {
return true;
};
client.listen(key, values, {}, {}, [&](bool ok) {
auto unlocked = client.searchLock_.try_lock();
if (unlocked)
client.searchLock_.unlock();
CPPUNIT_ASSERT(unlocked);
CPPUNIT_ASSERT(ok);
++firstCalls;
CPPUNIT_ASSERT(client.cancelListen(key, second));
});
second = client.listen(key, values, {}, {}, [&](bool) { ++secondCalls; });
auto request = client.searches_.at(key).listeners.begin()->second.request;
std::string marker = "{\"sync\":{\"complete\":true}}\n";
request->cbs_.on_body(marker.data(), marker.size());
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT_EQUAL(1u, firstCalls);
CPPUNIT_ASSERT_EQUAL(0u, secondCalls);
}
#ifdef OPENDHT_PUSH_NOTIFICATIONS
void
DhtProxyClientTester::testRejectsUnrevisionedPushes()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("unrevisioned-push-rejection");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto value = std::make_shared<Value>("obsolete");
value->id = 42;
finishSync(client, listener.request, {value}, 5);
auto synchronized = listener.request;
auto requestCount = client.requests_.size();
for (const auto& notification : std::vector<std::map<std::string, std::string>> {
{{"key", key.toString()}, {"t", "10000"}},
{{"key", key.toString()}, {"expired", "true"}, {"ids", "42"}},
{{"key", key.toString()}, {"epoch", "test-epoch"}},
{{"key", key.toString()}, {"revision", "6"}}
}) {
CPPUNIT_ASSERT(client.pushNotificationReceived(notification) == PushNotificationResult::Error);
CPPUNIT_ASSERT(listener.request == synchronized);
CPPUNIT_ASSERT_EQUAL(requestCount, client.requests_.size());
CPPUNIT_ASSERT_EQUAL(uint64_t(5), listener.syncRevision);
CPPUNIT_ASSERT(listener.opstate->ok.load());
CPPUNIT_ASSERT(client.getLocalById(key, 42));
}
CPPUNIT_ASSERT(
client.pushNotificationReceived({
{"key", key.toString()},
{"epoch", "test-epoch" },
{"revision", "6" },
{"expired", "true" },
{"ids", "42" }
})
== PushNotificationResult::ValuesExpired);
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
client.restartListeners({});
auto validation = listener.request;
CPPUNIT_ASSERT(client.pushNotificationReceived({
{"key", key.toString()}
})
== PushNotificationResult::Error);
finishSync(client, validation, {}, 6, true, false);
CPPUNIT_ASSERT(listener.request == validation);
CPPUNIT_ASSERT(listener.opstate->ok.load());
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
}
void
DhtProxyClientTester::testInitialReadinessFailures()
{
for (unsigned failure = 0; failure < 4; ++failure) {
bool push = failure == 1 || failure == 3;
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", push ? "push-token" : "");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("initial-readiness-failure");
std::vector<bool> results;
CPPUNIT_ASSERT(client.listen(
key,
[](const std::vector<Sp<Value>>&, bool) { return true; },
{},
{},
[&](bool ok) { results.push_back(ok); })
!= 0);
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto request = listener.request;
if (failure == 0) {
std::string marker = "{\"sync\":{\"complete\":false}}\n";
request->cbs_.on_body(marker.data(), marker.size());
} else if (failure == 3) {
finishSync(client, request, {}, 1, false);
} else {
dht::http::Response response;
response.status_code = 503;
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
}
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
client.shutdown({}, true);
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
}
}
void
DhtProxyClientTester::testReadinessRecoveryIsOneShot()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("readiness-recovery");
auto values = [](const std::vector<Sp<Value>>&, bool) {
return true;
};
std::vector<bool> first;
CPPUNIT_ASSERT(client.listen(key, values, {}, {}, [&](bool ok) { first.push_back(ok); }) != 0);
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto request = listener.request;
dht::http::Response response;
response.status_code = 503;
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(first == std::vector<bool>({false}));
std::vector<bool> second;
CPPUNIT_ASSERT(client.listen(key, values, {}, {}, [&](bool ok) { second.push_back(ok); }) != 0);
CPPUNIT_ASSERT(second.empty());
client.handleResubscribe({}, key, 1, listener.opstate);
finishSync(client, listener.request, {}, 1);
CPPUNIT_ASSERT(first == std::vector<bool>({false}));
CPPUNIT_ASSERT(second == std::vector<bool>({true}));
client.shutdown({}, true);
CPPUNIT_ASSERT_EQUAL(size_t(1), first.size());
CPPUNIT_ASSERT_EQUAL(size_t(1), second.size());
}
#endif
void
DhtProxyClientTester::testResubscribeUsesKeyRoute()
{
#ifdef OPENDHT_PUSH_NOTIFICATIONS
DhtProxyClient client({}, {}, [] {}, "http://127.0.0.1:8080", "OpenDHT-Test", "client-id", "push-token");
DhtProxyClient::Listener listener {OpValueCache([](const std::vector<Sp<Value>>&, bool) { return true; })};
DhtProxyClient::Listener listener {ValueCallback([](const std::vector<Sp<Value>>&, bool) { return true; })};
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
listener.needsRecovery = false;
listener.syncEpoch = "test-epoch";
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
return true;
};
@@ -48,8 +352,7 @@ DhtProxyClientTester::testResubscribeUsesKeyRoute()
CPPUNIT_ASSERT(listener.request);
CPPUNIT_ASSERT_EQUAL(std::string("/key/") + key.toString(), std::string(listener.request->header_.request_target()));
CPPUNIT_ASSERT(listener.request->body_.find("\"refresh\"") != std::string::npos);
CPPUNIT_ASSERT(listener.request->body_.find("true") != std::string::npos);
CPPUNIT_ASSERT(listener.request->body_.find("test-epoch") != std::string::npos);
#else
CPPUNIT_ASSERT(true);
#endif
@@ -67,12 +370,15 @@ DhtProxyClientTester::testSetPushNotificationTokenResubscribesWithNewToken()
auto& search = client.searches_[key];
auto [it, inserted] = search.listeners.emplace(std::piecewise_construct,
std::forward_as_tuple(1),
std::forward_as_tuple(OpValueCache(
std::forward_as_tuple(ValueCallback(
[](const std::vector<Sp<Value>>&, bool) { return true; })));
CPPUNIT_ASSERT(inserted);
auto& listener = it->second;
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
listener.needsRecovery = false;
listener.syncEpoch = "test-epoch";
listener.syncRevision = 4;
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
return true;
};
@@ -83,11 +389,644 @@ DhtProxyClientTester::testSetPushNotificationTokenResubscribesWithNewToken()
CPPUNIT_ASSERT_EQUAL(std::string("/key/") + key.toString(), std::string(listener.request->header_.request_target()));
CPPUNIT_ASSERT(listener.request->body_.find("new-token") != std::string::npos);
CPPUNIT_ASSERT(listener.request->body_.find("old-token") == std::string::npos);
CPPUNIT_ASSERT(listener.request->body_.find("\"refresh\"") != std::string::npos);
CPPUNIT_ASSERT(listener.request->body_.find("true") != std::string::npos);
CPPUNIT_ASSERT(listener.request->body_.find("test-epoch") != std::string::npos);
#else
CPPUNIT_ASSERT(true);
#endif
}
void
DhtProxyClientTester::testRestartListenersFetchesValuesAfterFailedSubscribe()
{
#ifdef OPENDHT_PUSH_NOTIFICATIONS
DhtProxyClient client({}, {}, [] {}, "http://127.0.0.1:8080", "OpenDHT-Test", "client-id", "push-token");
auto addListener = [&](const InfoHash& key, bool ok) -> DhtProxyClient::Listener& {
auto& search = client.searches_[key];
auto [it, inserted] = search.listeners.emplace(std::piecewise_construct,
std::forward_as_tuple(1),
std::forward_as_tuple(ValueCallback(
[](const std::vector<Sp<Value>>&, bool) { return true; })));
CPPUNIT_ASSERT(inserted);
auto& listener = it->second;
listener.opstate = std::make_shared<DhtProxyClient::OperationState>();
listener.opstate->ok = ok;
listener.cb = [](const std::vector<Sp<Value>>&, bool, system_clock::time_point) {
return true;
};
return listener;
};
// The subscription of this listener failed (e.g. no network): values stored on the
// key since then were never received.
auto failedKey = InfoHash::get("proxy-client-failed-subscribe");
auto& failed = addListener(failedKey, false);
auto healthyKey = InfoHash::get("proxy-client-healthy-subscribe");
auto& healthy = addListener(healthyKey, true);
client.restartListeners({});
CPPUNIT_ASSERT(failed.request);
CPPUNIT_ASSERT_EQUAL(std::string("/key/") + failedKey.toString(),
std::string(failed.request->header_.request_target()));
CPPUNIT_ASSERT(failed.request->body_.find("push-token") != std::string::npos);
// Without "refresh", the server answers with the values it stores for the key.
CPPUNIT_ASSERT(failed.request->body_.find("\"refresh\"") == std::string::npos);
CPPUNIT_ASSERT(healthy.request);
CPPUNIT_ASSERT(healthy.request->body_.find("\"sync\"") != std::string::npos);
#else
CPPUNIT_ASSERT(true);
#endif
}
#ifdef OPENDHT_PUSH_NOTIFICATIONS
void
DhtProxyClientTester::testWrongSessionPushDoesNotTouchSubscriptions()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("wrong-session-isolation");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto value = std::make_shared<Value>("current-session-value");
value->id = 42;
finishSync(client, listener.request, {value}, 5);
client.statusIpv4_ = NodeStatus::Disconnected;
client.statusIpv6_ = NodeStatus::Disconnected;
client.launchConnectedCbs_ = false;
auto request = listener.request;
auto generation = listener.opstate->generation.load();
auto expiration = listener.refreshSubscriberTimer->expiry();
std::vector<std::map<std::string, std::string>> notifications {
{{"s", "old-session"}, {"key", key.toString()}, {"epoch", "old-epoch"}, {"revision", "99"}},
{{"s", "old-session"}, {"key", key.toString()}, {"exp", "42"}},
{{"s", "old-session"}, {"timeout", key.toString()}},
{{"s", "old-session"}, {"timeout", key.toString()}, {"vid", "42"}}
};
for (const auto& notification : notifications) {
CPPUNIT_ASSERT(client.pushNotificationReceived(notification) == PushNotificationResult::IgnoredWrongSession);
CPPUNIT_ASSERT(listener.request == request);
CPPUNIT_ASSERT_EQUAL(generation, listener.opstate->generation.load());
CPPUNIT_ASSERT(!listener.needsRecovery);
CPPUNIT_ASSERT(listener.opstate->ok.load());
CPPUNIT_ASSERT(listener.refreshSubscriberTimer->expiry() == expiration);
CPPUNIT_ASSERT_EQUAL(std::string("test-epoch"), listener.syncEpoch);
CPPUNIT_ASSERT_EQUAL(uint64_t(5), listener.syncRevision);
CPPUNIT_ASSERT(client.getLocalById(key, 42));
CPPUNIT_ASSERT(client.statusIpv4_ == NodeStatus::Disconnected);
CPPUNIT_ASSERT(client.statusIpv6_ == NodeStatus::Disconnected);
CPPUNIT_ASSERT(!client.launchConnectedCbs_.load());
CPPUNIT_ASSERT(client.callbacks_.empty());
}
}
void
DhtProxyClientTester::testNewSessionSynchronizesIndependently()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("new-session-independent-sync");
CPPUNIT_ASSERT(client.pushNotificationReceived({
{"s", "old-session" },
{"key", key.toString()}
})
== PushNotificationResult::IgnoredWrongSession);
CPPUNIT_ASSERT(client.searches_.empty());
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
Json::Value body;
std::string error;
auto reader = std::unique_ptr<Json::CharReader>(Json::CharReaderBuilder {}.newCharReader());
const auto& requestBody = listener.request->body_;
CPPUNIT_ASSERT(reader->parse(requestBody.data(), requestBody.data() + requestBody.size(), &body, &error));
CPPUNIT_ASSERT_EQUAL(client.pushSessionId_, body["session_id"].asString());
CPPUNIT_ASSERT(body["sync"].isObject() && body["sync"].empty());
auto value = std::make_shared<Value>("new-session-snapshot");
value->id = 43;
finishSync(client, listener.request, {value});
CPPUNIT_ASSERT(listener.opstate->ok.load());
CPPUNIT_ASSERT(!listener.needsRecovery);
CPPUNIT_ASSERT(client.getLocalById(key, 43)->data == value->data);
}
void
DhtProxyClientTester::testFailedTimerRetryFetchesValues()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("failed-timer-retry");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
listener.opstate->ok = false;
client.handleResubscribe({}, key, 1, listener.opstate);
CPPUNIT_ASSERT(listener.request->body_.find("\"refresh\"") == std::string::npos);
}
void
DhtProxyClientTester::testFullRetryRemovesAbsentValues()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("full-retry-absent-values");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto value = std::make_shared<Value>("expired-while-offline");
value->id = 42;
listener.cb({value}, false, system_clock::time_point(std::chrono::seconds(10)));
listener.opstate->ok = false;
client.restartListeners({});
auto request = listener.request;
finishSync(client, request, {});
CPPUNIT_ASSERT(!client.getLocalById(key, value->id));
}
void
DhtProxyClientTester::testStreamingFallbackExpiresSnapshot()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("full-retry-push-ordering");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto value = std::make_shared<Value>("live-value");
value->id = 42;
listener.cb({value}, false, system_clock::time_point(std::chrono::seconds(10)));
listener.opstate->ok = false;
client.restartListeners({});
finishSync(client, listener.request, {value}, 10);
client.deviceKey_.clear();
client.restartListeners({});
auto expired = value->toJson();
expired["expired"] = true;
auto body = Json::writeString(client.jsonBuilder_, expired) + "\n";
listener.request->cbs_.on_body(body.data(), body.size());
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(!client.getLocalById(key, value->id));
}
void
DhtProxyClientTester::testLateRetryResponseDoesNotResurrectExpiredValue()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("full-retry-late-response");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto value = std::make_shared<Value>("already-expired-value");
value->id = 42;
finishSync(client, listener.request, {value}, 10);
listener.opstate->ok = false;
client.restartListeners({});
auto staleRequest = listener.request;
client.pushNotificationReceived({
{"key", key.toString()},
{"exp", "42" },
{"epoch", "test-epoch" },
{"revision", "11" }
});
finishSync(client, staleRequest, {value}, 10);
client.handleResubscribe({}, key, 1, listener.opstate);
finishSync(client, listener.request, {}, 11);
finishSync(client, staleRequest, {value}, 10);
CPPUNIT_ASSERT(!client.getLocalById(key, value->id));
}
void
DhtProxyClientTester::testTruncatedRecoveryRemainsFailed()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("truncated-full-retry");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
listener.opstate->ok = false;
client.restartListeners({});
auto request = listener.request;
request->response_.status_code = 200;
request->cbs_.on_body("{\"id\":", 6);
request->terminate(asio::error::connection_reset);
CPPUNIT_ASSERT(!listener.opstate->ok.load());
}
void
DhtProxyClientTester::testTruncatedRecoveryAtEofFails()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("truncated-recovery-eof");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto request = listener.request;
request->init_parser();
std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n{";
CPPUNIT_ASSERT(llhttp_execute(request->parser_.get(), response.data(), response.size()) == HPE_OK);
request->handle_response(asio::error::eof, 0);
CPPUNIT_ASSERT(!listener.opstate->ok.load());
}
void
DhtProxyClientTester::testMissingCompletionDoesNotReconcile()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("missing-completion");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto value = std::make_shared<Value>("keep-value");
value->id = 42;
finishSync(client, listener.request, {value});
client.restartListeners({});
auto request = listener.request;
request->cbs_.on_body("{}\n", 3);
dht::http::Response response;
response.status_code = 200;
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(!listener.opstate->ok.load());
CPPUNIT_ASSERT(client.getLocalById(key, 42));
CPPUNIT_ASSERT(listener.refreshSubscriberTimer->expiry() <= system_clock::now() + std::chrono::seconds(2));
}
void
DhtProxyClientTester::testSnapshotExpirationIgnoresClockRollback()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("snapshot-clock-rollback");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto value = std::make_shared<Value>("expired-value");
value->id = 42;
listener.cb({value}, false, system_clock::now() + std::chrono::hours(1));
finishSync(client, listener.request, {});
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
}
void
DhtProxyClientTester::testSupersededFailureDoesNotPoisonSuccessfulRetry()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("superseded-full-retry");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto oldRequest = listener.request;
oldRequest->response_.status_code = 503;
oldRequest->terminate(asio::error::eof);
client.periodic(nullptr, 0, {}, clock::now());
client.restartListeners({});
auto newRequest = listener.request;
CPPUNIT_ASSERT(newRequest != oldRequest);
finishSync(client, newRequest, {});
dht::http::Response response;
response.status_code = 503;
oldRequest->cbs_.on_state_change(dht::http::Request::State::DONE, response);
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(listener.opstate->ok.load());
}
void
DhtProxyClientTester::testSyncCoalescesConnectivityChanges()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("coalesced-sync");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto request = listener.request;
client.restartListeners({});
client.restartListeners({});
CPPUNIT_ASSERT(listener.request == request);
finishSync(client, request, {}, 5);
CPPUNIT_ASSERT(listener.request == request);
client.restartListeners({});
CPPUNIT_ASSERT(listener.request != request);
auto body = listener.request->body_;
CPPUNIT_ASSERT(body.find("test-epoch") != std::string::npos);
CPPUNIT_ASSERT(body.find("\"revision\":5") != std::string::npos);
auto validation = listener.request;
client.restartListeners({});
client.restartListeners({});
CPPUNIT_ASSERT(listener.request == validation);
finishSync(client, listener.request, {}, 5, true, false);
CPPUNIT_ASSERT(listener.opstate->ok.load());
}
void
DhtProxyClientTester::testTokenRotationDuringRecovery()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "old-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
client.statusIpv4_ = NodeStatus::Connected;
auto key = InfoHash::get("rotation-during-recovery");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto recovery = listener.request;
client.setPushNotificationToken("new-token");
CPPUNIT_ASSERT(listener.request == recovery);
finishSync(client, recovery, {}, 1);
CPPUNIT_ASSERT(listener.request != recovery);
CPPUNIT_ASSERT(listener.request->body_.find("new-token") != std::string::npos);
finishSync(client, listener.request, {}, 1, true, false);
CPPUNIT_ASSERT(listener.opstate->ok.load());
}
void
DhtProxyClientTester::testTokenRotationBackToPreviousEpoch()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "first-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
client.statusIpv4_ = NodeStatus::Connected;
auto key = InfoHash::get("rotation-back-to-previous-epoch");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
finishSync(client, listener.request, {}, 1);
client.setPushNotificationToken("second-token");
finishSync(client, listener.request, {}, 1, true, true, "second-epoch");
CPPUNIT_ASSERT(listener.opstate->ok.load());
client.setPushNotificationToken("first-token");
finishSync(client, listener.request, {}, 2);
CPPUNIT_ASSERT(listener.opstate->ok.load());
CPPUNIT_ASSERT_EQUAL(std::string("test-epoch"), listener.syncEpoch);
}
void
DhtProxyClientTester::testAuthoritativeEpochAfterStalePush()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("authoritative-epoch-after-stale-push");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
finishSync(client, listener.request, {}, 1);
client.restartListeners({});
auto stale = listener.request;
client.pushNotificationReceived({
{"key", key.toString() },
{"epoch", "intervening-epoch"},
{"revision", "1" }
});
finishSync(client, stale, {}, 1);
finishSync(client, listener.request, {}, 1, true, true, "current-epoch");
CPPUNIT_ASSERT(listener.opstate->ok.load());
CPPUNIT_ASSERT_EQUAL(std::string("current-epoch"), listener.syncEpoch);
auto current = listener.request;
client.pushNotificationReceived({
{"key", key.toString() },
{"epoch", "intervening-epoch"},
{"revision", "2" }
});
CPPUNIT_ASSERT(listener.request != current);
CPPUNIT_ASSERT(listener.request->body_.find("current-epoch") != std::string::npos);
finishSync(client, listener.request, {}, 1, true, false, "current-epoch");
CPPUNIT_ASSERT(listener.opstate->ok.load());
}
void
DhtProxyClientTester::testSyncEditsAndDuplicateValues()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("snapshot-edits");
unsigned additions = 0;
unsigned expirations = 0;
client.listen(key,
[&](const std::vector<Sp<Value>>& values, bool expired) {
(expired ? expirations : additions) += values.size();
return true;
},
{},
{});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto identity = dht::crypto::generateIdentity("SnapshotEditAuthor");
auto original = std::make_shared<Value>("original");
original->id = 42;
original->seq = 1;
original->sign(*identity.first);
finishSync(client, listener.request, {original}, 1);
client.restartListeners({});
finishSync(client, listener.request, {}, 1, true, false);
CPPUNIT_ASSERT_EQUAL(1u, additions);
auto edited = std::make_shared<Value>("edited");
edited->id = 42;
edited->seq = 2;
edited->sign(*identity.first);
client.pushNotificationReceived({
{"key", key.toString()},
{"epoch", "test-epoch" },
{"revision", "2" }
});
finishSync(client, listener.request, {edited}, 2);
CPPUNIT_ASSERT_EQUAL(2u, additions);
CPPUNIT_ASSERT_EQUAL(0u, expirations);
CPPUNIT_ASSERT(client.getLocalById(key, 42)->data == edited->data);
client.pushNotificationReceived({
{"key", key.toString()},
{"epoch", "test-epoch" },
{"revision", "1" }
});
CPPUNIT_ASSERT(client.getLocalById(key, 42)->data == edited->data);
}
void
DhtProxyClientTester::testIncompleteSnapshotDoesNotReconcile()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("incomplete-snapshot");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto value = std::make_shared<Value>("keep-until-complete");
value->id = 42;
finishSync(client, listener.request, {value}, 1);
listener.opstate->ok = false;
client.restartListeners({});
finishSync(client, listener.request, {}, 2, false);
CPPUNIT_ASSERT(client.getLocalById(key, 42));
CPPUNIT_ASSERT(!listener.opstate->ok.load());
client.handleResubscribe({}, key, 1, listener.opstate);
auto recovery = listener.request;
finishSync(client, recovery, {}, 2);
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
}
void
DhtProxyClientTester::testSubscriptionDeadline()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
client.subscriptionRequestTimeout_ = std::chrono::seconds(0);
auto key = InfoHash::get("subscription-deadline");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto request = listener.request;
CPPUNIT_ASSERT(listener.opstate->inFlight.load());
client.httpContext_.restart();
client.httpContext_.poll();
client.httpContext_.stop();
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(!listener.opstate->inFlight.load());
CPPUNIT_ASSERT(!listener.opstate->ok.load());
CPPUNIT_ASSERT(request->finishing_.load());
CPPUNIT_ASSERT(listener.needsRecovery);
CPPUNIT_ASSERT(listener.refreshSubscriberTimer->expiry() <= system_clock::now() + std::chrono::seconds(2));
client.handleResubscribe({}, key, 1, listener.opstate);
CPPUNIT_ASSERT(listener.request != request);
}
void
DhtProxyClientTester::testInvalidPushRevision()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("invalid-push-revision");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
finishSync(client, listener.request, {}, 5);
auto request = listener.request;
for (const auto& revision : {"-1", "+6", " 6", "6 ", "6x", "", "18446744073709551616"}) {
CPPUNIT_ASSERT(client.pushNotificationReceived({
{"key", key.toString()},
{"epoch", "test-epoch" },
{"revision", revision }
})
== PushNotificationResult::Error);
CPPUNIT_ASSERT_EQUAL(uint64_t(5), listener.observedRevision);
CPPUNIT_ASSERT_EQUAL(uint64_t(5), listener.syncRevision);
CPPUNIT_ASSERT(listener.request == request);
CPPUNIT_ASSERT(!listener.needsRecovery);
}
client.pushNotificationReceived({
{"key", key.toString()},
{"epoch", "test-epoch" },
{"revision", "6" }
});
CPPUNIT_ASSERT(listener.request != request);
finishSync(client, listener.request, {}, 6);
CPPUNIT_ASSERT(listener.opstate->ok.load());
}
void
DhtProxyClientTester::testRecoveryBackoff()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("recovery-backoff");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
for (unsigned attempt = 0; attempt < 8; ++attempt) {
auto request = listener.request;
auto generation = listener.opstate->generation.load();
Json::Value marker;
marker["sync"]["epoch"] = "test-epoch";
marker["sync"]["revision"] = Json::UInt64(1);
marker["sync"]["snapshot"] = true;
marker["sync"]["complete"] = false;
marker["sync"]["retry"] = Json::Value(Json::objectValue);
auto body = Json::writeString(client.jsonBuilder_, marker) + "\n";
request->cbs_.on_body(body.data(), body.size());
dht::http::Response response;
response.status_code = 200;
request->cbs_.on_state_change(dht::http::Request::State::DONE, response);
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(listener.request == request);
CPPUNIT_ASSERT_EQUAL(generation, listener.opstate->generation.load());
CPPUNIT_ASSERT(!listener.opstate->inFlight.load());
CPPUNIT_ASSERT(!listener.opstate->ok.load());
CPPUNIT_ASSERT(listener.retryDelay <= std::chrono::seconds(60));
client.handleResubscribe({}, key, 1, listener.opstate);
CPPUNIT_ASSERT(listener.request != request);
}
CPPUNIT_ASSERT(listener.retryDelay == std::chrono::seconds(60));
finishSync(client, listener.request, {}, 1);
CPPUNIT_ASSERT(listener.retryDelay == std::chrono::seconds(1));
CPPUNIT_ASSERT(listener.refreshSubscriberTimer->expiry() > system_clock::now() + std::chrono::minutes(1));
}
void
DhtProxyClientTester::testContiguousExpiration()
{
DhtProxyClient client({}, {}, [] {}, "", "OpenDHT-Test", "client-id", "push-token");
client.httpContext_.stop();
client.httpClientThread_.join();
client.proxyUrl_ = "http://127.0.0.1:1";
auto key = InfoHash::get("contiguous-expiration");
client.listen(key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {});
auto& listener = client.searches_.at(key).listeners.begin()->second;
auto first = std::make_shared<Value>("first");
first->id = 42;
auto second = std::make_shared<Value>("second");
second->id = 43;
finishSync(client, listener.request, {first, second}, 5);
CPPUNIT_ASSERT(listener.cache.onValuesAdded({first, second}, system_clock::now()));
auto request = listener.request;
client.pushNotificationReceived({
{"key", key.toString()},
{"epoch", "test-epoch" },
{"revision", "6" },
{"expired", "true" },
{"ids", "42,43" }
});
client.periodic(nullptr, 0, {}, clock::now());
CPPUNIT_ASSERT(listener.request == request);
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
CPPUNIT_ASSERT(!client.getLocalById(key, 43));
CPPUNIT_ASSERT_EQUAL(uint64_t(6), listener.syncRevision);
client.resubscribe(key, 1, listener);
finishSync(client, listener.request, {}, 6, true, false);
CPPUNIT_ASSERT(!client.getLocalById(key, 42));
CPPUNIT_ASSERT(!client.getLocalById(key, 43));
CPPUNIT_ASSERT(listener.opstate->ok.load());
request = listener.request;
client.pushNotificationReceived({
{"key", key.toString()},
{"epoch", "test-epoch" },
{"revision", "8" },
{"exp", "42" }
});
CPPUNIT_ASSERT(listener.request != request);
CPPUNIT_ASSERT(listener.needsRecovery);
}
#endif
} // namespace test
+62
View File
@@ -12,11 +12,73 @@ class DhtProxyClientTester : public CppUnit::TestFixture
CPPUNIT_TEST_SUITE(DhtProxyClientTester);
CPPUNIT_TEST(testResubscribeUsesKeyRoute);
CPPUNIT_TEST(testSetPushNotificationTokenResubscribesWithNewToken);
CPPUNIT_TEST(testRestartListenersFetchesValuesAfterFailedSubscribe);
CPPUNIT_TEST(testReadinessShutdown);
CPPUNIT_TEST(testReadinessReentrancy);
CPPUNIT_TEST(testCachedReadinessDispatch);
CPPUNIT_TEST(testReadinessCancelsAnotherListener);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
CPPUNIT_TEST(testInitialReadinessFailures);
CPPUNIT_TEST(testReadinessRecoveryIsOneShot);
CPPUNIT_TEST(testWrongSessionPushDoesNotTouchSubscriptions);
CPPUNIT_TEST(testNewSessionSynchronizesIndependently);
CPPUNIT_TEST(testFailedTimerRetryFetchesValues);
CPPUNIT_TEST(testFullRetryRemovesAbsentValues);
CPPUNIT_TEST(testStreamingFallbackExpiresSnapshot);
CPPUNIT_TEST(testLateRetryResponseDoesNotResurrectExpiredValue);
CPPUNIT_TEST(testTruncatedRecoveryRemainsFailed);
CPPUNIT_TEST(testTruncatedRecoveryAtEofFails);
CPPUNIT_TEST(testMissingCompletionDoesNotReconcile);
CPPUNIT_TEST(testSnapshotExpirationIgnoresClockRollback);
CPPUNIT_TEST(testSupersededFailureDoesNotPoisonSuccessfulRetry);
CPPUNIT_TEST(testSyncCoalescesConnectivityChanges);
CPPUNIT_TEST(testTokenRotationDuringRecovery);
CPPUNIT_TEST(testTokenRotationBackToPreviousEpoch);
CPPUNIT_TEST(testAuthoritativeEpochAfterStalePush);
CPPUNIT_TEST(testSyncEditsAndDuplicateValues);
CPPUNIT_TEST(testIncompleteSnapshotDoesNotReconcile);
CPPUNIT_TEST(testSubscriptionDeadline);
CPPUNIT_TEST(testInvalidPushRevision);
CPPUNIT_TEST(testRecoveryBackoff);
CPPUNIT_TEST(testContiguousExpiration);
CPPUNIT_TEST(testRejectsUnrevisionedPushes);
#endif
CPPUNIT_TEST_SUITE_END();
public:
void testResubscribeUsesKeyRoute();
void testSetPushNotificationTokenResubscribesWithNewToken();
void testRestartListenersFetchesValuesAfterFailedSubscribe();
void testReadinessShutdown();
void testReadinessReentrancy();
void testCachedReadinessDispatch();
void testReadinessCancelsAnotherListener();
#ifdef OPENDHT_PUSH_NOTIFICATIONS
void testInitialReadinessFailures();
void testReadinessRecoveryIsOneShot();
void testWrongSessionPushDoesNotTouchSubscriptions();
void testNewSessionSynchronizesIndependently();
void testFailedTimerRetryFetchesValues();
void testFullRetryRemovesAbsentValues();
void testStreamingFallbackExpiresSnapshot();
void testLateRetryResponseDoesNotResurrectExpiredValue();
void testTruncatedRecoveryRemainsFailed();
void testTruncatedRecoveryAtEofFails();
void testMissingCompletionDoesNotReconcile();
void testSnapshotExpirationIgnoresClockRollback();
void testSupersededFailureDoesNotPoisonSuccessfulRetry();
void testSyncCoalescesConnectivityChanges();
void testTokenRotationDuringRecovery();
void testTokenRotationBackToPreviousEpoch();
void testAuthoritativeEpochAfterStalePush();
void testSyncEditsAndDuplicateValues();
void testIncompleteSnapshotDoesNotReconcile();
void testSubscriptionDeadline();
void testInvalidPushRevision();
void testRecoveryBackoff();
void testContiguousExpiration();
void testRejectsUnrevisionedPushes();
#endif
};
} // namespace test
+793 -1
View File
@@ -16,6 +16,58 @@ using namespace std::literals;
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerTester);
CPPUNIT_TEST_SUITE_REGISTRATION(DhtRunnerPushTester);
namespace {
#if defined(OPENDHT_PROXY_CLIENT) && defined(OPENDHT_PUSH_NOTIFICATIONS)
constexpr auto stoppedPushResult = dht::PushNotificationResult::IgnoredStopped;
#else
constexpr auto stoppedPushResult = dht::PushNotificationResult::IgnoredDisabled;
#endif
} // namespace
void
DhtRunnerPushTester::testPushBeforeRun()
{
dht::DhtRunner node;
auto result = node.pushNotificationReceived({});
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
}
void
DhtRunnerPushTester::testPushAfterJoin()
{
dht::DhtRunner node;
node.run(0);
node.join();
auto result = node.pushNotificationReceived({});
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
}
void
DhtRunnerPushTester::testPushBurstAfterJoin()
{
dht::DhtRunner node;
node.run(0);
node.join();
for (unsigned i = 0; i < 32; ++i) {
auto result = node.pushNotificationReceived({});
CPPUNIT_ASSERT(result.wait_for(0s) == std::future_status::ready);
CPPUNIT_ASSERT(result.get() == stoppedPushResult);
}
}
void
DhtRunnerPushTester::testPushWhileRunning()
{
dht::DhtRunner node;
node.run(0);
auto result = node.pushNotificationReceived({});
CPPUNIT_ASSERT(result.wait_for(5s) == std::future_status::ready);
CPPUNIT_ASSERT(result.get() != dht::PushNotificationResult::IgnoredStopped);
}
template<typename T>
T
@@ -25,6 +77,152 @@ getFutureValue(std::future<T> future, std::chrono::steady_clock::duration timeou
return future.get();
}
void
DhtRunnerTester::testListenReadiness()
{
auto key = dht::InfoHash::get("native-listen-readiness");
auto value = std::make_shared<dht::Value>("initial-value");
value->id = 42;
std::promise<bool> stored;
auto storedFuture = stored.get_future();
node2.put(key, value, [&](bool ok) { stored.set_value(ok); });
CPPUNIT_ASSERT(storedFuture.wait_for(10s) == std::future_status::ready);
CPPUNIT_ASSERT(storedFuture.get());
bool received = false;
std::promise<bool> ready;
auto readyFuture = ready.get_future();
auto token = node1.listen(
key,
[&](const std::vector<dht::Sp<dht::Value>>& values, bool expired) {
if (!expired)
for (const auto& current : values)
received = received || current->id == 42;
return true;
},
{},
{},
[&](bool ok) { ready.set_value(ok && received); });
CPPUNIT_ASSERT(readyFuture.wait_for(10s) == std::future_status::ready);
CPPUNIT_ASSERT(readyFuture.get());
node1.cancelListen(key, token.get());
dht::DhtRunner isolated;
isolated.run(0);
std::promise<bool> emptyReady;
auto emptyFuture = emptyReady.get_future();
auto emptyToken = isolated.listen(
key,
[](const std::vector<dht::Sp<dht::Value>>&, bool) { return true; },
{},
{},
[&](bool ok) { emptyReady.set_value(ok); });
CPPUNIT_ASSERT(emptyFuture.wait_for(5s) == std::future_status::ready);
CPPUNIT_ASSERT(emptyFuture.get());
isolated.cancelListen(key, emptyToken.get());
isolated.join();
}
void
DhtRunnerTester::testListenReadinessStopped()
{
dht::DhtRunner stopped;
auto key = dht::InfoHash::get("stopped-listen-readiness");
std::vector<bool> results;
unsigned deliveries = 0;
auto listen = [&] {
auto token = stopped.listen(
key,
[&](const std::vector<dht::Sp<dht::Value>>&, bool) {
++deliveries;
return true;
},
{},
{},
[&](bool ok) { results.push_back(ok); });
CPPUNIT_ASSERT(token.wait_for(0s) == std::future_status::ready);
CPPUNIT_ASSERT_EQUAL(size_t(0), token.get());
};
listen();
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
stopped.run(0);
stopped.join();
listen();
CPPUNIT_ASSERT(results == std::vector<bool>({false, false}));
CPPUNIT_ASSERT_EQUAL(0u, deliveries);
}
void
DhtRunnerTester::testListenReadinessInvalidKey()
{
auto ready = std::make_shared<std::promise<bool>>();
auto future = ready->get_future();
auto deliveries = std::make_shared<std::atomic<unsigned>>(0);
auto token = node1.listen(
dht::InfoHash {},
[deliveries](const std::vector<dht::Sp<dht::Value>>&, bool) {
++*deliveries;
return true;
},
{},
{},
[ready](bool ok) { ready->set_value(ok); });
CPPUNIT_ASSERT_EQUAL(size_t(0), getFutureValue(std::move(token), 5s));
CPPUNIT_ASSERT(!getFutureValue(std::move(future), 5s));
CPPUNIT_ASSERT_EQUAL(0u, deliveries->load());
}
void
DhtRunnerTester::testListenReadinessFilteredSignedValues()
{
dht::DhtRunner source;
dht::DhtRunner::Config config;
config.dht_config.id = dht::crypto::generateIdentity();
source.run(0, config);
auto bound = node1.getBound();
if (bound.isUnspecified())
bound.setLoopback();
source.bootstrap(bound);
auto key = dht::InfoHash::get("signed-listen-readiness");
auto initial = std::make_shared<dht::Value>("initial-signed-value");
initial->id = 42;
auto stored = std::make_shared<std::promise<bool>>();
auto storedFuture = stored->get_future();
source.putSigned(key, initial, [stored](bool ok) { stored->set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(std::move(storedFuture), 10s));
auto ignored = std::make_shared<dht::Value>("filtered-value");
ignored->id = 43;
auto ignoredStored = std::make_shared<std::promise<bool>>();
auto ignoredFuture = ignoredStored->get_future();
source.put(key, ignored, [ignoredStored](bool ok) { ignoredStored->set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(std::move(ignoredFuture), 10s));
struct InitialValues
{
unsigned count {0};
bool valid {true};
std::promise<bool> ready;
};
auto state = std::make_shared<InitialValues>();
auto future = state->ready.get_future();
auto token = source.listen(
key,
[state](const std::vector<dht::Sp<dht::Value>>& values, bool expired) {
if (!expired)
for (const auto& value : values) {
++state->count;
state->valid = state->valid && value->id == 42 && value->isSigned() && value->checkSignature();
}
return true;
},
[](const dht::Value& value) { return value.id == 42; },
{},
[state](bool ok) { state->ready.set_value(ok && state->valid && state->count == 1); });
CPPUNIT_ASSERT(getFutureValue(std::move(future), 10s));
source.cancelListen(key, getFutureValue(std::move(token), 5s));
source.join();
}
void
DhtRunnerTester::setUp()
{
@@ -91,6 +289,42 @@ DhtRunnerTester::testGetPut()
CPPUNIT_ASSERT(vals.front()->data == val_data);
}
void
DhtRunnerTester::testStoreEmptyValue()
{
dht::DhtRunner::Config config;
config.dht_config.node_config.max_peer_req_per_sec = -1;
config.dht_config.node_config.max_req_per_sec = -1;
config.dht_config.node_config.max_store_size = 100; // Small size to force expireStore()
config.dht_config.node_config.max_store_keys = -1;
dht::DhtRunner testNode;
testNode.run(0, config);
testNode.bootstrap(node1.getBound());
auto key = testNode.getId();
auto val = std::make_shared<dht::Value>();
val->data.clear(); // Ensure size is 0
CPPUNIT_ASSERT_EQUAL((size_t) 0, val->size());
std::promise<bool> p;
testNode.put(key, val, [&](bool ok) { p.set_value(ok); });
p.get_future().get(); // Wait for put to finish
// Put a large value to exceed max_store_size and trigger expireStore()
auto key2 = dht::InfoHash::get("large_value_test");
auto val2 = std::make_shared<dht::Value>();
val2->data = std::vector<uint8_t>(200, 0); // 200 bytes
std::promise<bool> p2;
testNode.put(key2, val2, [&](bool ok) { p2.set_value(ok); });
p2.get_future().get(); // Wait for put to finish
// Wait a bit to ensure the value is processed
std::this_thread::sleep_for(std::chrono::milliseconds(200));
testNode.join();
}
void
DhtRunnerTester::testPutDuplicate()
{
@@ -587,6 +821,115 @@ DhtRunnerTester::testSavedPortFallback()
std::remove((config.dht_config.node_config.persist_path + "_port.txt").c_str());
}
void
DhtRunnerTester::testListenValueEdit()
{
// Register an editable type on both nodes so the DHT allows edits.
static constexpr dht::ValueType::Id EDITABLE_TYPE_ID = 9999;
const dht::ValueType editableType {EDITABLE_TYPE_ID,
"editable-test",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(editableType);
node2.registerType(editableType);
auto key = dht::InfoHash::get("listenValueEdit");
auto identity = dht::crypto::generateIdentity("ListenEditTester");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events; // (id, expired?)
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals) {
events.emplace_back(v->id, expired);
}
cv.notify_all();
return true;
});
// Put initial value (signed, editable type)
auto val1 = std::make_shared<dht::Value>("version1");
val1->type = EDITABLE_TYPE_ID;
val1->id = 42;
val1->seq = 1;
val1->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, val1, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for the first value to be received
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Edit the value (same id, higher seq)
auto val2 = std::make_shared<dht::Value>("version2");
val2->type = EDITABLE_TYPE_ID;
val2->id = 42;
val2->seq = 2;
val2->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, val2, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for the edited value to be received
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.second)
adds++;
return adds >= 2;
}));
}
// Give some extra time for any stray callbacks
std::this_thread::sleep_for(500ms);
// Verify: we should have exactly 2 add events, no expire events.
// On value edition the listener should see only the new version
// (as an add), not an expiration of the old version.
{
std::lock_guard lk(mtx);
int addCount = 0;
int expireCount = 0;
for (const auto& e : events) {
if (e.second)
expireCount++;
else
addCount++;
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive 2 add callbacks (original + edit)", 2, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive no expire callbacks on edit", 0, expireCount);
}
// Now wait for the edited value to actually expire.
// Type expiration is 2s. The cache may add a grace period.
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Edited value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return e.second; });
}));
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListen()
{
@@ -799,7 +1142,7 @@ DhtRunnerTester::testIdOps()
valueCountEdit++;
cv.notify_all();
});
node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool /*expired*/) {
auto listenToken = node2.listen(key2, [&](const std::vector<std::shared_ptr<dht::Value>>& values, bool /*expired*/) {
for (const auto& v : values) {
if (v->seq == 0)
CPPUNIT_ASSERT_EQUAL("v1"s, dht::unpackMsg<std::string>(v->data));
@@ -843,6 +1186,8 @@ DhtRunnerTester::testIdOps()
});
std::unique_lock lk(mutex);
CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return valueCountEdit == 4u; }));
lk.unlock();
node2.cancelListen(key2, listenToken.get());
}
void
@@ -933,4 +1278,451 @@ DhtRunnerTester::testMultithread()
CPPUNIT_ASSERT_EQUAL(2 * N, putOkCount);
}
void
DhtRunnerTester::testGetAfterListen()
{
// Test that get() after a synced listen delivers values exactly once
// from the cache, and that the GetCallback return value is honored.
auto key = dht::InfoHash::get("getAfterListen");
// Put a value first
auto val = std::make_shared<dht::Value>("cached_val");
{
std::promise<bool> p;
node2.put(key, val, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Start a listen on node1 and wait for it to sync
std::mutex mtx;
std::condition_variable cv;
std::atomic_int listenAddCount {0};
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (!expired)
listenAddCount += vals.size();
cv.notify_all();
return true;
});
// Wait for listen to receive the value
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return listenAddCount.load() >= 1; }));
}
// Now do a get — should be served from the synced cache
auto vals = getFutureValue(node1.get(key));
CPPUNIT_ASSERT_MESSAGE("get() should return values", not vals.empty());
CPPUNIT_ASSERT(val->data == vals.front()->data);
// The listen callback should not have been called again by the get
std::this_thread::sleep_for(200ms);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Listen should not fire extra callbacks from get()", 1, listenAddCount.load());
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenDuplicatePut()
{
// Test that putting the exact same value multiple times only triggers
// one add callback in the listener (no duplicate delivery).
auto key = dht::InfoHash::get("listenDupPut");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.emplace_back(v->id, expired);
cv.notify_all();
return true;
});
// Put the same value 3 times
auto val = std::make_shared<dht::Value>("duplicate_test");
val->id = 123;
for (int i = 0; i < 3; i++) {
std::promise<bool> p;
node2.put(key, val, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for at least 1 add to arrive
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Give time for any spurious callbacks
std::this_thread::sleep_for(500ms);
// Should have exactly 1 add, 0 expires
{
std::lock_guard lk(mtx);
int addCount = 0, expireCount = 0;
for (const auto& e : events) {
if (e.second)
expireCount++;
else
addCount++;
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Duplicate puts should produce only 1 add callback", 1, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callbacks expected", 0, expireCount);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenMultiSourceExpire()
{
// Test that when multiple nodes store the same value, the listener sees
// exactly 1 add (no duplicates). When the value expires naturally, the
// listener sees exactly 1 expire.
static constexpr dht::ValueType::Id SHORT_TYPE_ID = 8888;
const dht::ValueType shortType {SHORT_TYPE_ID,
"short-lived",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(shortType);
node2.registerType(shortType);
auto key = dht::InfoHash::get("multiSourceExpire");
auto identity = dht::crypto::generateIdentity("MultiSourceTester");
std::mutex mtx;
std::condition_variable cv;
std::vector<std::pair<dht::Value::Id, bool>> events;
// node1 listens; node1 and node2 put
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.emplace_back(v->id, expired);
cv.notify_all();
return true;
});
// Both node1 and node2 put the same value (same id, same seq)
auto val = std::make_shared<dht::Value>("multi_source");
val->type = SHORT_TYPE_ID;
val->id = 55;
val->seq = 1;
val->sign(*identity.first);
{
std::promise<bool> p1, p2;
node1.put(key, val, [&](bool ok) { p1.set_value(ok); });
node2.put(key, val, [&](bool ok) { p2.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p1.get_future()));
CPPUNIT_ASSERT(getFutureValue(p2.get_future()));
}
// Wait for at least one add
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return !e.second; });
}));
}
// Allow time for duplicate callbacks
std::this_thread::sleep_for(1s);
// Should have exactly 1 add (deduplication in OpValueCache)
{
std::lock_guard lk(mtx);
int addCount = 0;
for (const auto& e : events)
if (!e.second)
addCount++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Multiple sources should produce only 1 add callback", 1, addCount);
}
// Wait for natural expiration (type is 2s + grace period)
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const auto& e) { return e.second; });
}));
}
// Should have exactly 1 expire (no duplicate expires from multiple sources)
{
std::lock_guard lk(mtx);
int expireCount = 0;
for (const auto& e : events)
if (e.second)
expireCount++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive exactly 1 expire callback", 1, expireCount);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenEditChainThenExpire()
{
// Chain of 5 rapid edits (seq 1→5) on the same value, then natural expiration.
// Must see 5 adds (one per edit), 0 expires during edits, then exactly 1 expire.
static constexpr dht::ValueType::Id CHAIN_TYPE_ID = 7654;
const dht::ValueType chainType {CHAIN_TYPE_ID,
"chain-edit",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(chainType);
node2.registerType(chainType);
auto key = dht::InfoHash::get("editChainExpire");
auto identity = dht::crypto::generateIdentity("ChainEditTester");
std::mutex mtx;
std::condition_variable cv;
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::vector<Event> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.push_back({v->id, v->seq, expired});
cv.notify_all();
return true;
});
// Rapid chain of 5 edits — wait for the first to arrive before continuing
constexpr int NUM_EDITS = 5;
{
auto v = std::make_shared<dht::Value>("version1");
v->type = CHAIN_TYPE_ID;
v->id = 99;
v->seq = 1;
v->sign(*identity.first);
std::promise<bool> p;
node2.put(key, v, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for first add to arrive before sending more edits
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return !e.expired; });
}));
}
// Now send remaining edits rapidly
for (int seq = 2; seq <= NUM_EDITS; seq++) {
auto v = std::make_shared<dht::Value>("version" + std::to_string(seq));
v->type = CHAIN_TYPE_ID;
v->id = 99;
v->seq = seq;
v->sign(*identity.first);
std::promise<bool> p;
node2.put(key, v, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for edits to arrive (at least 2: initial + one or more updates)
// Note: rapid edits may coalesce — the DHT stores only the latest seq,
// so the listener may not see every intermediate version.
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 15s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.expired)
adds++;
return adds >= 2;
}));
}
// Wait a bit more for any remaining events
std::this_thread::sleep_for(1s);
// Verify: multiple adds, 0 expires during edits (no phantom expiry)
int totalAdds;
{
std::lock_guard lk(mtx);
totalAdds = 0;
int expireCount = 0;
for (const auto& e : events) {
if (e.expired)
expireCount++;
else
totalAdds++;
}
CPPUNIT_ASSERT_MESSAGE("Should receive at least 2 add callbacks", totalAdds >= 2);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expires during edit chain", 0, expireCount);
// Verify the latest add has the highest seq
uint16_t maxSeq = 0;
for (const auto& e : events)
if (!e.expired && e.seq > maxSeq)
maxSeq = e.seq;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Latest add should have the final seq", (uint16_t) NUM_EDITS, maxSeq);
}
// Wait for natural expiration
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Value should eventually expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.expired; });
}));
}
// Verify exactly 1 expire with the latest seq
{
std::lock_guard lk(mtx);
int expireCount = 0;
uint16_t expiredSeq = 0;
for (const auto& e : events) {
if (e.expired) {
expireCount++;
expiredSeq = e.seq;
}
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("Should receive exactly 1 expire", 1, expireCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the latest version", (uint16_t) NUM_EDITS, expiredSeq);
}
node1.cancelListen(key, ftoken.get());
}
void
DhtRunnerTester::testListenMultiValuePartialExpire()
{
// Two values with different types: one short-lived (2s), one long-lived (60s).
// The short one expires while the long one stays. Verify isolation:
// no spurious expire for the long-lived value.
static constexpr dht::ValueType::Id SHORT_TYPE_ID = 3001;
static constexpr dht::ValueType::Id LONG_TYPE_ID = 3002;
const dht::ValueType shortType {SHORT_TYPE_ID,
"short-partial",
std::chrono::seconds(2),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
const dht::ValueType longType {LONG_TYPE_ID,
"long-partial",
std::chrono::seconds(600),
dht::ValueType::DEFAULT_STORE_POLICY,
[](dht::InfoHash,
const std::shared_ptr<dht::Value>&,
std::shared_ptr<dht::Value>&,
const dht::InfoHash&,
const dht::SockAddr&) { return true; }};
node1.registerType(shortType);
node1.registerType(longType);
node2.registerType(shortType);
node2.registerType(longType);
auto key = dht::InfoHash::get("multiValuePartialExpire");
auto identity = dht::crypto::generateIdentity("PartialExpireTester");
std::mutex mtx;
std::condition_variable cv;
struct Event
{
dht::Value::Id id;
bool expired;
};
std::vector<Event> events;
auto ftoken = node1.listen(key, [&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
std::lock_guard lk(mtx);
for (const auto& v : vals)
events.push_back({v->id, expired});
cv.notify_all();
return true;
});
// Put short-lived value (id=1)
auto shortVal = std::make_shared<dht::Value>("short");
shortVal->type = SHORT_TYPE_ID;
shortVal->id = 1;
shortVal->seq = 1;
shortVal->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, shortVal, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Put long-lived value (id=2)
auto longVal = std::make_shared<dht::Value>("long");
longVal->type = LONG_TYPE_ID;
longVal->id = 2;
longVal->seq = 1;
longVal->sign(*identity.first);
{
std::promise<bool> p;
node2.put(key, longVal, [&](bool ok) { p.set_value(ok); });
CPPUNIT_ASSERT(getFutureValue(p.get_future()));
}
// Wait for both adds
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] {
int adds = 0;
for (const auto& e : events)
if (!e.expired)
adds++;
return adds >= 2;
}));
}
// Wait for short-lived value to expire
{
std::unique_lock lk(mtx);
CPPUNIT_ASSERT_MESSAGE("Short-lived value should expire", cv.wait_for(lk, 60s, [&] {
return std::any_of(events.begin(), events.end(), [](const Event& e) { return e.id == 1 && e.expired; });
}));
}
// Give extra time for any spurious callbacks
std::this_thread::sleep_for(1s);
// Verify: value 1 expired, value 2 did NOT expire
{
std::lock_guard lk(mtx);
bool longExpired = std::any_of(events.begin(), events.end(), [](const Event& e) {
return e.id == 2 && e.expired;
});
CPPUNIT_ASSERT_MESSAGE("Long-lived value should NOT be expired", !longExpired);
int shortExpires = 0;
for (const auto& e : events)
if (e.id == 1 && e.expired)
shortExpires++;
CPPUNIT_ASSERT_EQUAL_MESSAGE("Short-lived value should expire exactly once", 1, shortExpires);
}
node1.cancelListen(key, ftoken.get());
}
} // namespace test
+44
View File
@@ -10,6 +10,22 @@
namespace test {
class DhtRunnerPushTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(DhtRunnerPushTester);
CPPUNIT_TEST(testPushBeforeRun);
CPPUNIT_TEST(testPushAfterJoin);
CPPUNIT_TEST(testPushBurstAfterJoin);
CPPUNIT_TEST(testPushWhileRunning);
CPPUNIT_TEST_SUITE_END();
public:
void testPushBeforeRun();
void testPushAfterJoin();
void testPushBurstAfterJoin();
void testPushWhileRunning();
};
class DhtRunnerTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(DhtRunnerTester);
@@ -18,7 +34,13 @@ class DhtRunnerTester : public CppUnit::TestFixture
CPPUNIT_TEST(testPutDuplicate);
CPPUNIT_TEST(testPutOverride);
CPPUNIT_TEST(testListen);
CPPUNIT_TEST(testListenReadiness);
CPPUNIT_TEST(testListenReadinessStopped);
CPPUNIT_TEST(testListenReadinessInvalidKey);
CPPUNIT_TEST(testListenReadinessFilteredSignedValues);
CPPUNIT_TEST(testListenValueEdit);
CPPUNIT_TEST(testListenLotOfBytes);
CPPUNIT_TEST(testStoreEmptyValue);
CPPUNIT_TEST(testIdOps);
CPPUNIT_TEST(testImportValuesPreservesRemoteQuota);
CPPUNIT_TEST(testExportValuesUsesSystemTime);
@@ -29,6 +51,11 @@ class DhtRunnerTester : public CppUnit::TestFixture
CPPUNIT_TEST(testBootstrapMissingNodeThenPutFails);
CPPUNIT_TEST(testShutdownCompletesWithPendingPut);
CPPUNIT_TEST(testSavedPortFallback);
CPPUNIT_TEST(testGetAfterListen);
CPPUNIT_TEST(testListenDuplicatePut);
CPPUNIT_TEST(testListenMultiSourceExpire);
CPPUNIT_TEST(testListenEditChainThenExpire);
CPPUNIT_TEST(testListenMultiValuePartialExpire);
CPPUNIT_TEST_SUITE_END();
dht::DhtRunner node1 {};
@@ -63,6 +90,10 @@ public:
* Test listen method
*/
void testListen();
void testListenReadiness();
void testListenReadinessStopped();
void testListenReadinessInvalidKey();
void testListenReadinessFilteredSignedValues();
/**
* Test methods requiring a node identity
*/
@@ -76,10 +107,23 @@ public:
void testBootstrapMissingNodeThenPutFails();
void testShutdownCompletesWithPendingPut();
void testSavedPortFallback();
void testGetAfterListen();
void testListenDuplicatePut();
void testListenMultiSourceExpire();
void testListenEditChainThenExpire();
void testListenMultiValuePartialExpire();
/**
* Test that editing a value only triggers an add callback, no expire
*/
void testListenValueEdit();
/**
* Test listen method with lot of datas
*/
void testListenLotOfBytes();
/**
* Test storing an empty value
*/
void testStoreEmptyValue();
/**
* Test multithread
*/
+843 -4
View File
@@ -94,8 +94,8 @@ public:
return 0;
}
bool hasIPv4() const override { return true; }
bool hasIPv6() const override { return false; }
bool hasIPv4() const override { return ipv4; }
bool hasIPv6() const override { return ipv6; }
const SockAddr& getBoundRef(sa_family_t family = AF_UNSPEC) const override
{
(void) family;
@@ -112,6 +112,8 @@ public:
SockAddr bound_;
std::vector<SentPacket> sends {};
bool ipv4 {true};
bool ipv6 {false};
};
static Blob
@@ -230,7 +232,9 @@ makeEngine(std::unique_ptr<TestDatagramSocket>&& socket,
[](Sp<Node>) { return net::RequestAnswer {}; },
[](Sp<Node>, const InfoHash&, want_t) { return net::RequestAnswer {}; },
[](Sp<Node>, const InfoHash&, want_t, const Query&) { return net::RequestAnswer {}; },
[](Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int) { return net::RequestAnswer {}; },
[](Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int, const std::optional<net::ListenSync>&) {
return net::RequestAnswer {};
},
[](Sp<Node>, const InfoHash&, const Blob&, const std::vector<Sp<Value>>&, const time_point&) {
return net::RequestAnswer {};
},
@@ -252,6 +256,164 @@ addPendingGetRequest(net::NetworkEngine& engine, const InfoHash& id, const SockA
return node;
}
struct ListenSnapshotTransport
{
InfoHash key {InfoHash::get("initial-listen-snapshot")};
std::vector<Value::Id> received;
std::vector<bool> results;
std::vector<size_t> snapshots;
std::vector<TestDatagramSocket::SentPacket> delayedUpdates;
TestDatagramSocket* clientPackets;
TestDatagramSocket* serverPackets;
SockAddr clientAddr;
SockAddr serverAddr;
std::unique_ptr<Dht> client;
std::unique_ptr<Dht> server;
size_t token {0};
explicit ListenSnapshotTransport(uint64_t serverSeed = 31)
{
Config config {};
config.max_req_per_sec = -1;
config.max_peer_req_per_sec = -1;
auto clientSocket = std::make_unique<TestDatagramSocket>();
clientPackets = clientSocket.get();
clientAddr = clientPackets->bound_;
client = std::make_unique<Dht>(std::move(clientSocket),
config,
Sp<Logger> {},
std::make_unique<std::mt19937_64>(30));
auto serverSocket = std::make_unique<TestDatagramSocket>();
serverPackets = serverSocket.get();
serverPackets->bound_.setPort(5031);
serverAddr = serverPackets->bound_;
server = std::make_unique<Dht>(std::move(serverSocket),
config,
Sp<Logger> {},
std::make_unique<std::mt19937_64>(serverSeed));
}
~ListenSnapshotTransport()
{
if (token)
client->cancelListen(key, token);
}
static ParsedMessage parse(const TestDatagramSocket::SentPacket& packet)
{
ParsedMessage message;
message.msgpack_unpack(msgpack::unpack((const char*) packet.data.data(), packet.data.size()).get());
return message;
}
void store(size_t count, size_t payloadSize = 20)
{
for (size_t index = 0; index < count; ++index) {
auto value = std::make_shared<Value>(std::string(payloadSize, 'x'));
value->id = index + 1;
CPPUNIT_ASSERT(server->storageStore(key, value, server->scheduler.time()));
}
}
void listen()
{
token = client->listen(
key,
[this](const std::vector<Sp<Value>>& values, bool expired) {
if (!expired)
for (const auto& value : values)
received.emplace_back(value->id);
return true;
},
{},
{},
[this](bool ok) {
results.push_back(ok);
snapshots.emplace_back(received.size());
});
CPPUNIT_ASSERT(token != 0);
}
void start(bool confirm = true)
{
listen();
auto search = client->searches(AF_INET).at(key);
auto node = client->network_engine.insertNode(server->getNodeId(), serverAddr);
auto* searchNode = search->getNode(node);
CPPUNIT_ASSERT(searchNode);
searchNode->token = server->makeToken(clientAddr, false);
searchNode->last_get_reply = client->scheduler.time();
client->searchStep(search);
if (confirm)
acknowledge();
}
void acknowledge(bool legacy = false, bool updatesFirst = false, bool includeToken = true)
{
auto requests = std::move(clientPackets->sends);
clientPackets->sends.clear();
unsigned registrations = 0;
Blob legacyToken;
for (const auto& packet : requests) {
if (parse(packet).type == MessageType::Listen) {
++registrations;
if (legacy) {
auto message = parse(packet);
auto node = server->network_engine.insertNode(client->getNodeId(), clientAddr);
auto answer = server->onListen(node, key, message.token, message.socket_id, message.query, 1, {});
legacyToken = std::move(answer.ntoken);
} else
server->network_engine.processMessage(packet.data.data(), packet.data.size(), clientAddr);
}
}
CPPUNIT_ASSERT_EQUAL(1u, registrations);
auto replies = std::move(serverPackets->sends);
serverPackets->sends.clear();
std::vector<TestDatagramSocket::SentPacket> confirmations;
for (const auto& packet : replies) {
if (parse(packet).type == MessageType::Reply)
confirmations.emplace_back(packet);
else
delayedUpdates.emplace_back(packet);
}
if (updatesFirst)
deliverUpdates();
if (legacy) {
ParsedMessage acknowledgment;
acknowledgment.type = MessageType::Reply;
if (includeToken)
acknowledgment.token = std::move(legacyToken);
initialRequest()->setDone(std::move(acknowledgment));
}
for (const auto& packet : confirmations)
client->network_engine.processMessage(packet.data.data(), packet.data.size(), serverAddr);
}
void getRound(bool reversed = false)
{
for (const auto& packet : clientPackets->sends)
CPPUNIT_ASSERT(parse(packet).type != MessageType::GetValues);
if (reversed)
std::reverse(delayedUpdates.begin(), delayedUpdates.end());
deliverUpdates();
}
void deliverUpdates()
{
for (const auto& packet : delayedUpdates)
client->network_engine.processMessage(packet.data.data(), packet.data.size(), serverAddr);
delayedUpdates.clear();
}
Sp<net::Request> initialRequest() const
{
auto search = client->searches(AF_INET).at(key);
auto& status = search->nodes.front()->listenStatus.begin()->second;
CPPUNIT_ASSERT(status.req);
return status.req;
}
};
void
NetworkEngineTester::setUp()
{}
@@ -479,7 +641,8 @@ NetworkEngineTester::testListenConfirmationCarriesToken()
[](Sp<Node>) { return net::RequestAnswer {}; },
[](Sp<Node>, const InfoHash&, want_t) { return net::RequestAnswer {}; },
[](Sp<Node>, const InfoHash&, want_t, const Query&) { return net::RequestAnswer {}; },
[responseToken](Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int) {
[responseToken](
Sp<Node>, const InfoHash&, const Blob&, Tid, const Query&, int, const std::optional<net::ListenSync>&) {
net::RequestAnswer answer {};
answer.ntoken = responseToken;
return answer;
@@ -555,6 +718,682 @@ NetworkEngineTester::testListenConfirmationUpdatesSearchNodeToken()
CPPUNIT_ASSERT(!searchNodePtr->isSynced(localDht.scheduler.time()));
}
void
NetworkEngineTester::testListenReadinessWaitsForInitialUpdate()
{
Config config {};
config.max_req_per_sec = -1;
config.max_peer_req_per_sec = -1;
auto clientSocket = std::make_unique<TestDatagramSocket>();
auto* clientPackets = clientSocket.get();
auto clientAddr = clientPackets->bound_;
Dht client(std::move(clientSocket), config, {}, std::make_unique<std::mt19937_64>(20));
auto serverSocket = std::make_unique<TestDatagramSocket>();
auto* serverPackets = serverSocket.get();
serverPackets->bound_.setPort(5020);
auto serverAddr = serverPackets->bound_;
Dht server(std::move(serverSocket), config, {}, std::make_unique<std::mt19937_64>(21));
auto key = InfoHash::get("readiness-reordered-update");
auto value = std::make_shared<Value>("initial-value");
value->id = 42;
CPPUNIT_ASSERT(server.storageStore(key, value, server.scheduler.time()));
std::vector<Value::Id> received;
std::vector<size_t> snapshots;
auto token = client.listen(
key,
[&](const std::vector<Sp<Value>>& values, bool expired) {
if (!expired)
for (const auto& current : values)
received.emplace_back(current->id);
return true;
},
{},
{},
[&](bool ok) {
CPPUNIT_ASSERT(ok);
snapshots.emplace_back(received.size());
});
CPPUNIT_ASSERT(token != 0);
auto search = client.searches(AF_INET).at(key);
auto node = client.network_engine.insertNode(server.getNodeId(), serverAddr);
auto* searchNode = search->getNode(node);
CPPUNIT_ASSERT(searchNode);
searchNode->token = server.makeToken(clientAddr, false);
searchNode->last_get_reply = client.scheduler.time();
CPPUNIT_ASSERT(search->isSynced(client.scheduler.time()));
client.searchStep(search);
CPPUNIT_ASSERT(snapshots.empty());
auto listen = std::find_if(clientPackets->sends.begin(), clientPackets->sends.end(), [](const auto& packet) {
ParsedMessage message;
message.msgpack_unpack(msgpack::unpack((const char*) packet.data.data(), packet.data.size()).get());
return message.type == MessageType::Listen;
});
CPPUNIT_ASSERT(listen != clientPackets->sends.end());
server.network_engine.processMessage(listen->data.data(), listen->data.size(), clientAddr);
CPPUNIT_ASSERT(serverPackets->sends.size() >= 2);
std::vector<TestDatagramSocket::SentPacket> updates;
std::vector<TestDatagramSocket::SentPacket> confirmations;
for (const auto& packet : serverPackets->sends) {
ParsedMessage message;
message.msgpack_unpack(msgpack::unpack((const char*) packet.data.data(), packet.data.size()).get());
if (message.type == MessageType::Reply)
confirmations.emplace_back(packet);
else if (message.type == MessageType::UpdateValue)
updates.emplace_back(packet);
}
CPPUNIT_ASSERT_EQUAL(size_t(1), confirmations.size());
CPPUNIT_ASSERT(!updates.empty());
for (const auto& packet : confirmations)
client.network_engine.processMessage(packet.data.data(), packet.data.size(), serverAddr);
CPPUNIT_ASSERT(snapshots.empty());
serverPackets->sends.clear();
for (const auto& packet : clientPackets->sends)
CPPUNIT_ASSERT(ListenSnapshotTransport::parse(packet).type != MessageType::GetValues);
for (const auto& packet : updates)
client.network_engine.processMessage(packet.data.data(), packet.data.size(), serverAddr);
CPPUNIT_ASSERT(client.cancelListen(key, token));
CPPUNIT_ASSERT(received == std::vector<Value::Id>({42}));
CPPUNIT_ASSERT_EQUAL(size_t(1), snapshots.size());
CPPUNIT_ASSERT_EQUAL(size_t(1), snapshots.front());
}
void
NetworkEngineTester::testListenReadinessSnapshotVariants()
{
for (size_t count : {size_t(0), size_t(3)}) {
ListenSnapshotTransport transport;
transport.store(count, 6000);
transport.start();
if (count != 0)
CPPUNIT_ASSERT(transport.results.empty());
transport.getRound(true);
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
CPPUNIT_ASSERT(transport.snapshots == std::vector<size_t>({count}));
transport.deliverUpdates();
CPPUNIT_ASSERT_EQUAL(count, transport.received.size());
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
}
}
void
NetworkEngineTester::testListenReadinessSnapshotLegacy()
{
for (size_t count : {size_t(0), size_t(3)})
for (bool updatesFirst : {false, true})
for (bool includeToken : {false, true}) {
ListenSnapshotTransport transport;
transport.store(count, 6000);
transport.start(false);
auto request = transport.initialRequest();
auto search = transport.client->searches(AF_INET).at(transport.key);
auto query = search->nodes.front()->listenStatus.begin()->first;
auto socketId = search->nodes.front()->listenStatus.at(query).socketId;
transport.acknowledge(true, updatesFirst, includeToken);
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
CPPUNIT_ASSERT(transport.snapshots == std::vector<size_t>({updatesFirst ? count : 0}));
CPPUNIT_ASSERT(request->node->getSocket(socketId));
transport.getRound(true);
CPPUNIT_ASSERT_EQUAL(count, transport.received.size());
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
std::vector<bool> reusedResults;
size_t reusedValues = 0;
auto reused = transport.client->listen(
transport.key,
[&](const std::vector<Sp<Value>>& values, bool expired) {
if (!expired)
reusedValues += values.size();
return true;
},
{},
{},
[&](bool ok) { reusedResults.emplace_back(ok); });
CPPUNIT_ASSERT(reused != 0);
CPPUNIT_ASSERT_EQUAL(count, reusedValues);
CPPUNIT_ASSERT(reusedResults == std::vector<bool>({true}));
CPPUNIT_ASSERT(transport.client->cancelListen(transport.key, reused));
}
}
void
NetworkEngineTester::testListenLegacyLateValuesAndRenewal()
{
ListenSnapshotTransport transport;
transport.start(false);
transport.acknowledge(true, false, false);
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
auto search = transport.client->searches(AF_INET).at(transport.key);
auto node = transport.initialRequest()->node;
auto* source = search->getNode(node);
auto query = source->listenStatus.begin()->first;
auto socketId = source->listenStatus.at(query).socketId;
auto token = source->token;
CPPUNIT_ASSERT(!token.empty());
transport.client->scheduler.syncTime(transport.client->scheduler.time() + std::chrono::seconds(35));
transport.client->scheduler.run();
CPPUNIT_ASSERT(node->getSocket(socketId));
source = search->getNode(node);
CPPUNIT_ASSERT(source);
CPPUNIT_ASSERT(source->token == token);
auto first = std::make_shared<Value>("late-legacy-value");
first->id = 42;
CPPUNIT_ASSERT(transport.server->storageStore(transport.key, first, transport.server->scheduler.time()));
transport.delayedUpdates = std::move(transport.serverPackets->sends);
transport.serverPackets->sends.clear();
transport.deliverUpdates();
CPPUNIT_ASSERT(transport.received == std::vector<Value::Id>({42}));
if (!source->listenStatus.at(query).req->pending()) {
transport.clientPackets->sends.clear();
source->listenStatus.at(query).req->reply_time = transport.client->scheduler.time()
- transport.client->getListenExpiration();
transport.client->searchSynchedNodeListen(search, *source);
}
transport.acknowledge(true, false, false);
CPPUNIT_ASSERT(transport.delayedUpdates.empty());
CPPUNIT_ASSERT(node->getSocket(socketId));
CPPUNIT_ASSERT_EQUAL(socketId, source->listenStatus.at(query).socketId);
CPPUNIT_ASSERT(source->token == token);
auto second = std::make_shared<Value>("after-legacy-renewal");
second->id = 43;
CPPUNIT_ASSERT(transport.server->storageStore(transport.key, second, transport.server->scheduler.time()));
transport.delayedUpdates = std::move(transport.serverPackets->sends);
transport.serverPackets->sends.clear();
transport.getRound();
CPPUNIT_ASSERT(transport.received == std::vector<Value::Id>({42, 43}));
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
}
void
NetworkEngineTester::testListenReadinessMixedPeers()
{
ListenSnapshotTransport transport;
transport.store(1);
transport.start(false);
auto nativeRequests = std::move(transport.clientPackets->sends);
transport.clientPackets->sends.clear();
ListenSnapshotTransport legacy(32);
legacy.serverAddr.setPort(5032);
legacy.serverPackets->bound_ = legacy.serverAddr;
legacy.store(2);
auto search = transport.client->searches(AF_INET).at(transport.key);
auto node = transport.client->network_engine.insertNode(legacy.server->getNodeId(), legacy.serverAddr);
auto* source = search->getNode(node);
CPPUNIT_ASSERT(source);
source->token = legacy.server->makeToken(transport.clientAddr, false);
source->last_get_reply = transport.client->scheduler.time();
transport.client->searchSynchedNodeListen(search, *source);
CPPUNIT_ASSERT_EQUAL(size_t(1), source->listenStatus.size());
auto& status = source->listenStatus.begin()->second;
auto peer = legacy.server->network_engine.insertNode(transport.client->getNodeId(), transport.clientAddr);
auto query = *source->listenStatus.begin()->first;
legacy.server->storageAddListener(transport.key, peer, status.socketId, std::move(query), 1, {});
ParsedMessage acknowledgment;
acknowledgment.type = MessageType::Reply;
status.req->setDone(std::move(acknowledgment));
CPPUNIT_ASSERT(transport.results.empty());
for (const auto& packet : legacy.serverPackets->sends)
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), legacy.serverAddr);
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.received.size());
CPPUNIT_ASSERT(transport.results.empty());
transport.clientPackets->sends = std::move(nativeRequests);
transport.acknowledge();
CPPUNIT_ASSERT(transport.results.empty());
transport.getRound();
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.received.size());
}
void
NetworkEngineTester::testListenReadinessSnapshotFailure()
{
for (unsigned failure = 0; failure < 3; ++failure) {
ListenSnapshotTransport transport;
transport.store(1);
transport.start(false);
auto request = transport.initialRequest();
if (failure == 0) {
request->setExpired();
} else {
ParsedMessage answer;
answer.type = MessageType::Reply;
if (failure == 1) {
auto search = transport.client->searches(AF_INET).at(transport.key);
auto registration = search->nodes.front()->listenStatus.begin()->second.registration;
answer.listenSync = net::ListenSync {registration, 1, 0, 0};
}
if (failure == 2) {
answer.token = transport.server->makeToken(transport.clientAddr, false);
answer.fields.emplace_back(std::make_shared<FieldValueIndex>());
}
request->setDone(std::move(answer));
}
CPPUNIT_ASSERT(transport.results == std::vector<bool>({false}));
transport.deliverUpdates();
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
}
}
void
NetworkEngineTester::testListenReadinessSnapshotCancellation()
{
ListenSnapshotTransport transport;
transport.store(1);
transport.start();
CPPUNIT_ASSERT(transport.client->cancelListen(transport.key, transport.token));
transport.token = 0;
transport.getRound();
transport.getRound();
CPPUNIT_ASSERT(transport.results.empty());
transport.listen();
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
CPPUNIT_ASSERT(transport.snapshots == std::vector<size_t>({1}));
}
void
NetworkEngineTester::testListenReadinessSnapshotReplacement()
{
ListenSnapshotTransport transport;
transport.store(1);
transport.start();
auto previous = transport.initialRequest();
auto search = transport.client->searches(AF_INET).at(transport.key);
auto* node = search->nodes.front().get();
auto oldUpdates = std::move(transport.delayedUpdates);
node->listenStatus.clear();
transport.clientPackets->sends.clear();
transport.client->searchSynchedNodeListen(search, *node);
transport.acknowledge();
CPPUNIT_ASSERT(transport.results.empty());
ParsedMessage answer;
answer.token = transport.server->makeToken(transport.clientAddr, false);
answer.values = transport.server->getLocal(transport.key);
previous->setDone(std::move(answer));
for (const auto& packet : oldUpdates)
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.serverAddr);
CPPUNIT_ASSERT(transport.results.empty());
transport.getRound();
transport.getRound();
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
CPPUNIT_ASSERT(transport.snapshots == std::vector<size_t>({1}));
}
void
NetworkEngineTester::testListenReadinessSnapshotRenewal()
{
ListenSnapshotTransport transport;
transport.store(1);
transport.start();
transport.getRound();
transport.getRound();
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
transport.deliverUpdates();
transport.clientPackets->sends.clear();
auto search = transport.client->searches(AF_INET).at(transport.key);
auto* node = search->nodes.front().get();
node->listenStatus.begin()->second.req->reply_time = transport.client->scheduler.time()
- transport.client->getListenExpiration();
transport.client->searchSynchedNodeListen(search, *node);
transport.acknowledge();
CPPUNIT_ASSERT(transport.delayedUpdates.empty());
for (const auto& packet : transport.clientPackets->sends)
CPPUNIT_ASSERT(ListenSnapshotTransport::parse(packet).type != MessageType::GetValues);
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
}
void
NetworkEngineTester::testListenUpdatesBeforeConfirmation()
{
ListenSnapshotTransport transport;
transport.store(40, 6000);
transport.start(false);
auto request = transport.initialRequest();
auto listen = std::find_if(transport.clientPackets->sends.begin(),
transport.clientPackets->sends.end(),
[](const auto& packet) {
return ListenSnapshotTransport::parse(packet).type == MessageType::Listen;
});
CPPUNIT_ASSERT(listen != transport.clientPackets->sends.end());
transport.server->network_engine.processMessage(listen->data.data(), listen->data.size(), transport.clientAddr);
auto replies = std::move(transport.serverPackets->sends);
std::vector<TestDatagramSocket::SentPacket> confirmations;
std::reverse(replies.begin(), replies.end());
for (const auto& packet : replies) {
if (ListenSnapshotTransport::parse(packet).type == MessageType::Reply)
confirmations.emplace_back(packet);
else
transport.client->network_engine.processMessage(packet.data.data(),
packet.data.size(),
transport.serverAddr);
}
CPPUNIT_ASSERT(transport.results.empty());
CPPUNIT_ASSERT(transport.received.empty());
CPPUNIT_ASSERT_EQUAL(size_t(1), confirmations.size());
auto& confirmation = confirmations.front();
transport.client->network_engine.processMessage(confirmation.data.data(),
confirmation.data.size(),
transport.serverAddr);
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
CPPUNIT_ASSERT_EQUAL(size_t(40), transport.received.size());
for (const auto& packet : replies)
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.serverAddr);
CPPUNIT_ASSERT_EQUAL(size_t(40), transport.received.size());
}
void
NetworkEngineTester::testListenLiveChangesBeforeSnapshot()
{
ListenSnapshotTransport transport;
transport.store(1);
transport.start();
auto& storage = transport.server->store.at(transport.key);
auto removed = storage.remove(transport.key, 1);
CPPUNIT_ASSERT(removed);
transport.server->storageRemoved(transport.key, storage, {removed}, removed->size());
auto value = std::make_shared<Value>("new-live-value");
value->id = 99;
CPPUNIT_ASSERT(transport.server->storageStore(transport.key, value, transport.server->scheduler.time()));
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.serverPackets->sends.size());
for (const auto& packet : transport.serverPackets->sends)
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.serverAddr);
CPPUNIT_ASSERT(transport.received.empty());
CPPUNIT_ASSERT(transport.results.empty());
transport.deliverUpdates();
auto search = transport.client->searches(AF_INET).at(transport.key);
CPPUNIT_ASSERT(!search->cache.get(1));
CPPUNIT_ASSERT(search->cache.get(99));
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
for (const auto& packet : transport.serverPackets->sends)
transport.client->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.serverAddr);
CPPUNIT_ASSERT(!search->cache.get(1));
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.received.size());
}
void
NetworkEngineTester::testListenDuplicateRegistration()
{
ListenSnapshotTransport transport;
transport.store(40);
transport.start(false);
auto listen = std::find_if(transport.clientPackets->sends.begin(),
transport.clientPackets->sends.end(),
[](const auto& packet) {
return ListenSnapshotTransport::parse(packet).type == MessageType::Listen;
});
CPPUNIT_ASSERT(listen != transport.clientPackets->sends.end());
auto packet = *listen;
transport.acknowledge();
CPPUNIT_ASSERT_EQUAL(size_t(2),
size_t(std::count_if(transport.delayedUpdates.begin(),
transport.delayedUpdates.end(),
[](const auto& update) {
return ListenSnapshotTransport::parse(update).type
== MessageType::UpdateValue;
})));
transport.server->network_engine.processMessage(packet.data.data(), packet.data.size(), transport.clientAddr);
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.serverPackets->sends.size());
auto confirmation = ListenSnapshotTransport::parse(transport.serverPackets->sends.front());
CPPUNIT_ASSERT(confirmation.type == MessageType::Reply);
CPPUNIT_ASSERT(confirmation.listenSync);
CPPUNIT_ASSERT_EQUAL(uint64_t(2), confirmation.listenSync->snapshotEnd);
transport.deliverUpdates();
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
CPPUNIT_ASSERT_EQUAL(size_t(40), transport.received.size());
}
void
NetworkEngineTester::testListenLargeSnapshotDefaultLimits()
{
ListenSnapshotTransport transport;
auto socket = std::make_unique<TestDatagramSocket>();
socket->bound_.setPort(5031);
transport.serverPackets = socket.get();
transport.serverAddr = socket->bound_;
transport.server = std::make_unique<Dht>(std::move(socket),
Config {},
Sp<Logger> {},
std::make_unique<std::mt19937_64>(31));
transport.store(4096);
transport.start();
CPPUNIT_ASSERT(transport.results.empty());
size_t batches = 0;
size_t payloads = 0;
for (const auto& packet : transport.delayedUpdates) {
auto message = ListenSnapshotTransport::parse(packet);
if (message.type == MessageType::UpdateValue) {
++batches;
payloads += message.values.size() + message.value_parts.size();
}
}
CPPUNIT_ASSERT_EQUAL(size_t(128), batches);
CPPUNIT_ASSERT_EQUAL(size_t(4096), payloads);
transport.getRound(true);
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
CPPUNIT_ASSERT_EQUAL(size_t(4096), transport.received.size());
auto search = transport.client->searches(AF_INET).at(transport.key);
CPPUNIT_ASSERT_EQUAL(size_t(4096), search->cache.get(Value::Filter {}).size());
for (const auto& packet : transport.clientPackets->sends)
CPPUNIT_ASSERT(ListenSnapshotTransport::parse(packet).type != MessageType::GetValues);
}
void
NetworkEngineTester::testListenLiveUpdateOrdering()
{
ListenSnapshotTransport transport;
transport.store(1);
transport.start();
transport.deliverUpdates();
for (Value::Id id : {2, 3}) {
auto value = std::make_shared<Value>("live");
value->id = id;
CPPUNIT_ASSERT(transport.server->storageStore(transport.key, value, transport.server->scheduler.time()));
}
CPPUNIT_ASSERT_EQUAL(size_t(2), transport.serverPackets->sends.size());
auto search = transport.client->searches(AF_INET).at(transport.key);
auto& later = transport.serverPackets->sends.back();
transport.client->network_engine.processMessage(later.data.data(), later.data.size(), transport.serverAddr);
CPPUNIT_ASSERT(!search->cache.get(3));
auto& earlier = transport.serverPackets->sends.front();
transport.client->network_engine.processMessage(earlier.data.data(), earlier.data.size(), transport.serverAddr);
CPPUNIT_ASSERT(search->cache.get(2));
CPPUNIT_ASSERT(search->cache.get(3));
CPPUNIT_ASSERT(transport.results == std::vector<bool>({true}));
}
void
NetworkEngineTester::testListenInitializationTimeout()
{
ListenSnapshotTransport transport;
transport.store(1);
transport.start();
CPPUNIT_ASSERT(transport.results.empty());
transport.client->scheduler.syncTime(transport.client->scheduler.time() + std::chrono::seconds(31));
transport.client->scheduler.run();
CPPUNIT_ASSERT(transport.results == std::vector<bool>({false}));
transport.deliverUpdates();
CPPUNIT_ASSERT_EQUAL(size_t(1), transport.results.size());
}
void
NetworkEngineTester::testListenRegistrationFailure()
{
ListenSnapshotTransport transport;
transport.store(1);
transport.start(false);
auto search = transport.client->searches(AF_INET).at(transport.key);
auto* node = search->nodes.front().get();
auto request = node->listenStatus.begin()->second.req;
CPPUNIT_ASSERT(request->pending());
CPPUNIT_ASSERT(!node->node->isExpired());
request->setExpired();
CPPUNIT_ASSERT(transport.results == std::vector<bool>({false}));
CPPUNIT_ASSERT(!node->node->isExpired());
transport.clientPackets->sends.clear();
transport.client->searchStep(search);
transport.acknowledge();
transport.getRound();
transport.getRound();
CPPUNIT_ASSERT(transport.results == std::vector<bool>({false}));
}
void
NetworkEngineTester::testListenRegistrationCapacity()
{
for (bool dualStack : {false, true}) {
auto socket = std::make_unique<TestDatagramSocket>();
socket->ipv6 = dualStack;
Config config;
config.max_searches = 1;
Dht localDht(std::move(socket), config, {}, std::make_unique<std::mt19937_64>(40));
auto occupiedKey = InfoHash::get("occupied-search");
auto rejectedKey = InfoHash::get("rejected-search");
auto values = [](const std::vector<Sp<Value>>&, bool) {
return true;
};
if (dualStack)
CPPUNIT_ASSERT(localDht.search(occupiedKey, AF_INET6));
else
CPPUNIT_ASSERT(localDht.listen(occupiedKey, values) != 0);
std::vector<bool> results;
CPPUNIT_ASSERT_EQUAL(size_t(0),
localDht.listen(rejectedKey, values, {}, {}, [&](bool ok) { results.push_back(ok); }));
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
CPPUNIT_ASSERT(localDht.store.at(rejectedKey).local_listeners.empty());
if (dualStack) {
auto partial = localDht.searches(AF_INET).at(rejectedKey);
CPPUNIT_ASSERT(!partial->cache.cancelListen(1, localDht.scheduler.time()));
partial->cache.onReady();
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
}
}
}
void
NetworkEngineTester::testListenReadinessAddressFamilies()
{
auto key = InfoHash::get("readiness-address-families");
for (unsigned families = 1; families <= 3; ++families) {
auto socket = std::make_unique<TestDatagramSocket>();
socket->ipv4 = families & 1;
socket->ipv6 = families & 2;
Dht localDht(std::move(socket), Config {}, {}, std::make_unique<std::mt19937_64>(families));
std::vector<bool> results;
auto token = localDht.listen(
key,
[](const std::vector<Sp<Value>>&, bool) { return true; },
{},
{},
[&](bool ok) { results.push_back(ok); });
CPPUNIT_ASSERT(token != 0);
CPPUNIT_ASSERT(results.empty());
if (families & 1) {
localDht.searchStep(localDht.searches(AF_INET).at(key));
if (families & 2)
CPPUNIT_ASSERT(results.empty());
}
if (families & 2)
localDht.searchStep(localDht.searches(AF_INET6).at(key));
CPPUNIT_ASSERT(results == std::vector<bool>({true}));
CPPUNIT_ASSERT(localDht.cancelListen(key, token));
results.clear();
CPPUNIT_ASSERT_EQUAL(size_t(0),
localDht.listen(
InfoHash {},
[](const std::vector<Sp<Value>>&, bool) { return true; },
{},
{},
[&](bool ok) { results.push_back(ok); }));
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
}
}
void
NetworkEngineTester::testListenReadinessFailedSearch()
{
Dht localDht(std::make_unique<TestDatagramSocket>(), Config {}, {}, std::make_unique<std::mt19937_64>(22));
auto key = InfoHash::get("readiness-failed-search");
std::vector<bool> results;
auto token = localDht.listen(
key, [](const std::vector<Sp<Value>>&, bool) { return true; }, {}, {}, [&](bool ok) { results.push_back(ok); });
auto search = localDht.searches(AF_INET).at(key);
auto node = std::make_shared<Node>(InfoHash::getRandom(localDht.rd),
makeIPv4("127.0.0.2", 5022),
localDht.rd,
false);
node->setExpired();
search->nodes.emplace_back(std::make_unique<Dht::SearchNode>(node));
localDht.searchStep(search);
CPPUNIT_ASSERT(search->expired);
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
search->cache.onReady();
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
CPPUNIT_ASSERT(localDht.cancelListen(key, token));
}
void
NetworkEngineTester::testListenReadinessReusesExpiredSearch()
{
Dht localDht(std::make_unique<TestDatagramSocket>(), Config {}, {}, std::make_unique<std::mt19937_64>(23));
auto key = InfoHash::get("readiness-reused-search");
auto values = [](const std::vector<Sp<Value>>&, bool) {
return true;
};
std::vector<bool> results;
auto ready = [&](bool ok) {
results.push_back(ok);
};
auto first = localDht.listen(key, values, {}, {}, ready);
auto search = localDht.searches(AF_INET).at(key);
search->expire();
CPPUNIT_ASSERT(results == std::vector<bool>({false}));
CPPUNIT_ASSERT(localDht.cancelListen(key, first));
auto second = localDht.listen(key, values, {}, {}, ready);
CPPUNIT_ASSERT(second != 0);
CPPUNIT_ASSERT(search == localDht.searches(AF_INET).at(key));
CPPUNIT_ASSERT(!search->expired);
CPPUNIT_ASSERT(!search->done);
CPPUNIT_ASSERT(localDht.scheduler.time() == search->nextSearchStep->t_);
localDht.searchStep(search);
CPPUNIT_ASSERT(results == std::vector<bool>({false, true}));
CPPUNIT_ASSERT(localDht.cancelListen(key, second));
}
void
NetworkEngineTester::testListenReadinessShutdown()
{
for (bool stop : {false, true}) {
std::vector<bool> results;
auto localDht = std::make_unique<Dht>(std::make_unique<TestDatagramSocket>(),
Config {},
Sp<Logger> {},
std::make_unique<std::mt19937_64>(24));
CPPUNIT_ASSERT(localDht->listen(
InfoHash::get("readiness-shutdown"),
[](const std::vector<Sp<Value>>&, bool) { return true; },
{},
{},
[&](bool ok) { results.push_back(ok); })
!= 0);
CPPUNIT_ASSERT(results.empty());
if (stop) {
localDht->shutdown({}, true);
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
CPPUNIT_ASSERT(!results.front());
}
localDht.reset();
CPPUNIT_ASSERT_EQUAL(size_t(1), results.size());
CPPUNIT_ASSERT(!results.front());
}
}
void
NetworkEngineTester::testListenReopensSocketAfterNodeExpiration()
{
+42
View File
@@ -22,6 +22,27 @@ class NetworkEngineTester : public CppUnit::TestFixture
CPPUNIT_TEST(testSeparatesPartialSessionsBySource);
CPPUNIT_TEST(testListenConfirmationCarriesToken);
CPPUNIT_TEST(testListenConfirmationUpdatesSearchNodeToken);
CPPUNIT_TEST(testListenReadinessWaitsForInitialUpdate);
CPPUNIT_TEST(testListenReadinessSnapshotVariants);
CPPUNIT_TEST(testListenReadinessSnapshotLegacy);
CPPUNIT_TEST(testListenLegacyLateValuesAndRenewal);
CPPUNIT_TEST(testListenReadinessMixedPeers);
CPPUNIT_TEST(testListenReadinessSnapshotFailure);
CPPUNIT_TEST(testListenReadinessSnapshotCancellation);
CPPUNIT_TEST(testListenReadinessSnapshotReplacement);
CPPUNIT_TEST(testListenReadinessSnapshotRenewal);
CPPUNIT_TEST(testListenUpdatesBeforeConfirmation);
CPPUNIT_TEST(testListenLiveChangesBeforeSnapshot);
CPPUNIT_TEST(testListenDuplicateRegistration);
CPPUNIT_TEST(testListenLargeSnapshotDefaultLimits);
CPPUNIT_TEST(testListenLiveUpdateOrdering);
CPPUNIT_TEST(testListenInitializationTimeout);
CPPUNIT_TEST(testListenRegistrationFailure);
CPPUNIT_TEST(testListenRegistrationCapacity);
CPPUNIT_TEST(testListenReadinessAddressFamilies);
CPPUNIT_TEST(testListenReadinessFailedSearch);
CPPUNIT_TEST(testListenReadinessReusesExpiredSearch);
CPPUNIT_TEST(testListenReadinessShutdown);
CPPUNIT_TEST(testListenReopensSocketAfterNodeExpiration);
CPPUNIT_TEST(testUnauthorizedListenFlushClearsListenState);
#endif
@@ -43,6 +64,27 @@ public:
void testSeparatesPartialSessionsBySource();
void testListenConfirmationCarriesToken();
void testListenConfirmationUpdatesSearchNodeToken();
void testListenReadinessWaitsForInitialUpdate();
void testListenReadinessSnapshotVariants();
void testListenReadinessSnapshotLegacy();
void testListenLegacyLateValuesAndRenewal();
void testListenReadinessMixedPeers();
void testListenReadinessSnapshotFailure();
void testListenReadinessSnapshotCancellation();
void testListenReadinessSnapshotReplacement();
void testListenReadinessSnapshotRenewal();
void testListenUpdatesBeforeConfirmation();
void testListenLiveChangesBeforeSnapshot();
void testListenDuplicateRegistration();
void testListenLargeSnapshotDefaultLimits();
void testListenLiveUpdateOrdering();
void testListenInitializationTimeout();
void testListenRegistrationFailure();
void testListenRegistrationCapacity();
void testListenReadinessAddressFamilies();
void testListenReadinessFailedSearch();
void testListenReadinessReusesExpiredSearch();
void testListenReadinessShutdown();
void testListenReopensSocketAfterNodeExpiration();
void testUnauthorizedListenFlushClearsListenState();
#endif
+951
View File
@@ -0,0 +1,951 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "test_opcache.h"
// Hack to test internal class
#include "../src/op_cache.cpp"
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(OpCacheTester);
namespace {
using Values = std::vector<std::shared_ptr<dht::Value>>;
struct Event
{
dht::Value::Id id;
uint16_t seq;
bool expired;
};
std::shared_ptr<dht::Value>
makeValue(dht::Value::Id id, uint16_t seq, std::string data)
{
auto v = std::make_shared<dht::Value>();
v->id = id;
v->seq = seq;
v->data = {data.begin(), data.end()};
return v;
}
dht::ValueCallback
recordEvents(std::vector<Event>& events)
{
return [&events](const Values& vals, bool expired) {
for (const auto& v : vals)
events.push_back({v->id, v->seq, expired});
return true;
};
}
unsigned
count(const std::vector<Event>& events, bool expired)
{
unsigned n = 0;
for (const auto& e : events)
if (e.expired == expired)
n++;
return n;
}
} // namespace
void
OpCacheTester::setUp()
{}
void
OpCacheTester::testInitialListenerReadiness()
{
dht::OpCache cache;
auto query = std::make_shared<dht::Query>();
unsigned calls = 0;
auto callback = [&](bool ok) {
CPPUNIT_ASSERT(ok);
++calls;
};
CPPUNIT_ASSERT(cache.addListener(1, [](const Values&, bool) { return true; }, query, {}, callback));
CPPUNIT_ASSERT_EQUAL(0u, calls);
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT_EQUAL(1u, calls);
cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
CPPUNIT_ASSERT(cache.addListener(4, [](const Values&, bool) { return true; }, query, {}, callback));
CPPUNIT_ASSERT_EQUAL(1u, calls);
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT_EQUAL(2u, calls);
CPPUNIT_ASSERT(cache.addListener(2, [](const Values&, bool) { return true; }, query, {}, callback));
CPPUNIT_ASSERT_EQUAL(3u, calls);
dht::OpCache isolated;
CPPUNIT_ASSERT(isolated.addListener(3, [](const Values&, bool) { return true; }, query, {}, callback));
isolated.onReady();
CPPUNIT_ASSERT_EQUAL(4u, calls);
dht::OpCache searching(true, true);
CPPUNIT_ASSERT(searching.addListener(5, [](const Values&, bool) { return true; }, query, {}, callback));
searching.onNodeChanged(dht::ListenSyncStatus::ADDED);
searching.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT_EQUAL(4u, calls);
searching.onSearchReady();
CPPUNIT_ASSERT_EQUAL(5u, calls);
}
void
OpCacheTester::testInitialReadinessCancellation()
{
dht::OpCache cache(true, true);
auto query = std::make_shared<dht::Query>();
auto values = [](const Values&, bool) {
return true;
};
unsigned calls = 0;
auto ready = [&](bool) {
++calls;
};
CPPUNIT_ASSERT(cache.addListener(1, values, query, {}, ready));
CPPUNIT_ASSERT(cache.removeListener(1, dht::clock::now()));
cache.onReady();
CPPUNIT_ASSERT_EQUAL(0u, calls);
dht::OpCache cleared(true, true);
CPPUNIT_ASSERT(cleared.addListener(1, values, query, {}, ready));
cleared.removeAll();
cleared.onReady(false);
CPPUNIT_ASSERT_EQUAL(0u, calls);
dht::OpCache reentrant(true, true);
CPPUNIT_ASSERT(reentrant.addListener(1, values, query, {}, [&](bool ok) {
CPPUNIT_ASSERT(ok);
CPPUNIT_ASSERT(reentrant.removeListener(2, dht::clock::now()));
}));
CPPUNIT_ASSERT(reentrant.addListener(2, values, query, {}, ready));
reentrant.onReady();
CPPUNIT_ASSERT_EQUAL(0u, calls);
dht::OpCache clearing(true, true);
CPPUNIT_ASSERT(clearing.addListener(1, values, query, {}, [&](bool) { clearing.removeAll(); }));
CPPUNIT_ASSERT(clearing.addListener(2, values, query, {}, ready));
clearing.onReady();
CPPUNIT_ASSERT_EQUAL(0u, calls);
}
void
OpCacheTester::testInitialReadinessFailure()
{
dht::OpCache cache(true, true);
auto query = std::make_shared<dht::Query>();
auto values = [](const Values&, bool) {
return true;
};
std::vector<bool> results;
auto ready = [&](bool ok) {
results.push_back(ok);
};
CPPUNIT_ASSERT(cache.addListener(1, values, query, {}, ready));
CPPUNIT_ASSERT(cache.addListener(2, values, query, {}, ready));
cache.onReady(false);
CPPUNIT_ASSERT(results == std::vector<bool>({false, false}));
cache.onReady();
CPPUNIT_ASSERT_EQUAL(size_t(2), results.size());
cache.onReady(false);
CPPUNIT_ASSERT(cache.addListener(3, values, query, {}, ready));
CPPUNIT_ASSERT_EQUAL(size_t(2), results.size());
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT_EQUAL(size_t(2), results.size());
cache.onSearchReady();
CPPUNIT_ASSERT(results == std::vector<bool>({false, false, true}));
dht::SearchCache searches(true, true);
auto first = std::make_shared<dht::Query>(dht::Select {}, dht::Where {}.id(42));
auto second = std::make_shared<dht::Query>(dht::Select {}, dht::Where {}.id(43));
dht::SyncCallback secondSource;
auto onListen = [&](dht::Sp<dht::Query> query, dht::ValueCallback, dht::SyncCallback source) {
source(dht::ListenSyncStatus::ADDED);
if (query == second)
secondSource = std::move(source);
return size_t(1);
};
std::vector<bool> firstResults;
std::vector<bool> secondResults;
CPPUNIT_ASSERT(searches.listen(values, first, {}, onListen, [&](bool ok) { firstResults.push_back(ok); }) != 0);
CPPUNIT_ASSERT(searches.listen(values, second, {}, onListen, [&](bool ok) { secondResults.push_back(ok); }) != 0);
searches.onReady(first, false);
CPPUNIT_ASSERT(firstResults == std::vector<bool>({false}));
CPPUNIT_ASSERT(secondResults.empty());
secondSource(dht::ListenSyncStatus::SYNCED);
searches.onSearchReady();
CPPUNIT_ASSERT(secondResults == std::vector<bool>({true}));
}
void
OpCacheTester::testInitialReadinessMultipleSources()
{
dht::OpCache cache(true, true);
auto query = std::make_shared<dht::Query>();
unsigned calls = 0;
CPPUNIT_ASSERT(cache.addListener(
1,
[](const Values&, bool) { return true; },
query,
{},
[&](bool ok) {
CPPUNIT_ASSERT(ok);
++calls;
}));
cache.onSearchReady();
CPPUNIT_ASSERT_EQUAL(0u, calls);
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT_EQUAL(0u, calls);
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT_EQUAL(1u, calls);
cache.onSearchReady();
cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT_EQUAL(1u, calls);
}
void
OpCacheTester::testInitialReadinessCachedValues()
{
dht::OpCache cache(true, true);
auto query = std::make_shared<dht::Query>();
auto value = makeValue(42, 0, "initial");
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValue({value, makeValue(43, 0, "filtered")}, false);
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
cache.onSearchReady();
std::vector<std::string> events;
CPPUNIT_ASSERT(cache.addListener(
1,
[&](const Values& values, bool expired) {
CPPUNIT_ASSERT(!expired);
CPPUNIT_ASSERT_EQUAL(size_t(1), values.size());
CPPUNIT_ASSERT_EQUAL(value->id, values.front()->id);
events.emplace_back("value");
return true;
},
query,
[](const dht::Value& current) { return current.id == 42; },
[&](bool ok) {
CPPUNIT_ASSERT(ok);
events.emplace_back("ready");
}));
CPPUNIT_ASSERT(events == std::vector<std::string>({"value", "ready"}));
unsigned calls = 0;
CPPUNIT_ASSERT(!cache.addListener(2, [](const Values&, bool) { return false; }, query, {}, [&](bool) { ++calls; }));
cache.onReady();
CPPUNIT_ASSERT_EQUAL(0u, calls);
}
void
OpCacheTester::testInitialReadinessReusedOperation()
{
dht::SearchCache cache(true, true);
auto query = std::make_shared<dht::Query>();
dht::SyncCallback source;
unsigned operations = 0;
unsigned calls = 0;
auto onListen = [&](dht::Sp<dht::Query>, dht::ValueCallback values, dht::SyncCallback sync) {
++operations;
source = std::move(sync);
source(dht::ListenSyncStatus::ADDED);
values({makeValue(42, 0, "cached")}, false);
source(dht::ListenSyncStatus::SYNCED);
return size_t(1);
};
auto values = [](const Values&, bool) {
return true;
};
auto ready = [&](bool ok) {
CPPUNIT_ASSERT(ok);
++calls;
};
auto first = cache.listen(values, query, {}, onListen, ready);
CPPUNIT_ASSERT(first != 0);
CPPUNIT_ASSERT_EQUAL(0u, calls);
cache.onSearchReady();
CPPUNIT_ASSERT_EQUAL(1u, calls);
CPPUNIT_ASSERT(cache.cancelListen(first, dht::clock::now()));
auto second = cache.listen(values, std::make_shared<dht::Query>(), {}, onListen, ready);
CPPUNIT_ASSERT(second != 0);
CPPUNIT_ASSERT_EQUAL(1u, operations);
CPPUNIT_ASSERT_EQUAL(2u, calls);
source(dht::ListenSyncStatus::UNSYNCED);
CPPUNIT_ASSERT(cache.listen(values, query, {}, onListen, ready) != 0);
CPPUNIT_ASSERT_EQUAL(2u, calls);
source(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT_EQUAL(3u, calls);
}
void
OpCacheTester::tearDown()
{}
void
OpCacheTester::testBasicAddExpire()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(10, 0, "A");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT(cache.get(10));
std::vector<dht::Value::Id> ids = {v1->id};
cache.onValuesExpired(ids);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT(!cache.get(10));
}
void
OpCacheTester::testMultipleSources()
{
// Two sources report the same value: it must survive until both have
// expired it, and the user must see exactly one add and one expire.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v = makeValue(20, 0, "B");
cache.onValuesAdded({v}); // source A
cache.onValuesAdded({v}); // source B
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
cache.onValuesExpired({v}); // source A goes away
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value still held by source B", (size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
cache.onValuesExpired({v}); // source B goes away
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testEditResetsRefCount()
{
// A and B hold v1. A reports the edit v2: only A is known to hold v2, so
// the refcount restarts at 1. B then confirms v2 (refcount 2).
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(1, 1, "data1");
auto v2 = makeValue(1, 2, "data2");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(1)->seq);
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
cache.onValuesExpired({v2}); // A expires v2 -> nobody known to hold it
CPPUNIT_ASSERT_EQUAL_MESSAGE("Edit reset the refcount to the editing source", (size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
// Same scenario, but B confirms v2 before A expires it.
events.clear();
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits
cache.onValuesAdded({v2}); // B confirms
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL_MESSAGE("Still held by B", (size_t) 1, cache.size());
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testEditChainMultiSource()
{
// Rapid edit chain seen from two sources with interleaving: the user
// must see each version once as an add and a single final expire.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(7, 1, "v1");
auto v2 = makeValue(7, 2, "v2");
auto v3 = makeValue(7, 3, "v3");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A
cache.onValuesAdded({v3}); // A (B skipped v2)
cache.onValuesAdded({v2}); // B, late and stale: ignored
cache.onValuesAdded({v3}); // B
CPPUNIT_ASSERT_EQUAL(3u, count(events, false));
CPPUNIT_ASSERT_EQUAL((uint16_t) 3, cache.get(7)->seq);
cache.onValuesExpired({v3}); // A
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValuesExpired({v3}); // B
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((uint16_t) 3, events.back().seq);
}
void
OpCacheTester::testStaleAddIgnored()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(30, 10, "C");
auto v2 = makeValue(30, 20, "D");
auto v3 = makeValue(30, 5, "E");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL((uint16_t) 10, cache.get(30)->seq);
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL((uint16_t) 20, cache.get(30)->seq);
// Older version from a lagging source: neither stored nor counted
cache.onValuesAdded({v3});
CPPUNIT_ASSERT_EQUAL((uint16_t) 20, cache.get(30)->seq);
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_MESSAGE("Stale add must not have incremented the refcount", !cache.get(30));
}
void
OpCacheTester::testStaleExpireIgnored()
{
// A source that still holds an older version expiring it must not
// affect the newer version held by others.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(31, 1, "old");
auto v2 = makeValue(31, 2, "new");
cache.onValuesAdded({v1}); // A
cache.onValuesAdded({v1}); // B
cache.onValuesAdded({v2}); // A edits; B never receives v2
cache.onValuesExpired({v1}); // B expires its stale copy
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
CPPUNIT_ASSERT_EQUAL(0u, count(events, true));
cache.onValuesExpired({v2}); // A
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
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()
{
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(40, 0, "");
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
// Duplicate from another source: counted, but not re-delivered
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
auto v2 = makeValue(40, 100, "MOD");
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL(2u, count(events, false));
// The edit reset the refcount: one expire removes it
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testFilters()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(50, 0, "");
v1->type = 1;
auto v2 = makeValue(51, 0, "");
v2->type = 2;
cache.onValuesAdded({v1, v2});
auto res1 = cache.get(dht::Value::TypeFilter(1));
CPPUNIT_ASSERT_EQUAL((size_t) 1, res1.size());
CPPUNIT_ASSERT_EQUAL(v1->id, res1[0]->id);
auto res2 = cache.get(dht::Value::TypeFilter(2));
CPPUNIT_ASSERT_EQUAL((size_t) 1, res2.size());
CPPUNIT_ASSERT_EQUAL(v2->id, res2[0]->id);
CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.getValues().size());
}
void
OpCacheTester::testSyncStatus()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT(cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
CPPUNIT_ASSERT(cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
CPPUNIT_ASSERT(!cache.isSynced());
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT(cache.isSynced());
}
void
OpCacheTester::testGetWhileSynced()
{
dht::SearchCache searchCache;
int getCallCount = 0;
bool doneCalled = false;
bool doneSuccess = false;
auto v1 = makeValue(100, 0, "X");
auto v2 = makeValue(101, 0, "Y");
auto query = std::make_shared<dht::Query>();
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback vcb,
dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1, v2}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool result = searchCache.get(
{},
query,
[&](const Values& vals) {
getCallCount++;
CPPUNIT_ASSERT_EQUAL((size_t) 2, vals.size());
return true;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT_MESSAGE("get() should be served from a synced cache", result);
CPPUNIT_ASSERT_EQUAL(1, getCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT(doneSuccess);
}
void
OpCacheTester::testGetWhileNotSynced()
{
// An unsynced cache must not answer a get: the caller falls back to the
// network and values are delivered once, from the network only.
dht::SearchCache searchCache;
std::vector<dht::Value::Id> deliveries;
auto v1 = makeValue(200, 0, "A");
auto query = std::make_shared<dht::Query>();
dht::SyncCallback storedScb;
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback vcb,
dht::SyncCallback scb) -> size_t {
storedScb = scb;
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool doneCalled = false;
auto gcb = [&](const Values& vals) {
for (const auto& v : vals)
deliveries.emplace_back(v->id);
return true;
};
auto dcb = [&](bool, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
};
CPPUNIT_ASSERT(!searchCache.get({}, query, gcb, dcb));
CPPUNIT_ASSERT(deliveries.empty());
CPPUNIT_ASSERT(!doneCalled);
// Simulated network reply
gcb({v1});
CPPUNIT_ASSERT_EQUAL((size_t) 1, deliveries.size());
storedScb(dht::ListenSyncStatus::SYNCED);
deliveries.clear();
CPPUNIT_ASSERT(searchCache.get({}, query, gcb, dcb));
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT_EQUAL((size_t) 1, deliveries.size());
}
void
OpCacheTester::testGetEmptySynced()
{
dht::SearchCache searchCache;
int getCallCount = 0;
bool doneCalled = false;
bool doneSuccess = false;
auto query = std::make_shared<dht::Query>();
auto token = searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&,
dht::ValueCallback,
dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
CPPUNIT_ASSERT(token != 0);
bool result = searchCache.get(
{},
query,
[&](const Values&) {
getCallCount++;
return true;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT(result);
CPPUNIT_ASSERT_EQUAL(0, getCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT(doneSuccess);
}
void
OpCacheTester::testGetWithoutCallback()
{
// Dht::query() passes no GetCallback: the cache cannot answer it and
// must let the network path run instead of calling an empty function.
dht::SearchCache searchCache;
auto v1 = makeValue(300, 0, "Q");
auto query = std::make_shared<dht::Query>();
searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
bool doneCalled = false;
CPPUNIT_ASSERT(!searchCache.get({}, query, {}, [&](bool, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
}));
CPPUNIT_ASSERT(!doneCalled);
}
void
OpCacheTester::testGetCallbackReturnsFalse()
{
// A consumer returning false means "I have enough", not a failure.
dht::SearchCache searchCache;
auto v1 = makeValue(1000, 0, "X");
auto query = std::make_shared<dht::Query>();
searchCache.listen([](const Values&, bool) { return true; },
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
scb(dht::ListenSyncStatus::ADDED);
vcb({v1}, false);
scb(dht::ListenSyncStatus::SYNCED);
return 1;
});
bool doneCalled = false;
bool doneSuccess = false;
int gcbCallCount = 0;
bool result = searchCache.get(
{},
query,
[&](const Values&) {
gcbCallCount++;
return false;
},
[&](bool success, const std::vector<std::shared_ptr<dht::Node>>&) {
doneCalled = true;
doneSuccess = success;
});
CPPUNIT_ASSERT(result);
CPPUNIT_ASSERT_EQUAL(1, gcbCallCount);
CPPUNIT_ASSERT(doneCalled);
CPPUNIT_ASSERT_MESSAGE("Stopping early is reported as success, like the network path", doneSuccess);
}
void
OpCacheTester::testTimestampOrdering()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v1 = makeValue(400, 0, "T");
auto t1 = std::chrono::system_clock::now();
auto t2 = t1 + std::chrono::seconds(10);
cache.onValue({v1}, false, t2);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValue({v1}, true, t1);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Stale expiration should be ignored", (size_t) 1, cache.size());
cache.onValue({v1}, true, t2);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testSourceDropKeepsValue()
{
// Two per-node ValueCaches feed one Search-level OpCache. When one node
// leaves the search (its ValueCache is destroyed, expiring everything it
// held) the value must stay visible as long as the other node holds it.
std::vector<Event> events;
dht::SearchCache searchCache;
auto query = std::make_shared<dht::Query>();
dht::ValueCallback toOpCache;
dht::SyncCallback toOpCacheSync;
searchCache.listen(recordEvents(events),
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback scb) -> size_t {
toOpCache = std::move(vcb);
toOpCacheSync = std::move(scb);
return 1;
});
dht::TypeStore types;
auto now = dht::clock::now();
auto v = makeValue(42, 0, "hello");
auto makeNodeCache = [&] {
return std::make_unique<dht::ValueCache>([&](const Values& vals, bool expired) { toOpCache(vals, expired); },
[&](dht::ListenSyncStatus s) { toOpCacheSync(s); });
};
auto nodeA = makeNodeCache();
auto nodeB = makeNodeCache();
nodeA->onValues({v}, {}, {}, types, now);
nodeA->onSynced(true);
nodeB->onValues({v}, {}, {}, types, now);
nodeB->onSynced(true);
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
CPPUNIT_ASSERT_EQUAL((size_t) 1, searchCache.get(dht::Value::Filter {}).size());
nodeA.reset();
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire while another node holds the value", 0u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 1, searchCache.get(dht::Value::Filter {}).size());
nodeB->onValues({v}, {}, {}, types, now + std::chrono::seconds(1));
CPPUNIT_ASSERT_EQUAL_MESSAGE("Refresh from the remaining node is not re-delivered", 1u, count(events, false));
nodeB.reset();
CPPUNIT_ASSERT_EQUAL_MESSAGE("Last holder gone: expire once", 1u, count(events, true));
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()
{
dht::OpValueCache cache([](const Values&, bool) { return true; });
auto v = makeValue(800, 0, "CHURN");
for (int i = 0; i < 50; i++) {
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
}
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
// Overlapping churn: add A, add B, expire A, expire B
for (int i = 0; i < 20; i++) {
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
cache.onValuesAdded({v});
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value kept while one source remains", (size_t) 1, cache.size());
cache.onValuesExpired({v});
cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
// Extra expirations from sources that never added must be no-ops
cache.onValuesExpired({v});
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
OpCacheTester::testValueUpdateSingleSourcePhantom()
{
// Editing a value (same id, higher seq) is delivered as a single add of
// the new version, without a phantom expiration of the old one.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(700, 1, "OLD");
auto v2 = makeValue(700, 2, "NEW");
cache.onValuesAdded({v1});
events.clear();
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 1, events.size());
CPPUNIT_ASSERT(!events[0].expired);
CPPUNIT_ASSERT_EQUAL(v2->id, events[0].id);
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(700)->seq);
}
void
OpCacheTester::testExpireAfterUpdate()
{
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events));
auto v1 = makeValue(1200, 1, "V1");
auto v2 = makeValue(1200, 5, "V2");
cache.onValuesAdded({v1});
cache.onValuesAdded({v2});
CPPUNIT_ASSERT_EQUAL(v2->seq, cache.get(1200)->seq);
cache.onValuesExpired({v2});
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL_MESSAGE("Expired value should be the updated one", v2->seq, events.back().seq);
// Once gone, an older version is accepted again
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL(v1->seq, cache.get(1200)->seq);
}
void
OpCacheTester::testSingleSourceMode()
{
// Single-source caches (proxy client) get the same values re-delivered
// on refresh: duplicates must not be counted, one expire removes.
std::vector<Event> events;
dht::OpValueCache cache(recordEvents(events), false);
auto v = makeValue(1100, 0, "DUP");
for (int i = 0; i < 10; i++)
cache.onValuesAdded({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
cache.onValuesExpired({v});
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
}
void
OpCacheTester::testSingleSourceModeSearchCache()
{
std::vector<Event> events;
dht::SearchCache searchCache(false);
auto query = std::make_shared<dht::Query>();
dht::ValueCallback vcbStored;
searchCache.listen(recordEvents(events),
query,
{},
[&](const std::shared_ptr<dht::Query>&, dht::ValueCallback vcb, dht::SyncCallback) -> size_t {
vcbStored = std::move(vcb);
return 1;
});
auto v = makeValue(1300, 0, "S");
vcbStored({v}, false);
vcbStored({v}, false); // re-delivery after listen restart
CPPUNIT_ASSERT_EQUAL(1u, count(events, false));
vcbStored({v}, true);
CPPUNIT_ASSERT_EQUAL(1u, count(events, true));
CPPUNIT_ASSERT_EQUAL((size_t) 0, searchCache.get(dht::Value::Filter {}).size());
}
} // namespace test
+79
View File
@@ -0,0 +1,79 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
namespace test {
class OpCacheTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(OpCacheTester);
CPPUNIT_TEST(testBasicAddExpire);
CPPUNIT_TEST(testMultipleSources);
CPPUNIT_TEST(testEditResetsRefCount);
CPPUNIT_TEST(testEditChainMultiSource);
CPPUNIT_TEST(testStaleAddIgnored);
CPPUNIT_TEST(testStaleExpireIgnored);
CPPUNIT_TEST(testSameSeqConflictExpireIgnored);
CPPUNIT_TEST(testCallbacks);
CPPUNIT_TEST(testFilters);
CPPUNIT_TEST(testSyncStatus);
CPPUNIT_TEST(testInitialListenerReadiness);
CPPUNIT_TEST(testInitialReadinessCancellation);
CPPUNIT_TEST(testInitialReadinessFailure);
CPPUNIT_TEST(testInitialReadinessMultipleSources);
CPPUNIT_TEST(testInitialReadinessCachedValues);
CPPUNIT_TEST(testInitialReadinessReusedOperation);
CPPUNIT_TEST(testGetWhileSynced);
CPPUNIT_TEST(testGetWhileNotSynced);
CPPUNIT_TEST(testGetEmptySynced);
CPPUNIT_TEST(testGetWithoutCallback);
CPPUNIT_TEST(testGetCallbackReturnsFalse);
CPPUNIT_TEST(testTimestampOrdering);
CPPUNIT_TEST(testSourceDropKeepsValue);
CPPUNIT_TEST(testSeqOnlyRefreshNotCounted);
CPPUNIT_TEST(testHighChurnRefCountConsistency);
CPPUNIT_TEST(testValueUpdateSingleSourcePhantom);
CPPUNIT_TEST(testExpireAfterUpdate);
CPPUNIT_TEST(testSingleSourceMode);
CPPUNIT_TEST(testSingleSourceModeSearchCache);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void testBasicAddExpire();
void testMultipleSources();
void testEditResetsRefCount();
void testEditChainMultiSource();
void testStaleAddIgnored();
void testStaleExpireIgnored();
void testSameSeqConflictExpireIgnored();
void testCallbacks();
void testFilters();
void testSyncStatus();
void testInitialListenerReadiness();
void testInitialReadinessCancellation();
void testInitialReadinessFailure();
void testInitialReadinessMultipleSources();
void testInitialReadinessCachedValues();
void testInitialReadinessReusedOperation();
void testGetWhileSynced();
void testGetWhileNotSynced();
void testGetEmptySynced();
void testGetWithoutCallback();
void testGetCallbackReturnsFalse();
void testTimestampOrdering();
void testSourceDropKeepsValue();
void testSeqOnlyRefreshNotCounted();
void testHighChurnRefCountConsistency();
void testValueUpdateSingleSourcePhantom();
void testExpireAfterUpdate();
void testSingleSourceMode();
void testSingleSourceModeSearchCache();
};
} // namespace test
+63
View File
@@ -105,6 +105,69 @@ void
ParsedMessageTester::setUp()
{}
void
ParsedMessageTester::testParseListenSync()
{
for (const auto& fields : std::vector<std::vector<int64_t>> {
{1, 2, 3, 1},
{0, 2, 3, 1},
{1, 2, -1, 1},
{1, 2, 3},
{1, 2, 3, 1, 0}
}) {
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> packer(buffer);
packer.pack_map(3);
packer.pack(KEY_Y);
packer.pack(KEY_R);
packer.pack(KEY_TID);
packer.pack(1);
packer.pack(KEY_R);
packer.pack_map(2);
packer.pack(KEY_VERSION);
packer.pack(2);
packer.pack(KEY_LISTEN_SYNC);
packer.pack(fields);
if (fields == std::vector<int64_t>({1, 2, 3, 1})) {
auto message = parsePackedMessage(buffer);
CPPUNIT_ASSERT(message.listenSync);
CPPUNIT_ASSERT_EQUAL(uint64_t(1), message.listenSync->registration);
CPPUNIT_ASSERT_EQUAL(uint64_t(2), message.listenSync->epoch);
CPPUNIT_ASSERT_EQUAL(uint64_t(3), message.listenSync->sequence);
CPPUNIT_ASSERT_EQUAL(uint64_t(1), message.listenSync->snapshotEnd);
} else {
CPPUNIT_ASSERT_THROW(parsePackedMessage(buffer), msgpack::type_error);
}
}
}
void
ParsedMessageTester::testParseRejectsMalformedSyncedValues()
{
for (unsigned failure = 0; failure < 3; ++failure) {
msgpack::sbuffer buffer;
msgpack::packer<msgpack::sbuffer> packer(buffer);
packer.pack_map(3);
packer.pack(KEY_Y);
packer.pack(KEY_R);
packer.pack(KEY_TID);
packer.pack(1);
packer.pack(KEY_R);
packer.pack_map(3);
packer.pack(KEY_VERSION);
packer.pack(2);
packer.pack(KEY_LISTEN_SYNC);
packer.pack(ListenSync {1, 2, 1, 1});
packer.pack(KEY_REQ_VALUES);
packer.pack_array(1);
if (failure == 0)
packer.pack_nil();
else
packer.pack(failure == 1 ? MAX_VALUE_SIZE + 33 : 0);
CPPUNIT_ASSERT_THROW(parsePackedMessage(buffer), msgpack::type_error);
}
}
void
ParsedMessageTester::tearDown()
{}
+4
View File
@@ -27,6 +27,8 @@ class ParsedMessageTester : public CppUnit::TestFixture
CPPUNIT_TEST(testParseRejectsEmptyFieldSelection);
CPPUNIT_TEST(testParseRejectsEmptyErrorArray);
CPPUNIT_TEST(testParseRejectsNonMapRequest);
CPPUNIT_TEST(testParseListenSync);
CPPUNIT_TEST(testParseRejectsMalformedSyncedValues);
CPPUNIT_TEST(testParseErrorOnlyMessageKeepsRequestFieldsEmpty);
CPPUNIT_TEST(testParseIgnoresIncompleteValueDataEntry);
CPPUNIT_TEST(testCompleteEmpty);
@@ -60,6 +62,8 @@ public:
void testParseRejectsEmptyFieldSelection();
void testParseRejectsEmptyErrorArray();
void testParseRejectsNonMapRequest();
void testParseListenSync();
void testParseRejectsMalformedSyncedValues();
void testParseErrorOnlyMessageKeepsRequestFieldsEmpty();
void testParseIgnoresIncompleteValueDataEntry();
void testCompleteEmpty();
+601
View File
@@ -0,0 +1,601 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#include "test_valuecache.h"
#include "../src/value_cache.h"
namespace test {
CPPUNIT_TEST_SUITE_REGISTRATION(ValueCacheTester);
void
ValueCacheTester::setUp()
{}
void
ValueCacheTester::tearDown()
{}
void
ValueCacheTester::testUpdate()
{
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 v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 1;
v1->data = {'A'};
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 2;
v2->data = {'B'};
auto now = std::chrono::steady_clock::now();
// Add v1
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
// Update v1 -> v2
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Callback should be called for update", 2, addCount);
}
void
ValueCacheTester::testUpdateExpiration()
{
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->seq = 1;
v1->data = {'A'};
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 2;
v2->data = {'B'};
auto now = std::chrono::steady_clock::now();
// Add v1
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
// Update v1 -> v2: only the new value is emitted as an add,
// no expire callback for the old value.
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(2, addCount);
CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callback on edit", 0, expireCount);
}
void
ValueCacheTester::testExpiration()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 10;
v1->data = {'A'};
// Default expiration is 10 minutes
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
// Advance time past expiration
auto future = now + std::chrono::minutes(11);
// expireValues returns next expiration time
cache.expireValues(future);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testRefresh()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 20;
v1->data = {'B'};
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
// Refresh at +5 mins
auto later = now + std::chrono::minutes(5);
cache.onValues({}, {v1->id}, {}, types, later);
// Should not expire at +11 mins (original expiration was +10, so +10 from start)
// But refresh should extend it to +10 from 'later' = +15 from start.
auto checkTime = now + std::chrono::minutes(11);
cache.expireValues(checkTime);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Should expire at +16 mins
auto future = now + std::chrono::minutes(16);
cache.expireValues(future);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
}
void
ValueCacheTester::testExplicitExpiration()
{
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
auto v1 = std::make_shared<dht::Value>();
v1->id = 30;
v1->data = {'C'};
auto now = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, now);
cache.onValues({}, {}, {v1->id}, types, now);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testClear()
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
auto v2 = std::make_shared<dht::Value>();
v2->id = 2;
cache.onValues({v1, v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.size());
auto cbs = cache.clear();
// Execute callbacks
for (auto& cb : cbs)
cb();
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testSyncStatus()
{
std::vector<dht::ListenSyncStatus> statuses;
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {},
[&](dht::ListenSyncStatus status) { statuses.push_back(status); });
CPPUNIT_ASSERT_EQUAL((size_t) 1, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::ADDED);
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
// Calling onSynced(true) again should not trigger the callback
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
cache.onSynced(false);
CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::UNSYNCED);
// Calling onSynced(false) again should not trigger the callback
cache.onSynced(false);
CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
// Set to synced before destruction to test UNSYNCED on destruction
cache.onSynced(true);
CPPUNIT_ASSERT_EQUAL((size_t) 4, statuses.size());
CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
}
// Destructor should have been called
CPPUNIT_ASSERT_EQUAL((size_t) 6, statuses.size());
CPPUNIT_ASSERT(statuses[4] == dht::ListenSyncStatus::UNSYNCED);
CPPUNIT_ASSERT(statuses[5] == dht::ListenSyncStatus::REMOVED);
}
void
ValueCacheTester::testMaxValues()
{
dht::ValueCache cache([](const std::vector<std::shared_ptr<dht::Value>>&, bool) {});
dht::TypeStore types;
auto now = std::chrono::steady_clock::now();
// MAX_VALUES is 4096
for (int i = 0; i < 4100; ++i) {
auto v = std::make_shared<dht::Value>();
v->id = (dht::Value::Id) i + 1;
cache.onValues({v}, {}, {}, types, now + std::chrono::milliseconds(i));
}
// Should be capped at 4096
CPPUNIT_ASSERT_EQUAL((size_t) 4096, cache.size());
}
void
ValueCacheTester::testUpdateTypeExpiration()
{
// Test that when a value is updated with a different type, the expiration
// uses the new type's expiration period, not the old one.
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
// Register a custom type with a short expiration (2 minutes)
dht::TypeStore types;
dht::ValueType shortType(42, "short", std::chrono::minutes(2));
types.registerType(shortType);
// Register a custom type with a long expiration (30 minutes)
dht::ValueType longType(99, "long", std::chrono::minutes(30));
types.registerType(longType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 42; // short type: 2 min expiration
v1->data = {'A'};
auto now = std::chrono::steady_clock::now();
// Add v1 with short type
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
// Update with new type (long expiration)
auto v2 = std::make_shared<dht::Value>();
v2->id = 1;
v2->seq = 1;
v2->type = 99; // long type: 30 min expiration
v2->data = {'B'};
cache.onValues({v2}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(2, addCount);
// At +3 minutes: should NOT be expired (long type has 30 min expiration)
// Before the fix, the old type (2 min) would be used, causing expiration here
expireCount = 0;
auto at3min = now + std::chrono::minutes(3);
cache.expireValues(at3min);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value updated to long-expiry type should NOT expire at 3 min", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// At +31 minutes: should be expired
auto at31min = now + std::chrono::minutes(31);
cache.expireValues(at31min);
CPPUNIT_ASSERT_EQUAL(1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testSameSeqEditIgnored()
{
// Like OpValueCache, an edit is only recognized by an increasing seq:
// a different payload with the same seq is a refresh, not an edit.
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 = 3;
v1->data = {'A'};
cache.onValues({v1}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto sameSeq = std::make_shared<dht::Value>();
sameSeq->id = 1;
sameSeq->seq = 3;
sameSeq->data = {'B'};
cache.onValues({sameSeq}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto older = std::make_shared<dht::Value>();
older->id = 1;
older->seq = 2;
older->data = {'C'};
cache.onValues({older}, {}, {}, types, now);
CPPUNIT_ASSERT_EQUAL(1, addCount);
auto newer = std::make_shared<dht::Value>();
newer->id = 1;
newer->seq = 4;
newer->data = {'D'};
cache.onValues({newer}, {}, {}, types, now);
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()
{
// Demonstrates that a value with a short expiration type survives
// past its nominal expiration when the listen is synced, thanks to
// a proportional grace period (= typeExpiration).
//
// For a 5s type: effective expiration when synced = expiration + 5s = 10s
//
// Scenario:
// t=0 : Value V added → nominal expiration t+5s, effective t+10s
// t=6 : Past nominal but within grace → alive
// t=8 : Refresh arrives → V refreshed → expiration reset to t8+5s, effective t8+10s=t18s
// t=14: Still alive (within refreshed effective range)
// t=20: No more refreshes → past t8+10s=t18s → expired
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
// Register a type with a very short expiration (5 seconds)
dht::TypeStore types;
dht::ValueType shortType(100, "short_lived", std::chrono::seconds(5));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 100;
v1->data = {'V'};
auto t0 = std::chrono::steady_clock::now();
// t=0: Listen response arrives with V
cache.onValues({v1}, {}, {}, types, t0);
CPPUNIT_ASSERT_EQUAL(1, addCount);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Mark cache as synced (listen is active)
cache.onSynced(true);
// t=6: Past nominal 5s expiration, but grace (5s) gives effective 10s
auto t6 = t0 + std::chrono::seconds(6);
cache.expireValues(t6);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past nominal expiration thanks to proportional grace",
0,
expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=8: Value refresh arrives → expiration reset to t8+5s=t13s, effective t8+10s=t18s
auto t8 = t0 + std::chrono::seconds(8);
cache.onValues({}, {v1->id}, {}, types, t8);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=14: Within refreshed effective range (t18s) → alive
auto t14 = t0 + std::chrono::seconds(14);
cache.expireValues(t14);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past short expiration after refresh", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=20: No more refreshes → past effective t18s → must expire
auto t20 = t0 + std::chrono::seconds(20);
cache.expireValues(t20);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Value must eventually expire even while synced if server stops refreshing",
1,
expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testShortExpirationHighChurn()
{
// Simulates high churn: multiple rapid add/expire/refresh cycles with
// short-lived values, verifying the cache stays consistent.
int addCount = 0;
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
else
addCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(101, "fast_churn", std::chrono::seconds(3));
types.registerType(shortType);
auto now = std::chrono::steady_clock::now();
// Simulate 10 churn cycles: add value, advance time, refresh, advance, expire
for (int cycle = 0; cycle < 10; cycle++) {
auto v = std::make_shared<dht::Value>();
v->id = static_cast<dht::Value::Id>(500 + cycle);
v->type = 101;
v->data = {static_cast<uint8_t>('A' + cycle)};
auto t = now + std::chrono::seconds(cycle * 10);
// Add the value
cache.onValues({v}, {}, {}, types, t);
// Refresh halfway through
auto tRefresh = t + std::chrono::seconds(2);
cache.onValues({}, {v->id}, {}, types, tRefresh);
// Explicit expire from server
auto tExpire = t + std::chrono::seconds(8);
cache.onValues({}, {}, {v->id}, types, tExpire);
}
// All 10 values were added and explicitly expired
CPPUNIT_ASSERT_EQUAL(10, addCount);
CPPUNIT_ASSERT_EQUAL(10, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testGracePeriodWhileSynced()
{
// Verify that when the listen is synced, the proportional grace period
// (= typeExpiration) delays time-based expiration but doesn't prevent
// it entirely. For a 2s type: effective expiration = 2s + 2s = 4s.
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(110, "short", std::chrono::seconds(2));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 110;
v1->data = {'X'};
auto t0 = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, t0);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// Mark as synced
cache.onSynced(true);
// t=3: Past nominal 2s, within effective 4s → grace protects
auto t3 = t0 + std::chrono::seconds(3);
cache.expireValues(t3);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: grace period should prevent early expiry", 0, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
// t=5: Past effective 4s (2s + 2s grace) → must expire
auto t5 = t0 + std::chrono::seconds(5);
cache.expireValues(t5);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: value must eventually expire after grace period", 1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
void
ValueCacheTester::testUnsyncExpiresImmediately()
{
// When the cache goes from synced to unsynced, the grace period is
// removed and already-past-due values expire on the next check.
// Type expiration = 5s. Grace when synced = 5s → effective = 10s.
int expireCount = 0;
dht::ValueCache cache([&](const std::vector<std::shared_ptr<dht::Value>>& vals, bool expired) {
if (expired)
expireCount += vals.size();
});
dht::TypeStore types;
dht::ValueType shortType(120, "short", std::chrono::seconds(5));
types.registerType(shortType);
auto v1 = std::make_shared<dht::Value>();
v1->id = 1;
v1->type = 120;
v1->data = {'U'};
auto t0 = std::chrono::steady_clock::now();
cache.onValues({v1}, {}, {}, types, t0);
cache.onSynced(true);
// t=7: past nominal 5s, within effective 10s while synced → alive
auto t7 = t0 + std::chrono::seconds(7);
cache.expireValues(t7);
CPPUNIT_ASSERT_EQUAL(0, expireCount);
// Now unsync (node went away / connectivity loss)
cache.onSynced(false);
// Same time point t=7: past nominal 5s, no grace → expires
cache.expireValues(t7);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Unsynced cache should immediately expire past-due values", 1, expireCount);
CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
}
} // namespace test
+51
View File
@@ -0,0 +1,51 @@
// Copyright (c) 2014-2026 Savoir-faire Linux Inc.
// SPDX-License-Identifier: MIT
#pragma once
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
namespace test {
class ValueCacheTester : public CppUnit::TestFixture
{
CPPUNIT_TEST_SUITE(ValueCacheTester);
CPPUNIT_TEST(testUpdate);
CPPUNIT_TEST(testUpdateExpiration);
CPPUNIT_TEST(testExpiration);
CPPUNIT_TEST(testRefresh);
CPPUNIT_TEST(testExplicitExpiration);
CPPUNIT_TEST(testClear);
CPPUNIT_TEST(testSyncStatus);
CPPUNIT_TEST(testMaxValues);
CPPUNIT_TEST(testUpdateTypeExpiration);
CPPUNIT_TEST(testSameSeqEditIgnored);
CPPUNIT_TEST(testSameContentHigherSeqIsRefresh);
CPPUNIT_TEST(testShortExpirationRefreshLost);
CPPUNIT_TEST(testShortExpirationHighChurn);
CPPUNIT_TEST(testGracePeriodWhileSynced);
CPPUNIT_TEST(testUnsyncExpiresImmediately);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void testUpdate();
void testUpdateExpiration();
void testExpiration();
void testRefresh();
void testExplicitExpiration();
void testClear();
void testSyncStatus();
void testMaxValues();
void testUpdateTypeExpiration();
void testSameSeqEditIgnored();
void testSameContentHigherSeqIsRefresh();
void testShortExpirationRefreshLost();
void testShortExpirationHighChurn();
void testGracePeriodWhileSynced();
void testUnsyncExpiresImmediately();
};
} // namespace test
+2 -1
View File
@@ -405,7 +405,8 @@ cmd_loop(std::shared_ptr<DhtRunner>& node,
return true;
},
{},
dht::Where {rem});
dht::Where {rem},
[](bool synced) { std::cout << "Listen: " << (synced ? "synced" : "not synced") << std::endl; });
auto t = token.get();
std::cout << "Listening, token: " << t << std::endl;
}
+4 -1
View File
@@ -1,9 +1,12 @@
{
"default-registry": {
"kind": "git",
"baseline": "f3e10653cc27d62a37a3763cd84b38bca07c6075",
"baseline": "319504a5326aa870edde46438c5455fa76305a56",
"repository": "https://github.com/microsoft/vcpkg"
},
"overlay-ports": [
"ports"
],
"registries": [
{
"kind": "artifact",
+9 -2
View File
@@ -1,6 +1,6 @@
{
"name": "opendht",
"version-string": "4.3.0",
"version-string": "4.4.1",
"port-version": 1,
"description": "OpenDHT: A lightweight C++17 Distributed Hash Table library.",
"homepage": "https://opendht.net",
@@ -16,7 +16,14 @@
},
"fmt",
"nettle",
"shiftmedia-libgnutls",
{
"name": "shiftmedia-libgnutls",
"platform": "windows & !mingw"
},
{
"name": "libgnutls",
"platform": "!windows | mingw"
},
"msgpack",
"jsoncpp",
"asio",