mirror of
https://github.com/savoirfairelinux/opendht.git
synced 2026-11-08 12:28:42 +00:00
Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53eb8682f5 | ||
|
|
6f1ff2e10b | ||
|
|
497d3d2543 | ||
|
|
43937eb454 | ||
|
|
e216d1c8a0 | ||
|
|
88773525bb | ||
|
|
5b1fa36920 | ||
|
|
f7aeab7713 | ||
|
|
a950e06d10 | ||
|
|
743a82af6f | ||
|
|
1890726251 | ||
|
|
e413af7eb5 | ||
|
|
524d9acc27 | ||
|
|
81cbd90609 | ||
|
|
732d82c31b | ||
|
|
727fabb5a1 | ||
|
|
194e9b8b15 | ||
|
|
223eb6a7a9 | ||
|
|
bd8c72bb07 | ||
|
|
a853279de7 | ||
|
|
cded92e9f5 | ||
|
|
ad05b85798 | ||
|
|
57a3b658ae | ||
|
|
ab0e922e02 | ||
|
|
3478255fd8 | ||
|
|
621b44c8e8 | ||
|
|
e1c088f2e4 | ||
|
|
a6e0e441ac | ||
|
|
dcc2ba3492 | ||
|
|
ccf00d6b09 | ||
|
|
d8250574fc | ||
|
|
0d698c76e8 | ||
|
|
e4e3b4e73b | ||
|
|
b26d622f5d | ||
|
|
78694751bd | ||
|
|
f9b90c5c1c | ||
|
|
56b848bf47 | ||
|
|
dd875230c3 | ||
|
|
de1d428e63 | ||
|
|
55e3a6514f | ||
|
|
f4bbef210c | ||
|
|
2c1470cef6 | ||
|
|
adb6aa57e6 | ||
|
|
acd0abdb70 | ||
|
|
b9c8f5e0fd | ||
|
|
18fedf431f | ||
|
|
fc6edb4d20 | ||
|
|
d1093c77a3 | ||
|
|
b543f1e3cb | ||
|
|
be210e27da | ||
|
|
f7243fca37 | ||
|
|
1f6efd5736 | ||
|
|
54fbad3a7e | ||
|
|
1315489a7b | ||
|
|
c189fa0e82 | ||
|
|
3eae03289e | ||
|
|
e5e5a45b91 | ||
|
|
7a71beb892 | ||
|
|
74a894229f | ||
|
|
c81c1f70c8 | ||
|
|
3a383ac873 | ||
|
|
3149fe60c4 | ||
|
|
d5956e0778 | ||
|
|
8baf53d078 | ||
|
|
82ab21dc3f | ||
|
|
d676aef027 | ||
|
|
a2d9fdf334 | ||
|
|
0cec4135c0 | ||
|
|
6f76686a3e | ||
|
|
c211b3c376 | ||
|
|
0179b6ecc7 | ||
|
|
44e1ad839e | ||
|
|
dddd97daff | ||
|
|
632bec459e | ||
|
|
910ca10a5f | ||
|
|
0826822a4d | ||
|
|
8aa6b009c3 |
No files matched your search
@@ -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"
|
||||
DEBIAN12_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]
|
||||
|
||||
@@ -65,12 +65,12 @@ jobs:
|
||||
- name: Run Unit tests
|
||||
run: ctest --output-on-failure --test-dir build
|
||||
|
||||
build-debian12:
|
||||
name: Debian 12/GCC build
|
||||
build-debian13:
|
||||
name: Debian 13/GCC build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: debian:12
|
||||
image: debian:13
|
||||
volumes:
|
||||
- .:/workspace
|
||||
options: --workdir /workspace
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y $DEBIAN12_DEPS
|
||||
apt-get install -y $DEBIAN13_DEPS
|
||||
|
||||
- name: Build llhttp dependency
|
||||
run: |
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -9,8 +9,8 @@ if(POLICY CMP0167)
|
||||
cmake_policy(SET CMP0167 NEW)
|
||||
endif()
|
||||
|
||||
set (opendht_VERSION_MAJOR 4)
|
||||
set (opendht_VERSION_MINOR 2.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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -204,7 +204,13 @@ struct OPENDHT_PUBLIC PrivateKey
|
||||
* @param algo : the public key algorithm to use, GNUTLS_PK_RSA or GNUTLS_PK_RSA_OAEP
|
||||
*/
|
||||
static PrivateKey generate(unsigned key_length = 4096, gnutls_pk_algorithm_t algo = GNUTLS_PK_RSA);
|
||||
static PrivateKey generateEC();
|
||||
|
||||
/**
|
||||
* Generate a new EC key pair
|
||||
* @param curve : the elliptic curve to use. If GNUTLS_ECC_CURVE_INVALID is provided, the default curve will be used
|
||||
* (currently GNUTLS_ECC_CURVE_SECP384R1).
|
||||
*/
|
||||
static PrivateKey generateEC(gnutls_ecc_curve_t curve = GNUTLS_ECC_CURVE_INVALID);
|
||||
|
||||
gnutls_privkey_t key {};
|
||||
gnutls_x509_privkey_t x509_key {};
|
||||
@@ -272,7 +278,8 @@ public:
|
||||
* and incremented on subsequent ones.
|
||||
*/
|
||||
void sign(const PrivateKey&, const Certificate&, duration validity_period = {}, uint64_t number = 0);
|
||||
void sign(const Identity& id, duration validity_period = {}, uint64_t number = 0) {
|
||||
void sign(const Identity& id, duration validity_period = {}, uint64_t number = 0)
|
||||
{
|
||||
sign(*id.first, *id.second, validity_period, number);
|
||||
}
|
||||
|
||||
|
||||
+21
-4
@@ -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,
|
||||
|
||||
@@ -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_ {};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
@@ -489,8 +509,12 @@ private:
|
||||
static constexpr size_t MTU {1280};
|
||||
static constexpr size_t MAX_PACKET_VALUE_SIZE {600};
|
||||
static constexpr size_t MAX_MESSAGE_VALUE_SIZE {56 * 1024};
|
||||
static constexpr size_t RX_MAX_EARLY_BYTES {MAX_MESSAGE_VALUE_SIZE + MAX_VALUE_SIZE + 32};
|
||||
static constexpr size_t RX_MAX_EARLY_PARTS {RX_MAX_EARLY_BYTES / MTU + 2};
|
||||
|
||||
void process(std::unique_ptr<ParsedMessage>&&, const SockAddr& from);
|
||||
bool isExpectedReply(const ParsedMessage&, const SockAddr& from);
|
||||
bool tryCompleteMessage(PartialMessage&, const SockAddr& from);
|
||||
|
||||
bool rateLimit(const SockAddr& addr);
|
||||
|
||||
@@ -527,7 +551,8 @@ private:
|
||||
{
|
||||
return packValueHeader(buf, values.begin(), values.end());
|
||||
}
|
||||
void maintainRxBuffer(Tid tid);
|
||||
using PartialMessageKey = std::pair<SockAddr, Tid>;
|
||||
void maintainRxBuffer(const PartialMessageKey& key);
|
||||
|
||||
/*************
|
||||
* Answers *
|
||||
@@ -547,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 */
|
||||
@@ -573,7 +601,7 @@ private:
|
||||
|
||||
// requests handling
|
||||
std::map<Tid, Sp<Request>> requests {};
|
||||
std::map<Tid, PartialMessage> partial_messages;
|
||||
std::map<PartialMessageKey, PartialMessage> partial_messages;
|
||||
|
||||
MessageStats in_stats {}, out_stats {};
|
||||
std::set<SockAddr> blacklist {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -85,6 +85,14 @@ using system_clock = std::chrono::system_clock;
|
||||
using time_point = clock::time_point;
|
||||
using duration = clock::duration;
|
||||
|
||||
OPENDHT_PUBLIC time_point from_system_time(system_clock::time_point t,
|
||||
time_point steady_now,
|
||||
system_clock::time_point system_now);
|
||||
OPENDHT_PUBLIC time_point from_system_time(system_clock::time_point t);
|
||||
OPENDHT_PUBLIC system_clock::time_point to_system_time(time_point t,
|
||||
time_point steady_now,
|
||||
system_clock::time_point system_now);
|
||||
OPENDHT_PUBLIC system_clock::time_point to_system_time(time_point t);
|
||||
OPENDHT_PUBLIC time_point from_time_t(std::time_t t);
|
||||
OPENDHT_PUBLIC std::time_t to_time_t(time_point t);
|
||||
|
||||
|
||||
+35
-2
@@ -5,7 +5,7 @@ project(
|
||||
'cython',
|
||||
license: 'MIT',
|
||||
license_files: 'LICENSE',
|
||||
version: '4.2.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}
|
||||
@@ -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})
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+5
-34
@@ -1294,14 +1294,16 @@ PrivateKey::generate(unsigned key_length, gnutls_pk_algorithm_t algo)
|
||||
}
|
||||
|
||||
PrivateKey
|
||||
PrivateKey::generateEC()
|
||||
PrivateKey::generateEC(gnutls_ecc_curve_t curve)
|
||||
{
|
||||
gnutls_x509_privkey_t key;
|
||||
if (gnutls_x509_privkey_init(&key) != GNUTLS_E_SUCCESS)
|
||||
throw CryptoException("Unable to initialize private key.");
|
||||
int err = gnutls_x509_privkey_generate(key,
|
||||
GNUTLS_PK_EC,
|
||||
gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA),
|
||||
curve == GNUTLS_ECC_CURVE_INVALID
|
||||
? gnutls_sec_param_to_pk_bits(GNUTLS_PK_EC, GNUTLS_SEC_PARAM_ULTRA)
|
||||
: GNUTLS_CURVE_TO_BITS(curve),
|
||||
0);
|
||||
if (err != GNUTLS_E_SUCCESS) {
|
||||
gnutls_x509_privkey_deinit(key);
|
||||
@@ -1657,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;
|
||||
@@ -1733,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;
|
||||
|
||||
+226
-51
@@ -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)
|
||||
@@ -2363,6 +2508,9 @@ Dht::confirmNodes()
|
||||
std::vector<ValuesExport>
|
||||
Dht::exportValues() const
|
||||
{
|
||||
const auto& now = scheduler.time();
|
||||
const auto system_now = system_clock::now();
|
||||
|
||||
std::vector<ValuesExport> e {};
|
||||
e.reserve(store.size());
|
||||
for (const auto& h : store) {
|
||||
@@ -2375,7 +2523,7 @@ Dht::exportValues() const
|
||||
pk.pack_array(vals.size());
|
||||
for (const auto& v : vals) {
|
||||
pk.pack_array(4);
|
||||
pk.pack(v.created.time_since_epoch().count());
|
||||
pk.pack(to_system_time(v.created, now, system_now));
|
||||
v.data->msgpack_pack(pk);
|
||||
if (const auto& store_addr = v.store_bucket ? v.store_bucket->getAddr() : SockAddr {}) {
|
||||
pk.pack_bin(store_addr.getLength());
|
||||
@@ -2383,7 +2531,7 @@ Dht::exportValues() const
|
||||
} else {
|
||||
pk.pack_bin(0);
|
||||
}
|
||||
pk.pack(v.expiration.time_since_epoch().count());
|
||||
pk.pack(to_system_time(v.expiration, now, system_now));
|
||||
}
|
||||
ve.second = {buffer.data(), buffer.data() + buffer.size()};
|
||||
e.push_back(std::move(ve));
|
||||
@@ -2395,6 +2543,7 @@ void
|
||||
Dht::importValues(const std::vector<ValuesExport>& import)
|
||||
{
|
||||
const auto& now = scheduler.time();
|
||||
const auto system_now = system_clock::now();
|
||||
|
||||
unsigned imported = 0, ignored = 0;
|
||||
size_t imported_size = 0;
|
||||
@@ -2419,8 +2568,15 @@ Dht::importValues(const std::vector<ValuesExport>& import)
|
||||
Value tmp_val;
|
||||
SockAddr store_addr;
|
||||
try {
|
||||
val_time = time_point {
|
||||
time_point::duration {valel.via.array.ptr[0].as<time_point::duration::rep>()}};
|
||||
const bool uses_system_time = valel.via.array.ptr[0].type == msgpack::type::EXT;
|
||||
if (uses_system_time) {
|
||||
val_time = from_system_time(valel.via.array.ptr[0].as<system_clock::time_point>(),
|
||||
now,
|
||||
system_now);
|
||||
} else {
|
||||
val_time = time_point {
|
||||
time_point::duration {valel.via.array.ptr[0].as<time_point::duration::rep>()}};
|
||||
}
|
||||
tmp_val.msgpack_unpack(valel.via.array.ptr[1]);
|
||||
if (valel.via.array.size >= 3) {
|
||||
auto addr_blob = valel.via.array.ptr[2].as<Blob>();
|
||||
@@ -2430,8 +2586,14 @@ Dht::importValues(const std::vector<ValuesExport>& import)
|
||||
}
|
||||
}
|
||||
if (valel.via.array.size >= 4) {
|
||||
expiration = time_point {
|
||||
time_point::duration {valel.via.array.ptr[3].as<time_point::duration::rep>()}};
|
||||
if (uses_system_time) {
|
||||
expiration = from_system_time(valel.via.array.ptr[3].as<system_clock::time_point>(),
|
||||
now,
|
||||
system_now);
|
||||
} else {
|
||||
expiration = time_point {
|
||||
time_point::duration {valel.via.array.ptr[3].as<time_point::duration::rep>()}};
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
if (logger_)
|
||||
@@ -2697,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_)
|
||||
@@ -2711,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;
|
||||
}
|
||||
@@ -2855,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
@@ -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
@@ -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 {
|
||||
|
||||
+21
-3
@@ -554,8 +554,14 @@ DhtRunner::registerInsecureType(const ValueType& type)
|
||||
void
|
||||
DhtRunner::importValues(const std::vector<ValuesExport>& values)
|
||||
{
|
||||
std::lock_guard lck(dht_mtx);
|
||||
dht_->importValues(values);
|
||||
{
|
||||
std::lock_guard lck(dht_mtx);
|
||||
dht_->importValues(values);
|
||||
}
|
||||
// Recompute the runner wakeup after import schedules value expirations.
|
||||
std::lock_guard lck(storage_mtx);
|
||||
pending_ops_prio.emplace([](SecureDht&) {});
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
unsigned
|
||||
@@ -813,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();
|
||||
@@ -1233,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));
|
||||
|
||||
+35
-9
@@ -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;
|
||||
@@ -308,7 +329,7 @@ issuer_from_chain(STACK_OF(X509) * fullchain)
|
||||
if ((issuer_name = X509_get_issuer_name(cert)) == nullptr)
|
||||
return nullptr;
|
||||
|
||||
issuer = X509_find_by_subject(fullchain, issuer_name);
|
||||
issuer = X509_find_by_subject(fullchain, const_cast<X509_NAME*>(issuer_name));
|
||||
return issuer;
|
||||
}
|
||||
|
||||
@@ -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_)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+229
-78
@@ -40,8 +40,44 @@ struct NetworkEngine::PartialMessage
|
||||
time_point start;
|
||||
time_point last_part;
|
||||
std::unique_ptr<ParsedMessage> msg;
|
||||
std::vector<std::unique_ptr<ParsedMessage>> early_parts;
|
||||
size_t early_bytes {};
|
||||
bool request_quota_consumed {};
|
||||
};
|
||||
|
||||
bool
|
||||
NetworkEngine::isExpectedReply(const ParsedMessage& msg, const SockAddr& from)
|
||||
{
|
||||
auto request = requests.find(msg.tid);
|
||||
if (request != requests.end() && request->second->pending() && !request->second->node->id
|
||||
&& request->second->node->getAddr() == from)
|
||||
return true;
|
||||
|
||||
auto node = cache.getNode(msg.id, from.getFamily());
|
||||
if (!node || node->getAddr() != from)
|
||||
return false;
|
||||
|
||||
auto pending = node->getRequest(msg.tid);
|
||||
return (pending && pending->pending()) || node->getSocket(msg.tid);
|
||||
}
|
||||
|
||||
bool
|
||||
NetworkEngine::tryCompleteMessage(PartialMessage& partial, const SockAddr& from)
|
||||
{
|
||||
try {
|
||||
if (!partial.msg->complete())
|
||||
return false;
|
||||
process(std::move(partial.msg), from);
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->warn("Error while processing partial message: {}", e.what());
|
||||
} catch (...) {
|
||||
if (logger_)
|
||||
logger_->warn("Unknown error while processing partial message");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<Blob>
|
||||
serializeValues(const std::vector<Sp<Value>>& st)
|
||||
{
|
||||
@@ -67,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,
|
||||
@@ -108,7 +145,7 @@ NetworkEngine::~NetworkEngine()
|
||||
clear();
|
||||
}
|
||||
|
||||
void
|
||||
std::vector<Sp<Request>>
|
||||
NetworkEngine::tellListener(const Sp<Node>& node,
|
||||
Tid socket_id,
|
||||
const InfoHash& hash,
|
||||
@@ -118,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);
|
||||
}
|
||||
@@ -131,6 +169,7 @@ NetworkEngine::tellListener(const Sp<Node>& node,
|
||||
if (logger_)
|
||||
logger_->error("Unable to send value: buffer not large enough.");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
@@ -139,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);
|
||||
@@ -147,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) {
|
||||
@@ -210,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);
|
||||
@@ -218,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) {
|
||||
@@ -466,35 +523,58 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
|
||||
|
||||
// partial value data
|
||||
if (msg->type == MessageType::ValueData) {
|
||||
auto pmsg_it = partial_messages.find(msg->tid);
|
||||
if (pmsg_it == partial_messages.end()) {
|
||||
if (logIncoming_)
|
||||
if (logger_)
|
||||
logger_->debug("Unable to find partial message");
|
||||
if (msg->fragment_parts.empty()
|
||||
|| std::any_of(msg->fragment_parts.begin(), msg->fragment_parts.end(), [](const auto& fragment) {
|
||||
return fragment.second.second.empty();
|
||||
})) {
|
||||
rateLimit(from);
|
||||
return;
|
||||
}
|
||||
if (!pmsg_it->second.from.equals(from)) {
|
||||
if (logger_)
|
||||
logger_->debug("Received partial message data from unexpected IP address");
|
||||
rateLimit(from);
|
||||
PartialMessageKey key {from, msg->tid};
|
||||
auto pmsg_it = partial_messages.find(key);
|
||||
if (pmsg_it == partial_messages.end()) {
|
||||
if (!rateLimit(from))
|
||||
return;
|
||||
auto inserted = partial_messages.emplace(key, PartialMessage {});
|
||||
pmsg_it = inserted.first;
|
||||
pmsg_it->second.from = from;
|
||||
pmsg_it->second.start = now;
|
||||
pmsg_it->second.last_part = now;
|
||||
pmsg_it->second.request_quota_consumed = true;
|
||||
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
|
||||
}
|
||||
auto& pmsg = pmsg_it->second;
|
||||
if (!pmsg.msg) {
|
||||
if (pmsg.early_parts.size() >= RX_MAX_EARLY_PARTS) {
|
||||
partial_messages.erase(pmsg_it);
|
||||
return;
|
||||
}
|
||||
size_t fragment_bytes {};
|
||||
for (const auto& [index, fragment] : msg->fragment_parts) {
|
||||
(void) index;
|
||||
if (fragment.second.size() > RX_MAX_EARLY_BYTES - fragment_bytes) {
|
||||
partial_messages.erase(pmsg_it);
|
||||
return;
|
||||
}
|
||||
fragment_bytes += fragment.second.size();
|
||||
}
|
||||
if (fragment_bytes > RX_MAX_EARLY_BYTES - pmsg.early_bytes) {
|
||||
partial_messages.erase(pmsg_it);
|
||||
return;
|
||||
}
|
||||
pmsg.early_bytes += fragment_bytes;
|
||||
pmsg.early_parts.emplace_back(std::move(msg));
|
||||
pmsg.last_part = now;
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
|
||||
return;
|
||||
}
|
||||
// append data block
|
||||
if (pmsg_it->second.msg->append(*msg)) {
|
||||
pmsg_it->second.last_part = now;
|
||||
// check data completion
|
||||
if (pmsg_it->second.msg->complete()) {
|
||||
try {
|
||||
// process the full message
|
||||
process(std::move(pmsg_it->second.msg), from);
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->warn("Error while processing partial message: {}", e.what());
|
||||
}
|
||||
if (pmsg.msg->append(*msg)) {
|
||||
pmsg.last_part = now;
|
||||
if (tryCompleteMessage(pmsg, from)) {
|
||||
partial_messages.erase(pmsg_it);
|
||||
} else
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, msg->tid));
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, key));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -505,7 +585,32 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg->type > MessageType::Reply) {
|
||||
if (msg->value_parts.empty()) {
|
||||
if (msg->type > MessageType::Reply) {
|
||||
/* Rate limit requests. */
|
||||
if (!rateLimit(from)) {
|
||||
if (logger_)
|
||||
logger_->warn("Dropping request due to rate limiting");
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
process(std::move(msg), from);
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->warn("Error while processing message: {}", e.what());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PartialMessageKey partial_key {from, msg->tid};
|
||||
if (msg->type == MessageType::Reply && !isExpectedReply(*msg, from))
|
||||
return;
|
||||
|
||||
const auto partial = partial_messages.find(partial_key);
|
||||
const bool request_quota_consumed = partial != partial_messages.end() && !partial->second.msg
|
||||
&& partial->second.request_quota_consumed;
|
||||
if (msg->type > MessageType::Reply && !request_quota_consumed) {
|
||||
/* Rate limit requests. */
|
||||
if (!rateLimit(from)) {
|
||||
if (logger_)
|
||||
@@ -514,28 +619,28 @@ NetworkEngine::processMessage(const uint8_t* buf, size_t buflen, SockAddr f)
|
||||
}
|
||||
}
|
||||
|
||||
if (msg->value_parts.empty()) {
|
||||
try {
|
||||
process(std::move(msg), from);
|
||||
} catch (const std::exception& e) {
|
||||
if (logger_)
|
||||
logger_->warn("Error while processing message: {}", e.what());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// starting partial message session
|
||||
auto k = msg->tid;
|
||||
auto& pmsg = partial_messages[k];
|
||||
if (not pmsg.msg) {
|
||||
// starting partial message session
|
||||
auto inserted = partial_messages.emplace(partial_key, PartialMessage {});
|
||||
auto& pmsg = inserted.first->second;
|
||||
if (not pmsg.msg) {
|
||||
if (inserted.second) {
|
||||
pmsg.from = from;
|
||||
pmsg.msg = std::move(msg);
|
||||
pmsg.start = now;
|
||||
pmsg.last_part = now;
|
||||
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, k));
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, k));
|
||||
} else if (logger_)
|
||||
logger_->error("Partial message with given TID {} already exists", k);
|
||||
}
|
||||
scheduler.add(now + RX_MAX_PACKET_TIME, std::bind(&NetworkEngine::maintainRxBuffer, this, partial_key));
|
||||
}
|
||||
pmsg.msg = std::move(msg);
|
||||
pmsg.last_part = now;
|
||||
for (const auto& part : pmsg.early_parts)
|
||||
pmsg.msg->append(*part);
|
||||
pmsg.early_parts.clear();
|
||||
pmsg.early_bytes = 0;
|
||||
if (tryCompleteMessage(pmsg, from)) {
|
||||
partial_messages.erase(inserted.first);
|
||||
} else {
|
||||
scheduler.add(now + RX_TIMEOUT, std::bind(&NetworkEngine::maintainRxBuffer, this, partial_key));
|
||||
}
|
||||
} else if (logger_)
|
||||
logger_->error("Partial message from {} with given TID {} already exists", from.toString(), msg->tid);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -700,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: {
|
||||
@@ -1223,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;
|
||||
@@ -1232,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);
|
||||
@@ -1282,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);
|
||||
@@ -1387,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>
|
||||
@@ -1419,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);
|
||||
@@ -1429,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);
|
||||
@@ -1614,9 +1765,9 @@ NetworkEngine::sendError(const SockAddr& addr, Tid tid, uint16_t code, const std
|
||||
}
|
||||
|
||||
void
|
||||
NetworkEngine::maintainRxBuffer(Tid tid)
|
||||
NetworkEngine::maintainRxBuffer(const PartialMessageKey& key)
|
||||
{
|
||||
auto msg = partial_messages.find(tid);
|
||||
auto msg = partial_messages.find(key);
|
||||
if (msg != partial_messages.end()) {
|
||||
const auto& now = scheduler.time();
|
||||
if (msg->second.start + RX_MAX_PACKET_TIME < now || msg->second.last_part + RX_TIMEOUT < now) {
|
||||
|
||||
+32
-20
@@ -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
@@ -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
|
||||
+47
-7
@@ -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;
|
||||
}
|
||||
@@ -274,14 +280,21 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
|
||||
|
||||
if (!parsed.a && !parsed.r && !parsed.e && !parsed.u)
|
||||
throw msgpack::type_error();
|
||||
auto& req = parsed.a ? *parsed.a : (parsed.r ? *parsed.r : (parsed.u ? *parsed.u : *parsed.e));
|
||||
|
||||
if (parsed.e) {
|
||||
if (parsed.e->type != msgpack::type::ARRAY)
|
||||
if (parsed.e->type != msgpack::type::ARRAY or parsed.e->via.array.size < 1)
|
||||
throw msgpack::type_error();
|
||||
error_code = parsed.e->via.array.ptr[0].as<uint16_t>();
|
||||
}
|
||||
|
||||
/* An error-only message carries no request map. The error array must never
|
||||
be reached through the map view of the msgpack object union, and a
|
||||
request field that is not a map must be rejected rather than
|
||||
reinterpreted. */
|
||||
const msgpack::object* req = parsed.a ? parsed.a : (parsed.r ? parsed.r : parsed.u);
|
||||
if (req and req->type != msgpack::type::MAP)
|
||||
throw msgpack::type_error();
|
||||
|
||||
struct ParsedReq
|
||||
{
|
||||
msgpack::object* values;
|
||||
@@ -290,8 +303,9 @@ ParsedMessage::msgpack_unpack(const msgpack::object& msg)
|
||||
msgpack::object* want;
|
||||
} parsedReq {};
|
||||
|
||||
for (unsigned i = 0; i < req.via.map.size; i++) {
|
||||
auto& o = req.via.map.ptr[i];
|
||||
const uint32_t req_size = req ? req->via.map.size : 0;
|
||||
for (unsigned i = 0; i < req_size; i++) {
|
||||
auto& o = req->via.map.ptr[i];
|
||||
if (o.key.type != msgpack::type::STR)
|
||||
continue;
|
||||
auto key = o.key.as<std::string_view>();
|
||||
@@ -329,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();
|
||||
@@ -352,24 +380,36 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (parsedReq.fields) {
|
||||
if (auto rfields = findMapValue(*parsedReq.fields, "f"sv)) {
|
||||
auto vfields = rfields->as<std::set<Value::Field>>();
|
||||
if (vfields.empty())
|
||||
throw msgpack::type_error();
|
||||
if (auto rvalues = findMapValue(*parsedReq.fields, "v"sv)) {
|
||||
if (rvalues->type != msgpack::type::ARRAY)
|
||||
throw msgpack::type_error();
|
||||
|
||||
+158
-18
@@ -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
@@ -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
@@ -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());
|
||||
|
||||
+53
-14
@@ -238,28 +238,67 @@ operator==(const SockAddr& a, const SockAddr& b)
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
time_point
|
||||
from_system_time(system_clock::time_point t, time_point steady_now, system_clock::time_point system_now)
|
||||
{
|
||||
if (t == system_clock::time_point::min())
|
||||
return time_point::min();
|
||||
if (t == system_clock::time_point::max())
|
||||
return time_point::max();
|
||||
// Saturate before the cast: the steady clock duration may hold a narrower range than the system one.
|
||||
constexpr auto max_dt = std::chrono::duration_cast<system_clock::duration>(duration::max());
|
||||
constexpr auto min_dt = std::chrono::duration_cast<system_clock::duration>(duration::min());
|
||||
auto system_dt = t - system_now;
|
||||
if (system_dt >= max_dt)
|
||||
return time_point::max();
|
||||
if (system_dt <= min_dt)
|
||||
return time_point::min();
|
||||
auto dt = std::chrono::duration_cast<duration>(system_dt);
|
||||
if (dt > duration(0) and steady_now > time_point::max() - dt)
|
||||
return time_point::max();
|
||||
else if (dt < duration(0) and steady_now < time_point::min() - dt)
|
||||
return time_point::min();
|
||||
return steady_now + dt;
|
||||
}
|
||||
|
||||
time_point
|
||||
from_system_time(system_clock::time_point t)
|
||||
{
|
||||
return from_system_time(t, clock::now(), system_clock::now());
|
||||
}
|
||||
|
||||
system_clock::time_point
|
||||
to_system_time(time_point t, time_point steady_now, system_clock::time_point system_now)
|
||||
{
|
||||
if (t == time_point::min())
|
||||
return system_clock::time_point::min();
|
||||
if (t == time_point::max())
|
||||
return system_clock::time_point::max();
|
||||
// Cast first: mixing clock durations of different periods would overflow the bound computations below.
|
||||
auto dt = std::chrono::duration_cast<system_clock::duration>(t - steady_now);
|
||||
if (dt > system_clock::duration(0) and system_now >= system_clock::time_point::max() - dt)
|
||||
return system_clock::time_point::max();
|
||||
else if (dt < system_clock::duration(0) and system_now <= system_clock::time_point::min() - dt)
|
||||
return system_clock::time_point::min();
|
||||
return system_now + dt;
|
||||
}
|
||||
|
||||
system_clock::time_point
|
||||
to_system_time(time_point t)
|
||||
{
|
||||
return to_system_time(t, clock::now(), system_clock::now());
|
||||
}
|
||||
|
||||
time_point
|
||||
from_time_t(std::time_t t)
|
||||
{
|
||||
auto dt = system_clock::from_time_t(t) - system_clock::now();
|
||||
auto now = clock::now();
|
||||
if (dt > system_clock::duration(0) and now > time_point::max() - dt)
|
||||
return time_point::max();
|
||||
else if (dt < system_clock::duration(0) and now < time_point::min() - dt)
|
||||
return time_point::min();
|
||||
return now + dt;
|
||||
return from_system_time(system_clock::from_time_t(t));
|
||||
}
|
||||
|
||||
std::time_t
|
||||
to_time_t(time_point t)
|
||||
{
|
||||
auto dt = t - clock::now();
|
||||
auto now = system_clock::now();
|
||||
if (dt > duration(0) and now >= system_clock::time_point::max() - dt)
|
||||
return system_clock::to_time_t(system_clock::time_point::max());
|
||||
else if (dt < duration(0) and now <= system_clock::time_point::min() - dt)
|
||||
return system_clock::to_time_t(system_clock::time_point::min());
|
||||
return system_clock::to_time_t(now + std::chrono::duration_cast<system_clock::duration>(dt));
|
||||
return system_clock::to_time_t(to_system_time(t));
|
||||
}
|
||||
|
||||
Blob
|
||||
|
||||
+27
-6
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -327,6 +327,16 @@ CryptoTester::testCertificateSerialNumber()
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CryptoTester::testEcKeyCurve()
|
||||
{
|
||||
auto key = dht::crypto::PrivateKey::generateEC(GNUTLS_ECC_CURVE_SECP256R1);
|
||||
unsigned int bits = 0;
|
||||
CPPUNIT_ASSERT_EQUAL(static_cast<int>(GNUTLS_PK_EC),
|
||||
gnutls_x509_privkey_get_pk_algorithm2(key.x509_key, &bits));
|
||||
CPPUNIT_ASSERT_EQUAL(256u, bits);
|
||||
}
|
||||
|
||||
void
|
||||
CryptoTester::testOcsp()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ class CryptoTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testRevocationListPartitionedNumber);
|
||||
CPPUNIT_TEST(testCertificateRequest);
|
||||
CPPUNIT_TEST(testCertificateSerialNumber);
|
||||
CPPUNIT_TEST(testEcKeyCurve);
|
||||
CPPUNIT_TEST(testOcsp);
|
||||
CPPUNIT_TEST(testAesEncryption);
|
||||
CPPUNIT_TEST(testAesEncryptionWithMultipleKeySizes);
|
||||
@@ -68,6 +69,10 @@ public:
|
||||
* Test certificate serial number extraction
|
||||
*/
|
||||
void testCertificateSerialNumber();
|
||||
/**
|
||||
* Test EC private key generation with an explicit curve
|
||||
*/
|
||||
void testEcKeyCurve();
|
||||
/**
|
||||
* Test OCSP
|
||||
*/
|
||||
|
||||
+1814
-1
File diff suppressed because it is too large.
Load diff
+64
-1
@@ -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 {};
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+859
-50
@@ -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()
|
||||
{
|
||||
@@ -210,6 +444,42 @@ DhtRunnerTester::testImportValuesPreservesRemoteQuota()
|
||||
restored.join();
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testExportValuesUsesSystemTime()
|
||||
{
|
||||
auto key = dht::InfoHash::get("export-system-time");
|
||||
const auto created = dht::clock::now();
|
||||
msgpack::sbuffer importBuffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&importBuffer);
|
||||
pk.pack_array(1);
|
||||
pk.pack_array(2);
|
||||
pk.pack(created.time_since_epoch().count());
|
||||
dht::Value {std::string("persisted")}.msgpack_pack(pk);
|
||||
node1.importValues({
|
||||
dht::ValuesExport {key, {importBuffer.data(), importBuffer.data() + importBuffer.size()}}
|
||||
});
|
||||
|
||||
auto exported = node1.exportValues();
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), exported.size());
|
||||
|
||||
msgpack::unpacked msg;
|
||||
msgpack::unpack(msg, reinterpret_cast<const char*>(exported.front().second.data()), exported.front().second.size());
|
||||
const auto values = msg.get();
|
||||
CPPUNIT_ASSERT(values.type == msgpack::type::ARRAY);
|
||||
CPPUNIT_ASSERT_EQUAL(uint32_t(1), values.via.array.size);
|
||||
const auto& persistedValue = values.via.array.ptr[0];
|
||||
CPPUNIT_ASSERT_EQUAL(uint32_t(4), persistedValue.via.array.size);
|
||||
CPPUNIT_ASSERT(persistedValue.via.array.ptr[0].type == msgpack::type::EXT);
|
||||
CPPUNIT_ASSERT(persistedValue.via.array.ptr[3].type == msgpack::type::EXT);
|
||||
|
||||
const auto persistedCreated = persistedValue.via.array.ptr[0].as<std::chrono::system_clock::time_point>();
|
||||
const auto persistedExpiration = persistedValue.via.array.ptr[3].as<std::chrono::system_clock::time_point>();
|
||||
const auto delta = std::chrono::system_clock::now() - persistedCreated;
|
||||
CPPUNIT_ASSERT(delta >= std::chrono::system_clock::duration::zero());
|
||||
CPPUNIT_ASSERT(delta < 5s);
|
||||
CPPUNIT_ASSERT(persistedExpiration > persistedCreated);
|
||||
}
|
||||
|
||||
void
|
||||
DhtRunnerTester::testImportValuesPreservesStoredExpiration()
|
||||
{
|
||||
@@ -267,14 +537,25 @@ DhtRunnerTester::testImportValuesPreservesStoredExpiration()
|
||||
void
|
||||
DhtRunnerTester::testImportPermanentValueExpires()
|
||||
{
|
||||
// Simulate importing a value that was originally stored as permanent
|
||||
// (expiration = time_point::max()). After import the permanent flag is lost,
|
||||
// so the value must expire within the type's normal expiration window.
|
||||
static const dht::ValueType SHORT_TYPE {201, "Short perm", 2s};
|
||||
static const dht::ValueType SHORT_TYPE {201, "Short perm", 500ms};
|
||||
|
||||
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 = -1;
|
||||
config.dht_config.node_config.max_store_keys = -1;
|
||||
dht::DhtRunner restored;
|
||||
restored.run(0, config);
|
||||
restored.registerInsecureType(SHORT_TYPE);
|
||||
|
||||
// Ensure the runner is waiting on a previously computed scheduler deadline.
|
||||
std::promise<void> loopCompleted;
|
||||
auto loopCompletedFuture = loopCompleted.get_future();
|
||||
restored.getNodeInfo([&loopCompleted](auto) { loopCompleted.set_value(); });
|
||||
getFutureValue(std::move(loopCompletedFuture));
|
||||
|
||||
auto key = dht::InfoHash::get("import-permanent-expiration");
|
||||
auto now = dht::clock::now();
|
||||
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
pk.pack_array(1);
|
||||
@@ -282,63 +563,33 @@ DhtRunnerTester::testImportPermanentValueExpires()
|
||||
pk.pack(now.time_since_epoch().count());
|
||||
dht::Value {SHORT_TYPE.id, std::string("was-permanent")}.msgpack_pack(pk);
|
||||
pk.pack_bin(0);
|
||||
// Permanent values are exported with time_point::max() expiration
|
||||
pk.pack(dht::clock::time_point::max().time_since_epoch().count());
|
||||
|
||||
dht::DhtRunner restored;
|
||||
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 = -1;
|
||||
config.dht_config.node_config.max_store_keys = -1;
|
||||
restored.run(0, config);
|
||||
|
||||
restored.importValues({
|
||||
dht::ValuesExport {key, {buffer.data(), buffer.data() + buffer.size()}}
|
||||
});
|
||||
|
||||
// Value should be present right after import
|
||||
auto exportedBefore = restored.exportValues();
|
||||
CPPUNIT_ASSERT(!exportedBefore.empty());
|
||||
|
||||
// Wait longer than the default USER_DATA expiration (10 min) is not
|
||||
// practical, but the type is unregistered so the default 10 min expiration
|
||||
// applies. Instead, register the short type so the cap uses 2 s.
|
||||
// Re-import into a fresh node that knows the short type.
|
||||
dht::DhtRunner restored2;
|
||||
restored2.run(0, config);
|
||||
restored2.registerInsecureType(SHORT_TYPE);
|
||||
restored2.importValues({
|
||||
dht::ValuesExport {key, {buffer.data(), buffer.data() + buffer.size()}}
|
||||
});
|
||||
|
||||
CPPUNIT_ASSERT(!restored2.exportValues().empty());
|
||||
|
||||
// Wait for the short expiration to pass
|
||||
std::this_thread::sleep_for(3s);
|
||||
|
||||
auto restoredValues = restored2.exportValues();
|
||||
bool found = false;
|
||||
for (const auto& ve : restoredValues) {
|
||||
if (ve.first == key) {
|
||||
auto containsValue = [&] {
|
||||
for (const auto& [exportedKey, data] : restored.exportValues()) {
|
||||
if (exportedKey != key)
|
||||
continue;
|
||||
msgpack::unpacked msg;
|
||||
msgpack::unpack(msg, (const char*) ve.second.data(), ve.second.size());
|
||||
msgpack::unpack(msg, reinterpret_cast<const char*>(data.data()), data.size());
|
||||
auto valarr = msg.get();
|
||||
CPPUNIT_ASSERT(valarr.type == msgpack::type::ARRAY);
|
||||
// The value should have expired
|
||||
CPPUNIT_ASSERT_EQUAL(uint32_t(0), valarr.via.array.size);
|
||||
found = true;
|
||||
return valarr.via.array.size != 0;
|
||||
}
|
||||
}
|
||||
// Either the key was cleaned up entirely or it was found with 0 values
|
||||
CPPUNIT_ASSERT(found || restoredValues.empty()
|
||||
|| std::none_of(restoredValues.begin(), restoredValues.end(),
|
||||
[&](const auto& ve) { return ve.first == key; }));
|
||||
return false;
|
||||
};
|
||||
|
||||
CPPUNIT_ASSERT(containsValue());
|
||||
auto timeout = dht::clock::now() + 2s;
|
||||
while (containsValue() and dht::clock::now() < timeout)
|
||||
std::this_thread::sleep_for(10ms);
|
||||
CPPUNIT_ASSERT(!containsValue());
|
||||
|
||||
restored.shutdown();
|
||||
restored.join();
|
||||
restored2.shutdown();
|
||||
restored2.join();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -570,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()
|
||||
{
|
||||
@@ -782,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));
|
||||
@@ -826,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
|
||||
@@ -916,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
|
||||
@@ -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,9 +34,16 @@ 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);
|
||||
CPPUNIT_TEST(testImportValuesPreservesStoredExpiration);
|
||||
CPPUNIT_TEST(testImportPermanentValueExpires);
|
||||
CPPUNIT_TEST(testBootstrapSetsConnectingState);
|
||||
@@ -28,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 {};
|
||||
@@ -62,11 +90,16 @@ public:
|
||||
* Test listen method
|
||||
*/
|
||||
void testListen();
|
||||
void testListenReadiness();
|
||||
void testListenReadinessStopped();
|
||||
void testListenReadinessInvalidKey();
|
||||
void testListenReadinessFilteredSignedValues();
|
||||
/**
|
||||
* Test methods requiring a node identity
|
||||
*/
|
||||
void testIdOps();
|
||||
void testImportValuesPreservesRemoteQuota();
|
||||
void testExportValuesUsesSystemTime();
|
||||
void testImportValuesPreservesStoredExpiration();
|
||||
void testImportPermanentValueExpires();
|
||||
void testBootstrapSetsConnectingState();
|
||||
@@ -74,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
|
||||
*/
|
||||
|
||||
+993
-33
File diff suppressed because it is too large.
Load diff
@@ -13,11 +13,36 @@ class NetworkEngineTester : public CppUnit::TestFixture
|
||||
#ifdef _MSC_VER
|
||||
CPPUNIT_TEST(testDisabledOnMsvc);
|
||||
#else
|
||||
CPPUNIT_TEST(testIgnoresUnknownPartialData);
|
||||
CPPUNIT_TEST(testCompletesPartialSessionWithDataBeforeHeader);
|
||||
CPPUNIT_TEST(testBuffersLargeValueBeforeHeader);
|
||||
CPPUNIT_TEST(testIgnoresEmptyPartialData);
|
||||
CPPUNIT_TEST(testRejectsUnsolicitedPartialReply);
|
||||
CPPUNIT_TEST(testDropsMalformedCompletedPartialMessage);
|
||||
CPPUNIT_TEST(testCompletesPartialSessionFromSameSource);
|
||||
CPPUNIT_TEST(testKeepsSessionForWrongSourceFragment);
|
||||
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
|
||||
@@ -30,11 +55,36 @@ public:
|
||||
#ifdef _MSC_VER
|
||||
void testDisabledOnMsvc();
|
||||
#else
|
||||
void testIgnoresUnknownPartialData();
|
||||
void testCompletesPartialSessionWithDataBeforeHeader();
|
||||
void testBuffersLargeValueBeforeHeader();
|
||||
void testIgnoresEmptyPartialData();
|
||||
void testRejectsUnsolicitedPartialReply();
|
||||
void testDropsMalformedCompletedPartialMessage();
|
||||
void testCompletesPartialSessionFromSameSource();
|
||||
void testKeepsSessionForWrongSourceFragment();
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
{}
|
||||
@@ -328,6 +391,104 @@ ParsedMessageTester::testParseRejectsInvalidValuesField()
|
||||
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseRejectsEmptyFieldSelection()
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
|
||||
pk.pack_map(3);
|
||||
pk.pack(KEY_R);
|
||||
pk.pack_map(1);
|
||||
pk.pack(KEY_REQ_FIELDS);
|
||||
pk.pack_map(2);
|
||||
pk.pack("f");
|
||||
pk.pack_array(0);
|
||||
pk.pack("v");
|
||||
pk.pack_array(0);
|
||||
pk.pack(KEY_TID);
|
||||
pk.pack(1u);
|
||||
pk.pack(KEY_Y);
|
||||
pk.pack(KEY_R);
|
||||
|
||||
auto msg = msgpack::unpack(buffer.data(), buffer.size());
|
||||
ParsedMessage parsed;
|
||||
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseRejectsEmptyErrorArray()
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
|
||||
pk.pack_map(3);
|
||||
pk.pack(KEY_E);
|
||||
pk.pack_array(0);
|
||||
pk.pack(KEY_TID);
|
||||
pk.pack(1u);
|
||||
pk.pack(KEY_Y);
|
||||
pk.pack(KEY_E);
|
||||
|
||||
auto msg = msgpack::unpack(buffer.data(), buffer.size());
|
||||
ParsedMessage parsed;
|
||||
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseRejectsNonMapRequest()
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
|
||||
pk.pack_map(4);
|
||||
pk.pack(KEY_A);
|
||||
pk.pack_array(2);
|
||||
pk.pack(1u);
|
||||
pk.pack(2u);
|
||||
pk.pack(KEY_Q);
|
||||
pk.pack(QUERY_PING);
|
||||
pk.pack(KEY_TID);
|
||||
pk.pack(1u);
|
||||
pk.pack(KEY_Y);
|
||||
pk.pack("q");
|
||||
|
||||
auto msg = msgpack::unpack(buffer.data(), buffer.size());
|
||||
ParsedMessage parsed;
|
||||
CPPUNIT_ASSERT_THROW(parsed.msgpack_unpack(msg.get()), msgpack::type_error);
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseErrorOnlyMessageKeepsRequestFieldsEmpty()
|
||||
{
|
||||
msgpack::sbuffer buffer;
|
||||
msgpack::packer<msgpack::sbuffer> pk(&buffer);
|
||||
|
||||
// An error message with no request map: the error array must not be walked
|
||||
// as if it were a sequence of key/value pairs.
|
||||
pk.pack_map(3);
|
||||
pk.pack(KEY_E);
|
||||
pk.pack_array(2);
|
||||
pk.pack(401u);
|
||||
pk.pack("unauthorized");
|
||||
pk.pack(KEY_TID);
|
||||
pk.pack(9u);
|
||||
pk.pack(KEY_Y);
|
||||
pk.pack(KEY_E);
|
||||
|
||||
auto msg = msgpack::unpack(buffer.data(), buffer.size());
|
||||
ParsedMessage parsed;
|
||||
parsed.msgpack_unpack(msg.get());
|
||||
|
||||
CPPUNIT_ASSERT(parsed.type == MessageType::Error);
|
||||
CPPUNIT_ASSERT_EQUAL((uint16_t) 401, parsed.error_code);
|
||||
CPPUNIT_ASSERT(not parsed.id);
|
||||
CPPUNIT_ASSERT(parsed.token.empty());
|
||||
CPPUNIT_ASSERT(parsed.values.empty());
|
||||
CPPUNIT_ASSERT(parsed.nodes4_raw.empty());
|
||||
CPPUNIT_ASSERT(parsed.nodes6_raw.empty());
|
||||
}
|
||||
|
||||
void
|
||||
ParsedMessageTester::testParseIgnoresIncompleteValueDataEntry()
|
||||
{
|
||||
|
||||
@@ -24,6 +24,12 @@ class ParsedMessageTester : public CppUnit::TestFixture
|
||||
CPPUNIT_TEST(testParseRejectsNonMapPacket);
|
||||
CPPUNIT_TEST(testParseRejectsInvalidValueDataPayload);
|
||||
CPPUNIT_TEST(testParseRejectsInvalidValuesField);
|
||||
CPPUNIT_TEST(testParseRejectsEmptyFieldSelection);
|
||||
CPPUNIT_TEST(testParseRejectsEmptyErrorArray);
|
||||
CPPUNIT_TEST(testParseRejectsNonMapRequest);
|
||||
CPPUNIT_TEST(testParseListenSync);
|
||||
CPPUNIT_TEST(testParseRejectsMalformedSyncedValues);
|
||||
CPPUNIT_TEST(testParseErrorOnlyMessageKeepsRequestFieldsEmpty);
|
||||
CPPUNIT_TEST(testParseIgnoresIncompleteValueDataEntry);
|
||||
CPPUNIT_TEST(testCompleteEmpty);
|
||||
CPPUNIT_TEST(testCompleteIncomplete);
|
||||
@@ -53,6 +59,12 @@ public:
|
||||
void testParseRejectsNonMapPacket();
|
||||
void testParseRejectsInvalidValueDataPayload();
|
||||
void testParseRejectsInvalidValuesField();
|
||||
void testParseRejectsEmptyFieldSelection();
|
||||
void testParseRejectsEmptyErrorArray();
|
||||
void testParseRejectsNonMapRequest();
|
||||
void testParseListenSync();
|
||||
void testParseRejectsMalformedSyncedValues();
|
||||
void testParseErrorOnlyMessageKeepsRequestFieldsEmpty();
|
||||
void testParseIgnoresIncompleteValueDataEntry();
|
||||
void testCompleteEmpty();
|
||||
void testCompleteIncomplete();
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opendht",
|
||||
"version-string": "4.2.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",
|
||||
|
||||
Reference in new issue
Block a user